Java知识总结(八)

Synchronized同步锁

同步锁可以将任意一个不为null的对象当作锁。它属于独占式的悲观锁,同时属于可重入锁非公平锁

作用范围

(9条消息) Java中synchronized同步锁用法及作用范围_在梅边的专栏-优快云博客_java 同步锁

java中的对象锁和类锁

对象锁作用于:对象实例方法或者对象实例上。

类锁作用于:类的静态方法和类对应的Class对象。

一个类可以对应多个对象实列,一个对象拥有一个对象锁,多个对象对应多把锁,因此各个对象实例之间的对象锁互不干扰。

每个类只有一个,对应一把类锁。

注意:类锁只是概念上的,并不是真实存在的,这里是用来理解同步锁作用在静态方法中。

  • 非静态方法

    使用Synchronized修饰非静态方法,锁定的是调用当前方法的对象实例

        public synchronized void test(){
            // TODO
        }
    
    • 创建两个线程,同时调用同一个对象中的同步方法test(),那么同一时间只能是获得对象锁的线程执行,另一个被挂起,等待获取对象锁。
    • 创建两个线程,同时调用同一个对象中的同步方法test()、同步方法test1(),那么同一时间只能是获得对象锁的线程执行,另一个被挂起,等待获取对象锁。只要是调用同一个对象中的同步方法,只要对象锁被线程获取占用了,其他线程必须等待
    • 创建两个线程,同时调用同一个对象中的同步方法test()、非同步方法test1(),那么两个线程交替进行,说明获取对象锁只能确保被Synchronized修饰的方法或代码块同步,非同步方法不受影响,其他的线程可以访问同一对象中的非同步方法.。
  • 静态方法

    使用Synchronized关键字修饰静态方法,修饰静态代码块

        public static synchronized void test(){
            // TODO
        }
    
    	//需要引用当前的类
    public static void test(){
            synchronized (TestSynchronized.class) {
                // TODO
            }
        }
    
    • 创建两个线程,同时调用同一类中的同步静态方法test(),同一时间只能由一个线程执行test方法,另一个线程挂起等待。

      一个类可以创建很多对象,这些对象同属于一个类,静态方法是属于类,同步后的静态方法也是唯一的。

    • 创建两个线程,同时调用类中的同步静态方法test(),同步非静态方法test1(),发现两个线程交替,并没有产生阻塞,

      同步静态方法和同步非静态方法所需要的锁并非同一把锁,对象锁和类锁两个是相互独立的

  • 代码块

    使用Synchronized修饰代码块,根据传入的参数,来决定锁定的哪个。

        public void test(){
            synchronized (this) {
                // TODO
            }
        }
        //如果传入的参数是 this,那么锁定的也是当前的对象:
    

Synchronized底层实现

package com.paddx.test.concurrent;

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

Synchronized同步代码块的语义底层是基于对象内部的监视器锁(monitor),分别使用monitorentermonitorexit两条指令实现。

**monitorenter:**指令在编译为字节码后插入到同步代码块的开始位置。

每个对象都有一个监视器锁(monitor),当monitor被线程占用时当前就处于锁定的状态,线程执行monitorenter执行尝试获取monitor的控制权,尝试获取对象锁。过程:

  • 如果monitor当前的进入数为0,表示没有线程占用,线程可以进入monitor,之后将数值设置为1,成为该monitor的所有者。
  • 如果monitor已经被其他线程所占用,当前线程进入阻塞的状态,等待monitor的进入值为0时,尝试重新获取monitor的所有权。
  • 如果当前线程已经占用monitor,只是重新进入,则进入monitor的进入数加1

**monitorexit:**指令在编译为字节码后插入到同步代码块的结束位置

执行这个指令的线程必须是 objectref 所对应的 monitor 的所有者。执行指令,monitor的进入数减一,如果进入数变为0,则线程退出monitor,不再是monitor的所有者,其他被阻塞的线程尝试获取monitor的所有权。

每个monitorenter指令的执行都要对应monitorexit指令。

Synchronized同步方法中是用ACC_SYNCHRONIZED标识符来实现同步的

反编译:

img

相比于普通方法常量池中多了ACC_SYNCHRONIZED标识符来实现同步,如果该常量被设置了,执行线程将先获取monitor,获取monitor之后才可以执行方法体,执行完后释放monitor。它是一种隐式的同步的实现。

Synchronized核心组件

  1. Wait Set :调用wait方法被阻塞放入的地方
  2. Contention List(竞争队列):多个线程竞争请求锁。
  3. Entry List:竞争队列中多个线程中有资格成为候选资源的线程被放入Entry List中。
  4. OnDeck:只有一个线程能够获得锁的线程。
  5. Owner:已经获取锁资源的线程
  6. !Owner:已经释放了锁资源的线程
在这里插入图片描述

多个线程请求同一资源,如果资源被占用那么,线程被放入Contention List(竞争队列)中,在竞争队列中将具有成为候选资源的线程放入到Entry List中,在Entry List选取能够成为OnDeck的线程(能够获取monitor的线程),onDeck线程尝试获取资源的monitor,获取到monitor的所有权后进入到Owner,同时将monitor的进入数加1,线程运行。如果在线程运行的时候执行了wait方法,进入阻塞的状态,该线程会释放monitor并将其进入数减一,放入Wait Set(阻塞队列中)。阻塞队列中的线程和等待队列中的线程一同争夺锁定资源。

如果线程式正常运行完毕,也会释放monitor,其他线程争夺monitor的所有权。

在 JDK1.6 之后,出现了各种锁优化技术,如轻量级锁、偏向锁、适应性自旋、锁粗化、锁消除等,这些技术都是为了在线程间更高效的解决竞争问题,从而提升程序的执行效率。

Java并发编程:Synchronized底层优化(偏向锁、轻量级锁) - liuxiaopeng - 博客园 (cnblogs.com)

Java对象头

Hotspot虚拟机,java对象头主要包含:Mark Word(标记字段)、 Klass Pointer(类指针)

Mark Word:主要存储对象的HashCode、GC分代年龄、锁状态以及锁标志位

Klass Poniter:对象指向类的元数据的指针,表明该对象对应类的实例

Monitor

Synchronized通过给同步资源加锁实现同步操作,这个锁存在于Java对象头中,每个对象有都有一把锁,这把锁指得就是Monitor(监视器锁/内部锁)。

Monitor是线程私有的数据结构,每一个线程都有一个monitor record列表和一个全局列表。monitor中有一个Owner字段,若锁没有被线程占用,字段为NULL,若被线程占用,存放拥有该锁线程唯一标识。

Monitor是依赖底层操作系统的Mutex Lock(互斥锁)来实现线程的同步。操作系统实现线程之间的切换,也就是线程的阻塞和唤醒之间状态的转换,这种状态的转换需要耗费的时间和资源都是很多的。这也是起初Synchronized实现同步的方式,也是其效率低的原因

将依赖于底层操作系统Mutex Lock实现的锁称之为"重量级锁"。

为了减少重量级锁带来的时间和资源上的消耗,引入了“偏向锁”和"轻量级锁" ,JKD1.6中对Synchronized做出的优化。

锁的状态有四种:无锁、偏向锁、轻量级锁、 重量级锁,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

image-20210728135419630

无锁、偏向锁、轻量级锁、重量级锁

  • 无锁

    无锁表示对资源没有进行锁定,允许多个线程同时访问并修改同一资源,但是最终只能是一个线程对其修改成功。

    存放内容:对象hashCode,GC分代年龄,锁标志位:01,是否为偏向锁

  • 偏向锁

    偏向锁表示一段同步代码一直被一个线程所访问,该线程会自动获取锁,降低获取锁的代价。实现在只有一个线程执行同步代码时进一步提高性能。

    引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。因为轻量级锁的获取以及释放需要执行多次的CAS原子指令,而偏向锁只在设置线程ID时执行一次CAS操作。

    **存放内容:**线程id、GC分代年龄、是否为偏向锁、锁标志位:01

    获取偏向锁的过程:

    • 可偏向状态:检查Mark Word中偏向锁的表示是否为:1,锁标志位为:01
    • 检测线程ID:线程ID是否为当前线程,如果是,执行同步代码,如果不是,执行CAS操作。
    • 执行CAS操作:若线程ID不是当前线程,通过CAS操作竞争锁。若竞争成功,将线程ID设为当线程。若竞争失败,撤销偏向锁。
    • 撤销偏向锁:CAS操作无法获取锁,说明有多个线程在竞争同一把锁,当前无法再使用偏向锁,需要将偏向锁撤销转换成其他锁。

    偏向锁撤销:

    当偏向锁遇到其他线程尝试竞争时,持有偏向锁的线程才会主动释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先挂起拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。之后在安全点被阻塞的线程继续往下执行同步代码

    禁用偏向锁(-XX:-UseBiasedLocking)

  • 轻量级锁

    当锁是偏向锁时,有其他的线程尝试争取偏向锁,那么这个偏向锁就会升级为轻量级锁。轻量级锁适用于交替执行同步块的情况,减少线程之间切换带来的性能消耗,通过自旋的方式获取锁,不会产生阻塞,提升性能。注意:轻量级锁的出现并不是替代重量级锁,在一定条件下,轻量级锁会升级为重量级锁

    **存放内容:**指向栈中锁记录的指针。

    轻量级锁加锁过程:

    1. 在进入同步块时,如果同步对象锁状态为:无锁(锁标志:01,偏向锁为:0),JVM会在当前线程中的栈帧中建立一个锁记录(Lock Record),该空间是用来存储对象头中Mark Word的拷贝

      image-20210728144857021

      ​ 轻量级锁CAS操作之前堆栈与对象的状态

    2. 拷贝对象头中Mark Word复制到锁记录中

    3. 拷贝成功后,JVM使用CAS(Compare And Swap)操作尝试将对象头中的Mark Word指向锁记录,让锁记录中的**Owner(存放拥有该锁线程唯一标识)**指向对象头中的Mark Word。若更新成功,执行步骤4,若失败,执行步骤5

    4. 更新成功,该线程拥有的该对象的锁,对象头中锁标志:00。

      img

      ​ 轻量级锁CAS操作之后堆栈与对象的状态

    5. 更新失败,JVM会先去检查对象头中Mark Word 是否指向当前线程的栈帧,如果有说明当前线程已经拥有了该对象锁,可以直接进入同步块继续执行,如果没有,说明有多个线程竞争锁,这时轻量级锁就要晋升为重量级锁,锁标志:10,Mark Word中存储的就是指向重量级锁的指针,后面等待的线程也要进入阻塞状态了。

    轻量级锁解锁过程:

    1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换成当前的Mark Word

      image-20210823142051828
    2. 若替换成功,表明当前线程释放锁的过程,没有其他线程请求该锁资源,没有竞争发生。

    3. 若替换失败,说明有其他线程尝试获取该锁(此时锁已经膨胀),存在着竞争,将轻量级锁升级为重量级锁。

  • 重量级锁

    依赖于底层操作系统Mutex Lock实现的锁称之为"重量级锁"。

    操作系统实现线程之间的切换,也就是线程的阻塞和唤醒之间状态的转换,这种状态的转换需要耗费的时间和资源都是很多的。

重量级锁、轻量级锁和偏向锁之间转换

img
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值