配色: 字号:
解析Java线程编程中的线程安全与synchronized的使用
2016-12-22 | 阅:  转:  |  分享 
  
解析Java线程编程中的线程安全与synchronized的使用

这篇文章主要介绍了Java线程编程中的线程安全与synchronized的使用,synchronized多线程使用时一定要注意线程之间的冲突问题,需要的朋友可以参考下

一.什么时候会出现线程安全问题?

在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会存在一个问题:

由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。

举个简单的例子:

现在有两个线程分别从网络上读取数据,然后插入一张数据库表中,要求不能插入重复的数据。

那么必然在插入数据的过程中存在两个操作:

1)检查数据库中是否存在该条数据;

2)如果存在,则不插入;如果不存在,则插入到数据库中。

假如两个线程分别用thread-1和thread-2表示,某一时刻,thread-1和thread-2都读取到了数据X,那么可能会发生这种情况:

thread-1去检查数据库中是否存在数据X,然后thread-2也接着去检查数据库中是否存在数据X。

结果两个线程检查的结果都是数据库中不存在数据X,那么两个线程都分别将数据X插入数据库表当中。

这个就是线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。

这里面,这个资源被称为:临界资源(也有称为共享资源)。

也就是说,当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程安全问题。

不过,当多个线程执行一个方法,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。

二.如何解决线程安全问题?

那么一般来说,是如何解决线程安全问题的呢?

基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。

通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。

在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。

本文主要讲述synchronized的使用方法,Lock的使用方法在下一篇博文中讲述。

三.synchronized同步方法或者同步块

在了解synchronized关键字的使用方法之前,我们先来看一个概念:互斥锁,顾名思义:能到达到互斥访问目的的锁。

举个简单的例子:如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。

在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。

在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

下面通过几个简单的例子来说明synchronized关键字的使用:

1.synchronized方法

下面这段代码中两个线程分别调用insertData对象插入数据:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30 publicclassTest{

publicstaticvoidmain(String[]args){

finalInsertDatainsertData=newInsertData();

newThread(){

publicvoidrun(){

insertData.insert(Thread.currentThread());

};

}.start();

newThread(){

publicvoidrun(){

insertData.insert(Thread.currentThread());

};

}.start();

}

}

classInsertData{

privateArrayListarrayList=newArrayList();

publicvoidinsert(Threadthread){

for(inti=0;i<5;i++){

System.out.println(thread.getName()+"在插入数据"+i);

arrayList.add(i);

}

}

} 此时程序的输出结果为:



说明两个线程在同时执行insert方法。

而如果在insert方法前面加上关键字synchronized的话,运行结果为:

1

2

3

4

5

6

7

8

9

10 classInsertData{

privateArrayListarrayList=newArrayList();

publicsynchronizedvoidinsert(Threadthread){

for(inti=0;i<5;i++){

System.out.println(thread.getName()+"在插入数据"+i);

arrayList.add(i);

}

}

} 1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26 classInsertData{

privateArrayListarrayList=newArrayList();

publicvoidinsert(Threadthread){

synchronized(this){

for(inti=0;i<100;i++){

System.out.println(thread.getName()+"在插入数据"+i);

arrayList.add(i);

}

}

}

}

classInsertData{

privateArrayListarrayList=newArrayList();

privateObjectobject=newObject();

publicvoidinsert(Threadthread){

synchronized(object){

for(inti=0;i<100;i++){

System.out.println(thread.getName()+"在插入数据"+i);

arrayList.add(i);

}

}

}

} 从上面可以看出,synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步。

另外,每个类也会有一个锁,它可以用来控制对static数据成员的并发访问。

并且如果一个线程执行一个对象的非staticsynchronized方法,另外一个线程需要执行这个对象所属类的staticsynchronized方法,此时不会发生互斥现象,因为访问staticsynchronized方法占用的是类锁,而访问非staticsynchronized方法占用的是对象锁,所以不存在互斥现象。

看下面这段代码就明白了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35 publicclassTest{

publicstaticvoidmain(String[]args){

finalInsertDatainsertData=newInsertData();

newThread(){

@Override

publicvoidrun(){

insertData.insert();

}

}www.visa158.com.start();

newThread(){

@Override

publicvoidrun(){

insertData.insert1();

}

}.start();

}

}

classInsertData{

publicsynchronizedvoidinsert(){

System.out.println("执行insert");

try{

Thread.sleep(5000);

}catch(InterruptedExceptione){

e.printStackTrace();

}

System.out.println("执行insert完毕");

}

publicsynchronizedstaticvoidinsert1(){

System.out.println("执行insert1");

System.out.println("执行insert1完毕");

}

}

第一个线程里面执行的是insert方法,不会导致第二个线程执行insert1方法发生阻塞现象。

下面我们看一下synchronized关键字到底做了什么事情,我们来反编译它的字节码看一下,下面这段代码反编译后的字节码为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17 publicclassInsertData{

privateObjectobject=newObject();

publicvoidinsert(Threadthread){

synchronized(object){

}

}

publicsynchronizedvoidinsert1(Threadthread){

}

publicvoidinsert2(Threadthread){

}

}

从反编译获得的字节码可以看出,synchronized代码块实际上多了monitorenter和monitorexit两条指令。monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个线程对临界资源的访问。对于synchronized方法,执行中的线程识别该方法的method_info结构是否有ACC_SYNCHRONIZED标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

有一点要注意:对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

三.关于synchronized的其他一些值得注意的地方

1.synchronized与staticsynchronized的区别synchronized是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,注意这里是“类的当前实例”,类的两个不同实例就没有这种约束了。那么staticsynchronized恰好就是要控制类的所有实例的访问了,staticwww.hunanwang.netsynchronized是限制线程同时访问jvm中该类的所有实例同时访问对应的代码快。实际上,在类中某方法或某代码块中有synchronized,那么在生成一个该类实例后,该类也就有一个监视快,放置线程并发访问改实例synchronized保护快,而staticsynchronized则是所有该类的实例公用一个监视快了,也就是两个的区别了,也就是synchronized相当于this.synchronized,而staticsynchronized相当于Something.synchronized.一个日本作者-结成浩的《java多线程设计模式》有这样的一个列子:

1

2

3

4

5

6 pulbicclassSomething(){

publicsynchronizedvoidisSyncA(){}

publicsynchronizedvoidisSyncB(){}

publicstaticsynchronizedvoidcSyncA(){}

publicstaticsynchronizedvoidcSyncB(){}

}





















献花(0)
+1
(本文系白狐一梦首藏)