多线程
1.可重入锁
什么是可重入?
同一个线程针对同一个锁,连续加锁两次,如果出现了死锁,就是不可重入,如果不会死锁,就是可重入的。
synchronized public void increase(){
synchronized(this){
count++;
}
}
理论上看:
外层先加了一次锁,里层又加了一次锁
外层锁:进入方法,开始加锁,加锁成功
内层锁:进入代码块,加锁不成功,因为锁被外层占用,需要外层锁释放才能加锁成功
导致死锁
实际上:
synchronized实现了可重入锁,对于可重入锁来说,上述的连续加锁不会导致死锁,可重入锁的内部,会记录当前的锁是哪个线程占的,同时会记录一个“加锁次数”,线程a针对所第一次加锁的时候,可以加锁成功,加锁次数为1,后续线程a再对锁进行加锁,就不会真的加锁,而是将加锁次数自增,加锁次数为2,后续解锁的时候,先把加锁次数-1,再解锁一次就真正解锁了。
可重入锁的意义降低了程序员的负担,提高了开发效率
但是带来了代价,程序中需要有更高的开销,降低了运行效率
2.产生死锁
(1)死锁的方式
两个线程两个锁
两个线程都抢占了对方需要的资源,哪个线程都不想放弃
N个线程M把锁
哲学家进餐问题:
(2)产生死锁的必要条件
1.互斥使用,锁的本质,保证原子性
2.不可抢占,一个锁被一个线程占用之后,其他线程不能抢走
3.请求和保持
4.循环等待->解决死锁问题一般都是破坏这个条件
实际开发环境中,很少使用嵌套锁,所以没那么容易产生死锁情况,但是如果要使用的话,就一定要约定好加锁顺序
3.标准库中线程安全的类
左边是线程不安全的类,右边是线程安全的类
线程安全的类在关键方法上有synchronized关键字修饰
4.JMM(Java内存模型)
CPU缓存模型示意图
CPU Cache 的工作方式: 先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 i++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。
CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如MESI协议)或者其他手段来解决。 这个缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要遵守的原则和规范。不同的 CPU 中,使用的缓存一致性协议通常也会有所不同。
缓存一致性协议(MESI协议)
并发编程下,像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现一些问题。就比如说我们上面提到的指令重排序就可能会让多线程程序的执行出现问题,为此,JMM 抽象了 happens-before 原则来解决这个指令重排序问题。
JMM 说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。对于 Java 开发者说,你不需要了解底层原理,直接使用并发相关的一些关键字和类(比如 volatile
、synchronized
、各种 Lock
)即可开发出并发安全的程序。
JMM 为共享变量提供了可见性的保障
5.等待&通知
(1)wait and notify
处理线程调度随机性的问题
join也是控制顺序的方式,但是join更倾向于控制线程结束
wait和notify都是Object对象的方法,调用wait方法的线程,就会陷入阻塞,阻塞到有其他线程来notify来通知
//wait内部会做三件事
//1.先释放锁
//2.等待其他线程通知
//3.收到通知后,重新获得锁,并继续往下执行
//s所以wait要和synchronized搭配使用
public class demo3 {
public static Object locker=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
synchronized (locker){
System.out.println("wait 前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait 后");
}
});
t1.start();
Thread.sleep(3000);
Thread t2=new Thread(()->{
synchronized (locker){
System.out.println("notify 前");
locker.notify();
System.out.println("notify 后");
}
});
t2.start();
}
}
(2)notifyAll
现在有一个对象o,有十个线程都调用了o.wait,此时十个线程都是阻塞状态
如果调用o.notify,就会把其中一个线程唤醒
针对notifyAll就会唤醒十个线程,wait唤醒后就会取争取锁
6.关于多线程的案例--单例模式
设计一个线程安全的单例模式(单例模式是设计模式之一,有两个常见模式,单例模式和工厂模式)
单例模式,要求代码中个某个类,只能有一个实例,不能有多个
(1)单例模式的两种实现方式
饿汉模式:中午吃饭用了四个碗,吃完之后马上把碗洗了->比较着急的创建实例
懒汉模式:中午吃饭用了四个碗,吃完之后先不洗,晚上这顿只需要两个碗,那就洗两个碗->需要的时候再创建
//通过singleton这个类实现单例,保证singleton这个类只有唯一实例
//饿汉模式
class singleton{
//借助static进行修饰 保证唯一
//一个java程序中,一个类对象只存在一份,进一步也就保证了类的static成员也只有一份
private static singleton instance=new singleton();//使用static创建一个实例,并且立即创建实例,
// 这个instance就是类的唯一实例
//为了保证程序员不会再去new一个实例,将构造函数用private来修饰
private singleton(){}
public static singleton getInstance(){
return instance;
}
}
//懒汉模式
class singleton1{
private static singleton1 instance=null;
private singleton1(){}
public static singleton1 getInstance(){
//只有第一次用到 才会创建这个实例
if(instance==null){
instance=new singleton1();
}
return instance;
}
}
public class demo4 {
public static void main(String[] args) {
singleton s=singleton.getInstance();
singleton1 s1=singleton1.getInstance();
}
}
在类中,使用private static singleton instance来描述一个实例,static关键词限制了这个instance是唯一的,由这个实例是立即创建还是用的时候再创建将模式分为饿汉模式和懒汉模式,在类中,为了避免程序员在其他地方new这个实例,代码将构造方法用private来修饰,这样无法创建实例。并且由于instance是private的,我们需要提供一个getInstance方法(饿汉模式直接返回instance,懒汉模式需要判断是否第一次使用,是否进行创建实例)
(2)线程安全的单例模式案例(懒汉模式)--面试题
饿汉模式的getInstance是线程安全的,因为他只读
懒汉模式的getInstance存在线程安全问题,因为他既包含了读又包含了修改,而且这里的读和修改是分成两步进行的,不是原子的,存在线程安全问题
->懒汉模式在getInstance方法中,可能会new出来两个实例,是线程不安全的
如何保证懒汉模式的线程安全?->加锁
把getInstance的读和修改打包成原子操作:
public static singleton1 getInstance(){
//使用这里的类对象作为锁对象
synchronized (singleton1.class) {
if (instance == null) {
instance = new singleton1();
}
}
return instance;
}
synchronized指定的是类对象singleton1.class,类对象在程序中只有唯一一个,就能保证多个线程调用getInstance的时候都是针对的同一个对象进行的加锁
问题:对于这个代码来讲,只有在初始化之前是不安全的(又读又改)但是在创建实例之后就是线程安全的了(只读),代码这样虽然线程安全了,但是初始化后线程一直安全还是存在很多没必要的锁。->加锁确实会保证线程安全,但是也付出了一些代价
改进:让getInstance初始化前加锁,初始化后不进行加锁
//这个if判断是否要加锁
if(instance==null) {
synchronized (singleton1.class) {
//这个if判断是否创建实例
if (instance == null) {
instance = new singleton1();
}
}
}
问题:如果很多线程都去调用getInstance方法,就会造成大量的读instance内存的操作,了能就会发生编译器优化->可能编译器会把这个读内存操作优化成读寄存器操作,一旦发生优化,后续有现成已经完成了instance的修改其他线程会感知不到,仍然把instance==null判断通过。
->内存可见性,这个问题可能会导致第一个if判断失误(但是对第二个if影响不大,因为synchronized会保证内存可见性)
改进:给instance加上volatile
private static volatile singleton1 instance=null;
总结:1.正确的位置加锁
2.双重if判断是否加锁
3.内存可见性->volatile
7.关于多线程的案例--阻塞队列
也是先进先出,他还有其他方面功能1.线程安全2.产生阻塞效果(如果队列为空,尝试出队列就会出现阻塞,如果队列为满,尝试入队列就会出现阻塞)
基于上述特性,就可以实现“生产者-消费者模型”
(1)生产者消费者模型
是实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发的场景中,能降低耦合性
优点一:降低耦合
优点二:能够对于请求进行“销峰填谷”
未使用生产者消费者模型的时候,如果请求突然暴涨,B作为应用服务器压力很大
如果使用生产者消费者模型,阻塞队列压力会很大,B会被保护得很好
扩展:实际开发中使用到的“阻塞队列”并不是一个简单的数据结构了,而是一个/一组专门的服务器程序,并且他提供的功能更加丰富(数据持久化、支持多个数据通道,支持多节点容灾冗余备份,支持管理面板,方便配置参数........)这样的队列又起了一个新的名字->“消息队列”
(2)阻塞队列使用
Java标准库中的阻塞队列:
public class demo5 {
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> queue=new LinkedBlockingDeque<String>();
//入队列
queue.put("hello");
//出队列
String s=queue.take();
}
}
(3) 手动实现阻塞队列、生产者消费者模型
阻塞队列的实现:
先实现一个普通队列,再加上线程安全,再加上阻塞
线程安全实现:在类中声明一个locker实例,作为锁对象,由于整个put和take函数都是对变量的修改,所以整个函数都用synchronized来修饰。
阻塞实现:在put方法中,当队列满的时候,需要等待(locker.wait)等待take出队列成功来唤醒
当put方法成功时,需要唤醒(locker.notify)唤醒take中的等待
在take方法中,当队列空的时候,需要等待(locker.wait)等待put入队列成功来唤醒
当take方法成功时,需要唤醒(locker.notify)唤醒put中的等待
class myBlockQueue{
private Object locker=new Object();
private int[] data=new int[1000];
private int size=0;
private int head=0;
private int tail=0;
public void put(int val) throws InterruptedException {
synchronized (locker) {
if (size == data.length)
locker.wait();//队列中满数据,就先wait等待take来notify
data[tail] = val;
tail++;
if (tail >= data.length)
tail = 0;
size++;
locker.notify();//入队列成功就唤醒take中的等待
}
}
public Integer take() throws InterruptedException {
synchronized (locker){
if(size==0)
locker.wait();//队列为空,就先wait等待put来notify
int val=data[head];
head++;
if(head>= data.length)
head=0;
size--;
locker.notify();//出队成功,就可以唤醒put的等待
return val;
}
}
}
生产者消费者模型的实现:
两个线程,一个生产数据,存放(put)进阻塞队列中,一个消费数据,从阻塞队列中取出(take)
public class demo6 {
public static void main(String[] args) {
myBlockQueue queue=new myBlockQueue();
//实现一个简单的生产者消费者模型
Thread producer=new Thread(()->{
int num=0;
while(true){
try {
System.out.println("生产了"+num);
queue.put(num);
num++;
//Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
Thread customer=new Thread(()->{
while(true){
try {
System.out.println("消费了"+queue.take());
//Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
}
总结:线程对资源进行加锁操作,自我感觉很想操作系统中的PV操作都需要有一个锁对象,在Java中需要创建锁对象“locker”,线程通过对“locker”
的加锁,解锁,等待,唤醒实现线程安全