虽然网上关于Java并发、多线程的文章已是不胜枚举,但是读起来总感觉晦涩且千篇一律,不是你转载我,就是我复制你。在慕课网学习了一套悟空老师的教程:http://www.imooc.com/learn/1086 ,受益匪浅,因此决定自己总结一下,以便自己常读常新,同时由于并发和多线程这块是面试重点中的重点,也是为了以后的面试做一下准备。如果有需要转载的小伙伴,只需要注明转载来源即可,珍惜博主劳动成果,再次感谢悟空老师提供的教程。
1.Synchronized的作用
能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
2.Synchronized的地位
1.Synchronized是Java的关键字,被Java语言原生支持
2.是最基本的互斥同步手段
3.是并发编程中的元老级角色,是并发编程的必学内容
3.Synchronized的两个用法
对象锁
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
方法锁形式:synchronized修饰普通方法,锁对象默认为this
代码块形式:手动指定锁对象
类锁
指synchronized修饰静态的方法或指定锁为Class对象
概念(重要):
只有一个Class对象:Java类可能有很多个对象,但只有一个Class对象
本质:所谓的类锁,不过是Class对象的锁而已
用法和效果:类锁只能在同一时刻被一个对象拥有。
形式一:synchronized加在static方法上
形式二:synchronized(*.class)代码块
4.不使用并发手段会有什么后果?如何解决?
解决问题:两个线程同时a++,最后结果会比预计的少
原因
count++,它看上去只是一个操作,实际上包含了三个动作:
1.读取count
2.将count加一
3.将count的值写入到内存中
这三个操作,如果不按照原子去执行,就会带来并发问题
解决办法:
对象锁:
1.方法锁形式:在普通方法上加上sychronized关键字:
@Override
public synchronized void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
2.同步代码块加锁:
@Override
public void run() {
synchronized (this) {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
类锁:
1.静态方法上加锁:
@Override
public void run() {
count();
}
synchronized static void count() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
2.synchronized(*.class)代码块:
@Override
public void run() {
synchronized (DisappearRequest1.class) {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
4.多线程访问同步方法的七种情况(面试常考)
1.两个线程同时访问一个对象的同步方法(对象锁)
这种情况就是对象锁的方法锁情况。会相互等待,只能有一个线程持有锁。
2.两个线程访问的是两个对象的同步方法
不会加锁,因为访问的是不同的实例
3.两个线程访问的是synchronized的静态方法
这种情况就是类锁的静态方法锁。
4.同时访问同步方法与非同步方法
synchronized关键字只作用于当前方法,不会影响其他未加关键字的方法的并发行为。因此非同步方法不受到影响,还是会并发执行。
5.访问同一个对象的不同的普通同步方法(对象锁)
synchronized关键字虽然没有指定所要的那个锁对象,但是本质上是指定了this这个对象作为它的锁。所以对于同一个实例来讲,两个方法拿到的是同一把锁,因此会出现串行的情况。
6.同时访问静态synchronized和非静态synchronized方法
前者为类锁,锁为Class类;后者为对象锁,锁为this对象。因此两者的锁不同,会并行执行。
7.方法抛异常后,会释放锁么?
Lock类加锁时,如果出现异常,不显式手动释放锁的话,Lock是不会释放的。
而synchronized不同,一旦出现异常,会自动释放锁。
也就是说当第二个线程等待一个被synchronized修饰的方法时,若第一个线程出现异常退出,这把锁会立刻释放并且被第二个线程所获取到。JVM会自动把锁释放。
8.扩展:线程进入到一个被synchronized修饰的方法,而在这个方法里面调用了另外一个没有被synchronized修饰的方法,这个时候还是线程安全的吗?
答案:不是的。出了本方法后,由于另外的方法没有被synchronized修饰,所以说这个方法可以被多个线程同时访问的。
5.七种情况总结:三点核心思想
1.一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况);
2.每个实例都对应有自己的一把锁,不同实例之间互不影响;
例外:锁对象是*。class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁,这就是类锁的两种情况(对应第2、3、4、5种情况);
3.无论方法是正常执行完毕或者抛出异常,都会释放锁(对应第7种情况)。
只需要对这三点核心思想理解透彻了,所有情况都是这三点核心思想的实例化的表现。
6.Synchronized的两个性质
1.可重入(synchronized区别于其他锁的一个很重要的性质)
什么是可重入:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。也叫做递归锁。Java中两大递归锁:Synchronized和ReentrantLock
好比买车摇号,只要摇到一次号,可以给家里的第一辆车、第二辆车...所有车都上牌,直到我不需要上牌为止。这就叫做可重入性。如果每辆车上牌都需要摇一次号,这就叫做不可重入性。
锁的不可重入性:线程拿到一把锁了,如果想再次使用这把锁,必须先将锁释放,与其他线程再次进行竞争。
锁的可重入性:如果线程已经拿到了锁,试图去请求这个已经获得到的锁,而无需提前释放,直接就可以使用手里的这把锁,这就叫做可重入性
好处:避免死锁、提升封装性
1.假如有两个synchronized修饰的方法1和方法2,此时线程A执行到了方法1,并且获得了方法1的锁,此时方法1调用了方法2,由于方法2也是synchronized修饰的,假设synchronized不具备可重入性的话,那么线程A虽然拿到了方法1的锁,但是由于不可重入,它无法使用本身获得的方法1的这把锁。这样一来,它既想拿锁又不释放锁,这样就会永远等待,形成了死锁。所以由于synchronized具备可重入性,就避免了这种情况的发生。
2.避免了一次又一次的解锁加锁的过程,利用其可重入的性质提高了封装性,简化了并发编程的难度。
粒度:线程而非调用(用三种情况来说明和pthread的区别)
情况1:证明同一个方法是可重入的
情况2:证明可重入不要求是同一个方法
情况3:证明可重入不要求是同一个类中的
2.不可中断(相比于其他有的锁可以中断,这个性质是synchronized的一个劣势所在)
一旦这个锁已经被别的线程获得了,如果当前线程还想获得,只能选择等待或者阻塞,直到别的线程释放这个锁。如果别的线程 永远不释放锁,那么线程只能永远地等下去。
相比之下,Lock类,可以拥有中断的能力。
第一点,如果我觉得我等的时候太长了,有权中断现在已经获取到锁的线程的执行;
第二点,如果我觉得我等待的时间太长了不想再等了,也可以退出。
Lock比synchronized灵活很多,但是编码易出错。
7.深入原理
1.加锁和释放锁的原理:现象、时机、深入JVM看字节码
现象:
每一个类的实例对应一把锁,而每一个synchronized方法都必须先获得调用该方法的类的实例的锁方能执行,否则线程会阻塞。而方法一旦执行,它就独占了这把锁,直到该方法返回或者是抛出异常,才将锁释放。一旦锁释放之后,之前被阻塞的线程才能获得这把锁,从被阻塞的状态重新进入到可执行的状态。当一个对象中有synchronized修饰的方法或者代码块的时候,要想执行这段代码,就必须先获得这个对象锁,如果此对象的对象锁已经被其他调用者所占用,就必须等待它被释放。所有的Java对象都含有一个互斥锁,这个锁由JVM自动去获取和释放,我们只需要指定这个对象就行了,至于锁的释放和获取不需要我们操心。
获取和释放锁的时机:内置锁
我们知道每一个Java对象都可以用作一个同步的锁,这个锁叫做内部锁,或者叫做监视器锁--monitor lock。线程在进入到同步代码块之前,会自动获得这个锁,并且在退出同步代码块的时候会自动的释放这个锁,无论是正常途径退出还是抛出异常退出。获得这个内置锁的唯一途径就是进入这个锁保护的同步代码块或者同步方法中。这样一来就理解了时机。
等价代码:
Lock lock = new ReentrantLock();
public synchronized void method1 () {
System.out.println("我是synchronized形式的锁");
}
public void method2() {
lock.lock();
try {
System.out.println("我是lock形式的锁");
} finally {
lock.unlock();
}
}
第一个方法和第二个方法等价,相当于把synchronized拆分成lock
在进入方法的时候回隐形的获取一把锁,等价于第二个方法的lock.lock();代码所做的事情
而在退出或者抛出异常的时候会释放锁,等价于lock.unlock();代码所做的事情
深入JVM看字节码:反编译、monitor指令
1.编写一个类:
package demo;
/**
* @Description: 反编译字节码
* @Author: jiangfan
* @CreateTime 2019/1/14 上午10:22
*/
public class Decompliation14 {
private Object object = new Object();
public void insert(Thread thread) {
synchronized (object) {
}
}
}
2.javac编译该类:
javac Decompliation14.java
3.反编译后:
javap -verbose Decompliation14.class
线程既可以在方法完成之后退出,也可以在抛出异常后退出,因此monitorexit数量多于monitorenter。
monitorenter和monitorexit指令
monitorenter:线程每次进入时,计数器+1。如果重入,继续加
monitorexit:线程退出时,计数器-1.变为0时候,其他线程可以获取锁。
可重入原理:加锁次数计数器
1.每个对象都有一把锁,JVM负责跟踪对象被加锁的次数。
2.线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程再次对象上再次获得锁时,计数会递增。只有首先获得这个锁的线程才能继续的在这个对象上多次的获取这把锁。
3.每当任务离开时,计数递减,当计数为0的时候,锁被完全释放。
保证可见性的原理:Java内存模型
线程A向线程B发送数据的两个步骤:
1.线程A修改了本地内存A,并将其存储到主内存中。
2.线程B再从主内存中读取出来。
这个过程是由JMM(Java Memory Model)控制的。JMM通过控制主内存与每个线程的本地内存的交互来为Java程序员提供内存可见性的保证。
synchronized是如何做到内存可见性的实现?
一旦一个代码块或者方法被synchronized修饰之后,那么它在执行完毕之后被锁住的对象所做的任何修改都要在释放锁之前从线程内存写回到主内存中。所以下一个线程从主内存中读取到的数据一定是最新的。就是通过这样的原理,synchronized关键字保证了每一次执行都是可靠的,保证了可见性。
8.Synchronized的缺陷
1.效率低:
- 锁的释放情况少
- 试图获得锁时不能设定超时
- 不能中断一个正在试图获得锁的线程
2.不够灵活(读写锁更灵活:读操作的时候不会加锁,写操作的时候才会加锁):
- 加锁和释放的时机单一
- 每个锁仅有单一的条件(某个对象),可能是不够的
3.无法知道是否成功获取到锁
Lock可以,如果尝试成功了做一些逻辑判断,如果没有成功做另外一些逻辑判断.
Lock类:
lock.lock();
lock.unlock();
通过这两个方法,可以手动加锁和解锁。
lock.tryLock();
lock.tryLock(10, TimeUnit.MINUTES);
可以判断是否加锁,返回类型为boolean
9.常见面试问题
1.synchronized的使用注意点:
锁对象不能为空:锁的信息保存在对象头里面
作用域不宜过大:synchronized关键字包裹的范围。不需要串行工作的情况下,用并行的方式可以提高运行的效率
避免死锁
2.如何选择Lock和synchronized关键字?
1.如果可以的情况下,两者都不要选择,而是使用java.util.concurrent包中的各种各样的类,例如:CountDownLatch等。使用这些类,不需要自己做同步工作,更方便,也更不容易出错。
2.如果synchronized关键字在程序中适用,就优先实用这个关键字。因为这样可以减少需要编写的代码,就减少了出错的几率。
3.如果特别需要Lock这样结构独有的特性的时候,才使用Lock。
以上三点主要是基于减少代码出错为出发点。
3.多线程访问同步方法的各种具体情况
看4.多线程访问同步方法的七种情况
10.思考题
1.多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?
锁调度机制。对于synchronized内置锁,不同版本的JVM处理方式不同,blocked和running都有几率
2.synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
- 优化使用范围,让加锁区在业务允许的情况下足够小。
- 用其他类型的锁,例如读写锁,这样在读的时候就不止一个线程可以同时进入代码。
3.我想更灵活地控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?
自己实现一个Lock
4.什么是锁的升级、降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?
在之前的JVM版本中,synchronized性能不是特别的好,而经过不断的迭代,synchronized性能已经得到了显著的提高,这里面运用的技术就是偏斜锁、轻量级锁、重量级锁。JVM会根据synchronized关键字使用到的次数或者其他的种种指标对锁进行有效的优化使得性能得到大幅上涨,这里面还涉及到了对象头里面的字段。
11.总结
一句话介绍synchronized:
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入性和不可中断的性质。