您的位置:首页 > 编程语言

并发编程<五>并发处理之synchronized

2017-11-13 10:45 295 查看

序言-几个基本概念

在本系列中第四篇文章中已经讲述了并发产生的不可控结果,和结果不可控的根本原因。现在来说说Java所提供的并发处理方式之一的synchronized关键字。学习synchronized关键字之前,我们需要了解一下一下几个概念。

临界资源多个线程共同访问的资源我们称之为临界资源。
对象锁synchronized引入了锁的概念,每个对象都有一个锁标记,我们称其为对象锁。
互斥锁synchronized关键字可以用于标记象锁标记,这种锁在同一时间只能被一个线程所拥有,其他线程要拥有该对象锁需进入阻塞状态进行等待,等待当前拥有权的线程,跳出同步代码块或者释放所对象所有权,才能竞争该对象锁的拥有权。所以这种synchronized关键字标记的对象锁,我们亦称为互斥锁。
同步代码块 多个线程共同访问的代码,我们称之为同步代码(同步方法或者同步代码块),线程要访问该部分代码或者资源,需要拥有指定互斥锁的拥有权。由于互斥锁在同一时间只能被一个线程所拥有,所以同一时间也只能有一个线程能访问同步代码块。

synchronized用法

首先我们先来一个原始模型代码,得出一个结果,然后将synchronized关键字用上,比较彼此的结果。以这种方式去学习synchronized关键字。

数据类:

public class SynData {
private int number = 0;

public int getNumber() {
return number;
}

/**
* 对number做+1操作的方法
*/
public void doAddition() {
number += 1;
}
}


任务类:

/**
* Created by Dragon on 2017/11/9.
* 任务类,用于启动多个线程,产生并发
*/

public class AdditionRun implements Runnable {

SynData data;

public AdditionRun(SynData data) {
this.data = data;
}

@Override
public void run() {
try {
//线程启动后,先休眠0.5秒(达到并发的效果)。避免计算太快,别的线程还没启动,就已经做完了加法运算
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//该线程的作用就是调用data对象的doAddition方法,对number做+1操作
data.doAddition();
String threadName = Thread.currentThread().getName();
//打印出当前线程执行完毕,确保当前线程已经跑完
System.out.print(threadName + " is finished !\n");
}
}


程序入口:

public class MyClass {

public static void main(String[] args0) throws InterruptedException {

SynData synData = null;
AdditionRun additionRun = null;
Thread thread = null;

synData = new SynData();
additionRun = new AdditionRun(synData);
//启动10个线程做同一个任务,都对synData对象的number做+1操作
for (int i = 0; i < 10; i++) {
thread = new Thread(additionRun);
thread.setName("Thread_" + i);
thread.start();
}
//主线程休眠1秒,以便10个线程全部执行完任务
Thread.sleep(1000);
System.out.print("main-the final result is  :" + synData.getNumber());
}
}


很简单的一个程序代码,开启了10个线程对同一个对象data的number变量进行+1操作,等到所有的线程执行结束后,打印出number的值!不考虑并发的情况下,最后的结果应该是10。但是本案例中结果一定是10?那可不一定,自己多跑几次这个程序就会知道,结果是不确定的,将会是0-10之间的任意一个数。为什么会有这个结果,在《并发编程<四>并发不可预期结果的根本原因》一文中已经说得很清楚了。下面我们具体来看看synchronized!

有了以上4个概念的理解,以及一个原始模型代码,我们就可以着手synchronized关键字的使用了。synchronized字段用于修饰指定对象,所作用的对象可分为两类,synchronized的使用方式也将围绕着两类展开。

修饰于类的实例对象
修饰于类对象

请注意:实例对象和类对象是两个不同的概念。类对象:在类被JVM第一次加载的时候会调用“defineClass”方法对类生成一个代表该类的标识,这个标识我们称为类对象(比如:Class<SynData> synDataClass = SynData.class;这句代码的synDataClass 就是类对象),它被所有实例对象所共同拥有。实例对象:调用new关键字创建出来的具体的一个对象,我们称为某一个类的实例对象或者某一个类的实例。

无论是第一种还是第二种类型,说到底都是一句话:多线程竞争同一个对象锁时,没有竞争到对象锁使用权的线程进入锁池,进入阻塞状态,等待当前线程退出同步代码块或者释放锁的使用权。如果竞争不同对象锁,不会阻塞。

synchronized修饰类的实例的对象

#案例1:本案例属于synchronized修饰实例对象一类,因为synchronized修饰的object是通过new关键字创建的实例对象。
public class SynData {

//随便声明创建的一个对象,作为synchronized修饰对象,实例对象级别的锁
Object object = new Object();
//临界变量
private int number = 0;

public int getNumber() {
return number;
}

/**
* 对number做+1操作
*/
public void doAddition() {
synchronized (object) {
number += 1;
}
}
}


#案例2:本案例其实跟案例1属于换汤不换药,只是修饰了不同类的实例对象,都属于实例对象。运行结果当然也跟案例1一样,最后结果是确定值10.
public class SynData {

//临界变量
private int number = 0;

public int getNumber() {
return number;
}

/**
* 对number做+1操作
*/
public void doAddition() {
//synchronized修饰this相当于修饰SynData类的实例对象
synchronized (this) {
number += 1;
}
}
}


#案例3:改用直接修饰方法doAddition方法,其实还是跟案例1,案例2一样。因为非静态方法归实例对象所有,所以synchronized修饰的还是SynData 类的实例对象。
public class SynData {

//临界变量
private int number = 0;

public int getNumber() {
return number;
}

/**
* 对number做+1操作
*/
public synchronized void doAddition() {
number += 1;
}
}

#1,#,2#3三个案例中主程序类和任务类没有改动,只是将数据类做了一点小改动,在doAddition方法中增加了增加了synchronized关键字,并且synchronized都是修饰了实例对象。此时运行,最终结果都会是确定值10。

现在我们来分析一下,起到这个效果的原因。其实解决并发问题就是需要保证《并发编程<四>并发不可预期结果的根本原因》一文中提到的3个概念,原子性,可见性,有序性。其实保证了这三点,就解决了并发的问题。
因为增加synchronized字段,同一时间只能有一个线程访问同步代码块,没完成之前,别的线程不可能来访问number,必须等到当前线程完成任务,并将执行的结果值刷新到主内存中,并释放了对象锁,才能换一个线程来访问同步代码块,当前任务要么执行完,要么就不执行,这保证了保证了原子性。当线程做完所有操作之后,其他线程才能继续访问,保证访问同步代码的线程是有序进行的,这保证了有序性。当线程执行完毕,释放对象锁的时候,会将number最新值刷新到主内存,保证了每次线程访问到的number都是罪行的值,保证了可见性。保证了这三点,同步问题自然也就解决了。

synchronized修饰的代码块都是属于类的实例对象所有,我们看到MyClass->main方法中,启动了多个线程,但是启动线程的时候用到的SynData的实例对象是同一个。上面的案例就有了相同的特点:操作的变量是同一个,同步代码块是同一个,对象锁也是同一个。注意:当操作的变量是同一个的时候,并发才有了讨论的意义,如果多线程操作的对象都不是同一个了,那也就没有讨论的意义了。

现在我们将#1,#2,#3案例中的程序入口的代码稍作更改。
public class MyClass {

public static void main(String[] args0) throws InterruptedException {

SynData synData = null;
AdditionRun additionRun = null;
Thread thread = null;

//启动10个线程做同一个任务,都对synData对象的number做+1操作
for (int i = 0; i < 10; i++) {
//每一次创建线程用到的SynData实例对象都不相同
synData = new SynData();
additionRun = new AdditionRun(synData);
thread = new Thread(additionRun);
thread.setName("Thread_" + i);
thread.start();
}
//主线程休眠1秒,以便10个线程全部执行完任务
Thread.sleep(1000);
System.out.print("main-the final result is  :" + synData.getNumber());
}
}

原本创建10个线程使用的SynData类实例对象是同一个的情况下得出的结果是10。现在每个线程都匹配一个新的SynData实例对象,此时操作的变量就不是同一个,也就没有了并发的讨论意义。现在打印出来的结果只是最后一次i=9的时候创建的SynData实例对象中number的值,并且i的每一次循
de1e
环,得到的结果打印出来都应该是1。这时候上面三个案例访问的数据number是非静态,归实例对象所有。特点就是:操作变量不是用一个,同步代码块不同,锁对象也不同。

private static int number = 0;

现在我们保持主程序中用不同对象创建线程,将#1,#2,#3中SynData类的代码都稍作同样的更改,将number变量改为静态的用static进行修饰,此时的number归SynData类对象所有,10个线程中的SynData类的实例对象共享number变量,对这三个案例中的10个线程来说,有同一个特点:数据是同一份,同步代码块不是同一份,锁对象不是同一个。此时就有了并发的讨论意义,因为数据是同一份。此时的结果会是多少?当然还是不确定值,应该是1-10之间的随意一个数。因为10个线程锁竞争的对象锁是不同的,#1竞争的对象锁是SynData实例对象的object变量,#2,#3竞争的对象锁是SynData的10个实例对象。所以线程间不会产生互斥,阻塞。

static Object object = new Object();

如果我们将#1中的object变量也改为static修饰的静态变量呢?此时的object是归类对象所有,所以10个线程竞争的锁对象是同一个,彼此会存在互斥,阻塞。对于10个线程来说:操作的变量是同一个,同步代码块不同,锁对象是同一个。结果当然就是确定值10

总结上面三个案例的不同形态,得出以下结论:

1。有了临界资源(也即是多线程操作的变量是同一个)才有了并发的讨论意义

2。同步代码块是不是同一个对并发结果并不影响,因为决定进入同步代码块做运算的时机是根据是否拥有对象锁的使用权决定

3。竞争的对象锁是不是用一个,决定了是否互斥,是否会造成阻塞,决定着最后的结果。

对于第一种synchronized修饰实例对象,#1,#2,#3案例中已经讲述完了,下面我们就讲讲第二种synchronized修饰类对象。

synchronized修饰类对象

我们还是一样,用一个原型代码,跟上面差不多,原型案例中运行结果还是一样最后number的值是不确定值1-10之间的一个数。

程序入口:
public class MyClass {

public static void main(String[] args0) throws InterruptedException {

SynData synData = null;
AdditionRun additionRun = null;
Thread thread = null;

//启动10个线程做同一个任务,都对synData对象的number做+1操作
for (int i = 0; i < 10; i++) {
//每一次创建线程用到的SynData实例对象都不相同
synData = new SynData();
additionRun = new AdditionRun(synData);
thread = new Thread(additionRun);
thread.setName("Thread_" + i);
thread.start();
}
//主线程休眠1秒,以便10个线程全部执行完任务
Thread.sleep(2000);
System.out.print("main-the final result is  :" + synData.getNumber());
}
}

任务类跟上面的案例保持一样即可,不许做任何改动。
数据类:
public class SynData {

//临界变量
private static int number = 0;

public int getNumber() {
return number;
}

/**
* 对number做+1操作
*/
public static void doAddition() {
number += 1;
}
}

原型案例中将数据SynData类中的doAddition方法改成静态方法了,所以在main方法中启动线程用同一个SynData实例对象还是不同的SynData实例对象都不要紧。
案例#4:将doAddition方法用synchronized修饰,作为同步方法。
public class SynData {

//临界变量
private static int number = 0;

public int getNumber() {
return number;
}

/**
* 对number做+1操作
*/
public synchronized static void doAddition() {
number += 1;
}
}


案例#5:改为用同步代码块,修饰的对象是SynData.class,其实就是SynData的类对象
public class SynData {

//临界变量
private static int number = 0;

public int getNumber() {
return number;
}

/**
* 对number做+1操作
*/
public static void doAddition() {
synchronized (SynData.class) {
number += 1;
}
}
}

案例#4,案例#5本质是一样的,案例#4中静态方法归类对象所有,所以synchronized修饰的也是SynData的类对象。此时不论启动线程用的SynData实例对象是不是同一个,结果都不会变,都是确定值10。案例#4,案例#5完全一致。特点都是:操作变量相同,同步代码块相同,锁对象相同。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  编程 java 并发编程
相关文章推荐