高并发详解之同步synchronized关键字
-
两种用法:对象锁和类锁。
-
多线程访问同步方法的7种情况:是否是static、Synchronized方法等。
原理:加解锁原理,可重入原理,可见性原理
缺陷:效率低,不够灵活,无法预判是否成功获得到锁
Synchronized的作用
官方解释:
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果是一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过该同步方法完成的。
简单理解:
能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果
(synchronized修饰的代码以原子的方式执行,锁的使用)
Synchronized的地位
-
synchronized是java的关键字,被java语言原生支持
-
是最基本的同步互斥手段
-
是并发编程中的元老级角色,是并发编程的必学内容
??????不使用并发手段的后果
/**
* @author glong
* @date 2019/9/4 17:01
*
* 描述: 消失的请求数
*/
public class DisappearRequest1 implements Runnable{
static DisappearRequest1 instance = new DisappearRequest1();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
代码实战:两个线程同时a++,最后的结果比预计的少
原因:count++ 看上去是一个操作,实际上是三个动作:
-
1.读取count,
-
2.将coutn加一,
-
3.将count的值写入到内存中
/**
* @author glong
* @date 2019/9/4 20:04
* 描述:
*/
public class DisappearRequest2 implements Runnable{
static DisappearRequest2 instance = new DisappearRequest2();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
Synchronized的两种用法
对象锁
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己制定锁对象)
类锁
指synchronized修饰静态的方法或指定锁为Class对象
对象锁
代码块形式:手动指定锁对象
方法锁形式:synchronized修饰普通方法,锁对象默认为this
类锁
概念(重要):java类可能有很多个对象,但是只有一个Class对象
形式1:synchronized 加在static方法上
形式2:synchronized(*.class)代码块
概念:
-
只有一个class对象:java类可能有很多对象,但是只有一个class对象
-
本质:所谓的类锁,不过是Class对象的锁而已
-
用法和效果:类锁只能在同一时刻被一个对象拥有
面试常考
多线程访问同步方法的7种情况
-
-
两个线程同时访问一个对象的同步方法
-
两个线程同时访问两个对象的同步方法
-
两个线程访问的是synchronized的静态方法
-
同时访问同步方法与非同步方法 --> 非同步方法不受到影响
-
访问同一个对象的不同的普通同步方法 -->同步方法串行,因为使用的this是同一个对象
-
同时访问静态synchronized和非静态synchronized方法
-
方法抛出异常后,会释放锁
(在lock(类)中默认,即便抛出异常,没有显示的手动进行释放是不会释放锁的)
(在synchronized关键字中,抛出异常会主动释放锁)
-
-
一个对象有多个方法锁时候串行执行
总结
3点核心思想
-
一把锁只能被一个线程获取,没有拿到锁的线程必须等待(对应1,5情况)
-
每个实例都对应有自己的一把锁,不同实例之间互不影响;
例外:锁对象是*.class以及synchronized修饰的是static方法的时候(即锁是类锁),所有对象共用同一把锁(对应2,3,4,6情况);
-
无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应7情况)
原理部分
Synchronized的性质
-
可重入
-
不可中断(劣势所在)
一、可重入(也叫递归锁):
什么是可重入:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁(无需排队)
什么是不可重入:一个线程拿到一把锁,但是需要再次使用这把锁,必须释放以后和其他线程(需要这把锁的线程)进行竞争排队
好处:避免死锁、提高封装性(不可重入,当线程既想那锁又不释放锁就会永久等待)
粒度: 线程而非调用(用三种情况来说明和pthread的区别)
情况1:证明同一个方法是可重入的
情况2:证明可重入不要求是同一个方法
情况3:证明可重入不要求是同一个类中的
可重入结论
通过证明发现Synchronized的粒度是线程层面的,就是只要拿到了这把锁,在当前这个线程中都可以使用
二、不可中断性质
解释:
一旦这个锁被别人获得了,如果我还想获得,我只能等待或者阻塞,直到别的线程释放这个锁如果别人永远不释放锁,那么我只能永远的等待下去。
对比:
相比之下,未来会介绍的lock类,可以拥有中断的能力,第一点,如果我觉得我等的时间太长了,有权中断现在已经获得到锁的线程的执行;第二点,如果我觉得我等待的时间太长了不想在等了,也可以退出。
原理
-
加锁和释放锁的原理:现象、时机、深入jvm看字节码
-
可重入原理:加锁次数计数器
-
保证可见性的原理:内存模型
一、加锁和释放锁的原理
-
现象
每一个类的实例对应一把锁,而每一个synchronized方法都必须首先获得调用该方法的类的实例的锁才能执行。否则线程就会阻塞,而方法一旦执行,就会独占这把锁,直到该方法返回,或者抛出异常,才能释放锁。释放之后,其他被阻塞的线程才能获得这把锁,重新进入可执行的状态。
-
获取和释放锁的实际:内置锁(时机)
每一个java对象都可以用作一个实现同步的锁(内置锁或者叫做监视器锁monitor lock),线程在进入代码块之前会自动进入这把锁,并且在退出这个同步代码块的时候会自动释放锁,正常退出和异常退出都会释放。获得这个内置锁的唯一途径就是进入到这个锁所保护的同步代码块或者方法中
-
等价代码
// method1和method2两个方法等价
// 代码来源:java并发实战
public synchronized void method1(){
System.out.println("我是Synchronized形式的锁");
}
public void method2(){
// 锁住
lock.lock();
try {
System.out.println("我是lock形式的锁");
}finally {
lock.unlock();
}
} -
深入 JVM看字节码:反编译、monitor指令
加锁和释放锁的原理
深入jvm看字节码
java对象头有一个部分就是存储synchronized锁的
当线程访问同步代码块的时候就必须得到这把锁,退出的时候必须释放这把锁,这个锁存放在java对象头中的
进入锁和释放锁是基于monitor对象来实现同步方法和同步代码块
monitor重要的两个指令:monitorenter(插入到同步代码块开始的时候)monitorexit(插入同步代码块结束的时候和退出的时候)
一个monitorenter可以对应多个monitorexit,是因为进入之后度与退出的情况并不是一一对应的,多种退出方式使得exit数量可能大于enter的数量。
monitorenter使monitor计数器+1,monitorexit使计数器-1,如果变成没有变成0,说明之前是重入的,那么线程继续持有锁
当访问到monitorenter的时候就尝试获取这个对象所对应的monitor所有权(这个对象锁)
一个monitor的lock锁只能被一个线程在同一时间获取,一个对象在获取monitor会出现以下三种情况:
-
如果monitor计数器为0,说明目前还没有被获得,该现场立刻获得,并把monitor计数器+1(成功获得锁)
-
如果monitor已经拿到了锁的所有权,又重入了,计数器累加,再加一
-
如果monitor已经被其他线程获取了,我获取的时候则获取不了,进入阻塞,直到monitor为0
monitorexit 执行的时候会使monitor计数器-1,当monitor计数器为0,释放锁,不为0说明是可重入进来的,继续持有这把锁。
当计数器为0,其他的被阻塞的线程将会重新尝试获取该锁的所有权
synchronized关键字底层原理:monitorentrant进入锁,monitorexit退出锁
类锁可以理解为一种特殊的对象锁,锁住的是类所对应的.class对象
静态方法锁是类锁
1、一个线程释放锁,JVM如何决定下一个获取该锁的线程:
有可能是新来的获取到,有可能是老的处于阻塞状态的线程获取到,
随机,不公平的
2、synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
优化锁的使用范围,需要加锁的地方才加锁
可重入原理:加锁次数计数器
-
JVM负责跟踪对象(每个对象自动含有一把锁)被加锁的次数
-
线程第一次给对象加锁的时候,计数器为1.每当这个相同的线程在此对象上再次获得锁时,,计数回递增
-
每当任务离开的时候,计数递减,当计数为0的时候,锁被完全释放。
可见性原理:java内存模型
线程A与线程B通信:(两个步骤)
-
线程A把副本写到主存中,更新主内存(线程内存 -->主内存)
-
线程B从主内容读取数据(JVM执行)
sychnorized的可见性
一旦一个代码块或者方法被我们的synchronize关键字所修饰,他在执行完毕之后,被锁住的对象所做的任何修改都要在释放锁之前,从线程内存写会到主内存,也就是不会存在线程内存和主内存内容不一致的情况,同样在进入代码块得到锁之后,被锁定的对象的数据也是直接从主内存中读取出来的,而在释放的时候会把修改的内容写回到主内存中,所以从主内存中读取到的数据一定是最新的,通过这个原理synchronize保证了我们每一次的执行都是可靠的,保证了可见性
Synchronized的缺陷
-
效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程。
-
两种释放方式,一种是正常执行任务完释放,一种是异常JVM释放
-
不能设置超时,只能一直等待
-
-
不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的。
-
无法知道是否成功获取到锁。(无法判断状态)
Lock
-
lock();//获取锁
-
unlock();//释放锁
-
tryLock();//判断锁是否可用。返回值为:boolean;
-
tryLock(time,TimeUnit);//在规定的时间内,如果未获得锁,则就放弃。第一项表示规定的时间;第二项表示设置时间的单位
面试
-
synchronized关键字使用的注意事项
-
使用注意点:锁对象不能为空,作用域不宜过大,避免死锁
-
锁的信息保持在对象头中(没有对象就没有对象头)
-
锁对象必须是一个实例对象
-
-
如何选择Lock和synchronized关键字
1)建议都不使用,可以使用java.util.concurrent包中的Automic类、countDown等类
2)优先使用现成工具,如果没有就优先使用synchronized关键字,好处是写劲量少的代码就能实现功能。如果需要灵活的加解锁机制,则使用Lock接口
3)如果synchronized在程序中适用,优先使用这个关键字,这样可以减少需要编写的代码,减少出错的几率
-
多线程访问同步方法的各种具体情况
7种情况
思考题
-
在多个线程等待同一个synchronized锁的时候,JVM是如何选择下一个获取锁的是那个线程?
---》》涉及内部锁调度机制,线程有,进程也有调度机制
-
Synchronized是的同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
---》》一、优化使用范围;二、使用其他类型的lock
-
想更加灵活的控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?
---》》
-
什么是锁的升级、降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?
总结
一句话介绍synchronized
-
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质