精华内容
下载资源
问答
  • Java锁机制

    2020-11-05 13:09:56
    Java锁机制java的锁机制对象和对象头的结构同步锁synchronizedMoniter(管程/监视器)锁状态和锁升级1、无锁2、偏向锁3、轻量级锁4、重量级锁无锁编程AQS、CAS、JUC源码CAS(Compare And Swap)CAS多线程下难保证...

    java的锁机制

    在并发环境下,多个线程会对同一个资源进行争抢,可能会导致数据不一致的问题。

    在JAVA中每一个对象都有一把锁,存放在对象头中,锁中记录了当前对象被哪个线程所占用。

    对象和对象头的结构

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rw7jrSW5-1604552466502)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104155623859.png)]

    填充字节

    其中对齐填充字节是为了满足“Java对象的大小必须是8比特的倍数”设计的,为了帮助对象来对齐而填充的一些无用字节,说是能让cpu更快的计算出结果。

    实例数据

    你在初始化对象时设定的属性和对应的属性值

    对象头

    对象头存放了一些对象本身的运行时信息,对象头包含了两部分Mark Word和Class Pointer,相较于实例数据,对象头属于一些额外的存储开销,所以被设计的极小来提高效率。

    Class Pointer就是一个指针,指向了当前对象类型所在方法区中的类型数据。

    Mark Word存储了很多和当前对象运行时状态有关的数据,比如hashcode,指向锁记录的指针,偏向锁id等等。

    Mark Word只有32bit,并且是非结构化的,这样在不同的锁标志位下,不同的字段可以重用不同的比特位,能够节省空间。
    (mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。)
    数组对象的对象头还会存储着一个32bit的数组长度

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ge1OnsDJ-1604552466505)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104161150582.png)]

    对象中的锁就存放在对象头的Mark Word中,Mark Word的最后两位代表了锁标志位,分别是无锁、偏向锁、轻量级锁、重量级锁四种状态。

    同步锁synchronized

    Java中synchronized关键字可以用来同步线程,synchronized被编译后会生成monitorenter和monitorexit两个字节码指令。

    下面的同步锁例子,进行编译和反编译

    public class TestSync {
        private int num = 0;
    
        public void test() {
            for (int i = 0; i < 1000; i++) {
                synchronized (this) {
                    System.out.println("thread:" + Thread.currentThread().getId() + ",num:" + num++);
                }
            }
        }
    }
    
    public class Main2 {
        public static void main(String[] args) {
            TestSync sync = new TestSync();
    
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    sync.test();
                }
            });
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    sync.test();
                }
            });
            t1.start();
            t2.start();
        }
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dijg5KoN-1604552466508)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104165108826.png)]

    上面反编译后的字节码可以看到确实出现了monitorenter和monitorexit两个字节码指令

    Moniter(管程/监视器)

    你可以把它想象为一个房间,这个房间只能容纳一个线程,一个线程进入了moniter那么其他线程只能等待,只有当该线程退出,其他线程才有进入的机会

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GiIPpnZB-1604552466512)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104165711305.png)]

    首先Entry Set中聚集了一些想要进入monitor的线程,它们正处于waiting状态,假如一个名为A的线程成功进入monitor,那么它就处于Active状态,假设此时线程执行途中遇到了一个判断条件,需要它暂时让出执行权,那么它将进入Wait Set,此时线程的状态为Waiting,此时Entry Set中的线程就有机会进入monitor,假设一个线程B进入了monitor,并且执行完了自己的任务,那么它可以通过notify的形式去来唤醒Wait Set中的线程A,让线程A有机会能重新进入monitor,继续完成自己的任务。

    这就是synchronized同步机制,但是synchronized可能存在性能问题,因为synchronized被编译后实际上是monitorenter和monitorexit两个字节码指令,而Monitor是依赖操作系统的mutex lock来实现的,Java线程实际上是对操作系统线程的映射,所以每当唤醒和挂起一个线程的时候,都要转换到操作系统内核态,这种操作是重量级的,并且在一些情况下甚至切换时间本身将会超出线程执行任务的时间,使用synchronized将会对程序的性能产生严重的影响。

    后来从Java6开始,synchronized进行了优化,引入了偏向锁、轻量级锁,所以锁总共有四种状态,从低到高分别是无锁、偏向锁、轻量级锁、重量级锁,这就分别对应了Mark Word中的四种状态,注意,锁只能升级不能降级

    锁状态和锁升级

    1、无锁

    无锁,就是没有对资源进行锁定,所有线程都能访问同一资源,这就可能出现两种情况:

    1. 无竞争

      某个对象不会出现在多线程环境下或者说即使出现在了多线程环境下也不会出现竞争的情况,这样就无需对这个对象进行任何保护,这种情况让对象能被其他线程随意调用就可以了

    2. 存在竞争

      资源会被竞争但是我不想对资源进行锁定,不过还是想通过一些机制来控制多线程,比如说,如果有多个线程想要修改同一个值,我们不通过锁定资源的方式,而是通过其他方式来限制,同时,只有一个线程能修改成功,而修改失败的线程不断地去尝试修改,直至修改成功,这就是CAS(Compare and swap),CAS通过操作系统中的一条指令来实现,所以能保证操作的原子性,通过诸如CAS这种方式,我们可以进行无锁编程。

    上面也分析了依赖操作系统mutex lock导致性能低下的原因,所以在大部分情况下无锁的效率是很高的,但是并不能意味着无锁能够全面代替锁

    2、偏向锁

    假如一个对象被加锁了,但在实际运行时只有一个线程会获取这个对象锁,那么最理想的方式就是不通过线程状态切换,也不要通过CAS来获得锁,因为这多多少少还是会耗费一些资源。

    设想的是,最好对象能够认识这个线程,只要是这个线程过来,线程就会把锁交出去,“锁偏爱这个线程”称为偏向锁。

    偏向锁实现:在Mark Word中,当锁标志位是01时,那么判断倒数第3个bit是否为1。如果是1,那么代表当前对象的锁状态为偏向锁,否则为无锁,如果当前锁状态为偏向锁,于是再去读Mark Word前23个bit的线程ID,通过线程ID来确认当前想要获取对象锁的这个线程是不是之前的线程。

    假如情况发生了变化,对象发现目前不只有一个线程而是有多个线程在竞争锁,那么偏向锁将会升级为轻量级锁,当锁的状态还是偏向锁时,是通过Mark Word中的线程ID来找到占有这个锁的线程

    3、轻量级锁

    当升级为轻量级锁的时候,Mark Word已经不存放线程ID字段了,而是将前30位bit变为了指向线程栈中锁记录(Lock Record)的指针。

    当一个线程想要获取某个对象的锁时,假如看到锁标志位为00那么就知道它是轻量级锁,这时线程会在自己的虚拟机栈中开辟一块被称为Lock Record的空间中。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ddccvtd4-1604552466514)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104221052170.png)]

    Lock Record中存放的是对象头中的Mark Word的副本以及owner指针,线程通过CAS尝试获取锁,一旦获取那么将会复制该对象头中的Mark Word到Lock Record中,并将Lock Record中的owner指针指向该对象,另一方面对象的Mark Word的前30个bit会形成一个指针,指向线程虚拟机栈中的Lock Record,这样一来,就实现了线程和对象锁的绑定,互相知道了对方的存在。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vYMu5gPl-1604552466515)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201104222204396.png)]

    这时这个对象已经被绑定了,获取了这个对象锁的线程就可以去,指向一些任务,其他的线程将会自旋等待,自旋可以理解为一种轮询,线程自己在不断地循环,尝试着去看一下目标对象的锁有没有被释放,如果释放了那么就获取,如果没有释放那么就进行下一轮循环,这种方式区别于被操作系统挂起,因为如果对象的锁很快就会被释放的话,自旋就不需要进行系统中断和现场恢复,所以效率更高,但是自旋相当于CPU在空转,如果长时间自旋将会浪费CPU资源。

    于是出现了一种叫“适应性自旋”的优化,简单来说,自旋的时间不在固定了,而是由上一次在同一个锁上的自选时间以及锁状态,这两个条件决定,例如在同一个锁上,当前正在自旋等待的线程刚刚已经成功获得过锁,但是锁目前是被其他线程占用,那么虚拟机就会认为这次自旋很有可能会再次成功,进而它将允许更长的自旋时间,这就是适应性自旋

    4、重量级锁

    当有多个锁进行自旋,或者是自旋次数达到一定次数,轻量级锁将升级成为重量级锁,也就是使用moniter对线程进行管控。

    无锁编程

    AQS、CAS、JUC源码

    假设多个线程想要操作同一个资源对象,怎么确保每次操作的正确性?大部分人都会使用同步锁,但是同步锁是“悲观”的,“悲观”的意思简单来说就是操作系统会悲观的认为,如果不严格同步线程调用那么一定会产生异常错误,所以互斥锁会将资源锁定,只供一个线程调用,而阻塞其他线程,因此这种同步机制也叫做悲观锁,但是不是任何时候都适合用悲观锁的。

    比如在一些情况下,大部分调用可能都是读操作,那么就没有必要在每次调用的时候都锁定资源,或者在一些情况下,同步代码块执行的耗时远远小于线程切换的耗时,这样就有点本末倒置了,所以在这些情况下,我们不想让操作系统那么悲观,我们不想过度使用互斥锁。

    CAS(Compare And Swap)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6AugeTlQ-1604552466516)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105100939810.png)]

    为资源对象设置一个状态值,当资源对象的状态值为0的一瞬间A、B两个线程都读到了,此时这两条线程认为资源对象当前的状态值是0,于是它们将各自产生两个值,Old value代表之前读到的资源对象的状态值,New value代表想要将资源对象的状态值更新后的值。这里对A、B线程来说,Old value都是0,New value都是1,此时A、B线程争抢着去修改资源对象的状态值,然后占用它,假设A线程的运气比较好,率先获得了时间片,它将old value与资源对象的状态值进行compare发现一致,于是将资源对象的状态值swap为new value,而B线程因为迟了一步,此时资源对象的状态值已经被A线程修改成了1,所以B线程在compare的时候,发现Old value与预期的不相符,所以放弃swap操作。

    但是在实际应用中,我们不会让B线程就这么放弃,通常会使其进行自旋,自旋就是使其不断的重试CAS操作,通常会配置自旋次数来防止死循环。例如A线程在成功修改了资源对象状态值之后占用着资源对象,B线程会时不时关注资源对象的状态值,假如某一瞬间状态值变成了0,那么B线程将会重新去争抢资源对象。

    CAS多线程下难保证一致性

    CAS函数

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nP0X4C6b-1604552466516)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105100919989.png)]

    比如这个CAS没有进行任何的同步操作,说明还是线程不安全的。比如当A线程看到了资源的状态是0时,准备修改的一瞬间,很有可能B线程突然抢到了时间片,将资源状态修改成1,但是A线程对此是不知情的,也就资源状态修改成了1,这就出现了问题,A、B线程同时获得资源。(多条线程同时占用资源对象)

    所以compare和swap必须“被绑定”成为一个原子性操作,也就是对资源对象的状态值进行compare和swap,在某一时刻只能有一条线程进行操作。

    CAS要是原子性的

    各种不同架构的CPU都提供了指令级别的CAS原子操作,比如在x86架构下,通过cmpxchg指令支持cas。在ARM架构下,通过LL/SC来实现CAS。也就是说,不需要通过操作系统的同步原语(比如mutex),CPU已经原生地支持CAS,上层进行调用即可,这样就能够不再依赖锁来进行线程同步,但是不是说无锁能代替锁。

    乐观锁

    通过CAS来实现同步的工具,由于不会锁定资源,而且当线程需要修改共享资源的对象时,总是乐观地认为,对象状态值没有被其他线程修改过,而是每次自己都会主动尝试去compare状态值,相较于上面的悲观锁,这种同步机制也被称为乐观锁(实现上并没有用到锁,而是一种无锁的同步机制)。

    CAS在java中的应用

    现在假如想要用3条线程,将一个值,从0累加到100

    先来看不使用任何同步操作

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kyYFeNgM-1604552466517)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105103542281.png)]
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kgBfxoGh-1604552466518)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105103609267.png)]

    可以看到多条线程打印了相同的值,说明线程之间没有进行正确通信,常规的话,可以通过互斥锁来进行同步

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AJGEefbV-1604552466520)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105120041011.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-feOp682M-1604552466521)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105120112139.png)]

    可以看到是顺序执行的

    无锁同步(乐观锁)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FMHYuMrw-1604552466522)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\CAS7.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56rK04yc-1604552466523)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105121025042.png)]

    可以看到每个数字只输出了一次,线程正确同步

    接下来看源码

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8juJneqq-1604552466524)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105121350767.png)]

    AtomicInteger类主要的成员变量就是一个unsafe类型的实例和一个long类型的offset,是使用unsafe的cas操作对值进行更新,进一步看incrementAndGet()方法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QVNyw5mB-1604552466526)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\CAS8.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xXx6qKL5-1604552466527)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\CAS9.png)]

    可以看到直接调用了unsafe的getAndAddInt方法,确实就是调用了unsafe的compareAndSwapInt方法,从名字上看也可以知道它是一个CAS的操作,可以看到这里出现了一个循环,也就是自旋。

    假如这边CAS操作一直失败,也不会一直死循环下去,实际上自选的次数可以通过启动参数来配置,如果不配置默认是10。

    Unsafe类

    Unsafe类主要是用来执行一些底层的和平台相关的方法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qt0csl1j-1604552466528)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105123559751.png)]

    上面的例子,就是用到了unsafe调用底层cas的能力

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IXeKgsUT-1604552466529)(C:\Users\zhqsocool\AppData\Roaming\Typora\typora-user-images\image-20201105124229374.png)]

    可以看到compareAndSetInt修饰符是vative,说明它是一个本地方法,和具体的平台实现相关,如果你的cpu是x86架构,那么事实上这个本地方法将会调用系统的cmpxchg指令

    在openJDK源码中的两个目录的文件中可以看到对应的本地方法
    20201105125442928

    展开全文
  • java锁机制

    2019-08-10 16:36:43
    java锁机制 概念 java对象头Markword Java对象存储在堆(Heap)内存。那么一个Java对象到底包含什么呢?概括起来分为对象头、对象体和对齐字节。如下图所示: 对象的几个部分的作用: 1.对象头中的Mark Word...

    java锁机制

    概念

    java对象头Markword

    Java对象存储在堆(Heap)内存。那么一个Java对象到底包含什么呢?概括起来分为对象头、对象体和对齐字节。如下图所示:
    JVM中对象头的类型有两种(64位为例):

    普通对象

    Object Header(128 bits)
    Mark Word(64 bits) Klass Word(64 bits)

    数组对象

    Object Header (192 bits)
    Mark Word(64 bits) Klass Word(64 bits) array length(64bits)

    对象的几个部分的作用:

    1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,次要用途是配合GC、存放hashCode;

    2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

    3.数组长度也是占用32位(4字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

    4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

    5.对齐字是为了减少堆内存的碎片空间(不一定准确)。

    Mark Word(标记字)
    Java对象的状态主要靠Mark Word来标记,主要有5种,大部分与线程有关。这里以64位JVM为例:

    Mark Word (64 bits) State
    unused 25|identity_hashcode:31 |unused 1 | age:4 | biased_lock:1 | lock:2 Normal
    thread:54 | epoch:2 | unused 1|age:4 | biased_lock:1 | lock:2 Biased
    ptr_to_lock_record:62 | lock:2 Lightweight Locked
    ptr_to_heavyweight_monitor:62 | lock:2 Heavyweight Locked
    | lock:2 Marked for GC

    以上是Java对象处于5种不同状态时,Mark Word中64个位的表现形式,上面每一行代表对象处于某种状态时的样子。其中各部分的含义如下:

    lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。状态对应如下:
    biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。

    age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

    identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

    thread:持有偏向锁的线程ID。

    epoch:偏向时间戳。

    ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。

    ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

    以图的方式来单独描述的话,锁对象处于5种状态下的Mark Word分别表现如下:

    在这里插入图片描述

    Klass Word
    这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

    如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

    1. 每个Class的属性指针(即静态变量)

    2. 每个对象的属性指针(即对象变量)

    3. 普通对象数组的每个元素指针

    当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

    ** 数组长度 **
    如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

    synchronized介绍

    Java中提供了两种实现同步的基础语义:synchronized方法和synchronized块, 我们来看个demo:

    public class synchronize_class {
    	public void syncBlock(){
            synchronized (this){
                System.out.println("hello block");
            }
        }
        public synchronized void syncMethod(){
            System.out.println("hello method");
        }
    }
    

    通过javac synchronize_class.java将synchronize_class.java被编译成class文件的时候,synchronized关键字和synchronized方法的字节码略有不同,我们可以用javap -v 命令查看class文件对应的JVM字节码信息,部分信息如下:

    public com.thread.synchronize.synchronize_class();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
    
      public void syncBlock();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=3, args_size=1
             0: aload_0
             1: dup
             2: astore_1
             3: monitorenter
             4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             7: ldc           #3                  // String hello block
             9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            12: aload_1
            13: monitorexit
            14: goto          22
            17: astore_2
            18: aload_1
            19: monitorexit
            20: aload_2
            21: athrow
            22: return
          Exception table:
             from    to  target type
                 4    14    17   any
                17    20    17   any
          LineNumberTable:
            line 5: 0
            line 6: 4
            line 7: 12
            line 8: 22
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 17
              locals = [ class com/thread/synchronize/synchronize_class, class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
            frame_type = 250 /* chop */
              offset_delta = 4
    
      public synchronized void syncMethod();
        descriptor: ()V
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #5                  // String hello method
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 10: 0
            line 11: 8
    }
    SourceFile: "synchronize_class.java"
    

    从上面的中文注释处可以看到,对于synchronized关键字而言,javac在编译时,会生成对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。而对于synchronized方法而言,javac为其生成了一个ACC_SYNCHRONIZED关键字,在JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED修饰,则会先尝试获得锁。

    在JVM底层,对于这两种synchronized语义的实现大致相同,在后文中会选择一种进行详细分析。

    偏向锁

    Java是支持多线程的语言,因此在很多二方包、基础库中为了保证代码在多线程的情况下也能正常运行,也就是我们常说的线程安全,都会加入如synchronized这样的同步语义。但是在应用在实际运行时,很可能只有一个线程会调用相关同步方法。比如下面这个demo:

    import java.util.ArrayList;
    import java.util.List;
    
    public class SyncDemo1 {
    
        public static void main(String[] args) {
            SyncDemo1 syncDemo1 = new SyncDemo1();
            for (int i = 0; i < 100; i++) {
                syncDemo1.addString("test:" + i);
            }
        }
    
        private List<String> list = new ArrayList<>();
    
        public synchronized void addString(String s) {
            list.add(s);
        }
    
    }
    

    在这个demo中为了保证对list操纵时线程安全,对addString方法加了synchronized的修饰,但实际使用时却只有一个线程调用到该方法,对于轻量级锁而言,每次调用addString时,加锁解锁都有一个CAS操作;对于重量级锁而言,加锁也会有一个或多个CAS操作(这里的’一个‘、’多个‘数量词只是针对该demo,并不适用于所有场景)。

    在JDK1.6中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的CAS命令。我们来看看偏向锁是如何做的。

    对象创建
    当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(什么时候会关闭一个class的偏向模式下文会说,默认所有class的偏向模式都是是开启的),那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。

    加锁过程
    case 1:当该对象第一次被线程获得锁的时候,发现是匿名偏向状态,则会用CAS指令,将mark word中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。

    case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。

    case 3.当其他线程进入同步块时,发现已经有偏向的线程了,则会进入到撤销偏向锁的逻辑里,一般来说,会在safepoint中去查看偏向的线程是否还存活,如果存活且还在同步块中则将锁升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里;如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁。

    由此可见,偏向锁升级的时机为:当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。当然这个说法不绝对,因为还有批量重偏向这一机制。

    解锁过程
    当有其他线程尝试获得锁时,是根据遍历偏向线程的lock record来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的thread id。

    下图展示了锁状态的转换流程:
    在这里插入图片描述
    另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0来关闭延迟。

    批量重偏向与撤销
    从上文偏向锁的加锁解锁过程中可以看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。safe point这个词我们在GC中经常会提到,其代表了一个状态,在该状态下所有线程都是暂停的(大概这么个意思),详细可以看这篇文章。总之,偏向锁的撤销是有一定成本的,如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。

    存在如下两种情况:

    1.一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。

    2.存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列。

    批量重偏向(bulk rebias)机制是为了解决第一种场景。批量撤销(bulk revoke)则是为了解决第二种场景。

    其做法是:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。

    当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

    轻量锁

    JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。

    线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个Lock Record,其包括一个用于存储对象头中的 mark word(官方称之为Displaced Mark Word)以及一个指向对象的指针。下图右边的部分就是一个Lock Record

    img
    加锁过程
    1.在线程栈中创建一个Lock Record,将其obj(即上图的Object reference)字段指向锁对象。

    2.直接通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。

    3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。然后结束。

    4.走到这一步说明发生了竞争,需要膨胀为重量级锁。

    解锁过程
    1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

    2.如果Lock Record的Displaced Mark Word为null,代表这是一次重入,将obj设置为null后continue。

    3.如果Lock Record的Displaced Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为Displaced Mark Word。如果成功,则continue,否则膨胀为重量级锁。

    重量锁

    重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。

    重量级锁的状态下,对象的mark word为指向一个堆中monitor对象的指针。

    一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。

    其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。

    [外链图片转存失败(img-V6Bed1up-1565426115093)(C:\Users\sihangjun\Desktop\微信图片_20190810153329.png)]

    当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。

    如果一个线程在同步块中调用了Object#wait方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。

    以上只是对重量级锁流程的一个简述,其中涉及到的很多细节,比如ObjectMonitor对象从哪来?释放锁时是将cxq中的元素移动到EntryList的尾部还是头部?notfiy时,是将ObjectWaiter移动到EntryList的尾部还是头部?

    总结

    Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁 的顺序升级。
    在这里插入图片描述

    展开全文
  • java 锁机制

    2020-10-19 16:12:38
    Java多线程加锁机制,有两种: ...其实都比较坑,如果能先系统讲了Synchronized锁机制,接着讲显式Lock锁机制,那就很容易理解了。也不需要跨那么多章节。 那么接下来我们就开始吧~ 一、synchro

    Java多线程加锁机制,有两种:

    • Synchronized

    • 显式Lock

    不得不唠叨几句:

    • 在《Java核心技术卷 一》是先讲比较难的显式Lock,而再讲的是比较简单的Synchronized

    • 而《Java并发编程实战》在前4章零散地讲解了Synchronized,将显式Lock放到了13章

    其实都比较坑,如果能先系统讲了Synchronized锁机制,接着讲显式Lock锁机制,那就很容易理解了。也不需要跨那么多章节。

    那么接下来我们就开始吧~

    一、synchronized锁

    1.1synchronized锁是什么?

    synchronized是Java的一个关键字,它能够将代码块(方法)锁起来

    • 它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能~

        public synchronized void test() {
            // 关注公众号Java3y
            // doSomething
        }
    

    synchronized是一种互斥锁

    • 一次只能允许一个线程进入被锁住的代码块

    synchronized是一种内置锁/监视器锁

    • Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用对象的内置锁(监视器)来将代码块(方法)锁定的!

    1.2synchronized用处是什么?

    • synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)

    • synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)

    Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。

    1.3synchronized的原理

    我们首先来看一段synchronized修饰方法和代码块的代码:

    public class Main {
        //修饰方法
        public synchronized void test1(){
    
        }
    
    
        public void test2(){
            // 修饰代码块
            synchronized (this){
    
            }
        }
    }
    

    来反编译看一下:

     

    同步代码块

    • monitorenter和monitorexit指令实现的

    同步方法(在这看不出来需要看JVM底层实现)

    • 方法修饰符上的ACC_SYNCHRONIZED实现。

    synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有

    具体可参考:

    • https://blog.csdn.net/chenssy/article/details/54883355

    • https://blog.csdn.net/u012465296/article/details/53022317

    1.4synchronized如何使用

    synchronized一般我们用来修饰三种东西:

    • 修饰普通方法

    • 修饰代码块

    • 修饰静态方法

    1.4.1修饰普通方法:

    用的锁是Java3y对象(内置锁)

    public class Java3y {
    
    
        // 修饰普通方法,此时用的锁是Java3y对象(内置锁)
        public synchronized void test() {
            // 关注公众号Java3y
            // doSomething
        }
    
    }
    

    1.4.2修饰代码块:

    用的锁是Java3y对象(内置锁)--->this

    public class Java3y {
    
        public  void test() {
    
            // 修饰代码块,此时用的锁是Java3y对象(内置锁)--->this
            synchronized (this){
                // 关注公众号Java3y
                // doSomething
            }
        }
    }
    

    当然了,我们使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁)

    所以,我们可以这样干:

    public class Java3y {
    
    
        // 使用object作为锁(任何对象都有对应的锁标记,object也不例外)
        private Object object = new Object();
    
    
        public void test() {
    
            // 修饰代码块,此时用的锁是自己创建的锁Object
            synchronized (object){
                // 关注公众号Java3y
                // doSomething
            }
        }
    
    }
    

    上面那种方式(随便使用一个对象作为锁)在书上称之为-->客户端锁,这是不建议使用的

    书上想要实现的功能是:给ArrayList添加一个putIfAbsent(),这需要是线程安全的。

    假定直接添加synchronized是不可行的

     

    使用客户端锁,会将当前的实现与原本的list耦合了

     

    书上给出的办法是使用组合的方式(也就是装饰器模式)

     

    1.4.3修饰静态方法

    获取到的是类锁(类的字节码文件对象):Java3y.class

    public class Java3y {
    
        // 修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)-->Java3y.class
        public synchronized void test() {
    
            // 关注公众号Java3y
            // doSomething
        }
    }
    

    1.4.4类锁与对象锁

    synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。

    • 它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的

    public class SynchoronizedDemo {
    
        //synchronized修饰非静态方法
        public synchronized void function() throws InterruptedException {
            for (int i = 0; i <3; i++) {
                Thread.sleep(1000);
                System.out.println("function running...");
            }
        }
        //synchronized修饰静态方法
        public static synchronized void staticFunction()
                throws InterruptedException {
            for (int i = 0; i < 3; i++) {
                Thread.sleep(1000);
                System.out.println("Static function running...");
            }
        }
    
        public static void main(String[] args) {
            final SynchoronizedDemo demo = new SynchoronizedDemo();
    
            // 创建线程执行静态方法
            Thread t1 = new Thread(() -> {
                try {
                    staticFunction();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            // 创建线程执行实例方法
            Thread t2 = new Thread(() -> {
                try {
                    demo.function();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            // 启动
            t1.start();
            t2.start();
        }
    }
    

    结果证明:类锁和对象锁是不会冲突的

     

    1.5重入锁

    我们来看下面的代码:

    public class Widget {
    
        // 锁住了
        public synchronized void doSomething() {
            ...
        }
    }
    
    public class LoggingWidget extends Widget {
    
        // 锁住了
        public synchronized void doSomething() {
            System.out.println(toString() + ": calling doSomething");
            super.doSomething();
        }
    }
    
    1. 当线程A进入到LoggingWidget的doSomething()方法时,此时拿到了LoggingWidget实例对象的锁

    2. 随后在方法上又调用了父类Widget的doSomething()方法,它又是被synchronized修饰

    3. 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的doSomething()方法还需要一把锁吗?

    不需要的!

    因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续“开锁”进去的!

    这就是内置锁的可重入性

    1.6释放锁的时机

    1. 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。

    2. 当一个线程执行的代码出现异常时,其所持有的锁会自动释放

    • 不会由于异常导致出现死锁现象~

    二、Lock显式锁

    2.1Lock显式锁简单介绍

    Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的~

    Lock显式锁是一个接口,我们来看看:

     

    随便翻译一下他的顶部注释,看看是干嘛用的:

     

    可以简单概括一下:

    • Lock方式来获取锁支持中断、超时不获取、是非阻塞的

    • 提高了语义化,哪里加锁,哪里解锁都得写出来

    • Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁

    • 支持Condition条件对象

    • 允许多个读线程同时访问共享资源

    2.2synchronized锁和Lock锁使用哪个

    前面说了,Lock显式锁给我们的程序带来了很多的灵活性,很多特性都是Synchronized锁没有的。那Synchronized锁有没有存在的必要??

    必须是有的!!Lock锁在刚出来的时候很多性能方面都比Synchronized锁要好,但是从JDK1.6开始Synchronized锁就做了各种的优化(毕竟亲儿子,牛逼)

    • 优化操作:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁。

    • 详情可参考:https://blog.csdn.net/chenssy/article/details/54883355

    所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大!而Synchronized锁用起来又特别简单。Lock锁还得顾忌到它的特性,要手动释放锁才行(如果忘了释放,这就是一个隐患)

    所以说,我们绝大部分时候还是会使用Synchronized锁,用到了Lock锁提及的特性,带来的灵活性才会考虑使用Lock显式锁~

    2.3公平锁

    公平锁理解起来非常简单:

    • 线程将按照它们发出请求的顺序来获取锁

    非公平锁就是:

    • 线程发出请求的时可以“插队”获取锁

    Lock和synchronize都是默认使用非公平锁的。如果不是必要的情况下,不要使用公平锁

    • 公平锁会来带一些性能的消耗的

    四、最后

    本文讲了synchronized内置锁和简单描述了一下Lock显式锁,总得来说:

    • synchronized好用,简单,性能不差

    • 没有使用到Lock显式锁的特性就不要使用Lock锁了。

    展开全文
  • Java 锁机制

    2019-08-11 11:53:37
    一、按照性质分类 公平 / 非公平: 公平是指多个线程按照申请的顺序来获取。非公平是指多个线程获取的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取。对于...

    一、按照锁性质分类

    公平锁 / 非公平锁:

           公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。对于ReentrantLock而言,通过构造函数指定该锁是否为公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,只能是一种非公平锁

    乐观锁 / 悲观锁:

           乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

           乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新), 如果失败则要重复读-比较-写的操作。 java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败

           悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized。

    独享锁 / 共享锁 (互斥锁 / 读写锁)

           独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。

    可重入锁(递归锁):

           可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。

     

    二、按照锁设计方案分类:

    自旋锁 / 自适应自旋锁:

           自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗

           自适应自旋锁意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁并且持有的锁的线程正在进行中,那么虚拟机就会认为这次自旋也很有可能成功,进而它将允许自旋等待持续更长的时间。如果对于某个锁,自旋很少成功获取过,那在以后要获取这个锁时将可能省略掉自旋过程,以免浪费处理器资源。

    锁粗化 / 锁消除:

           锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。锁粗化是指如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。锁粗化和锁消除其实设计原理都差不多,都是为了减少没必要的加锁。

    偏向锁 / 轻量级锁 / 重量级锁:

           这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

           偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

           轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

           重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

    分段锁:

           分段锁其实是一种锁的设计,并不是具体的一种锁。分段锁的设计目的是细化锁的粒度当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

    展开全文
  • JAVA锁机制

    千次阅读 2018-11-23 23:09:59
    总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。  version方式...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 10,259
精华内容 4,103
关键字:

java锁机制

java 订阅