synchronized原理

一、问题

1、线程不安全原因

1)原子性

原子性主要保证一个或多个指令在执行过程中不允许被中断。

2)可见性

可见性主要保证一个线程对共享变量修改后,其他的线程立即能看到修改后的新值。

3)有序性

有序性主要保证单线程下程序运行结果的正确性,即使编译器和处理器为了优化性能会对指令进行重排序

2、作用

synchronized是一个同步锁,在同一时刻,被修饰的方法或代码块只有一个线程能执行,以保证线程安全。

二、使用方法

synchronized是java中加锁的关键字,可以用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程可以执行这段代码。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。

1、修饰实例方法

synchronized修饰实例方法,锁对象为当前实例对象,也称之为对象锁,进入同步代码需要获取当前实例对象的锁。进入同步实例方法时,如果锁对象不是同一实例对象,则不会形成资源竞争,线程之间互不影响。

// 修饰实例方法
public synchronized void test() {
    // 省略代码
    ......
}

如果多个线程分别调用一个示例对象的不同的synchronized实例方法,因为这些方法的锁对象是同一个,因此形成资源竞争,同一时刻只能有一个线程执行同步代码。

2、修饰静态方法

synchronized修饰静态方法,锁对象为当前类的Class对象,也称之为类锁,进入同步代码前需要获取类的Class对象的锁。

// 修饰静态方法
public static synchronized void test() {
    // 省略代码
    ......
}

单例模式的双重检查就是用的这种方式:

public class Singleton implements Serializable{
    /**
     * 1.不进行初始化
     */
    private volatile static Singleton instance = null;

    /**
     * 2.构造方法私有化
     */
    private Singleton(){
  		// 防止通过反射破坏单例
  		if(instance!=null){
			return instace;
		}
    }


    /**
     * 3.利用双重检索的方式进行初始化操作
     */
    public static Singleton getInstance(){
        if(Objects.isNull(instance)){
            synchronized (Singleton.class){
                if(Objects.isNull(instance)){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
 
    /**
    * 4.防止通过序列化破坏单例
    **/
    private Object readResolve(){
		return instance;
	}
}

如果多个线程分别调用一个类中的synchronized静态方法和非synchronized实例方法,虽然不是同一个锁对象,但是类锁是一个全局锁,因此形成资源竞争,同一时刻只能有一个线程执行同步代码。

3、修饰代码块

synchronized修饰代码块,取决于锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

public void test1() {
    // 修饰代码块,锁对象为实例对象
    synchronized (this) {
        // 省略代码
        ......
    }
}public void test2() {
    // 修饰代码块,锁对象为实例对象
    synchronized (new Object()) {
        // 省略代码
        ......
    }
}public void test3() {
    // 修饰代码块,锁对象为类的Class对象
    synchronized (Object.class) {
        // 省略代码
        ......
    }
}

synchronized修饰代码块,重点在于锁对象,锁对象为实例对象时,其效果与修饰实例方法一致,锁对象为类的Class对象时,其效果与修饰静态方法一致。

三、原理

1、锁标记存储

一个Java对象被初始化之后会存储在堆内存中,存储结构分为3个部分:对象头、实例数据、对齐填充。
对象存储结构
从上图中可以看到,对象头中Mark Word存储了锁相关的信息,我们来看看Mark Word的存储结构。

内置锁状态锁标记位(2bit)是否偏向锁(1bit)存储内容
无锁010对象哈希码、GC分代年龄
偏向锁011偏向线程id,偏向时间戳、对象分代年龄
轻量级锁00锁记录(Lock Record)指针
重量级锁10重量级锁指针
GC标记11

2、锁升级

1)偏向锁

在单线程环境下,访问synchronized修饰的同步代码,这个时候的锁状态就是偏向锁。
1️⃣在没有线程竞争的情况下,线程A去访问Synchronized修饰的方法/代码块;
2️⃣尝试通过偏向锁来获取锁资源(基于CAS);
3️⃣如果获取锁资源成功,则修改Mark Word中的锁标记,偏向锁标记为1,锁标记为01,并存储获得锁资源的线程id,然后执行代码;
4️⃣如果同一线程再来访问,直接获取锁资源,然后执行代码。
偏向锁
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令,代价就是一旦出现多线程竞争的情况就必须撤销偏向锁。

2)轻量级锁

所谓的轻量级锁,就是未获取到锁资源的线程,进行一定次数的自旋,重新尝试获取锁,如果在重试过程中获取到锁资源,那么此线程就不需要阻塞。
1️⃣线程A已获取偏向锁;
2️⃣线程B开始竞争锁资源,锁对象的线程id与线程B的线程id不一致,意味着出现锁竞;
3️⃣在线程B的栈帧中创建锁记录Lock Record,用于存储锁对象的Mark Word旧信息以及锁对象地址,并将Mark Word中的信息,例如对象哈希码、GC分代年龄拷贝到Lock Record中的Displaced Mark Word中,以便后续锁释放时使用;
4️⃣自旋尝试将锁对象头的Mark Word中锁指针记录更新为线程B栈帧中Lock Record的地址。
5️⃣如果更新成功,则修改Mark Word中锁标记修改为00,偏向锁标记为0,并将Lock Record的owner指针指向当前锁对象。
轻量级锁
⚠️自旋重试过程中,会一直占用CPU资源,如果持有锁的线程占用锁资源的时间比较短,自旋会明显地提升性能,如果持有锁的线程占用锁资源的时间比较长,那么自旋就会浪费CPU资源,因此需要限制线程自旋的次数。在JDK 1.6中默认的自旋次数是10次,我们可以通过-XX:PreBlockSpin参数来调整自旋次数。同时还引入的自适应自旋锁,来解决“锁竞争时间不确定”的问题,尽可能减少自旋次数。
释放锁与加锁一样,用CAS操作,如果对象的markword 仍然指向着线程的 LockRecord(锁记录 LR),那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。

3)重量级锁

重量级锁依赖于系统层面的Mutex Lock,会使线程阻塞,并由用户态转为内核态,这种切换的性能开销非常大。
重量级锁

4)锁升级

锁的状态可以为无锁、偏向锁、轻量级锁、重量级锁。锁的级别从低到高为:无锁➡️偏向锁➡️轻量级锁➡️重量级锁。
⚠️升级并不一定是一级一级生的,比如:直接由无锁状态升级为轻量级锁。

5)应用场景

锁类型优点缺点应用场景
偏向锁加锁和释放锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距如果线程间存在锁竞争,会有额外的锁撤销的消耗适用于只有一个线程访问同步块的场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度始终得不到锁竞争的线程,自旋会消耗CPU追求响应时间,同步块执行速度非常快
重量级锁线程竞争不适用自旋,不消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步快执行时间较长

3、重量级锁原理

synchronized关键字的底层是通过每个对象关联的监视器monitor来实现的,每个对象关联一个监视器monitor,线程通过修改monitor的计数值来获取和释放锁。

1)Monitor

Monitor可以理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)。
ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
ObjectMonitor

2)修饰方法

synchronized修饰方法时,会在访问标识符(flags)中加入ACC_SYNCHRONIZED标识,通过这个标识区分一个方法是否同步方法。
方法级同步是隐式执行的,它实现在方法调用和返回操作之中。当调用这些方法时,如果发现有ACC_SYNCHRONIZED标识,则会持有一个monitor,执行方法,然后退出monitor。无论方法调用正常还是发生异常,都会自动退出monitor,也就是释放锁。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。
修饰方法字节码

3)修饰代码块

synchroninzed修饰代码块时,会增加monitorenter和monitorexit指令。其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。
执行monitorenter指令的线程尝试获取锁对象关联的监视器monitor的所有权,如果monitor的计数为0,则该线程获得锁,并将计数+1,此时其他线程将阻塞等待。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放,设置该计数为0时,其他线程才有机会来获取监视器monitor的所有权。
修饰代码块字节码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值