精华内容
下载资源
问答
  • Java锁机制详解.pdf

    2017-11-02 17:05:48
    Java锁机制详解.pdf java线程 java多线程 Java锁机制详解.pdf java线程 java多线程
  • java锁升级过程

    万次阅读 多人点赞 2020-03-14 20:32:17
    java中对象有4种状态:(级别从低到高) 1.无锁状态 2.偏向状态 3.轻量级状态 4.重量级状态 对象头分两部分信息,第一部分用于存储哈希码、GC分代年龄等,这部分数据被称为"Mark Word"。在32位的HotSpot...

    java中对象锁有4种状态:(级别从低到高)
    1.无锁状态
    2.偏向锁状态
    3.轻量级锁状态
    4.重量级锁状态

    对象头分两部分信息,第一部分用于存储哈希码、GC分代年龄等,这部分数据被称为"Mark Word"。在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32bit空间中的25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容见下表:
    在这里插入图片描述
    锁升级的方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

    1.偏向锁
    偏向锁是JDK6中引入的一项锁优化,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
    偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。

    2.轻量级锁
    如果明显存在其它线程申请锁,那么偏向锁将很快升级为轻量级锁。

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

    4.重量级锁
    指的是原始的Synchronized的实现,重量级锁的特点:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。

    锁升级场景:

    场景1: 程序不会有锁的竞争。
    那么这种情况我们不需要加锁,所以这种情况下对象锁状态为无锁。

    场景2: 经常只有某一个线程来加锁。
    加锁过程:也许获取锁的经常为同一个线程,这种情况下为了避免加锁造成的性能开销,所以并不会加实际意义上的锁,偏向锁的执行流程如下:
    1、线程首先检查该对象头的线程ID是否为当前线程;
    2、A:如果对象头的线程ID和当前线程ID一直,则直接执行代码;B:如果不是当前线程ID则使用CAS方式替换对象头中的线程ID,如果使用CAS替换不成功则说明有线程正在执行,存在锁的竞争,这时需要撤销偏向锁,升级为轻量级锁。
    3、如果CAS替换成功,则把对象头的线程ID改为自己的线程ID,然后执行代码。
    4、执行代码完成之后释放锁,把对象头的线程ID修改为空。

    场景3: 有线程来参与锁的竞争,但是获取锁的冲突时间很短。
    当开始有锁的冲突了,那么偏向锁就会升级到轻量级锁;线程获取锁出现冲突时,线程必须做出决定是继续在这里等,还是回家等别人打电话通知,而轻量级锁的路基就是采用继续在这里等的方式,当发现有锁冲突,线程首先会使用自旋的方式循环在这里获取锁,因为使用自旋的方式非常消耗CPU,当一定时间内通过自旋的方式无法获取到锁的话,那么锁就开始升级为重量级锁了。

    场景4: 有大量的线程参与锁的竞争,冲突性很高。
    我们知道当获取锁冲突多,时间越长的时候,我们的线程肯定无法继续在这里死等了,所以只好先休息,然后等前面获取锁的线程释放了锁之后再开启下一轮的锁竞争,而这种形式就是我们的重量级锁。

    展开全文
  • Java锁消除和锁粗化

    万次阅读 多人点赞 2018-06-02 12:29:49
    转载自:Java锁消除非商业转载,可联系本人删除概述锁消除是Java虚拟机在JIT编译是,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。实验看如下代码:package...

    锁粗化

    通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

    一种极端的情况如下:

    public void doSomethingMethod(){
        synchronized(lock){
            //do some thing
        }
        //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
        synchronized(lock){
            //do other thing
        }
    }

    上面的代码是有两块需要同步操作的,但在这两块需要同步操作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么我们就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗,合并后的代码如下:

    public void doSomethingMethod(){
        //进行锁粗化:整合成一次锁请求、同步、释放
        synchronized(lock){
            //do some thing
            //做其它不需要同步但能很快执行完的工作
            //do other thing
        }
    }

    注意:这样做是有前提的,就是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。

    另一种需要锁粗化的极端的情况是:

    for(int i=0;i<size;i++){
        synchronized(lock){
        }
    }

    上面代码每次循环都会进行锁的请求、同步与释放,看起来貌似没什么问题,且在jdk内部会对这类代码锁的请求做一些优化,但是还不如把加锁代码写在循环体的外面,这样一次锁的请求就可以达到我们的要求,除非有特殊的需要:循环需要花很长时间,但其它线程等不起,要给它们执行的机会。

    锁粗化后的代码如下:

    synchronized(lock){
        for(int i=0;i<size;i++){
        }
    }

    锁消除

    锁消除是发生在编译器级别的一种锁优化方式。
    有时候我们写的代码完全不需要加锁,却执行了加锁操作。

    比如,StringBuffer类的append操作:

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

    从源码中可以看出,append方法用了synchronized关键词,它是线程安全的。但我们可能仅在线程内部把StringBuffer当作局部变量使用:

    package com.leeib.thread;
    public class Demo {
        public static void main(String[] args) {
            long start = System.currentTimeMillis();
            int size = 10000;
            for (int i = 0; i < size; i++) {
                createStringBuffer("Hyes", "为分享技术而生");
            }
            long timeCost = System.currentTimeMillis() - start;
            System.out.println("createStringBuffer:" + timeCost + " ms");
        }
        public static String createStringBuffer(String str1, String str2) {
            StringBuffer sBuf = new StringBuffer();
            sBuf.append(str1);// append方法是同步操作
            sBuf.append(str2);
            return sBuf.toString();
        }
    }

    代码中createStringBuffer方法中的局部对象sBuf,就只在该方法内的作用域有效,不同线程同时调用createStringBuffer()方法时,都会创建不同的sBuf对象,因此此时的append操作若是使用同步操作,就是白白浪费的系统资源。

    这时我们可以通过编译器将其优化,将锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析:

    -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

    其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。

    逃逸分析:比如上面的代码,它要看sBuf是否可能逃出它的作用域?如果将sBuf作为方法的返回值进行返回,那么它在方法外部可能被当作一个全局对象使用,就有可能发生线程安全问题,这时就可以说sBuf这个对象发生逃逸了,因而不应将append操作的锁消除,但我们上面的代码没有发生锁逃逸,锁消除就可以带来一定的性能提升。

    转载自: java锁优化的5个方法

    ====================================================

     锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。 
      也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。我们来看看下面代码清单13-6中的例子,这段非常简单的代码仅仅是输出三个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。 

      代码清单 13-6 一段看起来没有同步的代码 

    Java代码   收藏代码
    1. public String concatString(String s1, String s2, String s3) {  
    2.     return s1 + s2 + s3;  
    3. }  
      我们也知道,由于String是一个不可变的类,对字符串的连接操作总是通过生成新的String对象来进行的,因此Javac编译器会对String连接做自动优化。在JDK 1.5之前,会转化为StringBuffer对象的连续append()操作, 在JDK 1.5及以后的版本中,会转化为StringBuilder对象的连续append()操作。 即代码清单13-6中的代码可能会变成代码清单13-7的样子 。 

      代码清单 13-7 Javac转化后的字符串连接操作  
    Java代码   收藏代码
    1. public String concatString(String s1, String s2, String s3) {  
    2.     StringBuffer sb = new StringBuffer();  
    3.     sb.append(s1);  
    4.     sb.append(s2);  
    5.     sb.append(s3);  
    6.     return sb.toString();  
    7. }  
    (注1:实事求是地说,既然谈到锁削除与逃逸分析,那虚拟机就不可能是JDK 1.5之前的版本,所以实际上会转化为非线程安全的StringBuilder来完成字符串拼接,并不会加锁。但是这也不影响笔者用这个例子证明Java对象中同步的普遍性。)  

      现在大家还认为这段代码没有涉及同步吗?每个StringBuffer.append()方法中都有一个同步块,锁就是sb对象。虚拟机观察变量sb,很快就会发现它的动态作用域被限制在concatString()方法内部。也就是sb的所有引用永远不会“逃逸”到concatString()方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地削除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

    =====================================

    This is an example of manual "lock coarsening" and may have been done to get a performance boost.

    Consider these two snippets:

    StringBuffer b = new StringBuffer();
    for(int i = 0 ; i < 100; i++){
        b.append(i);
    }
    

    versus:

    StringBuffer b = new StringBuffer();
    synchronized(b){
      for(int i = 0 ; i < 100; i++){
         b.append(i);
      }
    }
    

    In the first case, the StringBuffer must acquire and release a lock 100 times (because append is a synchronized method), whereas in the second case, the lock is acquired and released only once. This can give you a performance boost and is probably why the author did it. In some cases, the compiler can perform this lock coarsening for you (but not around looping constructs because you could end up holding a lock for long periods of time).

    By the way, the compiler can detect that an object is not "escaping" from a method and so remove acquiring and releasing locks on the object altogether (lock elision) since no other thread can access the object anyway. A lot of work has been done on this in JDK7.

    转载自: Synchronization on the local variables

    ===================================

    转载自:Java锁消除

    非商业转载,可联系本人删除

    概述

    锁消除是Java虚拟机在JIT编译是,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

    实验

    看如下代码:

    package com.winwill.lock;
    
    /**
     * @author qifuguang
     * @date 15/6/5 14:11
     */
    public class TestLockEliminate {
        public static String getString(String s1, String s2) {
            StringBuffer sb = new StringBuffer();
            sb.append(s1);
            sb.append(s2);
            return sb.toString();
        }
    
        public static void main(String[] args) {
            long tsStart = System.currentTimeMillis();
            for (int i = 0; i < 1000000; i++) {
                getString("TestLockEliminate ", "Suffix");
            }
            System.out.println("一共耗费:" + (System.currentTimeMillis() - tsStart) + " ms");
        }
    }

    getString()方法中的StringBuffer数以函数内部的局部变量,进作用于方法内部,不可能逃逸出该方法,因此他就不可能被多个线程同时访问,也就没有资源的竞争,但是StringBuffer的append操作却需要执行同步操作:

        @Override
        public synchronized StringBuffer append(String str) {
            toStringCache = null;
            super.append(str);
            return this;
        }

    逃逸分析和锁消除分别可以使用参数-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(锁消除必须在-server模式下)开启。使用如下参数运行上面的程序:

    -XX:+DoEscapeAnalysis -XX:-EliminateLocks

    得到如下结果: 
    这里写图片描述

    使用如下命令运行程序:

    -XX:+DoEscapeAnalysis -XX:+EliminateLocks

    得到如下结果: 
    这里写图片描述

    结论

    由上面的例子可以看出,关闭了锁消除之后,StringBuffer每次append都会进行锁的申请,浪费了不必要的时间,开启锁消除之后性能得到了提高。

    展开全文
  • java锁总结

    千次阅读 2018-06-01 17:20:31
    在介绍java锁之前,先说下什么是markword,markword是java对象数据结构中的一部分,要详细了解java对象的结构可以 点击这里 ,这里只做markword的详细介绍,因为对象的markword和java各种类型的锁密切相关; markword...
    

    之前做过一个测试,详情见这篇文章《多线程 +1操作的几种实现方式,及效率对比》,当时对这个测试结果很疑惑,反复执行过多次,发现结果是一样的:
    1. 单线程下synchronized效率最高(当时感觉它的效率应该是最差才对);
    2. AtomicInteger效率最不稳定,不同并发情况下表现不一样:短时间低并发下,效率比synchronized高,有时甚至比LongAdder还高出一点,但是高并发下,性能还不如synchronized,不同情况下性能表现很不稳定;
    3. LongAdder性能稳定,在各种并发情况下表现都不错,整体表现最好,短时间的低并发下比AtomicInteger性能差一点,长时间高并发下性能最高(可以让AtomicInteger下台了);

    这篇文章我们就去揭秘,为什么会是这个测试结果!

    理解锁的基础知识

    如果想要透彻的理解java锁的来龙去脉,需要先了解以下基础知识。

    基础知识之一:锁的类型

    锁从宏观上分类,分为悲观锁与乐观锁。

    乐观锁

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

    java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

    悲观锁

    悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

    基础知识之二:java线程阻塞的代价

    java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

    1. 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
    2. 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

    synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

    明确java线程切换的代价,是理解java中各种锁的优缺点的基础之一。

    基础知识之三:markword

    在介绍java锁之前,先说下什么是markword,markword是java对象数据结构中的一部分,要详细了解java对象的结构可以点击这里,这里只做markword的详细介绍,因为对象的markword和java各种类型的锁密切相关;

    markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,如下表所示:

    状态标志位存储内容
    未锁定01对象哈希码、对象分代年龄
    轻量级锁定00指向锁记录的指针
    膨胀(重量级锁定)10执行重量级锁定的指针
    GC标记11空(不需要记录信息)
    可偏向01偏向线程ID、偏向时间戳、对象分代年龄

    32位虚拟机在不同状态下markword结构如下图所示:

    这里写图片描述

    了解了markword结构,有助于后面了解java锁的加锁解锁过程;

    小结

    前面提到了java的4种锁,他们分别是重量级锁、自旋锁、轻量级锁和偏向锁,
    不同的锁有不同特点,每种锁只有在其特定的场景下,才会有出色的表现,java中没有哪种锁能够在所有情况下都能有出色的效率,引入这么多锁的原因就是为了应对不同的情况;

    前面讲到了重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁,所以现在你就能够大致理解了他们的适用范围,但是具体如何使用这几种锁呢,就要看后面的具体分析他们的特性;

    java中的锁

    自旋锁

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

    但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。

    如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

    自旋锁的优缺点

    自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

    但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;

    自旋锁时间阈值

    自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!

    JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化

    1. 如果平均负载小于CPUs则一直自旋

    2. 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞

    3. 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞

    4. 如果CPU处于节电模式则停止自旋

    5. 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)

    6. 自旋时会适当放弃线程优先级之间的差异

    自旋锁的开启

    JDK1.6中-XX:+UseSpinning开启;
    -XX:PreBlockSpin=10 为自旋次数;
    JDK1.7后,去掉此参数,由jvm控制;

    重量级锁Synchronized

    Synchronized的作用

    在JDK1.5之前都是使用synchronized关键字保证同步的,Synchronized的作用相信大家都已经非常熟悉了;

    它可以把任意一个非NULL的对象当作锁。

    1. 作用于方法时,锁住的是对象的实例(this);
    2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
    3. synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。

    Synchronized的实现

    实现如下图所示;

    这里写图片描述

    它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

    1. Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;

    2. Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;

    3. Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;

    4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;

    5. Owner:当前已经获取到所资源的线程被称为Owner;

    6. !Owner:当前释放锁的线程。

    JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

    OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

    处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

    Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

    偏向锁

    Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。
    偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
    如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

    它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

    偏向锁的实现

    偏向锁获取过程:
    1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。

    2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。

    3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。

    4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)

    5. 执行同步代码。

    注意:第四步中到达安全点safepoint会导致stop the word,时间很短。

    偏向锁的释放:

    偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

    偏向锁的适用场景

    始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;
    在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;

    查看停顿–安全点停顿日志

    要查看安全点停顿,可以打开安全点日志,通过设置JVM参数 -XX:+PrintGCApplicationStoppedTime 会打出系统停止的时间,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 这两个参数会打印出详细信息,可以查看到使用偏向锁导致的停顿,时间非常短暂,但是争用严重的情况下,停顿次数也会非常多;

    注意:安全点日志不能一直打开:
    1. 安全点日志默认输出到stdout,一是stdout日志的整洁性,二是stdout所重定向的文件如果不在/dev/shm,可能被锁。
    2. 对于一些很短的停顿,比如取消偏向锁,打印的消耗比停顿本身还大。
    3. 安全点日志是在安全点内打印的,本身加大了安全点的停顿时间。

    所以安全日志应该只在问题排查时打开。
    如果在生产系统上要打开,再再增加下面四个参数:
    -XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log
    打开Diagnostic(只是开放了更多的flag可选,不会主动激活某个flag),关掉输出VM日志到stdout,输出到独立文件,/dev/shm目录(内存文件系统)。

    这里写图片描述

    此日志分三部分:
    第一部分是时间戳,VM Operation的类型
    第二部分是线程概况,被中括号括起来
    total: 安全点里的总线程数
    initially_running: 安全点开始时正在运行状态的线程数
    wait_to_block: 在VM Operation开始前需要等待其暂停的线程数

    第三部分是到达安全点时的各个阶段以及执行操作所花的时间,其中最重要的是vmop

    • spin: 等待线程响应safepoint号召的时间;
    • block: 暂停所有线程所用的时间;
    • sync: 等于 spin+block,这是从开始到进入安全点所耗的时间,可用于判断进入安全点耗时;
    • cleanup: 清理所用时间;
    • vmop: 真正执行VM Operation的时间。

    可见,那些很多但又很短的安全点,全都是RevokeBias, 高并发的应用会禁用掉偏向锁。

    jvm开启/关闭偏向锁

    • 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    • 关闭偏向锁:-XX:-UseBiasedLocking

    轻量级锁

    轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
    轻量级锁的加锁过程:

    1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图:
        这里写图片描述所示。

    2. 拷贝对象头中的Mark Word复制到锁记录中;

    3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。

    4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。
        这里写图片描述

    5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

    轻量级锁的释放

    释放锁线程视角:由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。

    因为重量级锁被修改了,所有display mark word和原来的markword不一样了。

    怎么补救,就是进入mutex前,compare一下obj的markword状态。确认该markword是否被其他线程持有。

    此时如果线程已经释放了markword,那么通过CAS后就可以直接进入线程,无需进入mutex,就这个作用。

    尝试获取锁线程视角:如果线程尝试获取锁的时候,轻量锁正被其他线程占有,那么它就会修改markword,修改重量级锁,表示该进入重量锁了。

    还有一个注意点:等待轻量锁的线程不会阻塞,它会一直自旋等待锁,并如上所说修改markword。

    这就是自旋锁,尝试获取锁的线程,在没有获得锁的时候,不被挂起,而转而去执行一个空循环,即自旋。在若干个自旋后,如果还没有获得锁,则才被挂起,获得锁,则执行代码。

    总结

    这里写图片描述

    synchronized的执行过程:
    1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
    2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
    3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
    4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
    5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
    6. 如果自旋成功则依然处于轻量级状态。
    7. 如果自旋失败,则升级为重量级锁。

    上面几种锁都是JVM自己内部实现,当我们执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用情况,决定如何执行同步操作;

    在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;

    偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;

    如果线程争用激烈,那么应该禁用偏向锁。

    锁优化

    以上介绍的锁不是我们代码中能够控制的,但是借鉴上面的思想,我们可以优化我们自己线程的加锁操作;

    减少锁的时间

    不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;

    减少锁的粒度

    它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;

    java中很多数据结构都是采用这种方法提高并发操作的效率:

    ConcurrentHashMap

    java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组

    Segment< K,V >[] segments

    Segment继承自ReenTrantLock,所以每个Segment就是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。

    LongAdder

    LongAdder 实现思路也类似ConcurrentHashMap,LongAdder有一个根据当前并发状况动态改变的Cell数组,Cell对象里面有一个long类型的value用来存储值;
    开始没有并发争用的时候或者是cells数组正在初始化的时候,会使用cas来将值累加到成员变量的base上,在并发争用的情况下,LongAdder会初始化cells数组,在Cell数组中选定一个Cell加锁,数组有多少个cell,就允许同时有多少线程进行修改,最后将数组中每个Cell中的value相加,在加上base的值,就是最终的值;cell数组还能根据当前线程争用情况进行扩容,初始长度为2,每次扩容会增长一倍,直到扩容到大于等于cpu数量就不再扩容,这也就是为什么LongAdder比cas和AtomicInteger效率要高的原因,后面两者都是volatile+cas实现的,他们的竞争维度是1,LongAdder的竞争维度为“Cell个数+1”为什么要+1?因为它还有一个base,如果竞争不到锁还会尝试将数值加到base上;

    LinkedBlockingQueue

    LinkedBlockingQueue也体现了这样的思想,在队列头入队,在队列尾出队,入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高;

    拆锁的粒度不能无限拆,最多可以将一个锁拆为当前cup数量个锁即可;

    锁粗化

    大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度;
    在以下场景下需要粗化锁的粒度:
    假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;

    使用读写锁

    ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写;

    读写分离

    CopyOnWriteArrayList 、CopyOnWriteArraySet
    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
     CopyOnWrite并发容器用于读多写少的并发场景,因为,读的时候没有锁,但是对其进行更改的时候是会加锁的,否则会导致多个线程同时复制出多个副本,各自修改各自的;

    使用cas

    如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择;

    消除缓存行的伪共享

    除了我们在代码中使用的同步锁和jvm自己内置的同步锁外,还有一种隐藏的锁就是缓存行,它也被称为性能杀手。
    在多核cup的处理器中,每个cup都有自己独占的一级缓存、二级缓存,甚至还有一个共享的三级缓存,为了提高性能,cpu读写数据是以缓存行为最小单元读写的;32位的cpu缓存行为32字节,64位cup的缓存行为64字节,这就导致了一些问题。
    例如,多个不需要同步的变量因为存储在连续的32字节或64字节里面,当需要其中的一个变量时,就将它们作为一个缓存行一起加载到某个cup-1私有的缓存中(虽然只需要一个变量,但是cpu读取会以缓存行为最小单位,将其相邻的变量一起读入),被读入cpu缓存的变量相当于是对主内存变量的一个拷贝,也相当于变相的将在同一个缓存行中的几个变量加了一把锁,这个缓存行中任何一个变量发生了变化,当cup-2需要读取这个缓存行时,就需要先将cup-1中被改变了的整个缓存行更新回主存(即使其它变量没有更改),然后cup-2才能够读取,而cup-2可能需要更改这个缓存行的变量与cpu-1已经更改的缓存行中的变量是不一样的,所以这相当于给几个毫不相关的变量加了一把同步锁;
    为了防止伪共享,不同jdk版本实现方式是不一样的:
    1. 在jdk1.7之前会 将需要独占缓存行的变量前后添加一组long类型的变量,依靠这些无意义的数组的填充做到一个变量自己独占一个缓存行;
    2. 在jdk1.7因为jvm会将这些没有用到的变量优化掉,所以采用继承一个声明了好多long变量的类的方式来实现;
    3. 在jdk1.8中通过添加sun.misc.Contended注解来解决这个问题,若要使该注解有效必须在jvm中添加以下参数:
    -XX:-RestrictContended

    sun.misc.Contended注解会在变量前面添加128字节的padding将当前变量与其他变量进行隔离;
    关于什么是缓存行,jdk是如何避免缓存行的,网上有非常多的解释,在这里就不再深入讲解了;



    转:https://blog.csdn.net/zqz_zqz/article/details/70233767

    其它方式等待着大家一起补充



    展开全文
  • Java锁及实现方式

    千次阅读 2019-11-26 14:03:50
    Java为了实现同步及线程安全,也会定义不同的。所谓的同步操作即原子操作(atomic operation)意为“不可被中断的一个或一系列操作”,类似数据库中的事务。 线程安全实现方式 互斥同步(机制) 互斥是实现...

    锁的概念在数据库出现比较多,为了实现数据库的不同隔离级别,数据库会定义不同的锁类型。Java为了实现同步及线程安全,也会定义不同的锁。所谓的同步操作即原子操作(atomic operation)意为“不可被中断的一个或一系列操作”,类似数据库中的事务。

    线程安全实现方式

    互斥同步(锁机制)

    互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。

    Java主要实现方式:synchronized和ReentrantLock

    ReentrantLock 实现等待可中断、 可实现公平锁, 以及锁可以绑定多个条件。

    等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

    公平锁是指多个线程在等待同一个锁时, 必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时, 任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的, 但可以通过带布尔值的构造函数要求使用公平锁。

    锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll() 方法可以实现一个隐含的条件, 如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition() 方法即可。

    非阻塞同步(使用循环CAS实现原子操作)

    基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。

    因为我们需要操作和冲突检测这两个步骤具备原子性,靠什么来保证呢? 如果这里再使用互斥同步来保证就失去意义了,所以我们只能靠硬件来完成这件事情,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有:

    • 测试并设置(Test-and-Set)
    • 获取并增加(Fetch-and-Increment)
    • 交换(Swap)
    • 比较并交换(Compare-and-Swap,下文称CAS)
    • 加载链接/条件存储(Load-Linked/Store-Conditional,下文称LL/SC)

    由于Unsafe类不是提供给用户程序调用的类(Unsafe.getUnsafe(的代码中限制了只有启动类加载器(Bootstrap ClassLoader) 加载的Class才能访问它),因此,如果不采用反射手段,我们只能通过其他的Java API来间接使用它,如J.U.C包里面的整数原子类,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。

    无同步方案

    可重入代码(Reentrant Code):这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。

    可重入代码有一些共同的特征, 例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。我们可以通过一个简单的原则来判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。

    线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

    Java 锁

    队列同步器 AbstractQueuedSynchronizer

    队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

    同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。

    同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

    重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

    • getState():获取当前同步状态。
    • setState(int newState):设置当前同步状态。
    • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

    队列同步器的实现分析

    同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

    独占式同步状态获取与释放

    通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。

    共享式同步状态获取与释放

    共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问。

    独占式超时获取同步状态

    通过调用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。该方法提供了传统Java同步操作(比如synchronized关键字)所不具备的特性。

    自定义同步组件——TwinsLock

    首先,确定访问模式。TwinsLock能够在同一时刻支持多个线程的访问,这显然是共享式访问,因此,需要使用同步器提供的acquireShared(int args)方法等和Shared相关的方法,这就要求TwinsLock必须重写tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,这样才能保证同步器的共享式同步状态的获取与释放方法得以执行。

    其次,定义资源数。TwinsLock在同一时刻允许至多两个线程的同时访问,表明同步资源数为2,这样可以设置初始状态status为2,当一个线程进行获取,status减1,该线程释放,则status加1,状态的合法范围为0、1和2,其中0表示当前已经有两个线程获取了同步资源,此时再有其他线程对同步状态进行获取,该线程只能被阻塞。在同步状态变更时,需要使用compareAndSet(int expect,int update)方法做原子性保障。

    public class TwinsLock implements Lock {
    	private final Sync sync = new Sync(2);
    
    	private static final class Sync extends AbstractQueuedSynchronizer {
    		Sync(int count) {
    			if (count <= 0) {
    				throw new IllegalArgumentException("count must large than zero.");
    			}
    			setState(count);
    		}
    
    		public int tryAcquireShared(int reduceCount) {
    			for (;;) {
    				int current = getState();
    				int newCount = current - reduceCount;
    				if (newCount < 0 || compareAndSetState(current, newCount)) {
    					return newCount;
    				}
    			}
    		}
    
    		public boolean tryReleaseShared(int returnCount) {
    			for (;;) {
    				int current = getState();
    				int newCount = current + returnCount;
    				if (compareAndSetState(current, newCount)) {
    					return true;
    				}
    			}
    		}
    	}
    
    	public void lock() {
    		sync.acquireShared(1);
    	}
    
    	public void unlock() {
    		sync.releaseShared(1);
    	}
    // 其他接口方法略
    	@Override
    	public void lockInterruptibly() throws InterruptedException {
    		// TODO Auto-generated method stub
    	}
    	@Override
    	public boolean tryLock() {
    		// TODO Auto-generated method stub
    		return false;
    	}
    	@Override
    	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    		// TODO Auto-generated method stub
    		return false;
    	}
    	@Override
    	public Condition newCondition() {
    		// TODO Auto-generated method stub
    		return null;
    	}
    }

    重入锁 ReentrantLock

    重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

    1. 线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
    2. 锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。

    读写锁 ReentrantReadWriteLock

    在没有读写锁支持的时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

    读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。

    LockSupport工具

    当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作。LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。

    Condition接口

    任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }
    public void conditionSignal() throws InterruptedException {
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

    JVM锁的优化

    锁状态类型

    目的

    原理
    无锁  

    偏向锁

    在无竞争的情况下消除整个同步使用的互斥量,连CAS操作都不做

    偏向锁会偏向第一个获得它的线程,如果在接下来的执行过程中该锁没有被其他线程获取,则持有偏向锁的线程以后进入这个锁相关的同步块不再需要进行同步

    1.锁对象第一次被获取时,虚拟机会把对象头中的锁标志位设置为01,即偏向锁模式;

    2.同时使用CAS操作将线程ID记录在对象的 Mark World 之中;

    3.若CAS操作成功,则以后进入这个锁相关的同步块时不再进行任何同步操作。

    当另一个线程尝试获取这个锁时,偏向模式结束。此时根据锁对象是否处于锁定状态,将锁对象恢复到未锁定状态或者轻量级锁状态(锁的升级,单向不可逆)。

    轻量级锁在无竞争的情况下使用CAS操作去消除同步使用的互斥量

    1.代码进入同步块的时候,若锁对象没有被锁定。

    2.虚拟机首先在栈帧中建立一个名为锁记录(Lock Record)的空间,存储锁对象的 Mark World 的拷贝;

    3.虚拟机使用CAS操作将对象的 Mark World 更新为指向 Lock Record 的指针;

    4.若这个CAS操作成功,则此对象处于轻量级锁定状态;

    5.若这个CAS操作失败,则检查对象的 Mark World 是否已经指向当前线程栈帧中的 Lock Record,

    6.若已经指向,则表明当前线程拥有该对象的锁,继续执行,否则膨胀为重量级锁。

    重量级锁 使用操作系统互斥量(mutex)来实现的传统锁。 当所有对锁的优化都失效时,将退回到重量级锁。它与轻量级锁不同竞争的线程不再通过自旋来竞争线程, 而是直接进入堵塞状态,此时不消耗CPU,然后等拥有锁的线程释放锁后,唤醒堵塞的线程, 然后线程再次竞争锁。但是注意,当锁膨胀(inflate)为重量锁时,就不能再退回到轻量级锁。
    优点缺点适用场景
    偏向锁加锁和解锁不需要额外的消耗,与执行非同步方法仅存在纳秒级的差距如果线程间存在竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块的情况
    轻量级锁竞争的线程不会堵塞,提高了程序的响应速度始终得不到锁的线程,使用自旋会消耗CPU追求响应时间,同步块执行速度非常块,只有两个线程竞争锁
    重量级锁线程竞争不使用自旋,不会消耗CPU线程堵塞,响应时间缓慢追求吞吐量,同步块执行速度比较慢,竞争锁的线程大于2个

     

     

    展开全文
  • Java锁详解

    千次阅读 多人点赞 2019-06-29 22:57:22
    文章目录什么是锁锁的实现方式涉及的几个重要概念类和对象(重要)synchronized实现原理 什么是 计算机还是单线程的时代,下面代码中的count,始终只会被一个线程累加,调用addOne()10次,count的值一定就...
  • 浅谈Java锁机制

    万次阅读 多人点赞 2018-08-04 11:35:55
    这两天一直在准备面试,看了很多篇关于的介绍的博客,今天就总结一下。 首先需要知道几个名词: 公平/非公平 可重入 独享/共享 互斥/读写 乐观/悲观 分段 偏向/轻量级/重量级 ...
  • JAVA锁的膨胀过程

    千次阅读 2019-07-18 11:25:52
    Java 语言中,使用 Synchronized 是能够实现线程同步的,即加锁。并且实现的是悲观,在操作同步资源的时候直接先加锁。 加锁可以使一段代码在同一时间只有一个线程可以访问,在增加安全性的同时,牺牲掉的是...
  • java锁的底层原理

    千次阅读 2019-05-09 20:46:46
    知识整理 Synchronized 内置,... JDK自带 偏置、轻量级(CAS操作)、自适应自旋、粗化、消除 Volatile 概念:非阻塞可见性、禁止指令重排序* 与syn区别: 无法实现原子操作、使用场景--单...
  • Java锁的种类

    千次阅读 2019-01-07 01:22:22
    Java中的主要有以下几种概念: 1、同步   同一时刻,一个同步只能被一个线程访问。以对象为依据,通过synchronized关键字来进行同步,实现对竞争资源的互斥访问。 2、独占(可重入的互斥)   互斥...
  • 转载自 java并发编程之4——Java锁分解锁分段技术   并发编程的所有问题,最后都转换成了,“有状态bean”的状态的同步与互斥修改问题。而最后提出的解决“有状态bean”的同步与互斥修改问题的方案是为所有修改...
  • JAVA锁中的CAS

    千次阅读 2018-04-10 18:06:04
    一 .CAS 在学习java.util.concurrent(简称JUC)包下的类时,了解到了CAS这个概念,整个JUC包的基础也是CAS,ReentrantLock也是基于它的。学习CAS,先从synchronized关键字说起,synchronized关键字能保证最基本的...
  • 一、什么是? 在计算机科学中,(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。旨在强制实施互斥排他、并发控制策略。 通常需要硬件支持才能有效实施。这种...
  • Java锁升级

    千次阅读 2018-12-07 16:44:15
    Java SE1.6为了减少获得和释放所带来的性能消耗,引入了“偏向”和“轻量级”,所以在Java SE1.6里一共有四种状态,无锁状态,偏向状态,轻量级状态和重量级状态,它会随着竞争情况逐渐升级。可以...
  • Java锁性能提高(锁升级)机制总结

    千次阅读 多人点赞 2017-03-21 11:38:37
    的使用很难避免,如何尽量提高的性能就显得比较重要了 偏向 所谓的偏向是指在对象实例的Mark Word(说白了就是对象内存中的开头几个字节保留的信息,如果把一个对象序列化后明显可以看见开头的这些信息)...
  • Java锁机制

    千次阅读 多人点赞 2019-06-20 12:57:09
    Java锁的划分 Java锁具体可分为悲观锁/乐观锁、自旋锁/适应性自旋锁、偏向锁、轻量级锁/重量级锁、公平锁和非公平锁、可重入锁/非可重入锁、共享锁/排他锁 具体划分如下: 乐观锁VS悲观锁 特征 对于同一个...
  • Java锁的实现原理

    千次阅读 2018-08-26 10:58:40
    https://www.cnblogs.com/duanxz/p/3559510.html https://blog.csdn.net/tingfeng96/article/details/52219649
  • Java锁Lock源码分析(一)

    千次阅读 2018-02-27 15:59:26
    Java中的Lock源码分析(一) Java中的有很多,同时也是了整个并发包的基础,可以说明白了整个并发包你也就能明白一半了,如果之前你有所了解的话java中的你或许对这些名词有些概念: 独占、共享 公平...
  • 关于Java锁的面试总结

    万次阅读 多人点赞 2017-09-04 21:24:34
    面试过程中,也被问过几次关于Java的问题,面试官一般是这么问:你了解Java中的吗?有几种?都有什么区别?讲一讲。  大致可以分为这几点:是什么,有什么用,有哪几种的区别。 一:是什么,...
  • [Java多线程 五]---JAVA锁有哪些种类

    万次阅读 多人点赞 2017-09-02 12:09:25
    上一篇既然提到了,这一篇来详细介绍JAVA中的,也为之后JUC下的做一个铺垫 其实如果按照名称来说,大概有以下名词: 自旋 ,自旋的其他种类,阻塞,可重入 ,读写 ,互斥 ,悲观 ,乐观 ...
  • 关于Java锁机制面试官会怎么问

    千次阅读 2018-07-09 13:39:49
    乐观与悲观悲观:总是假设最坏的情况,每次去拿数据的时候都认为...再比如Java里面的同步原语synchronized关键字的实现也是悲观。乐观:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所...
  • Java锁--Lock实现原理(底层实现)

    万次阅读 多人点赞 2018-05-08 17:35:18
    转载:...在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖jav...
  • java

    千次阅读 2018-10-23 23:39:22
    Java对象保存在内存中时,由以下三部分组成: 1,对象头 2,实例数据 3,对齐填充字节 一,对象头 java的对象头由以下三部分组成: 1,Mark Word 2,指向类的指针 3,数组长度(只有数组对象才有)   1,...
  • 从并发概念、场景分析出发,依次引出锁、等待队列等概念,直至分析清楚java锁机制实现的原理。并以java锁机制实现基类AbstractQueuedSynchronizer的实现为例,从类(核心属性、方法)设计思路,到对关键代码做注释...
  • 深入浅出Java锁--Lock实现原理(底层实现)

    千次阅读 多人点赞 2018-12-21 16:25:53
    java提供了两种方式来加锁,一种是关键字:synchronized,一种是concurrent包下的lock。 synchronized是java底层支持的,而concurrent包则是jdk实现。   关于synchronized的原理可以阅读再有人问你...
  • Java锁升级过程

    千次阅读 2019-10-12 10:31:14
    因为经过HotSpot的作者大量的研究发现,大多数时候是不存在竞争的,常常是一个线程多次获得同一个,因此如果每次都要竞争会增大很多没有必要付出的代价,为了降低获取的代价,才引入的偏向。 轻量级 ...
  • java锁机制Synchronized java锁机制Synchronized java锁机制Synchronized java锁机制Synchronized

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 577,742
精华内容 231,096
关键字:

java锁

java 订阅