synchronized从入门到踹门
synchronized是什么
总所周知,技术是把双刃剑。并发提高了程序的执行效率,同时也带来了许多并发安全问题,而解决并发安全问题的关键,在于维护并发下的三大特性,分别是:原子性,可见性,有序性。
原子性是这篇文章要解决的关键点,而synchronized就可用来解决并发场景下的原子性问题。
仔细阅读下面程序,思考运行结果是什么?
public class Main {private static int i = 0;public static void main(String[] args) throws InterruptedException {Main main = new Main();Thread a = new Thread(() -> {main.add10K();}, "A"); // 线程AThread b = new Thread(() -> {main.add10K();}, "B"); // 线程Ba.start(); // 启动线程Ab.start(); // 启动线程Ba.join(); // 等待线程A执行完毕b.join(); // 等待线程B执行完毕System.out.println(i); // 打印i的值,期望20000}// +10000操作public void add10K(){for (int j = 0; j < 10000; j++) {i++;}}
}
答案:小于20000。
请耐心看分析,看之前也可以先思考为什么会是小于20000这个答案。
从操作系统的角度思考,一条CPU指令是具有原子性的。问题也就是出现在这,i++在操作系统层面,并不仅是一条CPU指令,而是三条。
-
读取i的值;
-
对i进行+1操作;
-
装载i的值。
注意:下面的分析默认都是基于单核(多核其实也是同个原理)
并发,听起来很高级,其实就是操作系统划分了多个很小的时间片给到每个线程,时间片的轮转小到你看起来好像是同时执行的(例如:一边跟女朋友视频,一边给男朋友发信息,其实都是时间片的切换而已,并不是同时执行的),这就是并发的概念。

上面的程序,发生的并发安全问题为:
-
当线程A执行第2步的时候(对i进行+1操作),此时操作系统将资源分给了线程B
-
线程B此时读到i的值为0(因此线程A还没将i+1写回主内存),线程B对i进行了+1,然后将i=1的值写回主内存
-
此时CPU资源来到线程A,线程A将刚才i+1的值写回主内存(i=1)
期望i的值应该为2,却由于线程的上下文切换,导致i++被分开执行,这就是并发场景下的原子性问题。
文章开头已经提到用synchronized可以解决原子性问题。那么,synchronized是如何解决原子性问题的呢?
经过分析可知,问题是出现在线程的上下文快速切换,以至于某些线程对共享变量的值进行操作,来不及及时更新到主内存造成的。
synchronized是利用管程的概念,在操作共享变量时,将共享变量锁住,等待操作完毕后,再释放。
可能这个解释会有点晦涩,且synchronized的锁操作是隐式的,需要查看Java汇编指令才可以看到真正的锁操作,下面演示一下将使用synchronized关键字的代码转换为Java汇编指令。
public class Main {public static void main(String[] args) throws InterruptedException {}public void operate(){synchronized(this){}}}
(该汇编指令通过Javap命令得到)

图中monitorenter、monitorexit、monitorexit,就是synchronized的两个隐式(锁)指令。monitorenter代表加锁,monitorexit代表解锁。
为什么monitorexit有两个呢?正常情况下一个解锁就可以了,万一程序在解锁指令前发生异常,那共享资源会被持续锁住,其他线程将永远无法得到。因此需要在程序异常时再设置一条解锁指令来应对上面的问题。
synchronized作用范围
锁非静态方法
public class Main {public static void main(String[] args) throws InterruptedException {}public synchronized void operate(){}}
该程序的锁作用于非静态方法。该方法来源某个实例,根据传递原则,锁的作用范围就是new出来的那个实例。下面有个坑,看看你是不是真的理解了这个作用于非静态方法的锁🤭。
public class Main {public static void main(String[] args) throws InterruptedException {A a = new A();B b = new B();a.addMoney(b.money);}}class A {public Integer money = 100;public synchronized void addMoney(Integer targetMoney){money += targetMoney;System.out.println(money);}}class B {public Integer money = 200;}
提问:假设在执行addMoney方法时,恰好另一个线程修改了B的money为300,那么addMoney执行的结果是什么呢?
评论区留答案,让我看看你是否真的理解了。
非静态代码块
public class Main {public static void main(String[] args) throws InterruptedException {A a = new A();B b = new B();a.addMoney(b.money);}}class A {public Integer money = 100;public void addMoney(Integer targetMoney){synchronized(this){money += targetMoney;System.out.println(money);}}}class B {public Integer money = 200;}
跟锁非静态方法其实是一样的,锁的作用范围是实例。
public class Main {public static void main(String[] args) throws InterruptedException {A a = new A();B b = new B();a.addMoney(b.money);}}class A {public Integer money = 100;public void addMoney(Integer targetMoney){synchronized(Main.class){money += targetMoney;System.out.println(money);}}}class B {public Integer money = 200;}
这个与上面程序唯一不一样的地方是锁的对象,作用于Main.class,说明锁的Main类(与该类有关的类变量和类方法都会被锁住)。
锁静态方法
public class Main {public static void main(String[] args) throws InterruptedException {A a = new A();B b = new B();a.addMoney(b.money);}}class A {public Integer money = 100;public static void addMoney(Integer targetMoney){synchronized(Main.class){}}}class B {public Integer money = 200;}
这个与锁类一样,作用的是类,解释如上。
synchronized的优化
JDK1.6版本对synchronized进行了优化,为了提升性能,将锁调整为具有可升级特性。具体升级表现为:无锁-->偏向锁-->轻量级锁-->重量级锁。
无锁
编译器会判断加了synchronized关键字的代码是否真的会发生并发安全。如果不会发生,JVM会将synchronized优化掉,变成无锁化。【如下程序,只对i进行读操作,并不会发生并发安全】
public class Main {public static void main(String[] args) {A a = new A();a.readI();}}class A {public Integer i = 100;public synchronized void readI(){System.out.println(i);}}
查看Java汇编指令,发现被优化掉synchronized这个同步锁

偏向锁
偏向锁针对的是某一时刻只有一个线程在频繁的操作,因此JVM在实例的对象头直接把偏向锁ID设置为该线程的ID。只要是你这个家伙来访问这个变量,我可以提供类似无锁一样的操作。
开启偏向锁【默认关闭偏向锁】:-XX:+UseBiasedLocking,偏向锁开启后,默认是4秒才会生效

没有等4秒直接用,没使用到偏向锁(non-biasable)
public class Main {public static void main(String[] args) throws InterruptedException {// TimeUnit.SECONDS.sleep(5);A a = new A();new Thread(()->{a.writeI();}).start();// 打印一下加锁后的实例a的对象头信息System.out.println(ClassLayout.parseInstance(a).toPrintable());}}class A {public Integer i = 100;public synchronized void writeI(){i += 1;}}

对象头的打印可使用ClassLayout工具,maven依赖如下:
<dependencies><!--查看对象头工具--><dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.9</version></dependency><dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version></dependency>
</dependencies>
等待4秒,使用偏向锁,value为偏向锁ID
public class Main {public static void main(String[] args) throws InterruptedException {TimeUnit.SECONDS.sleep(5);A a = new A();new Thread(()->{a.writeI();}).start();// 打印一下加锁后的实例a的对象头信息System.out.println(ClassLayout.parseInstance(a).toPrintable());}}class A {public Integer i = 100;public synchronized void writeI(){i += 1;}}

轻量级锁
偏向锁其实指的是一般都是某个线程进行变量操作,但是实际场景其实是有多个线程进行操作的,因此在其他线程检查操作的对象头不是自己的ID时,通过CAS尝试再次获取锁,获取不到则转变成轻量级锁,获取到了就还是偏向锁。
重量级锁
多个线程同时操作同个共享资源会升级为重量级锁。
以上则是synchronized的所有概述,欢迎共勉。