目录
1.死锁
1.1造成死锁的4个条件(缺一不可)
- 互斥条件
- 拥有请求条件
- 不可剥夺条件(不主动释放,其他线程得不到资源)
- 环路等待条件(按序请求锁)
- 可通过修改2,4解决死锁问题
1.2死锁解决方案
按序请求锁,来破坏环路等待条件
1.3死锁排除工具
jconsole/vm/jmc
2.锁的策略
2.1乐观锁
乐观锁:它的任务一般情况下不会发生冲突,所以只有在进行数据更新的时候,才会检验并发冲突,如果没有冲突则执行修改,如果有冲突则返回失败。
常见名词:CAS(乐观锁)、ABA、JUC
2.1.1CAS(compare and swap,比较并且交换)
CAS内存中的值:
A(旧值)
B(新值)
V(内存值)
2.1.2CAS实现原理
V=A?true(没有并发冲突)-> V=B
false(并发冲突) ->自旋(将自己的A值修改成V值)
CAS底层实现原理:
Java层面CAS的实现是Unsafe类
unsafe类调用了C++的本地方法,通过调用不同操作系统的本地方法实现
2.1.3乐观锁的实现Atomic*
线程的解决方法:
- 加锁
- ThreadLock
- Atomic*(乐观锁实现)
乐观锁优点:性能比较高
CAS缺点:带来ABA问题 -> AtomicInteger是存在ABA问题的
ABA解决方案:统一版本号
使用AtomicStampedReference解决ABA问题,原理就是添加版本号,当旧版本号和新的版本号符合时,在进行操作。
2.1.4ABA问题叙述:转账(多线程并发引起的问题)
A向B转账,失误了点了两次,假设每次转账100元,在执行完第一次转账后,C给A转账100元,接着再去执行,A向B转账100元,此时使用Atomic*就会出现ABA问题。因为第一次转账时,A的旧值为100,预期新值为0;第二次转入时,A的旧值为0,预期新值为100,内存校验通过,转入成功;第三次转账时,A的旧值仍旧被识别为100(此100非彼100,一个是A本身有的,一个是C转入的),预期新值为0,内存效验通过;这就造成了ABA问题,再次少了100元。
2.2悲观锁
悲观锁:他认为通常情况下会出现并发冲突,所以在一开始就会加锁
在Java中的应用:synchronize是悲观锁(反过来说不是很准确,比如synchronize也是可重入锁)
2.3共享锁/非共享锁(独占锁)
共享锁:一把锁可以被多个线程拥有,这就叫共享锁;例如:读写锁中的读锁就是共享锁。
非共享锁:一把锁只能被一个线程拥有,synchronize
读写锁:就是把一把锁分成两个,一个用于读数据的锁,另一个叫写锁。
读写锁的具体实现:ReentrantReadWriteLock
读写锁优势:
- 锁的粒度更小,性能好
- 读锁和写锁是互斥的
2.4公平锁
锁的获取顺序必须和线程方法的先后顺序保持一致,就叫做公平锁
非公平锁:锁的获取顺序必须和线程方法的先后顺序无关,就叫做非公平锁(默认锁策略)
非公平锁优点:性能比较高
公平锁优点:执行是有序的,结果也是可以预期的
公平锁:new ReentranLock(true)
非公平锁:new ReentranLock()/new ReentranLock(false)/synchronize
2.5自旋锁
通过死循环一直尝试获取锁。例如:synchronize
2.6可重入锁
当一个线程获取锁后,可重新进入;例如:synchronize、lock
缺点:如果发生死锁,则会一直循环,所以会带来一定程度的额外开销
3.面试题:你是怎么理解乐观锁/悲观锁的,具体怎么实现
答:乐观锁-> CAS -> Atomic*,CAS是由V(内存值)A(预期旧值)B(预期新值)组成,执行的时候,使用V==A对比,true说明没有冲突,可以修改,false说明有冲突,不可修改。CAS是通过调用C++的unsafe中的本地方法(native),C++是通过调用操作系统的Atomic::cmpxchg的原子性操作指令
悲观锁 -> synchronize -> 在java中是将锁ID存在对象头来实现的(存放在偏向锁ID中,每次检查线程ID和偏向锁ID是否相同) - > JVM层面是通过监视器来实现的 - > 操作系统层面是通过mutex(互斥锁)实现的。
synchronize锁优化(锁消除):
在将端口1.6时候有锁升级的过程:
- 无锁
- 偏向锁(第一线程第一次访问,并将线程ID存放在对象头中的偏向锁ID里)
- 轻量级锁(自旋)
- 重量级锁(性能低)
4.单例模型
单例模型:整个程序的运行中,只存在一个对象。
创建方法:
- 饿汉方式:上来直接先创建对象。优点:不用加锁也是线程安全的。缺点:程序启动之后就会创建,但是创建完成之后可能不会使用,从而浪费系统资源。
- 懒汉方式:当程序启动之后,并不会进行初始化,什么时候调用什么时候初始化。
4.1饿汉模型
package Thread._0525;
/**
* Created with IntelliJ IDEA.
* Description:饿汉方式
* User:吴博
* Date:2021 05 25
* Time:20:18
*/
public class Test03 {
static class Singleton{
//1.创建一个私有构造函数:防止其他类直接创建
private Singleton(){
}
//2.定义私有变量
private static Singleton singleton = new Singleton();
//3.提供公共的获取实例的方法
public static Singleton getInstance(){
return singleton;
}
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
}
}
4.2懒汉模型
package Thread._0525;
/**
* Created with IntelliJ IDEA.
* Description:懒汉方式V4版本
* User:吴博
* Date:2021 05 25
* Time:20:28
*/
public class Test07 {
static class Singleton{
// //1.创建一个私有构造函数:防止其他类直接创建
private Singleton(){
}
//2.定义私有变量
private static volatile Singleton singleton = null;
//3.提供公共的获取实例的方法
public static Singleton getInstance(){
//判断是否第一次访问
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
static Singleton s1 = null;
static Singleton s2 = null;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getInstance();
}
});
t1.start();
s2 = Singleton.getInstance();
t1.join();
System.out.println(s1 == s2);
}
}
4.3单例模式注意问题
非安全的单例模式——懒汉方式:
public static Singleton getInstance(){
//判断是否第一次访问
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
多线程模式下初始化singleton都为null,都会去new Singleton(),会导致地址不同,造成线程不安全。
线程不安全解决方法:
- 加锁
- ThreadLocal
有问题的地方:
方法使用synchronize加锁后,仍旧会出现问题
singleton = new Singleton();处出现问题(原因是编译器优化,会进行指令重排序)
- 先在内存中开辟空间
- 初始化
- 将变量指向内存区域
指令重排序:123改为132
第一次进入后,加锁,new出对象未初始化,所以地址更改了,但不为null了,第二次进入判断后,因为不会null,所以直接return空对象,最后进行比较时,为false
最终版
为了解决指令重排序问题,需要将singleton对象加volatile关键字,并且使用双重校验DCL的方式来检测对象是否为空。
5.自定义阻塞队列
生产者消费者模型 —— 生产者生产数据,消费者消费生产者生产的数据。
当数据量已满的情况下,不要尝试给队列添加数据了,而是阻塞等待。使用wait()/notify Locksupport park/unpark。
当任务队列为空,进行阻塞等待。
package Thread._0525;
import java.util.Random;
/**
* Created with IntelliJ IDEA.
* Description:自定义阻塞队列
* User:吴博
* Date:2021 05 25
* Time:21:16
*/
public class Test08 {
static class MyBlockQueue{
//实际存储数据的数组
private int[] values;
//队首
private int first;
//队尾
private int last;
//队列元素长度
private int size;
//初始化
public MyBlockQueue(int initial){
values = new int[initial];
first = last = size = 0;
}
//添加
public void offer(int num){
synchronized (this){
//判断边界值
if(size == values.length){
//队列满
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
values[last] = num;
last++;
size++;
//判断是否为最后一个元素,循环队列
if(last == values.length){
last = 0;
}
//尝试唤醒消费zhe
this.notify();
}
}
//查询方法
public int poll(){
int ret = -1;
synchronized (this){
if(size == 0){
//队列为空
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取元素
ret = values[first];
first++;
size--;
//判断是否为最后一个元素
if(first == values.length){
first = 0;
}
//尝试唤醒生产者
this.notify();
}
return ret;
}
}
public static void main(String[] args) {
MyBlockQueue myBlockQueue = new MyBlockQueue(100);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
int num = new Random().nextInt(10);
System.out.println("生产了随机数" + num);
myBlockQueue.offer(num);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
int ret = myBlockQueue.poll();
System.out.println("消费了数据" + ret);
}
}
});
t2.start();
}
}