美文网首页
synchronized

synchronized

作者: Audience0 | 来源:发表于2020-09-23 13:49 被阅读0次

    同步锁synchronized关键字
    1>>修饰实例方法 对象锁为this
    2>>修饰静态方法 对象锁是当前类的字节码文件,即this.getClass();少用-->占内存,垃圾回收无法处理
    3>>修饰代码块 对象锁为synchronized(obj) 指定的obj
    4>>解决了线程不安全的情况,但是多个线程需要判断锁,抢锁,比较耗资源

    2>lock()锁
    1>>重入锁
    lock锁和synchronized的区别:
    1>synchronized 是内部锁,自动化的上锁与释放锁,而lock是手动的,需要人为的上锁和释放锁,lock比较灵活,但是代码相对较多
    2>lock接口异常的时候不会自动的释放锁,同样需要手动的释放锁,所以一般写在finally语句块中,而synchronized则会在异常的时候自动的释放锁
    3>lock超时获取锁:在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回
    4>lock尝试非阻塞的获取锁:当前线程尝试获取锁,如果这一时刻没有被其他线程获取到,则成功获取并持有锁。
    5>lock能被中断的获取锁:获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将被抛出,同事释放锁。

    volatile关键字作用:使变量在多个线程之间可见,强制线程去主内存中取该数据.
    volatile与synchronized区别:
    1>volatile轻量级,只能修饰变量,synchronized重量级,还可以修饰方法.
    2>volatile只能保证数据的可见性,不能保证线程的安全性(原子性)
    3>synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
    4>volatile 禁止重排序(重排序:CPU会对代码执行实现优化,但不会对有依赖关系的做重排序,代码可能改变顺序,但不会改变结果,重排序的意义是提高并行度,但是在多线程情况下有可能有影响到结果,此时需要用volatile)


    CAS算法理解

    对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

    java 中的锁:
    隐式锁:在Java代码中不能看到加锁过程的锁(Synchronized就是隐式锁);
    显式锁:在Java代码中能看到加锁过程的锁(java.util.concurrent包下的那些锁

    二、 乐观锁和悲观锁:
    1、乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读比较执行写操作。java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
    2、悲观锁是就是悲观思想,即认为写多读少,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block(阻塞等待)直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如ReentrantLock。

    三、 重入锁与非重入锁:
    在一个同步区域中有一个或多个同步区域,这两个或多个同步区域的锁对象是同一个,同一个线程拿到了最外层同步区域的锁后能够进入内层的同步区域,这样的锁机制就是重入锁,反之就是非重入锁。

    四、 读写锁:
    多线程并发或者并行读操作的时候,不进行互斥,一旦有写操作就进行互斥的锁的机制。(读锁与读锁不互斥,读锁与写锁互斥,写锁与写锁互斥)

    五、 偏向锁、轻量级锁、自旋锁、重量级锁(JVM 通过monitor指令去调用底层C++):
    1、偏向锁、轻量级锁、自旋锁属于乐观锁;
    2、重量级锁属于悲观锁。

    重量级锁(synchronized):
    1.synchronized代码块反编译后,输出的字节码有monitorenter和monitorexit语句
    2.每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。
    3.当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。其过程如下:
    若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
    若线程已拥有monitor的所有权,允许它重入monitor,并递增monitor的进入数
    若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
    4.能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
    执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

    monitor
    每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized圈起来的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。

    我们的java代码里不会显示地去创造这么一个monitor对象,我们也无需创建,事实上可以这么理解:我们是通过synchronized修饰符告诉JVM需要为我们的某个对象创建关联的monitor对象。

    在hotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于hotSpot虚拟机源码ObjectMonitor.hpp文件中。


    堆中的对象,由对象头,实例数据,对齐填充构成
    对象头:
    1.对象头形式
    普通对象头


    image.png

    数组对象头


    image.png

    对象头(Header):包含两部分,
    第一部分(mark word)用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’;
    第二部分(KLASS)是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,
    另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;


    image.png image.png
    /**
     * synchronized关键字的底层原理
     *
     * 由于虚拟机默认开启指针压缩,所以整个对象头的占12个字节
     * 在VM参数中加入:-XX:-UseCompressedOops关闭指针压缩。
     *
     * 由于Intel是采用小端存储的,所以是数据的低位保存在内存的低地址中,
     * 而数据的高位保存在内存的高地址中
     * 地址:以字节为单位低----------->高
     *(object header)     01 00 00 00  (0_0000_0_01 00000000 00000000 00000000) (1)
     *(object header)     00 00 00 00 (0_0000000 00000000 00000000 00000000) (0)
     *
     *前8位分析:0没有用到   0000表示GC的年龄    0表示是否偏向   01表示锁的级别和GC状态标识
     *
     *所以的得出结论:
     *关于锁的状态就观察对象头的第一个字节的后3位。
     *第一位表示是否是偏向锁状态,
     *第二、三位表示锁的级别和GC的标记。01无锁,00轻量级锁  10重量级锁  11 GC标记
     *综上所述:
     *001:无锁,101偏向锁, 末尾两位00轻量级锁  末尾两位10重量级锁
     *因为轻量级锁和重量级锁, 代表偏向的位用于其他表示了,不在代表偏向锁
     */
    public class Test {
    
        private int k = 0;
    
        private Object myLock = new Object();
    
        public static void main(String[] args) {
    
            Test test = new Test();
            System.out.println("计算hashCode之前----------------------");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
    
            System.out.println("计算hashCode之后----------------------");
            int hashCode = test.hashCode();
            System.out.println("hashCode:"+hashCode);
            System.out.println("转为16进制的hashCode:"+Integer.toHexString(hashCode));
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
        }
    }
    
    

    运行结果如下:


    image.png

    对象的状态有几种:
    无锁,偏向锁,轻量级锁,重量级锁,GC标志五个状态

    锁的膨胀过程:
    无锁:
    程序多线程执行过程中,没有去执行synchronized修饰区域或者方法

    偏向锁:
    发生在程序多线程过程中,由始至终只有一个线程去执行过synchronized修饰的区域或者方法,由于是由始至终只有一个线程去执行,所以,没有发生竞争,等待,抢锁的情况,他不会调用操作系统的函数去实现同步.

    轻量级锁:
    发生在程序多线程执行过程中有去执行synchronized修饰区域或方法,且没有发生竞争,等待,抢锁的情况或者发生了竞争,等待,抢锁,但是竞争,等待和抢锁的时间没有超过一个JVM设定的自旋时间或次数的时候,他是在虚拟机内部去实现的同步,不会调用操作系统的函数去实现同步.

    重量级锁:
    发生在程序多线程执行过程中有去执行synchronized修饰区域或者方法,且发生了竞争、等待、抢锁且竞争、等待和抢锁的时间已经超过一个JVM设定的自旋时间或者次数的时候,它会调用操作系统的函数去实现同步(调用操作系统实现同步,需要的步骤很多,导致性能相对于其它锁实现同步的方式就很低)。

    /**
         * synchronized关键字的底层原理
         * 加锁之后不睡眠则:变成了轻量级锁
         * 因为是JVM默认开启了偏向锁延迟开启的开关
         *
         * 注意:偏向锁:101,的第一个1表示的是这个是可偏向状态,
         * 而不是说它已经是一个偏向锁了,如何辨别呢?
         * 看其他字节是否有存储数据,如果有就是已经是偏向锁了,
         * 如果没有,则还是处于一个可偏向状态的无锁。
         * -XX:BiasedLockingStartupDelay=0  可以设置延迟开启偏向锁的时间
         * @author Peter
         */
        public static void main(String[] args) throws InterruptedException {
    
            /*
             * JVM启动需要启动很多我们不知道的线程,比如GC,
             * 要花费4秒时间,所以延迟开启偏向锁的时间默认为4秒
             */
           // Thread.sleep(4100);
            Test test = new Test();
            System.out.println("计算加锁之前----------------------");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
    
            synchronized (test){
                System.out.println("计算加锁之后----------------------");
                System.out.println(ClassLayout.parseInstance(test).toPrintable());
            }
        }
    

    运行结果如下:


    image.png

    保存偏向线程ID,表示当前的线程就是这个线程,如果下次还是这个线程的话,则直接放行(获取锁).

    /**
         * synchronized关键字的底层原理
         *
         *  -XX:BiasedLockingStartupDelay=0
         * 演示计算了hashCode的对象不能成为偏向锁
         * 会直接成为轻量级锁,因为没有地方存关于偏向锁的信息了
         * 就直接成为轻量级锁,并把hashCode放入记录的线程信息中
         */
        public static void main(String[] args) {
            Test test=new Test();
            //这里计算一下hashCode
            test.hashCode();
            System.out.println("------------------------------加锁之前--------------------------------");
            System.out.println(ClassLayout.parseInstance(test).toPrintable());
            synchronized (test) {
                System.out.println("-------------------------------加锁之后------------------------------");
                System.out.println(ClassLayout.parseInstance(test).toPrintable());
            }
    
        }
    

    运行结果如下:


    image.png
    /**
         * synchronized关键字的底层原理
         *
         * -XX:BiasedLockingStartupDelay=0
         * 如果调用wait方法,则立刻变为重量级锁
         * wait方法就是monitor实现的
         *wait表示等待,说明会有竞争抢锁的情况,并且时间不会短,所以直接变为重量级锁
         * synchronized内置的锁的膨胀过程不可逆
         */
        public static void main(String[] args) throws InterruptedException {
            final Object  testLock=new Object();
            System.out.println("------------------------------加锁之前------------------------");
            System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
            synchronized (testLock) {
                System.out.println("-----------------------加锁之后等待之前------------------");
                System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
                testLock.wait(1_000);
                System.out.println("--------------------加锁之后等待之后------------------------");
                System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
            }
            System.out.println("--------------------退出同步代码块之后----------------------");
            System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
        }
    

    运行结果如下:


    image.png
    /**
     * synchronized关键字的底层原理
     *
     * 证明:
     * 偏向锁偏向一个线程后,不会发生重偏向
     * 另一个线程的情况,只会膨胀为轻量级锁。
     * 注意:这里因为是主线程去进入同步代码区域
     * 所以会膨胀为轻量级锁
     * -XX:BiasedLockingStartupDelay=0
     */
    public static void main(String[] args) throws InterruptedException {
            final Object  testLock=new Object();
            final Thread t1 = new Thread("子线程:"){
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName+"---------------------加锁前------------------");
                    System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
                    synchronized (testLock) {
                        System.out.println(threadName+"--------------------加锁后-----------------");
                        System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
                    }
                }
            };
            t1.start();
            t1.join();
    
            System.out.println("主线程:------------------加锁之前--------------------------");
            System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
            synchronized (testLock) {
                System.out.println("主线程:----------------------加锁之后------------------------");
                System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
            }
        }
    

    运行结果如下:


    image.png

    说明,偏向锁不会重新偏向.

    /**
         * synchronized关键字的底层原理
         * -XX:BiasedLockingStartupDelay=0
         * 证明:
         * 偏向锁的含义不是只有两个线程在交替执行。
         *
         * 我认为:不管是多少个线程去执行,只要是没有产生竞争关系
         * 就不会膨胀为轻量级锁,但是这个是有一些前提的,后面讲解。
         *
         * 结论:不是网上说的只要有第三个线程去执行了且没有产生竞争关系
         * 时就会膨胀为轻量级锁。
         */
        public static void main(String[] args) throws InterruptedException {
    
            final Object  testLock=new Object();
    
    
            final Thread t1 = new Thread("线程1:"){
    
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName+"-----------------加锁前------------------------");
                    System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
                    synchronized (testLock) {
                        System.out.println(threadName+"-----------------加锁后--------------------");
                        System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
                    }
                }
            };
            t1.start();
            //保证t2在执行时不会和t1发生竞争
            t1.join();
    
            Thread t2 = new Thread("线程2:"){
    
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName+"-----------------加锁前-------------------");
                    System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
                    synchronized (testLock) {
                        System.out.println(threadName+"-------------------加锁后------------------");
                        System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
                    }
                }
            };
            t2.start();
            t2.join();
    
            Thread t3 = new Thread("线程3:"){
    
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName+"-----------------加锁前--------------------");
                    System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
                    synchronized (testLock) {
                        System.out.println(threadName+"------------------加锁后----------------");
                        System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
                    }
                }
            };
            t3.start();
            /*t3.join();
            System.out.println("主线程:--------------------------加锁前----------------------------");
            System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
            synchronized (testLock) {
                System.out.println("主线程:-------------------加锁后----------------------");
                System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
            }*/
        }
    

    运行结果,以上层序运行结果为,均是偏上线程1 的偏向锁.即无论多少个线程执行,只要没有竞争关系,都不会膨胀为轻量级锁.
    但是,如果其中一个线程是以上线程的主线程,则主线程加锁之后会直接变成轻量级锁.

    运行描述:
    线程1执行同步前,为可偏向无锁状态锁,
    线程1执行synchroized后,变成偏向锁,偏向线程1,
    线程1退出同步代码块,依然是偏向锁,偏向线程1,
    保持线程1存活,
    线程2执行synchronized,变成轻量级锁
    线程2执行同步结束,释放锁,变成不可偏向的无锁状态.
    线程1此时又进入同步代码块,锁直接变成轻量级锁.


    偏向锁获取过程


    image.png

    其他优化:
    1)自旋锁:
    互斥同步时,挂起和恢复线程都需要切换到内核态完成,这对性能并发带来了不少的压力。同时在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段较短的时间而去挂起和恢复线程并不值得。那么如果有多个线程同时并行执行,可以让后面请求锁的线程通过自旋(CPU忙循环执行空指令)的方式稍等一会儿,看看持有锁的线程是否会很快的释放锁,这样就不需要放弃CPU的执行时间适应性自旋

           在轻量级锁获取过程中,线程执行 CAS 操作失败时,需要通过自旋来获取重量级锁。如果锁被占的时间比较短,那么自旋等待的效果就会比较好,而如果锁占用的时间很长,自旋的线程则会白白浪费 CPU 资源。解决这个问题的最简答的办法就是:指定自旋的次数,如果在限定次数内还没获取到锁(例如10次),就按传统的方式挂起线程进入阻塞状态。JDK1.6 之后引入了自适应性自旋的方式,如果在同一锁对象上,一线程自旋等待刚刚成功获得锁,并且持有锁的线程正在运行中,那么JVM 会认为这次自旋也有可能再次成功获得锁,进而允许自旋等待相对更长的时间(例如100次)另一方面,如果某个锁自旋很少成功获得,那么以后要获得这个锁时将省略自旋过程,以避免浪费 CPU。
    

    2)锁消除:
    锁消除就是编译器运行时,对一些被检测到不可能存在共享数据竞争的锁进行消除。如果判断一
    段代码中,堆上的数据不会逃逸出去从而被其他线程访问到,则可以把他们当做栈上的数据对待,认为它们是线程私有的,不必要加锁

    3)锁粗化:
    锁粗化就是JVM检测到一串零碎的操作都对同一个对象加锁,则会把加锁同步的范围粗化到整个操作序列的外部。

    批量重偏向问题
    子线程创建了同一个类的多个对象并且对这个对象进行了加锁.
    主线程也在这些对象加锁后,也对这些对象加锁(没有发生竞争加锁)
    因为要执行CAS进行线程信息的替换(锁的升级),那么就会进行多次偏向锁的撤销,那么JVM就会认为后面的对象都需要批量重偏向,那么后面的对象就会是加偏向锁,而不再是轻量级锁
    偏向锁大量重偏向的门槛(阈值)
    intx BiasedLockingBulkRebiasThreshold = 20

    相关文章

      网友评论

          本文标题:synchronized

          本文链接:https://www.haomeiwen.com/subject/rauhfhtx.html