美文网首页
10. 锁概念和synchronized同步关键字原理

10. 锁概念和synchronized同步关键字原理

作者: Vander1991 | 来源:发表于2021-03-04 13:25 被阅读0次

    前言:上一节中所用到的Unsafed来实现递增操作,这种方式属于乐观锁,会假定能修改成功,但是假设修改的数据发现与之前的不一致,修改后就重试修改。下面主要是讲解同步关键字实现的悲观锁的原理,这种方式虽然性能上可能会慢,但是却是最容易实现线程安全的。

    10.1 锁的概念

    自旋锁:为了不放弃CPU执行事件,循环的使用CAS技术对数据尝试进行更新,直至成功。
    自旋锁说白了就是不断循环修改直到操作成功。

    悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。
    例如上面的synchronized关键字,不管三七二十一把所有资源占住,其它线程不仅不能来写也不能来读

    乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。
    自旋锁也是乐观锁的一种,它假定操作能成功,如果失败就重试

    独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)

    共享锁(读):给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)

    可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。
    同步关键字synchronized是可重入的,这里可能有点抽象。结合代码理解

    重入锁Demo:

    package szu.vander.lock;
    
    /**
     * @author : caiwj
     * @date :   2019/12/7
     * @description :
     */
    public class ReentrantSynchronizedDemo {
    
        private String value1;
    
        private String value2;
    
        public void setValue(String value) {
            synchronized (this) {
                this.value1 = value;
                System.out.println(String.format("Thread:%s 设置value为%s成功!"
                        , Thread.currentThread().toString()
                        , this.value1));
                synchronized (this) {
                    this.value2 = value;
                    System.out.println(String.format("Thread:%s 设置value为%s成功!"
                            , Thread.currentThread().toString()
                            , this.value2));
                }
            }
        }
    
        public static void main(String[] args) {
            new ReentrantSynchronizedDemo().setValue("synchronized is Reentrant Lock");
        }
    
    }
    

    运行结果:

    效果:(如果synchronized不是可重入的,那么上述程序将会造成死循环),因为第一层synchronized嵌套套着第二层synchronized,而执行到要再次拿到当前的对象的锁的时候,第一层嵌套的代码还没有结束,所以此时监视器锁还没有被释放,但是由于是可重入的,所以又可以再次获取当前对象的监视器锁。

    公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。(后一节Lock接口的时候进行说明)

    JDK中几种重要的锁实现方式:synchronized、ReentrantLock、ReentrantReadWriteLock,下面一一进行讲解。

    10.2 同步关键字synchronized

    10.2.1 synchronized关键字的相关概念

    synchronized关键字属于最基本的线程通信机制(基于线程通信可以这么理解,一个线程释放了锁,会通知在等待的线程来争抢这把锁),基于对象监视器(后面会说明)实现的。
    Java中的每个对象都与一个监视器相关联,一个线程可以锁定或解锁。

    一次只有一个线程可以锁定监视器,试图锁定该监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。

    特性:可重入、独享、悲观锁
    锁的范围:类锁、对象锁、锁消除、锁粗化

    **锁消除例子 **

    package concurrent.lock;
    
    /**
     * @author : Vander
     * @date :   2019/12/7
     * @description : 锁消除例子
     */
    public class LockEliminate {
    
        public synchronized void lockEliminate() {
            // jit
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append("a");
            stringBuffer.append("b");
            stringBuffer.append("c");
        }
    
        public static void main(String[] args){
            for(int i=0; i<10000; i++) {
                new LockEliminate().lockEliminate();
            }
        }
    
    }
    

    StringBuffer的append方法实际上是synchronized,但是当JIT编译器认为当前的stringBuffer作为局部变量不存在多线程竞争的情况下,会帮代码进行优化,将其中的锁消除掉,以达到更好的性能。(要想能看到锁消除的效果的话,可以用jitwatch来看)

    锁粗化例子

    而锁粗化也类似,即JIT编译器处于性能上的优化会将锁的访问扩大,防止频繁地加锁解锁造成大量性能的损耗。

    提示:同步关键字,不仅是实现同步,根据JMM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)

    10.2.2 synchronized关键字的基本用法

    1)锁方法

    import java.time.Instant;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author : Vander
     * @date :   2019/12/3
     * @description :
     */
    public class SynchronizedDemo {
    
        private String value;
    
        private synchronized void readValue() throws InterruptedException {
            Instant sendBefore = Instant.now();
            System.out.println(sendBefore);
            System.out.println("Present Value : " + value);
            TimeUnit.SECONDS.sleep(3);
            Instant sendAfter = Instant.now();
            System.out.println(sendAfter);
        }
    
        private synchronized void setValue(String value) throws InterruptedException {
            Instant sendBefore = Instant.now();
            System.out.println(sendBefore);
            System.out.println("Set Value : " + value);
            Instant sendAfter = Instant.now();
            System.out.println(sendAfter);
        }
    
        public static void main(String[] args) throws InterruptedException {
            SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("thread 1 started");
                        synchronizedDemo.readValue();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("thread 2 started");
                        synchronizedDemo.setValue("new Value");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            TimeUnit.SECONDS.sleep(5);
        }
    
    }
    

    运行结果:

    效果:说明将synchronized关键字放在方法上,默认是获取所在对象的监视器锁,所以两个线程虽然都启动了,但是线程2需要等线程1将锁释放后,线程2才能设值。

    2)锁对象

    package szu.vander.lock;
    
    
    import java.time.Instant;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author : caiwj
     * @date :   2019/12/7
     * @description :
     */
    public class SynchronizedObjDemo {
    
        private String value;
    
        private String setValue(String value) throws InterruptedException {
            Instant sendBefore = Instant.now();
            this.value = value;
            System.out.println(String.format("%s - Thread:%s 设置value为%s成功!"
                    , sendBefore
                    , Thread.currentThread().toString()
                    , value));
            TimeUnit.SECONDS.sleep(3);
            Instant sendAfter = Instant.now();
            System.out.println(String.format("%s - Thread:%s 设置value为%s成功!"
                    , sendAfter
                    , Thread.currentThread().toString()
                    , value));
            return this.value;
        }
    
        public static void main(String[] args){
            SynchronizedObjDemo synchronizedObjDemo1 = new SynchronizedObjDemo();
            SynchronizedObjDemo synchronizedObjDemo2 = new SynchronizedObjDemo();
            new Thread(() -> {
                try {
                    synchronized (synchronizedObjDemo1) {
                        synchronizedObjDemo1.setValue("I am synchronizedObjDemo1");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            new Thread(() -> {
                try {
                    synchronized (synchronizedObjDemo2) {
                        synchronizedObjDemo1.setValue("I am synchronizedObjDemo2");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    
    
    }
    

    运行结果:

    从时间看来,线程2先设置了值,然后线程1才设置,并且间隔很短,但是实际上两个线程是拿到了锁的,原因是锁对象,线程2拿的是synchronizedObjDemo2的监视器锁,而线程1拿的则是synchronizedObjDemo1的监视器锁,所以对synchronizedObjDemo1的value设值都是畅通无阻的。

    其实还有一种方式就是锁静态方法,因为静态方法是放在线程共享部分的,所以只要有线程用了带锁的静态方法,其它线程都要等此线程用完去抢到静态方法的锁才能用。

    3)锁静态变量

    package szu.vander.lock;
    
    import szu.vander.log.Logger;
    
    import java.util.concurrent.*;
    
    /**
     * @author : caiwj
     * @date :   2020/1/25
     * @description : 不使用常量作为锁对象
     */
    public class SynchronizedConstantDemo {
    
        private final static Logger log = new Logger();
    
        private static String constStr1 = "hello world";
    
        private static String constStr2 = "hello world";
    
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        test1();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        test2();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            TimeUnit.SECONDS.sleep(3);
            executorService.shutdown();
        }
    
        public static void test1() throws InterruptedException {
            synchronized (constStr1) {
                TimeUnit.MILLISECONDS.sleep(1000);
                log.info("test1 has been invoked!");
            }
        }
    
        public static void test2() throws InterruptedException {
            synchronized (constStr2) {
                TimeUnit.MILLISECONDS.sleep(1000);
                log.info("test2 has been invoked!");
            }
        }
    
    }
    

    实现效果:由于锁静态变量,静态变量位于静态区,constStr1、constStr2实际上是指向同块内存区域,所以没有起到锁两个不同变量的效果。

    同步关键字使用技巧:
    1)不要使用静态变量作为同步条件
    2)同步的代码块越少越好
    3)脏读问题,只对写方法加锁读方法不加锁则会导致脏读
    4)synchronized方法1可以调用了synchronized方法2,由于synchronized支持重入;Sub类继承Super类,Sub类实现了Super类的synchronized a(),同样也是可以重入的。
    5)方法抛异常时,如果代码块不catch掉这个异常,锁就会被释放掉
    6)静态方法上加synchronized实现同步,锁的是类对象,也就是跟synchronized(getClass())用的是同一把锁。

    10.2.3 synchronized实现原理

    在JDK官网中能找到这么一份文档:HotspotOverview.pdf(基于JDK 1.6的说法),里面有关synchronized关键字的实现
    https://www.cs.princeton.edu/picasso/mats/HotspotOverview.pdf
    https://wiki.openjdk.java.net/display/HotSpot/Synchronization
    HotSpot虚拟机的对象(对象头部分)的内存布局开始介绍,HotSpot虚拟机的对象头(Object Header)分为两部分信息,一部分用于存储对象自身的运行时数据,如HashCode、GC分代年龄等,这部分数据的长度在32位和64位的虚拟机中分为为32bit和64bit,官方称为“Mark Word”,这是实现轻量级锁和偏向锁的关键。另一部分则用于存储指向方法去对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。

    HotSpot中的对象存储

    Mark Word

    默认情况下JVM锁会经历:未被锁过->偏向锁->轻量级锁->重量级锁

    偏向锁时 ,Bitfields一部分存放占有锁的线程的线程ID
    轻量级锁时,Bitfields部分将存放Lock record address中
    重量级锁时,Bitfields部分将存放Monitor address(即存放指向重量级锁的指针)
    对象存储在内存中包括以上的内容。

    锁的升级过程(默认是开启偏向锁的)

    1)无锁->偏向锁
    JVM查看锁对象,发现是没有被锁过的,获取到锁将偏向状态位设置为1,并且将线程1的Thread ID CAS到锁对象的Markword中
    2)偏向锁->轻量级锁
    接着线程2来尝试获取锁,JVM发现锁对象的偏向锁标识位为1,此时对象的偏向锁将升级为轻量级锁,说明一发生多线程的争抢锁就会升级为轻量级锁,JVM往线程2的栈帧中添加Lock Record标志,将当前的Markword存放在此,然后CAS锁对象的Markword,使其指向线程2栈帧的Lock Record,CAS成功的话,将线程2栈帧中的owner指向锁对象,并将锁对象的锁标识为设置为“00”。

    偏向锁到轻量级锁的过程

    3)轻量级锁->重量级锁
    若JVM CAS锁对象的Markword失败,(它会通过自旋的方式来获取,一定次数之后,自旋都没有成功,视为失败),此时需要进行“锁升级”,将锁的标识位设置为“10”,线程2会将自己的相关信息放入该对象监视器中。监视器(管程)中有个集合存放,争抢当前对象的锁的线程,并且里面还有一个Owner属性来标志当前此对象的锁正在被哪个线程占用。(注意:每个JVM厂商设计的synchronized的实现都可能不同),但争抢锁的方式都是通过重量级锁的方式来争抢的。

    偏向锁
    JDK1.6引入的锁优化措施,如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做(即已经持有偏向锁的线程,执行同步块时,虚拟机不再进行任何同步操作),偏向标记第一次有用,出现过争抢后就没用了,-XX:-UseBiasedLocking禁止使用偏向锁定。
    偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步(直白点,JVM为了少干活:同步在JVM底层是有许多操作来实现的,如果是没有发生争抢,就不需要进行同步操作)

    偏向锁是为了让没有锁争抢的情况下,线程能够更快的获取锁,才引入了偏向锁的概念,因为如果直接用轻量级锁的话,需要在Lock record address对应的区域进行一系列的操作,但是如果用了偏向锁,就直接修改threadID就完了。

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

    重量级锁-监视器(monitor)
    修改Mark Word如果失败,会自旋CAS一定次数,该次数可以通过参数配置:
    超过次数,仍未抢到锁,则锁升级为重量级锁,进入阻塞。
    Monitor也叫管程,计算机操作系统原理有提及类似概念。一个对象会有一个对应的Monitor。

    Monitor的构造

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

    Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
    Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
    Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
    OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
    Owner:当前已经获取到所资源的线程被称为Owner;

    synchronized是不公平锁,因为它只有对进入了Waiting Queue中的线程公平,但是假设Owner释放了线程,有新的线程来CAS Owner也有可能会被新的线程所抢占,所以非公平。

    synchronized关键字的原理理解是为了理解Doug Lea在JDK1.6中引入各种各样的锁。

    相关文章

      网友评论

          本文标题:10. 锁概念和synchronized同步关键字原理

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