特性
Sync直接保证了多线程中的:
- 原子性
直接将数据上锁,同一时间只会有一个线程可以拿到锁,因此是将临界区的数据独占,将数据上的并行处理,在加锁的节点上变成了单线程的处理逻辑,因此也保证了多线程中的:
- 顺序性(后一个必须等到前一个释放锁,但内部可能发生重排序,这里的顺序性是代码块内外的顺序性)
- 可见性(其他的线程都不能读写加锁对象,因此也确保了线程持有副本的数据不会与主存中的不一致)
同时,Sync的锁特性为:
- 可重入性(见可重入锁)
- 不可中断性(线程不释放锁时不会被中断)
synchronized 在退出的时候,能保证 synchronized 块中对于共享变量的写入一定会刷入到主内存中。
synchronized 保证了释放监视器锁之前的代码一定会在释放锁之前被执行(如 temp 的初始化一定会在释放锁之前执行完 ),但是没有任何规则规定了,释放锁之后的代码不可以在释放锁之前先执行。
此处需要回顾一下JMM中对于sync的规定:
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
使用方法
Synchronized可以用于修饰:
- 变量
- 方法
- 代码块
使用目的
先看一个基础的:
public class SynTest{
private static int i = 0;
public void start() {
for(int m=0 ;m < 1000 ;m++,i++);
}
@Override
public void run() {
start();
}
//...........
public static void main(String[] args){
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new SynTest());
t.start();
}
Thread.sleep(3000);
System.out.println(i);
}
}
很经典的example,结果大概率小于10000。使用Synchronized将资源上锁。
正确使用方法
使用以下几种方式能达到结果正确:
-
private static final Object mutex = new Object(); public void start() { for(int m=0 ;m < 1000 ;m++){ synchronized (mutex) {i++;} } }加一个静态final对象并加锁。
-
对start方法进行方法声明的修改:
public static synchronized void start() { for(int m=0 ;m < 1000 ;m++){ i++; } }
错误使用方法
-
对非final的static对象加锁,此时锁的只是该对象实例:
-
对非静态方法使用synchronized关键字、
-
死锁
-
死锁四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
-
总结
synchronized可以在三个地方声明:
- 方法:
public synchronized void cr() - 变量:
synchronized (mutex){} - 代码块(同上)
原理
写了个示例类
public class SynTest3 {
public static synchronized void test(){
synchronized (new Object()){}
}
}
调用命令 javac -p 编译后,通过javap -v 查看编译后的文件内容如下:
public class threadReview.SynTest3
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#17 // java/lang/Object."<init>":()V
#2 = Class #18 // java/lang/Object
#3 = Class #19 // threadReview/SynTest3
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 LthreadReview/SynTest3;
#11 = Utf8 test
#12 = Utf8 StackMapTable
#13 = Class #18 // java/lang/Object
#14 = Class #20 // java/lang/Throwable
#15 = Utf8 SourceFile
#16 = Utf8 SynTest3.java
#17 = NameAndType #4:#5 // "<init>":()V
#18 = Utf8 java/lang/Object
#19 = Utf8 threadReview/SynTest3
#20 = Utf8 java/lang/Throwable
{
public threadReview.SynTest3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LthreadReview/SynTest3;
public static synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=2, args_size=0
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: dup
8: astore_0
9: monitorenter
10: aload_0
11: monitorexit
12: goto 20
15: astore_1
16: aload_0
17: monitorexit
18: aload_1
19: athrow
20: return
Exception table:
from to target type
10 12 15 any
15 18 15 any
LineNumberTable:
line 5: 0
line 6: 20
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 15
locals = [ class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "SynTest3.java"
代码块:
- 进入一个方法时,一旦执行到了monitorenter,那么直到方法的最后一个monitorexit之前,其他线程都无法获取到这个对象的使用权。
- 事实上如果在源代码里多加几个内部的
synchronized,会出现多个monitorenter连续出现后才出现第一个moniotrexit,这里就是可重入性的由来。
方法:
不知道大家注意到方法那的一个特殊标志位没,ACC_SYNCHRONIZED。
同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。
所以归根究底,还是monitor对象的争夺。
Monitor是个什么东西?
我说了这么多次这个对象,大家是不是以为就是个虚无的东西,其实不是,monitor监视器源码是C++写的,在虚拟机的ObjectMonitor.hpp文件中。
我看了下源码,他的数据结构长这样:
ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; // 线程重入次数 _object = NULL; // 存储Monitor对象 _owner = NULL; // 持有当前线程的owner _WaitSet = NULL; // wait状态的线程列表 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; // 单向列表 FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁状态block状态的线程列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }这块c++代码,我也放到了我的开源项目了,大家自行查看。
synchronized底层的源码就是引入了ObjectMonitor,我上面说的,还有大家经常听到的概念,在这里都能找到源码。
大家说熟悉的锁升级过程,其实就是在源码里面,调用了不同的实现去获取获取锁,失败就调用更高级的实现,最后升级完成。
锁实现原理
重量级锁就都在上面了,sync的锁升级有几个步骤(不可逆):
偏向锁->轻量级锁-> 重量级锁
偏向锁
像是更广义上的可重入性。
根据上面的Monitor,以及字节码中的monitorenter/exit,可知Synchronized事实上争夺的锁就是monitor对象。
锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。
这个过程是采用了CAS乐观锁操作的,每次同一线程进入,虚拟机就不进行任何同步的操作了,对标志位+1就好了,不同线程过来,CAS会失败,也就意味着获取锁失败。
轻量级锁
偏向锁关闭或多个线程同时竞争偏向锁时,会升级为轻量级锁。
如果这个对象是无锁的,jvm就会在当前线程的栈帧中建立一个叫锁记录(Lock Record)的空间,用来存储锁对象的Mark Word 拷贝,然后把Lock Record中的owner指向当前对象。
JVM接下来会利用CAS尝试把对象原本的Mark Word 更新到Lock Record的指针,成功就说明加锁成功,改变锁标志位,执行相关同步操作。
如果失败了,就会判断当前对象的Mark Word是否指向了当前线程的栈帧,是则表示当前的线程已经持有了这个对象的锁,否则说明被其他线程持有了,继续锁升级,修改锁的状态,之后等待的线程也阻塞。
参考
volatile
简单地说,volatile是JVM对于MESI规则的一个补充,并且是遵循JMM中happens-before规范的一个实现。
相比于sync的重量级并有三特性的保证(可见性,原子性,顺序性),volatile可以说是一个部分的sync:
- volatile的性能要比sync好一些
- volatile只能保证可见性和顺序性以及特殊情况下的原子性。
必要性
结合MESI协议,我们知道:
- 由于多级缓存、多CPU的存在,实际上数据的变更并不是立即写入到内存中的,甚至在缓存中都不一定能够保证是最新的。
在没有关键字介入的情况下,JMM仅保证了数据的声明在使用之前这一顺序性,那么当需要数据的M状态是实时通知的条件下,可能会出现预期之外的结果。
那么这样子就需要其他东西来保证这一条件了。
结合volatile在编码中的实际表现以及上期MESI的相关内容,其实volatile使用了内存屏障来实现相关功能。
- 写屏障 Store Memory Barrier(a.k.a. ST, SMB, smp_wmb)是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。
- 读屏障Load Memory Barrier (a.k.a. LD, RMB, smp_rmb)是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。
操作实例:单例DCL
DCL:double check lock
单例分为:饿汉与懒汉方式。
饿汉方式是预先创建的方式,因此不存在线程相关的问题。
懒汉方式,是调用时再判断,不存在则新建,这里就会出现问题:
- 如果多个同时进入调用,可能会导致多次创建。
那么就需要使用同步块。每一个特殊机制都在代码块里加上了注释。
public class DCLDemo {
//使用volatile,见【2】
private static volatile DCLDemo instance;
private DCLDemo() {
}
public static DCLDemo getInstance(){
if(instance == null){
//【1】sync:加锁防止其他地方的修改。注意:这里锁的是这个类
//【2】这里就是为什么要加上volatile:
//对象的构建分为3步:
//1.分配内存2.内存地址上初始化数据3.内存地址赋予变量
//如果没有volatile,那么这里可能发生重排序,先赋予变量再初始化数据
//此时内存地址有了,对象不为null,但数据为null;如果这里其他方法进入了,
//那么上面instance!=null了,就返回了这个半初始化的数据,就出错了
//因此这里需要加上volatile,防止数据assign先于init,保证不会返回这种数据
synchronized (DCLDemo.class){
if(instance == null){
instance = new DCLDemo();
}
}
}
return instance;
}
}