Java并发编程—让你彻底搞清楚Synchronized关键字

前言

Hello,大家好!我是Leo!

你是否曾经遇到过这样的情况:在多线程环境下,你的代码运行得像个疯子一样,输出的结果让你感到困惑不解?如果是这样,那么恭喜你,你已经体验到了数据竞争和线程不安全的痛苦。但是别担心,Java的Synchronized关键字可以帮助你摆脱这些问题,就像一位超级英雄一样,保护你的代码免受恶意线程的攻击。在本文中,我们将带你进入Synchronized关键字的世界,让你感受到线程同步的魔力!好了废话不多说,咱们开始吧!

synchronized关键字


synchronized关键字的使用

  • synchronized可以用来修饰方法或者代码块;可以把任意一个非NULL的对象当成是一把锁。它属于悲观锁,也属于可重入锁。
  • 一把锁只能同时被一个线程获取,没有获取到锁的线程只能等待。
  • 每个实例都对应的有自己的一把锁 ,不同实例之间互不影响。除了:锁对象是 .class以及synchronized修饰的静态方法的时候,所有对象公用同一把锁.
  • synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁。

synchronized的对象锁和类锁

  • 修饰方法
    • 普通方法(非静态方法):锁定的是方法的调用对象
    • 静态方法:锁定的是类
  • 修饰代码块:锁定的是传参的对象 注意: 判断Synchronized到底锁的资源是什么?如果锁的是相同资源,就是同步的;否则就是不同步的。

对象锁

指的是方法锁(默认锁对象为this<当前实例对象>)和同步代码块锁(自己指定锁对象)

代码块

手动指定锁定对象,this或者自定义的锁

this:

java 复制代码
public class SynchronizedTest implements Runnable{

    @Override
    public void run() {
        // 同步代码块形式------锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行
        synchronized (this) {
            System.out.println("当前线程: " + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 运行结束");
        }
    }

    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        Thread thread1 = new Thread(synchronizedTest);
        Thread thread2 = new Thread(synchronizedTest);
        thread1.start();
        thread2.start();
    }
}

运行结果:

java 复制代码
当前线程: Thread-0
Thread-0 运行结束
当前线程: Thread-1
Thread-1 运行结束

自定义的锁(可以是Object,也可以是任意指定对象)

java 复制代码
public class SynchronizedTest implements Runnable{

    // 创建两把锁
    Object obj1 = new Object();
    Object obj2 = new Object();

    @Override
    public void run() {
        // 当前代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此马上执行
        synchronized (obj1) {
            System.out.println("obj1锁线程: " + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("obj1锁线程" + Thread.currentThread().getName() + " 运行结束");
        }

        synchronized (obj2) {
            System.out.println("obj2锁线程: " + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("obj2锁线程" + Thread.currentThread().getName() + " 运行结束");
        }
    }

    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        Thread thread1 = new Thread(synchronizedTest);
        Thread thread2 = new Thread(synchronizedTest);
        thread1.start();
        thread2.start();
    }

运行结果:

java 复制代码
obj1锁线程: Thread-0
obj1锁线程Thread-0 运行结束
obj1锁线程: Thread-1
obj2锁线程: Thread-0
obj1锁线程Thread-1 运行结束
obj2锁线程Thread-0 运行结束
obj2锁线程: Thread-1
obj2锁线程Thread-1 运行结束
普通方法

synchronized修饰普通方法,锁对象默认为this。

java 复制代码
public class SynchronizedTest implements Runnable{

    @Override
    public void run() {
        a();
    }

    public synchronized void a(){
        System.out.println("当前线程: " + Thread.currentThread().getName());
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("当前线程" + Thread.currentThread().getName() + " 运行结束");
    }

    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        Thread thread1 = new Thread(synchronizedTest);
        Thread thread2 = new Thread(synchronizedTest);
        thread1.start();
        thread2.start();
    }
}

运行结果:

java 复制代码
当前线程: Thread-0
当前线程Thread-0 运行结束
当前线程: Thread-1
当前线程Thread-1 运行结束

类锁

类锁指的事 synchronized 修饰静态的方法或指定锁对象为Class对象或者 synchronized 修饰静态方法。

Synchronized修饰静态方法

java 复制代码
public class SynchronizedTest implements Runnable{

    @Override
    public void run() {
        a();
    }

    public static synchronized void a(){
        // synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,只需要一把锁。
        System.out.println("当前线程: " + Thread.currentThread().getName());
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("当前线程" + Thread.currentThread().getName() + " 运行结束");
    }

    public static void main(String[] args) {
        SynchronizedTest synchronizedTest1 = new SynchronizedTest();
        SynchronizedTest synchronizedTest2 = new SynchronizedTest();
        Thread thread1 = new Thread(synchronizedTest1);
        Thread thread2 = new Thread(synchronizedTest2);
        thread1.start();
        thread2.start();
    }
}

运行结果:

java 复制代码
当前线程: Thread-0
当前线程Thread-0 运行结束
当前线程: Thread-1
当前线程Thread-1 运行结束
synchronized指定锁对象为Class对象
java 复制代码
public class SynchronizedTest implements Runnable{

    @Override
    public void run() {
        synchronized (SynchronizedTest.class){
            // 所有线程需要同一把锁
            System.out.println("当前线程: " + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("当前线程" + Thread.currentThread().getName() + " 运行结束");
        }
    }

    public static void main(String[] args) {
        SynchronizedTest synchronizedTest1 = new SynchronizedTest();
        SynchronizedTest synchronizedTest2 = new SynchronizedTest();
        Thread thread1 = new Thread(synchronizedTest1);
        Thread thread2 = new Thread(synchronizedTest2);
        thread1.start();
        thread2.start();
    }
}

运行结果:

java 复制代码
当前线程: Thread-0
当前线程Thread-0 运行结束
当前线程: Thread-1
当前线程Thread-1 运行结束

Synchronized原理

加锁和释放锁的原理

深入JVM查看字节码,看Synchonized 在 JVM 里的实现原理。

首先创建一个Synchronized同步代码块,如下:

java 复制代码
public class SynchronizedTest implements Runnable{

    @Override
    public void run() {
        synchronized (SynchronizedTest.class){
            System.out.println("当前线程: " + Thread.currentThread().getName());
            System.out.println("当前线程" + Thread.currentThread().getName() + " 运行结束");
        }
    }
}

通过javac命令进行编译生成 .Class 文件:

sh 复制代码
javac SynchronizedTest.java

通过 javap 命令反编译生成 .Class 文件的信息:

sh 复制代码
javap -verbose SynchronizedTest.class

生成的文件信息如下:

monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处,JVM 要保证每个 monitorenter 必须有对应的monitorexit 与之配对。monitorenter和monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。任何对象在同一时间只能一个monitor相关联;当且一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:

  1. monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待。
  2. 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加。
  3. 这把锁已经被别的线程获取了,等待锁释放著作权归

monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是将monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

由上图看出来:任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态为阻塞会进入同步队列中,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

可重入锁原理

什么是可重入锁?

可重入锁,也叫做递归锁,是指在一个线程中可以多次获取同一把锁,比如:一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法(即可重入),而无需重新获得锁。

请看如下举例:

java 复制代码
public class SynchronizedTest {

    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        synchronizedTest.method1();
    }

    private synchronized void method1() {
        System.out.println("当前执行 method1方法...");
        method2();
    }

    private synchronized void method2() {
        System.out.println("当前执行 method2方法...");
        method3();
    }

    private synchronized void method3() {
        System.out.println("当前执行 method3方法...");
    }
}

结合Synchronized中的加锁和释放锁原理:

  1. 首先执行monitorenter获取锁
  • 当前monitor计数器=0,可获取锁
  • 执行method1()方法,monitor计数器+1 <当前计数:1> (获取到锁)
  • 执行method2()方法,monitor计数器+1 <当前计数:2>
  • 执行method3()方法,monitor计数器+1 <当前计数:3>
  1. 执行monitorexit命令
  • method3()方法执行完,monitor计数器-1 <当前计数:2>
  • method2()方法执行完,monitor计数器-1 <当前计数:1>
  • method2()方法执行完,monitor计数器-1 <当前计数:0> (释放了锁)
  • monitor计数器=0,锁被释放了

Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会 +1,释放锁后就会将计数器 -1。

Synchronized锁存储

Java对象头

synchronized 用的锁是存在 Java 对象头里的,在JVM中, 对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充

  1. 对象头:Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位。32位 JVM 的 Mark Word 的默认存储结构;
  2. 实例数据:存放类的属性数据信息,包括父类的属性信息;
  3. 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

Synchronized锁升级

聊到锁升级,接下来咱们就重点聊聊对象头中的Mark Word。 Mark Word:指的是【运行时数据】主要来代表Java对象的县城说状态以及GC标志位。线程锁状态分别为:无锁、偏向锁、轻量级锁、重量级锁。

JDK1.6之前,内置锁都是重量级锁,所以效率比较低下,具体表现在以下几个方面:

  1. 竞争激烈的性能问题:在高并发环境下,Synchronized锁的性能会受到严重影响,应为多个线程会抢占同一把锁,导致大量线程进入阻塞状态,从而降低执行效率。
  2. 重量级锁的问题:在1.6之前版本中,Synchronized是一种重量级锁,这就意味着每次获取锁和释放锁都需要进行额外的操作,这些操作会增加系统的延迟和负担。
  3. 不可中断性:在1.6之前版本中,Synchronized锁是不可以中断的,如果一个线程获取了锁并进入了阻塞阶段,其他的线程只能等待当前线程释放锁,无法中断他的执行。

因此,在Java 1.6及之前的版本中,Synchronized锁在高并发环境下的性能表现不佳,容易出现死锁、饥饿等问题。为了解决这些问题,Java引入了更轻量级的锁实现,如基于CAS算法的乐观锁和基于自旋的偏向锁、轻量级锁等。

JDK1.6之后 为了提高Synchronized的效率,引入了偏向锁和轻量级锁。 随着锁竞争逐渐激烈,其状态会按照无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁这个方向逐渐升级,并且升级是不可逆的,只可以进行锁升级,不可以进行锁降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

了解完Synchronized在1.6之前和之后锁的区别,接下来我们一块来看看Mark Word的四种不同的锁状态,探究其内部结构。(64位)

  1. 无锁 无锁就是不使用 synchronized 关键字,无锁的锁标志位为 01。
  1. 偏向锁

什么是偏向锁? 如果一个线程获得了锁,那么在未释放锁之前,该线程下一次获取锁时可以直接获得锁,而不需要重新竞争。原因是线程不会主动释放锁对象头中存储了锁偏向的线程ID,当尝试获取锁的线程ID与锁对象的对象头Mark Word中存储的偏向线程ID一致时,就认为该线程已持有锁,直接进入同步代码块。这种优化技术可以减少锁竞争带来的性能损失,从而提高程序的并发性能。

看到这里,你会发现无锁、偏向锁的 lock 标志位是一样的,即都是 01,这是因为无锁、偏向锁是靠字段 biased_lock 来区分的,0 代表没有使用偏向锁,1 代表启用了偏向锁。

  1. 轻量级锁

当有第二个线程参与竞争,就会立即膨胀为轻量级锁。企图抢占的线程一开始会使用自旋的方式尝试获取锁。JDK 1.7 之前是普通自旋,会设定一个最大的自旋次数,默认是 10 次,超过这个阈值就停止自旋。JDK 1.7 之后,引入了适应性自旋。简单来说就是:这次自旋获取到锁了,自旋的次数就会增加;这次自旋没拿到锁,自旋的次数就会减少。

  1. 重量级锁

上面提到,试图抢占的线程自旋达到阈值,就会停止自旋,那么此时锁就会膨胀成重量级锁。当其膨胀成重量级锁后,其他竞争的线程进来就不会自旋了,而是直接阻塞等待,并且 Mark Word 中的内容会变成一个监视器(monitor)对象,用来统一管理排队的线程。

小结一下

synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差;

synchronized就在JDK1.6做了领升级的优化

  • 无锁 : 当前对象没有作为锁存在

  • 偏向债 : 如果当前锁资源,只有一个线理在频繁的获取和释放,那么这个线理过来,只需要判断,当前指向的线程是否是当前线程。

    • 如果是,直接拿着锁资液走。
    • 如果当前线程不是,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。 (偏向锁状态出现了锁竞争的情况)。
  • 轻量级锁: 会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)

    • 如果成功获取到,拿着锁资源走。
    • 如果自旋了一定次数,没拿到锁资源,锁升级。
  • 重量级锁: 就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。

参考文章:

《Java并发编程艺术》《Java并发编程艺术实战》

相关推荐
代码小鑫3 分钟前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
落落落sss37 分钟前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
我救我自己37 分钟前
UE5运行时创建slate窗口
java·服务器·ue5
2401_853275731 小时前
ArrayList 源码分析
java·开发语言
爪哇学长1 小时前
SQL 注入详解:原理、危害与防范措施
xml·java·数据库·sql·oracle
MoFe11 小时前
【.net core】【sqlsugar】字符串拼接+内容去重
java·开发语言·.netcore
_江南一点雨1 小时前
SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
java·spring boot·后端
转转技术团队2 小时前
空间换时间-将查询数据性能提升100倍的计数系统实践
java·后端·架构
深情废杨杨2 小时前
后端-实现excel的导出功能(超详细讲解)
java·spring boot·excel
智汇探长2 小时前
EasyExcel自定义设置Excel表格宽高
java·excel·easyexcel