转载自 公众号 [彤哥说源码]
1 synchronized关键字
synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成 monitorenter 和 monitorexit 字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。
1.1 原理
两个指令:lock和unlock
lock:锁定,作用于主内存的变量,它把主内存中的变量标识为一条线程独占状态;
unlock:解锁,作用于主内存的变量,它把锁定的变量释放出来,释放出来的变量才可以被其它线程锁定。
高层次的monitorenter和monitorexit实际上使用的是lock和unlock指令。
根据JVM规范的要求,执行monitorenter指令的时候,首先去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,就把锁的计数器加1,相应地,在执行monitorexit的时候会把计数器减1,当计数器减小为0时,锁就释放了。
所以,基于上可以看出synchronized是可重入锁。
synchronized编译后的字节码:
public class SynchronizedTest {
public static void sync() {
synchronized (SynchronizedTest.class) {
synchronized (SynchronizedTest.class) {
}
}
}
public static void main(String[] args) {
}
}
上面的代码将SynchronizedTest.class对象加了两次synchronized
编译后的sync()方法的字节码指令如下:
字节码比较简单,我们的synchronized锁定的是SynchronizedTest类对象,可以看到它从常量池中加载了两次SynchronizedTest类对象,分别存储在本地变量0和本地变量1中,解锁的时候正好是相反的顺序,先解锁变量1,再解锁变量0,实际上变量0和变量1指向的是同一个对象,所以synchronized是可重入的。
1.2 原子性,可见性,有序性
分析下synchronized如何保证这3个特性呢?
lock和unlock在java内存模型中必须满足下面四条规则:
(1)原子性,可重入:一个变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一个线程执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才能被解锁。
(2)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
(3)如果一个变量没有被lock操作锁定,则不允许对其执行unlock操作,也不允许unlock一个其它线程锁定的变量;
(4)对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作;
可见性:通过1,2,4,每次lock和unlock都会从主内存加载变量或把变量刷新回主内存,而lock和unlock之间的变量(这里是指锁定的变量)是不会被其它线程修改的,所以,synchronized是具有可见性的。
有序性:通过规则1,3,我们知道所有对变量的加锁都要排队进行,且其它线程不允许解锁当前线程锁定的对象,所以,synchronized是具有有序性的。
总结:synchronized是可以保证原子性、可见性和有序性的
1.3 公平锁和非公平锁
公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得。
非公平锁:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待
非公平锁比公平锁性能高5-10倍,因为公平锁需要在多核情况下维护一个队列,如果当前线程不是队列的第一个无法获取锁,增加了线程切换次数
public class SyncTest {
private static void sync(String tips){
synchronized (SyncTest.class){
System.out.println(tips);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(()->sync("thread1")).start();
Thread.sleep(100);
new Thread(()->sync("thread2")).start();
Thread.sleep(100);
new Thread(()->sync("thread3")).start();
Thread.sleep(100);
new Thread(()->sync("thread4")).start();
Thread.sleep(100);
}
}
结果不是按照线程1,2,3,4排序,因此synchronized是非公平锁。
1.4 锁优化那么,synchronized有哪些进化中的状态呢?
我们这里稍做一些简单地介绍:
(1)偏向锁,是指一段同步代码一直被一个线程访问,那么这个线程会自动获取锁,降低获取锁的代价。
(2)轻量级锁,是指当锁是偏向锁时,被另一个线程所访问,偏向锁会升级为轻量级锁,这个线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能。
(3)重量级锁,是指当锁是轻量级锁时,当自旋的线程自旋了一定的次数后,还没有获取到锁,就会进入阻塞状态,该锁升级为重量级锁,重量级锁会使其他线程阻塞,性能降低。
总结:
(1)synchronized在编译时会在同步块前后生成monitorenter和monitorexit字节码指令;
(2)monitorenter和monitorexit字节码指令需要一个引用类型的参数,基本类型不可以哦;
(3)monitorenter和monitorexit字节码指令更底层是使用Java内存模型的lock和unlock指令;
(4)synchronized是可重入锁;
(5)synchronized是非公平锁;
(6)synchronized可以同时保证原子性、可见性、有序性;
(7)synchronized有三种状态:偏向锁、轻量级锁、重量级锁;
(8)synchronized是悲观锁;
如何使用synchronized:
见线程安全性-原子性
2 Lock接口
在jdk1.5以后,增加了juc并发包且提供了Lock接口用来实现锁的功能,它除了提供了与synchroinzed关键字类似的同步功能,还提供了比synchronized更灵活api实现。