一路谈谈锁

Synchronized

一、使用

修饰类的:实例方法、静态方法、代码块;

实例方法:锁对象为当前实例对象:

public synchronized void sayHello(){
	System.out.println("Hello World");
}

静态方法:锁对象为当前类Class对象:

public static synchronized void sayHello(){
	System.out.println("Hello World");
}

代码块:

//锁对象
synchronized(this){}
//锁对象
synchronized(""){}
//锁class
synchronized(xxx.class){}

二、原理

2.1、Java对象头(Object Header)

普通对象:

|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

数组对象:

|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

其中Mark Word(32位,JVM为32位):

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Unlocked     |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

  • identity_hashcode:自身hashcode
  • age:分代年龄,只有4位,最大值为15,所以-XX:MaxTenuringThreshold最大值为15;
  • thread:偏向锁的线程ID
  • lock:锁状态标记位,用尽可能少的二进制位表示尽可能多的信息。该标记的值不同,整个mark word表示的含义不同。
  • biased_lock:是否可偏向,1可,0不可。
  • epoch:本质是一个时间戳 , 代表了偏向锁的有效性。
  • state:
    • Unlocked:无锁(禁用偏向机制)。biased_lock:0;lock:01
    • Biased:已有偏向锁/可偏向。biased_lock:1;lock:01
    • Lightweight Locked:轻量级锁。lock:00;
    • Heavyweight Locked:重量级锁。lock:10;
    • Marked for GC:GC。lock:11;

2.2、Monitor 监视器

在这里插入图片描述

  • count:该线程获取锁的次数,一共获取了多少次锁;
  • recursions:锁的重入次数;
  • WaitSet:线程调wait()时进入;
  • EntryList:阻塞等待队列,竞争锁失败的线程会进入;
  • Owner:指向获取锁的线程。

2.3、CAS

JNI(Java Native Interface):Java本机编程接口,JDK的一部分,JNI提供了若干的API,实现了Java和其他语言(主要是C&C++)通信(Java调用C/C++,C/C++调用Java);

  • CAS (Compare And Swap) 指令是一个CPU层级的原子性操作指令。 在 Intel 处理器中, 其汇编指令为 cmpxchg。Java通过JNI调用;
  • CAS操作包含三个操作数:内存位置(V)、预期原值(A)、新值(B);
    如果 V 位置的值与 A 相匹配,那么改为 B 。否则不处理。无论哪种情况,都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。);
  • 类似乐观锁;
  • ABA问题:A在比较时,其实是A->B->A,代表其他线程已经处理过(CAS获取锁时代表已占有,并中间还有过一次),此时CAS必须失败,但结果确会成功。解决:使用版本号;
  • 循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
    • pause指令:
    1. 可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多资源,延迟时间取决具体实现版本,有的是0;
    2. 可以避免在退出循环时因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU执行效率;
  • 只能保证一个共享变量的原子操作,多个时:
    1. 用锁,
    2. i=2,j=a,合并为ij=2a,然后用CAS来操作ij;
    3. Java1.5开始JDK提供AtomicReference:引用对象之间的原子性;

2.4、加锁流程

整体说明:
  • 几乎无竞争:偏向锁;
  • 轻度竞争:偏向锁 -> 轻量级锁;
  • 在重度竞争:升级为重量级锁(不会下降);

如图:
在这里插入图片描述

  • JVM 提供了关闭偏向锁的机制, JVM 启动命令参数:-XX:-UseBiasedLocking
无锁 -> 偏向锁

偏向锁获取方式:MarkWord 标记线程ID, 以表示哪一个线程获得了偏向锁。

  • 首先读取 MarkWord, 判断状态:
  1. 可偏向状态(threadID为空;biased_lock:1;lock:01),尝试用 CAS 操作, 将自己的线程 ID 写入MarkWord;

    • 如果 CAS 成功, 则获得偏向锁,如图;
      在这里插入图片描述
    • 一个线程在执行完同步代码块以后, 并不会尝试将 MarkWord 中的 thread ID 赋回原值 。这样做的好处是: 该线程再次加锁,而没有被其他线程获取过锁,依旧停留在可偏向的状态下, 即可在不修改对象头的情况下, 直接认为偏向成功;
    • 如果 CAS 失败, 说明, 另外一个线程 B 抢先获取了偏向锁。 说明竞争比较激烈, 需要撤销 B 获得的偏向锁,将 B 持有的锁升级为轻量级锁。 该操作需要等待全局安全点;
  2. 已偏向状态(threadID有值;biased_lock:1;lock:01), 则检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID 。

    • 如果相等, 则证明本线程已经获取到偏向锁, 可以直接继续执行同步代码块;
    • 如果不等, 则证明该对象目前偏向于其他线程, 需要撤销偏向锁;
  • 偏向锁的 撤销(revoke) 是一个很特殊的操作, 需要等待全局安全点(Safe Point), 此时间点所有的工作线程都停止了字节码的执行。
偏向锁 -> 轻量级锁

超过一个线程竞争某一个对象时, 会发生偏向锁的撤销操作,偏向锁撤销后, 对象可能处于两种状态:不可偏向的无锁状态(biased_lock:0;lock:01)(之所以不允许偏向, 是因为已经检测到了多于一个线程的竞争, 升级到了轻量级锁的机制)、不可偏向的已锁(lock:00) ( 轻量级锁) ;

  • 首先根据标志位判断出对象状态处于不可偏向的无锁状态如图:
    在这里插入图片描述
    不可偏向的无锁状态情况步骤如下:
  1. 当前线程栈帧中建立锁记录(Lock Record)空间,用于存储锁对象Mark Word的拷贝(Displaced Mark Word)。
  2. 拷贝Mark Word -> 锁记录,如图:
    在这里插入图片描述
  3. CAS将Mark Word -> 锁记录的指针,并将锁记录的owner -> Mark Word。成功执行4,先自旋任失败再5;
  4. 程已拥有轻量级锁,并且Mark Word的锁标志位设置为“00”,如图:
    在这里插入图片描述
  5. 将膨胀为重量级锁;
重量级加锁:

轻量级锁在向重量级锁膨胀的过程中, 一个操作系统的互斥量(mutex)和条件变量( condition variable )会和这个被锁的对象关联起来。
具体而言, 在锁膨胀时, 被锁对象的 markword 会被通过 CAS 操作尝试更新为一个数据结构的指针, 这个数据结构中进一步包含了指向操作系统互斥量(mutex) 和 条件变量(condition variable) 的指针;

  1. 如果t0,t1,t2在进行获取锁时,如果t0获取锁资源成功(成功的标志:CAS成功把monitor的_owner字段设置为当前线程(非公平));
  2. 那么t1和t2是不会立马挂起的,而是先通过CAS自旋的方式再次尝试获取锁;
  3. 如果失败则入队列(EntryList队列);
  • 虽然自旋锁方式省去了阻塞线程的时间和空间(队列的维护等)开销,但是长时间自旋也是很低效的。所以自旋的次数一般控制在一个范围内,例如10,50等,在超出这个范围后,线程就进入排队队列;
  • 被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响;

进入Synchronized同步块后,会通过对象的Mark Word获取monitor,获取了monitor才能调wait(),notify(),notifyAll() ;

三、内存可见性

synchronized能解决内存可见性问题,被synchronized加锁后,他会做出以下操作:

  1. 获取同步锁;
  2. 清空内存;
  3. 从主内存中拷贝新的对象副本到工作线程中;
  4. 继续执行代码,刷新主内存的数据;
  5. 释放同步锁;

四、总结

  • 可保证原子性;
  • 可保证内存可见性;获取锁,从主内存获取最新;释放锁,把最新写入主内存;简单读取最新变量可用volatile;
  • 注意死锁:A和B线程分别持有A和B锁,分别等待B和A锁;应按顺序申请;
  • 可重入性:重入和退出时有计数器增减;

volatile关键字

一、相关概念:

  1. 程序运行时的临时数据存放在主存(物理内存)中,但从主存写入/读取数据的速度比CPU执行速度慢很多,为了效率,CPU里面加入高速缓存;
  2. 多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的);
  3. 修改过程:主存读取数据 -> 复制到高速缓存 -> CPU计算 -> 写入高速缓存 -> 刷新到主存;
  4. 引发内存可见性问题:多个线程共享一个变量,A线程修改(没有立马刷新到主存),B线程不能马上看到(读取的自己线程的高速缓存),甚至永远也看不到;
  5. MESI协议保证了缓存一致性。核心的思想:写入共享变量时,会发出信号通知其他CPU将该变量的缓存行置为无效状态,当其他CPU读这个变量时,发现是无效就重新读取。

二、volatile修饰的变量

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 内存可见性:
    1. 修改的值强制写入主存;
    2. 其他线程缓存无效;
    3. 其他线程读取时重新从主存获取;
  2. 禁止指令重排;
  • volatile不能保证原子性;
public class TestVolatile {
    
    public static void main(String[] args) {
        // 创建td线程,并启动
        ThreadDemo td = new ThreadDemo();
        Thread thread = new Thread(td);
        thread.start();

        // main线程负责检查flag是否被变为了true
        while (true){
        	// 此处如果没有volatile修饰将读取缓存值,对象可能是缓存对象引用地址
            if (td.isFlag()){
                // 如果完成了工作,那么就全部停止
                System.out.println("Thread td finished its work.");
                break;
            }
        }
    }
    
    /**
     * 此线程将自己类的属性进行修改,若属性不加volatile关键字,其他线程通过get方法获取的是旧的值
     */
    static class ThreadDemo implements Runnable{

        // 内置一个标志变量,使用volatile修饰,保证其他线程通过get方法获取新值
        private volatile boolean flag = false;

        // 工作任务是将自己线程内部的标志变量变为true
        @Override
        public void run() {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("Changed flag from false to " + isFlag() + ".");
        }

        public boolean isFlag() {
            return flag;
        }

        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    }
    
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值