慕课网课程《Java高并发之魂:synchronized深度解析》学习笔记
Synchronized的作用
官方解释:
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程所见,则对该对象变量的所有读取或写入都是通过同步方法完成的。
通俗解释
能够保证在***同一时刻***最多只有***一个***线程执行该段代码,以达到保证并发安全的效果。
分析:
一段代码被Synchronized所修饰,被修饰的这段代码就会以原子的方式执行,多个线程在执行这段代码的时候不会相互干扰影响,因为多个线程之间并不会同时执行这段代码,所以不会出现并发问题。如何做到不同时执行?如何知道已经有一个线程在执行,那我就不执行呢?
它们会有一把锁,这把锁在第一个线程去执行的时候被拿到,拿到后这个线程就独占这把锁,直到这个线程结束或一定条件之后,它才会释放这把锁。在这把锁释放之前,其他线程只能等待。
Synchronized是Java的关键字,被Java语言原生支持;是最基本的互斥同步手段;是并发编程中的元老级角色,是并发编程的必学内容。可以保证代码的原子性和可见性。
不使用并发手段会有什么后果?
代码实战:两个线程同时a++,最后结果会比预计的少。
原因:
count++,它看上去只是一个操作,实际上包含了三个动作:
1、读取count 2、将count加一 3、将count的值写入到内存中
线程不安全
Synchronized的两个用法
对象锁
包括***方法锁***(默认锁对象为this当前实例对象)和***同步代码块锁***(自己指定锁对象)
方法锁形式:Synchronized修饰普通方法,锁对象默认为this
代码块形式:手动指定锁对象,可指定自己创建的对象或this
类锁
值Synchronized修饰***静态***的方法或指定锁为***Class对象***
概念(重要):Java类可能有很多个对象,但***只有一个Class对象***
本质:所谓的类锁,不过是Class对象的锁而已。
分析:不同的实例,即不同的线程去访问类锁的时候,它们获取到的锁其实是Class对象,由于Class对象只有一个,所以不同线程(无论有哪一个对象实例过来)都只能获取这唯一的一个锁,类锁实际是一个概念性的东西,用来帮助我们理解实例方法和静态方法的区别。
由于类锁只有一个,所以不同实例之间会互斥,在同一时刻只能一个实例去访问被类锁锁住的方法。
用法和效果:类锁只能在同一时刻被一个对象拥有。
类锁的特殊之处在于,我们即便是不同的Runnable实例,线程所对应的类锁依然只有一个。
形式1:synchronized加在static方法上
形式2:synchronized(*.class)代码块
多线程访问同步方法的7种情况(面试常考)
1、两个线程同时访问一个对象的同步方法
Thread-0先执行,执行结束后,Thread-1再执行,即他们一个一个的执行,因为他们既是同一个实例,所争抢的也是同一把锁,同一时刻只能一个持有,必须相互等待。
2、两个线程访问的是两个对象的同步方法
两个线程几乎同时运行,同时结束,两者互不干扰,原因为他们所采用的锁对象不是同一个
3、两个线程访问的是synchronized的静态方法
他们会一个一个的执行,锁生效。
4、同时访问同步方法与非同步方法(指被synchronized修饰和不被synchronized修饰的方法)
两个线程几乎同时运行,同时结束,原因为synchronized关键字只能作用于被指定的一个方法,非同步方法不受影响。
5、访问同一个对象的不同的普通同步方法
他们会一个一个的执行,synchronized关键字虽然没有明确指定所要对象,其本质原理指定了this对象作为他的锁,对同一个实例来讲,两个方法拿到的this一样,两个方法串行运行。
6、同时访问静态synchronized和非静态synchronized方法
两个线程几乎同时运行,同时结束。原因为synchronized修饰static方法,锁住的对象是*.Class;Synchronized修饰普通方法,锁对象默认为对象实本身this,两个对象不一样,所以两者互不干扰。
7、方法抛异常后,是否会释放锁
会释放锁。抛出异常后锁由JVM释放。
总结:
1、一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况);
2、每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把锁(对应第22、3、4、6种情况);
3、无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)
synchronized的性质
可重入
可重入(递归锁):指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁
好处:避免死锁、提升封装性
粒度:默认加锁范围是线程而非调用(用3种情况来说明和pthread的区别)
情况1:证明同一个方法是可重入的 √
情况2:证明可重入不要求是同一个方法 √
情况3:证明可重入不要求是同一个类中的 √
不可中断
一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,那么我只能永远地等下去。
(Lock类,拥有中断的能力,有权中断现在已经获取到的锁的线程的执行,也可以退出。)
原理
加锁和释放锁的原理
现象:每一个类的实例对应一把锁,每一个synchronized的方法都必须首先获得调用该方法类的实例的锁,方能执行,否则线程阻塞;而方法一旦执行,它就独占这把锁,直到该方法返回或抛出异常,才将锁释放。
时间:获取和释放锁的时机:内置锁
等价代码
深入JVM看字节码
概况:synchronized使用的锁是在Java对象头里的一个字段,即Java对象头里有一个字段表示这个对象是否被锁住。
当线程访问一个同步代码块的时候它必须得到这把锁,退出整个代码块或抛出异常必须释放这把锁。进入锁和释放锁是基于monitor对象来实现同步方法和同步代码块的。monitor最重要的两个指令是monitorenter,这个指令会插入到同步代码块开始的位置,与monitorexit ,这个指令会插入到方法结束的时候和退出的时候。enter必须有exit对应,可能多个exit与一个enter对应。
可重入原理:加锁次数计数器
JVM负责跟踪对象被加锁的次数
线程第一次给对象加锁的时候,计数变为1.每当这个相同的线程在此对象上再次获得锁时,计数会递增。
每当任务离开时,计数递减,当计数为0的时候,锁被完全释放。
保证可见性的原理:Java内存模型
一旦一个方法或代码块被synchronized所修饰,那么它在执行完毕之后,被锁住的对象所做的任何修改都要在释放锁之前从线程内存写回到主内存中。在未获得的代码块或方法得到锁之后,被锁定的数据直接由主内存读取。
synchronized的缺陷
效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程。
不够灵活(读写锁更灵活:加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
无法知道是否成功获取到锁。
常见面试问题
1、synchronized使用注意点:锁对象不能为空、作用域不宜过大、避免死锁
2、如何选择Lock和synchronized关键字?
建议:如果可以的话,既不要使用Lock,也不要使用synchronized关键字,而是使用java.util.concurrent的包的各种类。如果在程序中synchronized关键字适用,那就优先使用这个关键字,因为这样可以减少我们所需要编写的代码,也就减少了出错的几率。如果特别需要用的Lock、condition独有的特性时,才使用它们。
3、多线程访问同步方法的各种具体情况,见上
思考题
1、在多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的线程?
竞争这把锁的又已经在等待中的线程、刚进入synchronized关键字所包裹代码块,处于Runnable状的线程。哪个线程将获取这把锁由JVM决定,与JVM的版本和具体实现都有关系,不能依赖算法,只能判定它是一个随及、不可控的。
2、synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
优化使用范围,使临界区在符合要求的情况下尽可能的小。
使用其他类型的lock,synchronized 使用的锁经过 jdk 版本的升级,性能已经大幅提升了,但相对于更加轻量级的锁(如读写锁)还是偏重一点,所以可以选择更合适的锁。
……
3、我想更灵活地控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?
根据需要自己实现Lock接口
4、什么是锁的升级、降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?
一句话介绍synchronized
JVM会自动通过monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。