01-java锁

  • image

1 并发编程面临的问题

1.1 上下文切换

  • 实现: CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,时间片一般是几十毫秒(ms)
  • 上下文切换也会影响多线程的执行速度
  • 测试工具
    • Lmbench3 可以测量上下文切换的时长。
    • vmstat可以测量上下文切换的次数
  • 如何减少上下文切换
    • 无锁并发编程
    • CAS算法
    • 使用最少线程
      • 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
    • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

1.2 死锁

  • 避免死锁的几个常见方法
    • 避免一个线程同时获取多个锁
    • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
    • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
    • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
  • 死锁产生的4个必要条件
    • 互斥条件
      • 资源是独占,且排他使用。
    • 不可剥夺条件
      • 进程获取到资源,在使用完毕之前,不能被其他进程强行剥夺,只能由持有该资源的进程释放。
    • 请求和保持条件
      • 一个进程因请求资源阻塞时,对已获得的资源保持不放。
    • 循环等待条件
      • 若干进程之间形成一种头尾相接的循环等待资源关系。

1.3 资源限制

  • 在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源
  • 问题
    • 并发编程中,将代码执行速度加快原则是将代码中串行部分变成并发执行。如果某个并发执行受限于资源,你继续并发执行会更慢,因为加了 上下文切换和系统调度时间。
  • 如何解决资源限制的问题
    • 硬件资源限制
      • 考虑使用集群并行执行程序 如 Hadoop
    • 软件资源限制
      • 考虑使用资源池将资源复用 如线程池

2 乐观锁,悲观锁

  • 线程同步的不同角度,在java和数据库都有应用

2.1 悲观锁

  • 概念:对于同一个数据的并发操作,认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
  • 实现
    • Java中,synchronized关键字和Lock的实现类
    • MySQL的读锁、写锁、行锁等
  • 适合场景
    • 写操作多的场景

2.2 乐观锁

  • 概念::对于同一个数据的并发操作,认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据
    • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
    • 如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)
  • 实现
    • java CAS算法
    • 取的时候获取 version 号,更新带上version
  • 适合场景
    • 读操作多的场景

3 Java并发底层实现原理

  • java代码->编译器编译成字节码(平台通用)->类加载器 加载到JVM-> JVM执行字节码->汇编指令在CPU上执行。
    • java并发依赖JVM实现和CPU的指令。

3.1 volatile

  • volatile 是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,不会引起线程上下文的切换和调度
  • 可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值
public class Main {
    private int m = 2;
    private static volatile String   istr = "istrValue";
    protected static volatile String   istr2 = "istrValue2";
    private final String finalX = "finalXValue";
    public static void main(String[] args) {
        istr = "21312";
        istr2 = "12312";
    }
}


0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
image.png

Main字节码
  • volatile是如何来保证可见性
    • JVM层面 标志生成内存屏障
      • 004A 对应istr字段描述符ACC_VOLATILE,ACC_STATIC,ACC_PRIVATE,ACC_VOLATILE 标明是是volatile
    • 汇编指令 lock前缀指令相对于 内存屏障的指令
      • volatile 修饰的共享变量,写操作会多出 lock前缀的指令
    • lock前缀的指令在多核处理器会引发两件事
      • 将当前处理器缓存行数据写回到系统内存。
        • 它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性
      • 一个处理器的缓存写回到系统内存操作,会使在其他处理器里缓存了该内存地址的数据无效
        • 处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。

3.2 synchronized

public class SynchronizedEXample {
    static volatile int i = 0;
    static  Object o = new Object();
    public static synchronized void method1(){
       synchronized (SynchronizedEXample.class) {
           i++;
       }
    }
    public static synchronized void method2(){
        synchronized (o) {
            i++;
        }
    }
}
//method1  字节码指令
ldc指令int、float或String型常量从常量池推送至栈顶
astore_0 栈顶ref对象数值存入第1局部变量

  public static synchronized void method1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=0
         0: ldc           #2                  // class com/mg/bfbcdys/chapter02/SynchronizedEXample
         2: dup
         3: astore_0
         4: monitorenter
         5: getstatic     #3                  // Field i:I
         8: iconst_1
         9: iadd
        10: putstatic     #3                  // Field i:I
        13: aload_0
        14: monitorexit
        15: goto          23
        18: astore_1
        19: aload_0
        20: monitorexit
        21: aload_1
        22: athrow
        23: return
  • synchronized使用

    • 对于普通同步方法,锁是当前实例对象
    • 对于静态同步方法,锁是当前类的Class对象
    • 对于同步方法块,锁是Synchonized括号里配置的对象
  • 方法同步和代码块同步异同

    • 同: JVM基于进入和退出Monitor对象来实现方法同步和代码块同步
      • Monitor依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步
        • 依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”
      • 如果同步代码过于简单,上下文切换时间可能比用户代码执行时间要长。
        • synchronized 最初就是这种实现,JDK6之前效率低的原因
        • JDK6中引入“偏向锁”和“轻量级锁”
    • 异:方法同步和代码块同步实习细节不一样
      • 方法同步
        • ACC_SYNCHRONIZED 标识
      • 代码块同步
        • monitorenter和monitorexit指令实现
          • monitorenter 编译后插入到同步代码块的开始位置
          • monitorexit 插入到方法结束处和异常处
          • JVM要保证每个monitorenter必须有对应的monitorexit与之配对
  • Synchronized 为什么能实现线程同步

    • 1 Java对象头

      • synchronized 悲观锁,在操作同步资源要先加锁,这把锁就是存在java对象头中
      • 对象头组成
        • image.png
        • MarK Word
          • 存储对象的HashCode,分代年龄,锁标记位,非固定数据结构。
          • 长度
            • 非数组对象2个字宽
            • 数组对象3个字宽
              • 32位虚拟机 一字宽4字节,即32bit
              • 64位虚拟机 一字宽8字节,即64bit
          • 32位
            image.png
          • 64位
            image.png
    • 2 Monitor c++实现

      • Monitor 关键属性
        • _cxq(ObjectWaiter *): 存放待获取锁对象 队列
        • _owner(Thread *):指向持有ObjectMonitor对象的线程
        • _WaitSet(ObjectWaiter *):存放处于wait状态的线程队列
        • _EntryList(ObjectWaiter *):存放处于等待锁block状态的线程队列
          • 当一个线程尝试获取锁,如果锁被占用,则将该线程封装应ObjectWaiter对象插入_cxq队尾,暂停当前线程,当持有锁的线程释放锁之前,将_cxq元素,放入到EntryList, 并唤醒EntryList队首线程
          • 同步块 调用Object#wait,线程对应ObjectWaiter从_EntryList移除加入到WaitSet,然后释放锁
          • 当wait的线程被notify 之后,会将对于的ObjectWaiter从WaitSet移到_EntryList
      • Monitor 描述
        • 任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态
          • 线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁
        • 每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表
    • Synchronized 锁的状态

      • 总结
        • 偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作
        • 轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能
        • 重量级锁是将除了拥有锁的线程以外的线程都阻塞
      • 偏向锁
        • 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,,为了让线程获得锁的代价更低而引入了偏向锁
        • 加锁:一个线程访问同步块并获取锁时,CAS把对象头存储锁偏向的线程ID(会在对象头和栈帧中的锁记录里存储锁偏向的线程ID),以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁
          • 成功,表示线程已经获得了锁
          • 失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1
        • 撤销:只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。需要等待全局安全点。其他线程会暂停拥有偏向锁的线程,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程任然活着,拥有偏向锁的线程继续执行,解锁之前,遍历偏向锁的记录,要么重新偏向于其他线程,要么恢复成无锁,或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
        • 关闭偏向锁
          • java 6,7默认开启,但是它在应用程序启动几秒之后激活
            • -XX:BiasedLockingStartupDelay=0 可以关闭延迟
          • 如果确定应用程序所有锁都有竞争状态
            • -XX:-UseBiasedLocking=false 偏向锁,程序默认进入轻量级锁状态
          • 偏向锁的获取和撤销详解
      • 轻量级锁
        • 当锁是无锁或者偏向锁,被另外线程访问,锁升级轻量级锁,其他线程自旋获取锁,不会阻塞。

        • 加锁:代码进入同步块,如果同步对象锁是无锁并且不是偏向锁(锁标志01状态,偏向锁为0),虚拟机首先将在当前线程的栈帧建立锁记录(Lock Record)的空间,将锁对象头中的MarK Word 复制到锁记录中,官方称Displaced Mark Word

          • 线程CAS将对象头 Mark Word更新为指向Lock Record的指针。
          • image.png
            • Atomic::cmpxchg_ptr(新值, 目标地址, 原值)
              • 如果目标地址是原值更新为新值,返回原值
              • 否则,不做更新,并且返回新值
          • 如果成功,当前线程获取锁,并且设置对象Mark Word的锁标志位设置为“00”
          • 如果失败,表示所对象不是无锁状态
            • 首先检查对象的Mark Word是否指向当前线程的栈帧,如果是继续执行
            • 否则说明其他线程竞争锁,若当前只有一个等待线程,则该线程通过自旋进行等待
            • 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁(设置对象头锁标志10)。
        • 解锁: 使用原子的CAS操作将Displaced Mark Word替换回到对象头的Mark World

          • 成功 没有竞争发生, 锁标志更新为01
          • 失败 如果是偏向锁,或者轻量级锁,继续替换, 否则膨胀重量级锁为后调用重量级锁的exit方法
      • 重量级锁
        • 锁标志"10" 等待的线程进入阻塞状态

4原子操作实现原理

  • 处理器如何实现原子操作
    • 处理器提供了很多Lock前缀的指令来实现,如:
      • 测试和修改指令:bts、btr、btc
      • 交换指令xadd、cmpxchg
      • 以及其他一些操作数和逻辑指令 add or
      • 被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它
  • java 如何实现原子操作
    • 通过锁和循环CAS的方式来实现原子操作
      • 使用循环CAS实现原子操作
        • JVM中CAS利用处理器提供的cmpxchg(),循环进行CAS操作直到成功为止
      • 锁方式来实现原子操作
        • 偏向锁、轻量级锁,重量级锁
  • JVM 对CAS的支持
    • 在支持CAS的平台上,运行时JVM编译成相应的机器指令
    • 在不支持CAS指令,JVM将使用自旋锁
  • CAS(Compare And Swap)
    • CAS算法涉及到三个操作数
      • 需要读写的内存值 V。
      • 进行比较的值 A。
      • 要写入的新值 B。
    • 当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作)
    • AtomicInteger 的 incrementAndGet 实现
      • valueOffset 的理解:理解对象内存布局,就是这个对象分配内存偏移量,相当于对象内变量的地址。
      • 对象头 虚拟机用3个字宽,非数组2个字宽,一个字宽4字节
        • UseCompressedOops 压缩后 AtomicInteger.valueOffset 64位JVM 12字节 没压缩 16字节。 32位JVM 8字节
      • compareAndSwapInt 方法中
        • 在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值
        • 后续JDK通过CPU的cmpxchg指令
    • 存在的问题
      • 1 ABA问题
        • 思路 变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”
        • JDK1.5 AtomicStampedReference类来解决ABA问题o,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等
      • 2 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
      • 3 只能保证一个共享变量的原子操作
        • 取巧 如两个共享变量i=2,j=a 合并就是ij=2a,然后cas来操作ij
        • JDK1.5 AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作
//AtomicStampedReference  compareAndSet 方法
public boolean compareAndSet(V   expectedReference, //预期引用
                                 V   newReference, //更新后的引用
                                 int expectedStamp, // 预取的标志
                                 int newStamp)  // 更新后的标志

// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
public final int getAndAddInt(Object o, long offset, int expected) {
  int var5;
  do {
      var5 = this.getIntVolatile(o, offset);
  } while(!this.compareAndSwapInt(o, offset, var5, var5 + expected));
  return var5;
}


public class AtomicIntegerExample {
    static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static void main(String[] args) throws Exception {
        System.out.println(ClassLayout.parseClass(AtomicInteger.class).toPrintable());
        Unsafe unsafe = getUnsafeInstance();
        System.out.println("=================");

        long offset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        atomicInteger.incrementAndGet();
        int va = unsafe.getInt(atomicInteger,offset);
        System.out.println("va: " + va);
        System.out.println("atomicInteger:"+atomicInteger);
    }

    public static Unsafe getUnsafeInstance() throws Exception {
        // 通过反射获取rt.jar下的Unsafe类
        Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeInstance.setAccessible(true);
        // return (Unsafe) theUnsafeInstance.get(null);是等价的
        return (Unsafe) theUnsafeInstance.get(Unsafe.class);
    }
}
// 输出
java.util.concurrent.atomic.AtomicInteger object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4    int AtomicInteger.value                       N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

=================
va: 1
atomicInteger:1

// -XX:-UseCompressedOops 去掉指针压缩后的输出
java.util.concurrent.atomic.AtomicInteger object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    16        (object header)                           N/A
     16     4    int AtomicInteger.value                       N/A
     20     4        (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=================
va: 1
atomicInteger:1

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351