前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >线程并发带来的安全性问题 之 同步锁(二)

线程并发带来的安全性问题 之 同步锁(二)

作者头像
架构探险之道
发布2023-03-04 11:02:22
3290
发布2023-03-04 11:02:22
举报
文章被收录于专栏:架构探险之道

线程并发带来的安全性问题 之 同步锁(二)

目录

  • 偏向锁的获取及原理
  • 重量级锁的获取

线程并发带来的安全性问题

  • 原子性
  • 可见性
  • 有序性

本地环境

  • jdk 11
  • gradle 6.6

前言

上一节我们学习了线程并发常见的安全性问题、锁的底层类型和对象结构的差异、锁升级相关知识。今天我们继续学习锁是如何升级的?

  • 锁的升级是基于线程竞争情况,如何实现从 偏向锁 到 轻量级锁 再到 重量级锁 的升级的?
  • 为什么这里(见上节篇末)明明没有竞争,它的锁的标记是轻量级锁呢?
  • 为何不是先偏向锁,再到轻量级锁呢?

偏向锁的获取及原理

默认情况下,偏向锁的开启是有个延迟,默认是4秒。为什么这么设计呢?

  • 因为 JVM 虚拟机自己有一些默认启动的线程,这些线程里面有很多的 Synchronized 代码,这些 Synchronized 代码启动的时候就会触发竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁的升级和撤销,效率较低。
  • 通过下面这个JVM参数可以讲延迟设置为0. XX:BiasedLockingStartupDelay=0

再次运行下面的代码。

代码语言:javascript
复制
public class LockDemo7 {

    static Object obj = new Object();

    public static void main(String[] args) {
        LockDemo7 demo = new LockDemo7(); // demo 和 obj 这2个对象,在内存中是如何存储和布局的。
        System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        System.out.println("-----------");
        // 如果有其他线程进入到下面的同步块,则先自旋
        // CAS 保证数据操作的原子性
        synchronized(demo){// 轻量级锁
            System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        }
    }
}
  • 输出结构如下

得到如下的对象布局,可以看到对象头中的的高位第一个字节最后三位数为[101],表示当前为偏向锁 状态。

这里的第一个对象和第二个对象的锁状态都是101,是因为偏向锁打开状态下,默认会有配置匿名的对象获得偏向锁。

代码语言:javascript
复制
com.example.juccode.LockDemo7 object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000[101] 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           54 c4 00 f8 (01010100 11000100 00000000 11111000) (-134167468)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

-----------
com.example.juccode.LockDemo7 object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 18 80 cb (00000[101] 00011000 10000000 11001011) (-880797691)
      4     4        (object header)                           b7 7f 00 00 (10110111 01111111 00000000 00000000) (32695)
      8     4        (object header)                           54 c4 00 f8 (01010100 11000100 00000000 11111000) (-134167468)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

为什么加锁和未加锁都是偏向锁呢?

main 方法启动的过程中,有很多内部匿名、默认线程执行,所以线程已经标记为偏向锁。

重量级锁的获取

在竞争比较激烈的情况下,线程一直无法获得锁的时候,就会升级到重量级锁。仔细观察下面的案例,通过两个线程来模拟竞争的场景。

  • 用户态到内核态的交换
  • 没有获得锁的线程会阻塞,再被唤醒
代码语言:javascript
复制
public class LockDemo8 {
    public static void main(String[] args) {
        LockDemo8 demo = new LockDemo8(); // demo 和 obj 这2个对象,在内存中是如何存储和布局的。

        new Thread(()->{
            synchronized (demo) {
                System.out.println("Thread get Lock");
                System.out.println(ClassLayout.parseInstance(demo).toPrintable());
            }
        }).start();

        synchronized(demo){// 如果当前锁是重量级锁,后面来的线程直接阻塞
            System.out.println("Main Thread get Lock");
            System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        }
    }
}
  • 得到如下输出:

从结果可以看出,在竞争的情况下锁的标记为 [010] ,其中所标记 [10]表示重量级锁

代码语言:javascript
复制
com.example.juccode.LockDemo8 object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           02 7e 01 a8 (00000[010] 01111110 00000001 10101000) (-1476297214)
      4     4        (object header)                           ab 7f 00 00 (10101011 01111111 00000000 00000000) (32683)
      8     4        (object header)                           40 70 06 00 (01000000 01110000 00000110 00000000) (421952)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Thread get Lock
com.example.juccode.LockDemo8 object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           02 7e 01 a8 (00000[010] 01111110 00000001 10101000) (-1476297214)
      4     4        (object header)                           ab 7f 00 00 (10101011 01111111 00000000 00000000) (32683)
      8     4        (object header)                           40 70 06 00 (01000000 01110000 00000110 00000000) (421952)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes

CAS机制

伪代码

代码语言:javascript
复制
// 自旋锁
for(;;){
    // condition 自旋次数
 if(cas){ // true ->
        // 只有一个线程可以进来更改
     break;
    }
}

两个线程竞争锁,如何通过 CAS 实现轻量级锁呢?

  • 线程 A 获得了偏向锁,线程 B 来抢占锁
  • 线程 B 来调用 CAS,把偏向 A 线程的指针指向自己
  • CAS ( Object,线程 A 的指针(预期值),线程 A 的指针(待更新的值))

CAS 原理

  • CAS这个在Synchronized底层用得非常多,它的全称有两种 Compare and swap 或 Compare and exchange 就是比较并交换的意思。它可以保证在多线程环境下对于一个变量修改的原子性。本质是乐观锁
  • CAS的原理很简单,包含三个值当前内存值(V)、预期原来的值(E)以及期待更新的值(N)。
  • 确保原子性操作:
    • 修改锁的标记
    • 修改线程的指针的指向
  • sun.misc.Unsafe#compareAndSwapInt
代码语言:javascript
复制
    /**
     * Atomically updates Java variable to {@code x} if it is currently
     * holding {@code expected}.
     *
     * <p>This operation has memory semantics of a {@code volatile} read
     * and write.  Corresponds to C11 atomic_compare_exchange_strong.
     *
     * @return {@code true} if successful
     */
    @ForceInline
    public final boolean compareAndSwapInt(Object o, long offset,
                                           int expected,
                                           int x) {
        return theInternalUnsafe.compareAndSetInt(o, offset, expected, x);
    }
  • jdk.internal.misc.Unsafe#compareAndSetInt
代码语言:javascript
复制
    /**
     * Atomically updates Java variable to {@code x} if it is currently
     * holding {@code expected}.
     *
     * <p>This operation has memory semantics of a {@code volatile} read
     * and write.  Corresponds to C11 atomic_compare_exchange_strong.
     *
     * @return {@code true} if successful
     */
    @HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);
  • ~/jdk/hotspot/hotspot-87ee5ee27509/src/share/vm/prims/unsafe.cpp
代码语言:javascript
复制
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
  • 我们看一下 linux x86 的实现 ~/jdk/hotspot/hotspot-87ee5ee27509/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
  • 多核时,CAS 底层的原理还是 Lock 缓存锁/总线锁指令
代码语言:javascript
复制
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
 // 判断是不是多核,如果是多核的话,增加一个 Lock 指令
    int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构探险之道 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 线程并发带来的安全性问题 之 同步锁(二)
    • 偏向锁的获取及原理
      • 重量级锁的获取
      • CAS机制
        • CAS 原理
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档