[JUC]Synchronized的使用和1.6优化

Synchronized关键字

概述

​ Synchronized作为Java中的一个关键字,被称为“同步锁”。顾名思义,它是一种锁,当某一时刻有多个线程对同一段程序进行操作时,使得只有一个线程能够获取到资源,保证了线程安全。

​ 在Java中,synchronized锁可能是我们最早接触的锁了,在 JDK1.5之前synchronized是一个重量级锁,相对于juc包中的Lock,synchronized显得比较笨重

庆幸的是在 Java 6 之后 Java 官⽅对从 JVM 层⾯对synchronized进行⼤优化,所以现在的 synchronized 锁效率也优化得很不错

​ 这里简单复习以下锁的相关概念,以便对Synchronized进行的优化阐述。

Java锁的相关概念

根据分类标准可以把锁分为以下 7 大类别,分别是:

  • 偏向锁/轻量级锁/重量级锁;
  • 可重入锁/非可重入锁;
  • 共享锁/独占锁;
  • 公平锁/非公平锁;
  • 悲观锁/乐观锁;
  • 自旋锁/非自旋锁;
  • 可中断锁/不可中断锁。

对于 Java 中的锁而言,一把锁也有可能同时占有多个标准,符合多种分类,比如 ReentrantLock 既是可中断锁,又是可重入锁。

1. 偏向锁/轻量级锁/重量级锁(java1.6出现)

偏向锁/轻量级锁/重量级锁,这三种锁特指 synchronized 锁的状态通过在对象头中的 mark word 来表明锁的状态。

偏向锁

​ 如果自始至终,对于这把锁都不存在竞争,那么其实就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。一个对象被初始化后,还没有任何线程来获取它的锁时,那么它就是可偏向的,当有第一个线程来访问它并尝试获取锁的时候,它就将这个线程记录下来,以后如果尝试获取锁的线程正是偏向锁的拥有者,就可以直接获得锁,开销很小,性能最好。

轻量级锁

​ JVM 开发者发现在很多情况下,synchronized 中的代码是被多个线程交替执行的,而不是同时执行的,也就是说并不存在实际的竞争,或者是只有短时间的锁竞争,用 CAS (比较并交换)就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。轻量级锁是指当锁原来是偏向锁的时候,被另一个线程访问,说明存在竞争,那么偏向锁就会升级为轻量级锁,线程会通过自旋的形式尝试获取锁,而不会陷入阻塞。(自旋的概念,我们放到自旋锁的时候补充)

重量级锁

​ 重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。重量级锁会让其他申请却拿不到锁的线程进入阻塞状态

2. 可重入锁/非可重入锁

​ 可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。同理,不可重入锁指的是虽然线程当前持有了这把锁,但是如果想再次获取这把锁,也必须要先释放锁后才能再次尝试获取。

Java中的可重入锁: ReentrantLock、synchronized修饰的方法或代码段

3. 共享锁/独占锁

​ 享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。

Java中用到的共享锁: ReentrantReadWriteLock

Java中用到的独占锁: synchronized,ReentrantLock

4. 公平锁/非公平锁

​ 公平锁是一种思想:多个线程按照申请锁的顺序来获取锁。在并发环境中,每个线程会先查看此锁维护的等待队列,如果当前等待队列为空,则占有锁,如果等待队列不为空,则加入到等待队列的末尾,按照FIFO的原则从队列中拿到线程,然后占有锁。

非公平锁是一种思想: 线程尝试获取锁,如果获取不到,则再采用公平锁的方式。多个线程获取锁的顺序,不是按照先到先得的顺序,有可能后申请锁的线程比先申请的线程优先获取锁。

**Java中的非公平锁:**synchronized是非公平锁,ReentrantLock通过构造函数指定该锁是公平的还是非公平的,默认是非公平的。

img

5. 悲观锁/乐观锁

乐观锁是一种乐观思想,假定当前环境是读多写少,遇到并发写的概率比较低,读数据时认为别的线程不会正在进行修改(所以没有上锁)。写数据时,判断当前 与期望值是否相同,如果相同则进行更新(更新期间加锁,保证是原子性的)。

Java中的乐观锁: CAS,比较并替换,比较当前值(主内存中的值),与预期值(当前线程中的值,主内存中值的一份拷贝)是否一样,一样则更新,否则继续进行CAS操作。、Atomic 原子类(本质也是CAS操作)

悲观锁是一种悲观思想,即认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为其他线程会修改,所以每次读、写数据时都会上锁。其他线程想要读写这个数据时,会被这个线程阻塞,直到这个线程释放锁其他线程才能获取到锁。

Java中的悲观锁synchronized修饰的方法和方法块、ReentrantLock

6. 自旋锁/非自旋锁

​ 自旋锁的理念是如果线程现在拿不到锁,并不直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”,就像是线程在“自我旋转”。

​ 相反,非自旋锁的理念就是没有自旋的过程,如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。

自适应自旋: 自适应意味着自旋的时间不再是固定的,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状态预测就会越来越精准。

Java中的自旋锁: CAS操作中的比较操作失败后的自旋等待。

7. 可中断锁/不可中断锁

​ 可中断锁和不可中断锁。在 Java 中,synchronized 关键字修饰的锁代表的是不可中断锁,一旦线程申请了锁,就没有回头路了,只能等到拿到锁以后才能进行其他的逻辑处理。而我们的 ReentrantLock 是一种典型的可中断锁,例如使用 lockInterruptibly 方法在获取锁的过程中,突然不想获取了,那么也可以在中断之后去做其他的事情,不需要一直傻等到获取到锁才离开。

8. 分段锁

分段锁是一种机制: 最好的例子来说明分段锁是ConcurrentHashMap。

​ **ConcurrentHashMap原理:**它内部细分了若干个小的 HashMap,称之为段(Segment)。 默认情况下一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 添加一项key-value,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该key-value应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的key-value不存放在同一个段中,则线程间可以做到真正的并行。

Synchronized 的常见使用

synchronized锁是jvm内置的锁,不同于ReentrantLock锁。synchronized关键字可以修饰方法,也可以修饰代码块。synchronized关键字修饰方法时可以修饰静态方法,也可以修饰非静态方法;
​ 同样,synchronized关键字修饰代码块时可以修饰对象,也可以修饰类。

1. Synchronized修饰实例方法

修饰实例方法: 此时,synchronized加的锁就是这个方法的调用对象,也就是说synchronized在修饰一个非静态方法的时候“锁”住的只是一个实例对象,并不会“锁”住其它的对象

public synchronized void add(){
       i++;
}

2. Synchronized修饰静态方法

synchronized修饰静态方法的使用与实例方法并无差别,在静态方法上加上synchronized关键字即可

public static synchronized void add(){
       i++;
}

此时,synchronized加锁的对象为当前静态方法所在类的Class对象。,也就是说,对于该类的所有对象,都会被锁住。

image-20231130215346455

3. 修饰代码块(锁的可以说对象或者类)

(this) 表示当前调用该方法的实例对象,也就是锁是当前实例对象

public class fancySyncTest {
    public void method1(){
        synchronized (this) {
            // 逻辑代码
        }
    }
}

总结

Synchronized原理浅析

​ 数据同步需要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖于JVM,而juc.Lock给出的答案是在硬件层面依赖特殊的CPU指令。

1. 给代码块加锁的原理:

public class SynchronizedDemo {
	public void method() {
		synchronized (this) {
			System.out.println("synchronized 代码块");
		}
	}
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class

image-20210210160804090

​ 通过java反编译可以了解到 synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。

​ 当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。

另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

​ 在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

​ 在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

2. 给方法加锁的原理

public class SynchronizedDemo2 {
	public synchronized void method() {
		System.out.println("synchronized 方法");
	}
}

反编译一下:

image-20210210161841561

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

虽然上述两者,原理有所不同,但本质都是对对象监视器 monitor 的获取。

Java 1.6 对Synchronized 的优化

​ 从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。

1. 对锁进行逐步状态升级

​ 锁被划分为四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,对着线程竞争激烈程度,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

image-20231130221302595

偏向锁性能最好,可以避免执行 CAS 操作。而轻量级锁利用自旋和 CAS 避免了重量级锁带来的线程阻塞和唤醒,性能中等。

2. 自适应的自旋锁

​ 在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。自适应意味着自旋的时间不再固定,而是会根据最近自旋尝试的成功率、失败率,以及当前锁的拥有者的状态等多种因素来共同决定。自旋的持续时间是变化的,自旋锁变“聪明”了。

3. 锁消除

​ 是指虚拟机即时编译器JIT在运行时,对一些代码上要求同步,但是会将检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中, 堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。举例理解:

public class MySynchronizedTest07 {

    public void method() {
        Object object = new Object();
        synchronized (object) {
            System.out.println("hello  world");
        }
    }
}
//上述代码我们可知将object变成了局部变量,在方法中,方法的的局部变量时线程独立的,并发的场景每个线程都有各自的object对象,这个时候的锁就无意义的。

4. 锁粗化

​ 是指把同步区域扩大,也就是只在最开始加一次锁,并且在最后直接解锁,那么就把中间无意义的解锁和加锁的过程消除,相当于是把几个 synchronized 块合并为一个较大的同步块。这样做的好处在于在线程执行这些代码时,就无须频繁申请与释放锁了,这样就减少了性能开销

资料借鉴

作者:悟空聊架构
链接:https://juejin.cn/post/6867922895536914446
来源:稀土掘金

作者:Linn
链接:https://juejin.cn/post/7000571331674669092 来源:稀土掘金

  • 44
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值