在理解线程安全之前,我们先来看了解明确好以下几个概念:
基本概念
线程安全:指当多个线程并发访问某个java对象时,无论系统如何调度这些线程将如何交替操作,这个对象都能表现出一致的、正常的行为。
临界区资源: 一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程则必须等待。在并发的情况下,临界区资源是受保护的对象。
临界区代码段:每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程在进入临界区代码段之前, 必须在进入区申请资源,申请成功之后执行临界区代码段,执行万传给你之后释放资源。
竟态条件:多个线程访问同一个资源,如果对资源的访问顺序敏感。如果多个线程在临界区内执行,但是由于代码的执行序列不同而导致结果无法预测,就称为发生了竞态条件。最常见的竞态条件是先检测后执行。 执行依赖于检测结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题。
synchronized 关键字介绍
synchronized 关键字是java的保留字,synchronized 关键字保障了原子性、可见性和有序性。
在java中,线程同步使用最多的方法是使用synchronized关键字,使用synchronized(synchObject)调用相当于获取synchObject的内置锁,对临界区代码段进行排他性保护。synchronized块是java提供的一种原子性内置锁,java中的每个对象都可以把它当做一个同步锁来使用,这些java内置的使用者看不到的锁被称为内部锁,也叫做监视器锁。
synchronized 关键字拥有重入锁(自己可以再次获取自己的内部锁)的功能,即在使用synchronized 时,当一个线程得到一个对象后,再次请求此对象时是可以再次得到该对象锁的。
synchronized 关键字包括monitor enter 和 monitor exit 两个JVM 指令,它能够保证在任何时候任何线程执行到monitor enter 成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存。
synchronized 的指令严格准守java happens-before规则,一个monitor exit 指令之前必定要有一个monitor enter。
synchronized同步方法
当使用synchronized 关键字修饰一个方法的时候,该方法被称为同步方法,synchronized关键字的位置处于同步方法的返回类型之前,对临界区代码段进行保护,例如:
public class SynchronizedDemo {
private int accountBalance;
/**
* 同步方法
*/
public synchronized void methodA(){
accountBalance++;
}
}
在方法声明中设置synchronized关键字,保证其方法的代码执行流程是排他性的。任何时间只允许一个线程进入同步方法,如果其他线程需要执行同一个方法,那么只能等待和排队。
synchronized 方法的同步实质上使用了this对象锁,使用synchronized(this)同步代码块将当前类的对象作为锁,代码如下:
/**
* 同步方法
*/
public void methodA(){
//对方法内部全部代码进行保护
synchronized(this){
accountBalance++;
}
}
synchronized同步方法在字节码指令中的原理
在方法上使用synchronized关键字实现同步的原因是使用了flags标记ACC_SYNCHRONIZED,当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行先持有同步锁,然后再执行方法,最后在方法完成时释放锁,以下代码为例:
public synchronized void methodA(){
accountBalance++;
}
编译完成生成.class文件后,在IDEA的terminal中使用javap命令进行查看, -v 表示输出附加信息,-c 表示对代码进行反汇编。如下:
生成.class 文件对应的字节码指令核心代码如下:
public synchronized void methodA();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field accountBalance:Ljava/lang/Integer;
4: astore_1
5: aload_0
6: aload_0
7: getfield #3 // Field accountBalance:Ljava/lang/Integer;
10: invokevirtual #8 // Method java/lang/Integer.intValue:()I
13: iconst_1
14: iadd
15: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
18: dup_x1
19: putfield #3 // Field accountBalance:Ljava/lang/Integer;
22: astore_2
23: aload_1
24: pop
25: return
LineNumberTable:
line 20: 0
line 21: 25
LocalVariableTable:
Start Length Slot Name Signature
0 26 0 this Lcom/th/thread/SynchronizedDemo;
在反编译的字节码指令中对public synchronized void methodA()方法使用了flag 标记ACC_SYNCHRONIZED,说明此方法是同步的。
synchronized 同步块
将synchronized加在方法上,如果其保护的临界区代码段包含的临界区资源多于一个,就会造成临界区资源的闲置等待,为了提升执行效率和吞吐量,可以将synchronized关键字放在函数体内,同步一个代码块。synchronized 同步块的写法如下:
//同步块
synchronized (accountBalance){
//临界区代码段的代码块
}
synchronized后面括号中是一个syncObject对象,意思是进入临界区代码段需要获取syncObject对象的监视锁,换句话说就是将syncObject 对象监视锁作为临界区代码段的同步锁。由于每一个Java对象都有一把监视锁,因此任何java对象都能作为synchronized的同步锁。当一个线程获得syncObject对象的监视锁后,其他线程就只能等待。
synchronized 方法和synchronized同步块的联系:
(1)synchronized方法是一种粗粒度并发控制,某一个时刻只能有一个线程执行该synchronized方法。
(2)synchronized代码块是一种细粒度的并发控制,处于synchronized块之外的其他代码是可以被多个现成并发访问的。
(3)synchronized代码块比synchronized方法更加细粒度地控制了多个线程的同步访问。
(4)在java的内部实现上, synchronized方法实际上等同于用一个synchronized代码块,这个代码块包含同步方法中的所有与,然后在synchronized代码块的括号中传入this关键字,使用this对象锁作为进入临界区的同步锁。
synchronized同步块在字节码指令中的原理
如果使用synchronized同步块,则使用monitorenter 和monitorexit 治理你进行同步处理,测试代码如下:
public int availableBalance;
public Integer availableLock = new Integer(1);
/**
* 同步方法
*/
public void methodB(){
synchronized (this.availableLock){
this.accountBalance++;
}
}
使用javap -c -v xx.class 命令生产.class文件对应的字节码指令核心代码如下:
public void methodB();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=6, args_size=1
0: aload_0
1: getfield #6 // Field availableLock:Ljava/lang/Integer;
4: dup
5: astore_1
6: monitorenter
7: aload_0
8: astore_2
9: aload_2
10: getfield #3 // Field accountBalance:Ljava/lang/Integer;
13: astore_3
14: aload_2
15: aload_2
16: getfield #3 // Field accountBalance:Ljava/lang/Integer;
19: invokevirtual #10 // Method java/lang/Integer.intValue:()I
22: iconst_1
23: iadd
24: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
27: dup_x1
28: putfield #3 // Field accountBalance:Ljava/lang/Integer;
31: astore 4
33: aload_3
34: pop
35: aload_1
36: monitorexit
37: goto 47
40: astore 5
42: aload_1
43: monitorexit
44: aload 5
46: athrow
47: return
Exception table:
from to target type
7 37 40 any
40 44 40 any
LineNumberTable:
line 34: 0
line 35: 7
line 36: 35
line 41: 47
从指令核心代码中可以看到使用了monitorenter 和monitorexit指令进行了处理。
每个对象都与一个monitor相关联,一个monitor 的lock的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权会发生如下几件事:
(1) 如果monitor的计算为0,则意味着该monitor的lock 还没有被获得,某个线程获得之后将立即对该计算器加1,从此该线程就是这个monitor的所有者。
(2)如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计算器再次累加。
(3)如果monitor已经被其他线程所有者,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。
释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是已获得所有权,释放monitor所有权的过程就是将monitor 的计数器减1,如果计数器的结果为0,那就意味着该线程不再拥有对该monitor的所有权,即解锁,于此同时被该monitor block的线程将再次尝试获得对该monitor的所有权。
synchronized修饰的静态同步方法
static 修饰的静态方法属于Class实例而不是当个Object实例,在静态方法内部是不可能访问Object实例的this引用的,所以修饰static 方法的synchronized关键字就没有办法获得Object实例的this对象的监视锁。
使用synchronized关键字修饰static方法时 ,synchronized的同步锁并不是普通Object 对象的监视锁,而是类所对应的Class对象的监视锁。
当synchronized关键字修饰static方法时,同步锁为Class对象的监视锁;当synchronized关键字修饰普通的成员方法(非静态方法)时,同步锁为Class对象的监视锁。由于类的对象实例可以有很多,但是每个类只有一个Class实例,因此使用类锁作为synchronized的同步锁时会造成同一个JVM内的所有线程只能互斥地进入临界区段。
一种场景是synchronized块(代码块或者方法)正确执行完毕,监视锁自动释放;另一种场景是程序出现异常,非正常退出synchronized块,监视锁也会自动释放。所以,使用synchronized块时不必担心监视锁的释放问题。
synchronized 关键字堆栈分析
在运行以上测试代码的时候使用JConsole(%java_home%bin)工具进行监控,JConsole.exe 文件路径如下图:
连接之前,需要先运行程序,连接时需要保证程序正在运行状态,连接界面如下:
会提示不安全连接,点击“不安全连接就可以了,进入JConsole控制台,将tab切换至【线程】,如下图:
随便选中一个线程,会发现只有一个线程状态是RUNNABLE,其他线程都进入了BLOCKE状态,运行状态:
阻塞状态:
也可以使用jstack pid 的方式进行查看,查看的结果只有一个线程状态是RUNNABLE,其他线程都进入了BLOCKE状态,如下图:
从结果可看出,Thread-6 持有 <0x0000000703206b80> 的锁并且处于RUNNABLE状态,其他的线程无法进入MethodA方法, 处于BLOCKED状态,并且等待获取 <0x0000000703206b80> 锁。
synchronized 的内存语义
进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。
其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存。
synchronized除可以解决共享变量内存可见性问题外,synchronized经常被用来实现原子性操作。但是synchronized关键字会引起线程上下文切换并带来线程调度开销。