synchronized是什么
synchronized是Java关键字,为了维护高并发是出现的原子性问题。技术是把双刃剑,多线程并发给我带来了前所未有的速率,然而在享受快速编程的过程,也给我们带来了原子性问题。
如下:
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"); // 线程A
Thread b = new Thread(() -> {
main.add10K();
}, "B"); // 线程B
a.start(); // 启动线程A
b.start(); // 启动线程B
a.join(); // 等待线程A执行完毕
b.join(); // 等待线程B执行完毕
System.out.println(i); // 打印i的值,期望20000
}
// +10000操作
public void add10K(){
for (int j = 0; j < 10000; j++) {
i++;
}
}
}
上面的程序,你细细品味一下结果会是多少?然后再回来看下面的结果。或者自己编程一下上面的代码,然后带着思考运行一下(面试高频点)。
下面的分析请耐心看,并思考。这就是面试要跟面试官聊的东西,聊越多,聊越细,证明你思考得越多。
答案是小于20000,其实了解过JVM的同学都知道,i++在CPU中其实不是一条CPU指令,而是三条。
读取i的值;
对i进行+1操作;
装载i的值。
那么多线程并发,其实就是每个线程分配一个时间片执行,时间片执行完毕后就轮到下一个线程。在上面的程序,可能会发生的事情:当线程A做到第2步的时候(对i进行+1操作),可能时间片得分给线程B了,此时线程A和线程B假设都读到i的值为0,这时线程B对i进行了+1操作后i的值为1,然后轮到线程A执行,线程A此时到了第三步,把刚才i+1的值装载回去(i=1)。问题就在这了,期望两个线程对i都+1后,期望值应该为2,然而此时却为1。这种情况还不少见,所以导致最终的结果小于期望值20000。
那么怎么解决这个问题呢?通过上面得分析我们知道,就是操作系统搞着时间片轮转运行造成的,不要轮转不就行了,确实可以。但是如果这么做了,又回到单线程时代,况且现在已经不是单核时代了,每个人得电脑至少双核起步吧,所以思路是对的,但是现实场景是骨感的。那么有没有一个可能,就是在线程A对i进行+1操作的时候,我把i这个参数给他锁住,先不要让别的线程操作它呢?这就对了,现在的synchronized、Lock就是这个思想,在操作某个变量时,我先在这个变量前面加个"栅栏"(也可以理解成锁),只有当我撤了这个栅栏(或者撤了这把锁),其他人才可以对这个变量进行操作,这不就没什么问题了。
synchronized其实就是利用这个原理做的这个关键字,但是它是隐式的,没有展现出来,但是其实在底层的"汇编指令",它其实是有展现的,带你们看一下。
这是Java代码
public class Main {
public static void main(String[] args) throws InterruptedException {
}
public void operate(){
synchronized(this){
}
}
}
这是"汇编指令",JVM自己约定的汇编指令,所以我加了双引号。(这个是通过:Javap -c Main.class指令得到的,大家有兴趣可以试试!)
可以看到上图,我画圈圈的东西,monitorenter、monitorexit、monitorexit,这其实就是synchronized的两个隐式"锁"指令了,monitorenter代表加锁,monitorexit代表解锁。为什么monitorexit有两个呢?原因其实也很简单,为了预防死锁用的,因为我们正常情况下当然是一个解锁就可以了,万一没运行到解锁那一行,程序挂了呢?那此时是不是在异常时设置一条解锁会好点?所以两个monitorexit是有道理的!
synchronized作用范围
锁非静态方法
public class Main {
public static void main(String[] args) throws InterruptedException {
}
public synchronized void operate(){
}
}
像上面的程序,锁的就是方法,这个方法是来源某个实例的,所以根据传递原则,其实锁的就是你new出来的那个实例,应该很好理解。下面来个例子,带你走走坑。
这个程序代码务必认真看,比你看100篇synchronized讲解有用!因为很多都是走马观花,没有落实到具体实践讲解,只让你知道锁的是实例,而实际场景中遇到的坑,你可能自己都理所当然,不知所以。
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执行的结果是什么呢?
思考一下,可以评论区说一下答案+理解,这个真的很重要。这是synchronized最关键的点了,我先设个坑,评论区回答认真看的,因为真的很重要、很重要、很重要。
非静态代码块
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,说明锁的是对象,那么有关该对象的变量和方法都会被锁住哦,其他形成访问该类的东西时,都会阻塞,等待该线程释放锁。
锁静态方法
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之后,JDK有对synchronized关键字进行了优化,主要是做了一些锁升级的过程:无锁--偏向锁--轻量级锁--重量级锁。
好好读下文,这个也很重要,不懂评论区留言,看到必回!
反向思考一下,加这个synchronized是为了干嘛?不就是为了当某个线程操作某个变量的时候,不然其他线程操作该变量吗?那就是阻塞咯。这个阻塞其实也就是我们上面一直讲解的重量级锁,确实一开始就是这样(JDK1.6之前)。那这很损耗性能的耶,所以搞JDK那群家伙就开始想办法优化这些思想了,我把synchronized做成一个动态化锁。
无锁
如果程序不会造成线程安全的,那我把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);
}
}
看似有锁,其实我们从"汇编指令"看,已经被JDK偷偷优化成无锁了。
偏向锁
但是实际场景可不是一直读哦,也会有某个线程一直在那里频繁的写写写,但是也无所谓啦,以为就你这个线程是把,那我就在实例对象头那里,直接把偏向锁ID,设置成你这个线程ID就可以了,只要是你这个家伙来访问这个变量,我直接也把锁优化掉。
Idea开启偏向锁VM参数:-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的所有概述,欢迎共勉。