目录
- synchronized
- 刷新内存
- synchronized的特性可重入的
- 出现死锁的情况
- 如何避免死锁(重点,死锁的成因和解决)
- volatile关键字
- wait和notify
- 多线程的代码案例
- 饿汉模式和懒汉模式的线程安全问题
- 指令重排序问题
- 阻塞队列
- 使用
- 自己实现一个阻塞队列
- 实现生产者消费者模型
synchronized
- synchronized除了修饰代码块之外,还可以修饰一个实例方法或者是修饰一个静态方法
synchronized修饰实例方法
class Counter{public int count;// 此方法是下面方法的简化版本synchronized public void increase(){count++;}// this作为了锁对象public void increase2(){synchronized(this){count++;}}
}
public class Demo14 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(()->{for(int i = 0;i < 50000;i++){counter.increase();}});Thread t2 = new Thread(()->{for(int i = 0;i < 50000;i++){counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
}
synchronized修饰静态方法
java的对象头:java的一个对象的内存空间中,除了你自己定义的属性之外,还有一些自带的属性,这些自带的属性就叫对象头
在对象头中,其中就有属性表示当前对象是否已经加锁
synchronized使用的是java对象头当中的锁
刷新内存
- synchronized刷新内存存疑,网上众说纷纭
synchronized的特性可重入的
-
可重入锁:指的是一个线程连续针对一把锁加锁两次,不会出现死锁,满足这个要求就是可重入的,不满足就是不可重入
-
死锁:就是同一个线程对同一个对象加锁两次,第二次的加锁需要第一次的解锁,而第一次的解锁需要第二次的加锁完毕,所以两者相互矛盾
-
死锁在日常的代码中出现,还不容易观察到,比如下面的代码,使用可重入锁可以解决死锁的问题
-
可重入锁:让锁记录一下,是哪个线程给它锁住的,后续再加锁的时候,如果加锁线程就是持有锁的线程就直接加锁成功
-
提一个问题:
不能释放锁,因为两个右大括号中可能有别的代码,解锁了就线程不安全了
-
利用引用计数,记录释放锁的时机
出现死锁的情况
- 两种情况:
- 两个线程,两把锁
public class Demo15 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){// 这里的sleep非常重要,要确保t1 和 t2 都分别拿到一把锁之后,再进行后续动作// 否则就会出现t1很快对两把锁都加锁了的情况,就不会出现死锁的情况了try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1加锁成功!");}}});Thread t2 = new Thread(()->{synchronized (locker2){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("t2加锁成功!");}}});t1.start();t2.start();}
}
3. N个线程,M把锁(相当于2的扩展)
这样更容易出现死锁了
有一个经典的描述N个线程,M把锁的模型,哲学家就餐问题
如何避免死锁(重点,死锁的成因和解决)
-
死锁的原因:
死锁有4个必要条件:
第4点也是(代码结构)
第4点循环等待比如是哲学家就餐问题 -
第1和第2点是锁的基本特性,只要3和4出现了就会形成死锁
解决死锁问题:
1.对于1和2是synchronized本身的特性,解决不了
2.对于3,请求等待可以把代码改成并列执行的顺序,先执行1再执行2
3.对于4,循环等待,可以规定小的编号的先执行,再执行大的编号(先大后小也可以)
4.有的时候必须要获取多个锁再操作,就需要编号了
解决死锁条件3
public class Demo15 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){// 这里的sleep非常重要,要确保t1 和 t2 都分别拿到一把锁之后,再进行后续动作// 否则就会出现t1很快对两把锁都加锁了的情况,就不会出现死锁的情况了try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}synchronized (locker2){System.out.println("t1加锁成功!");}});Thread t2 = new Thread(()->{synchronized (locker2){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}synchronized (locker1){System.out.println("t2加锁成功!");}});t1.start();t2.start();}
}
解决死锁条件4
public class Demo15 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){// 这里的sleep非常重要,要确保t1 和 t2 都分别拿到一把锁之后,再进行后续动作// 否则就会出现t1很快对两把锁都加锁了的情况,就不会出现死锁的情况了try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1加锁成功!");}}});Thread t2 = new Thread(()->{synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t2加锁成功!");}}});t1.start();t2.start();}
}
volatile关键字
- volatile用来解决线程不安全的两点
保证内存可见性
禁止指令重排序
内存可见性出现的线程安全问题:
import java.util.Scanner;public class Demo16 {private static int isQuit = 0;public static void main(String[] args) {// 读Thread t1 = new Thread(()->{while(isQuit == 0){// 循环里面什么都不做}System.out.println("t1线程结束!");});t1.start();// 修改isQuit的值使线程t1结束Thread t2 = new Thread(()->{System.out.println("请输入 isQuit的值:");Scanner scanner = new Scanner(System.in);isQuit = scanner.nextInt();});t2.start();}
}
出现内存可见性的原因:是编译器优化出现的bug
volatile可以解决上述问题
在多线程的环境下,编译器对于是否要进行优化,判定不一定准,这就需要我们使用volatile关键字告诉编译器,不要优化了
让判断isQuit和从内存中读达到平衡,让判断的操作每次读取isQuit
只需要给isQuit前加上volatile就可以了
import java.util.Scanner;public class Demo16 {private volatile static int isQuit = 0;public static void main(String[] args) {// 读Thread t1 = new Thread(()->{while(isQuit == 0){// 循环里面什么都不做// 什么都不做会飞快的运行多次}System.out.println("t1线程结束!");});t1.start();// 修改isQuit的值使线程t1结束Thread t2 = new Thread(()->{System.out.println("请输入 isQuit的值:");Scanner scanner = new Scanner(System.in);isQuit = scanner.nextInt();});t2.start();}
}
编译器优化,加了sleep就不会触发内存可见性问题了,原因如下:
-
内存的可见性问题:
main memory:主存
work memory:cpu寄存器 + 缓存 -
volatile不能保证原子性
-
synchronized可以保证内存可见性
-
volatile可以解决的线程安全问题主要是可见性问题和有序性问题,但不能解决原子性问题。
wait和notify
- wait:等待,让指定线程进入阻塞状态
- notify:通知,唤醒阻塞状态的进程
下面的代码就是在t2线程中,t1进行join,t2线程执行后,t2线程要等待t1线程执行完毕之后才能继续执行
public class Demo17 {public static void main(String[] args) {Thread t1 = new Thread(()->{for(int i = 0;i < 5;i++){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程执行完毕!");});Thread t2 = new Thread(()->{try {// join()在哪个线程中,哪个线程就会等待调用join()的线程先执行完毕,该线程才能继续执行t1.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2线程执行完毕!");});t1.start();t2.start();System.out.println("主线程执行完毕!");}
}
- wait在执行的时候要做三件事:
释放当前的锁
让线程进入阻塞
当线程被唤醒的时候,重新获取到锁
释放锁的前提是先加上锁
wait和notify都是Object的方法
随便一个对象都可以使用wait和notify
wait的代码,等待
public class Demo18 {public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized(object) {System.out.println("调用wait之前");// 把wait放在synchronized里面来调用,确实是先拿到了锁object.wait();// 然后阻塞等待,等待到其他线程调用nofity唤醒System.out.println("调用wait之后");}}
}
notify的代码,等待唤醒
public class Demo19 {public static void main(String[] args) {// 必须对同一个对象进行等待唤醒Object object = new Object();Thread t1 = new Thread(()->{synchronized (object){System.out.println("wait开始!");try {object.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("wait结束!");}});Thread t2 = new Thread(()->{synchronized (object){try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}object.notify();System.out.println("wait唤醒!");}});t1.start();t2.start();}
}
- 使用wait和notify也可以避免线程饿死
线程饿死:让1号一直在获取锁和释放锁之间反复横跳
解决方法:
5. wait还有一个超时时间的版本,避免了wait无休止的等待下去,比如:wait(3000)
多线程的代码案例
- 单例模式:非常经典的设计模式
设计模式:就是程序员的棋谱,开发过程中会遇到很多经典场景,针对这些场景,大佬就提出了很多解决方案,按照解决方案来写就不会很差
2. 最容易考的两种模式:单例模式和工厂模式
- 单例:单个实例(对象),有些场景下,我们希望有的类,只能有一个对象,使用单例模式
- 让编译器强制要求,只能new一次对象
- 单例模式语法上没有要求,要通过编程技巧来达到
单例模式:
1.在类的内部,自己提供一个现有的实例
2.构造方法设置为private修饰的,不让其他人进行创建新的实例
class Singleton{// 自己提供一个现有的实例private static Singleton instance = new Singleton();// 通过这个方法来获取这个实例public static Singleton getInstance(){return instance;}// 把它设置为私有,这样外面的其他代码就无法new出这个实例了private Singleton(){}
}
public class Demo20 {public static void main(String[] args) {// 这里又有一个实例了,就不是单例了// Singleton singleton = new Singleton();Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1 == s2);}
}
在类加载的时候创建,饿汉模式(比较急切)
懒汉模式(在第一次使用的时候,再去创建实例)
和饿汉模式的区别是:在调用getInstance()的时候才创建出实例
也只有唯一实例
// 懒汉模式
class SingletonLazy{public static SingletonLazy instance = null;public static SingletonLazy getInstance(){if(instance == null){instance = new SingletonLazy();}return instance;}private SingletonLazy(){}
}
public class Demo21 {public static void main(String[] args) {}
}
懒汉模式其实在计算机当中是更加高效的,下面就是一个例子:
饿汉模式和懒汉模式的线程安全问题
-
线程安全问题造成的原因:如果多个线程同时修改同一个变量,会出现线程安全问题
如果多个线程同时读取同一个变量,不会有线程安全问题 -
饿汉模式只读取了变量,是安全的
-
懒汉模式既读取了变量,又修改了变量是不安全的
不安全的原因:
如何保证懒汉模式是线程安全的?
需要再if的前面进行加锁,让它的操作变成原子的操作
一旦出现加锁,那就不是高性能了
我感觉是只加锁一次就行,后续,再使用这个获取实例的方法就不加锁,可以吗?
有什么办法既保证线程安全,又可以保证不怎么影响执行效率呢?
可以再加一个if
第一个if用来判定是否需要加锁
第二个if用来判定是否需要new对象,保证对象只new一次
// 懒汉模式
class SingletonLazy{public static SingletonLazy instance = null;public static SingletonLazy getInstance(){// 懒汉模式对类对象进行加锁// if和new的问题// 如果线程安全,就是已经new好了一个对象// 如果线程不安全(有不安全的风险),还没有newif(instance == null) {synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}return instance;}}}private SingletonLazy(){}
}
public class Demo21 {public static void main(String[] args) {}
}
指令重排序问题
- 指令重排序也是编译器的优化,编译器为了提高效率,会调整原有代码的执行顺序,前提是保证了逻辑是不变的
- 指令重排序可以再保证逻辑不变的前提下,提高代码的执行效率
- 指令重排序可能会对我们的代码产生影响,单线程下可以,多线程对逻辑不变会产生误判
4. 使用volatile可以解决指令重排序问题
5. 如果日常使用过程中少了1,2,3其中一个都会出现问题
6. 使用反射能否打破单例模式
使用序列化/反序列化能否打破单例模式
其实反射和序列化都是在特定场景下使用的,使用的比较克制
单例模式在面试过程中都是经常考到的
先写一个线程不安全的,再写一个加锁的,再写一个高性能,不要每次加锁的,最后写一个考虑指令重排序问题的
// 懒汉模式
class SingletonLazy{public static volatile SingletonLazy instance = null;public static SingletonLazy getInstance() {// 懒汉模式对类对象进行加锁// if和new的问题// 如果线程安全,就是已经new好了一个对象// 如果线程不安全(有不安全的风险),还没有newif (instance == null) {synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}}}return instance;}private SingletonLazy(){}
}
public class Demo21 {public static void main(String[] args) {}
}
阻塞队列
- 阻塞队列是多线程代码中比较常用到的一种数据结构
- 阻塞队列最大的意义就是可以用来实现生产者消费者模型
- 生产者消费者模型,一种常见的多线程代码编写方式
阻塞队列
1.线程安全的
2.带有阻塞特性的:
如果队列为空,再往队列中出元素,就会阻塞,就要一直阻塞到其他线程往队列中添加元素为止
如果队列为满,再往队列中入元素,就会阻塞,就要一直阻塞到其他线程往队列中取出元素为止
3. 生产者消费者模型的好处(意义)是什么?
可以降低资源的竞争,提高我们程序的效率
解耦合
比如服务器A和服务器B进行数据交互,A用来发送请求,B用来接收请求,A和B的耦合度比较高,如果B挂了的话,会影响到A,如果再加入一个C服务器,也和A交互,A的代码就需要进行修改
如果使用一个阻塞队列的话,A和B都把请求和响应放入阻塞队列中,A挂了不会影响B,再加入一个C也不需要修改A的代码
这样耦合度就降低了
2. 削峰填谷
峰:短时间内请求量比较多
谷:请求量比较少
有了这样的机制之后,就可以在有突发情况来临时,整个服务器系统让然可以正确执行
利用生产者消费者模型也可以得到解决
使用
阻塞队列
import jdk.nashorn.internal.ir.Block;import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class Demo22 {public static void main(String[] args) throws InterruptedException {BlockingQueue<String> queue = new LinkedBlockingQueue<>();queue.put("111");queue.put("222");queue.put("333");queue.put("444");System.out.println(queue.take());System.out.println(queue.take());System.out.println(queue.take());System.out.println(queue.take());// 最后一次会阻塞住System.out.println(queue.take());}
}
自己实现一个阻塞队列
- 基于一个普通的队列,加上线程安全,加上阻塞就可以了
- 使用size来判空和判满
普通的队列
修改会产生线程安全问题,直接加锁
使用wait和notify实现阻塞
彼此唤醒对方的wait,一个队列要么是空,要么是满
// 不写作泛型了,直接让队列存储String
class MyBlockingQueue{// 此处也可以使用构造方法,来指定数组的最大长度private String[] data = new String[1000];// 队列的起始位置// 都加上volatile,防止出现内存可见性问题private volatile int head = 0;// 队列的结束位置的下一个位置private volatile int tail = 0;// 队列中的有效元素个数private volatile int size = 0;// 入队列和出队列public void put(String elem) throws InterruptedException {synchronized (this) {while (size == data.length) {// 满了,再插入就阻塞了// 普通队列,直接returnthis.wait();}data[tail] = elem;tail++;// 如果自增之后来到了数组末尾,让它回到数组的开头,环形队列if (tail == data.length) {tail = 0;}size++;// 这个notify用来唤醒take中的waitthis.notify();}}public String take() throws InterruptedException {synchronized (this) {while (size == 0) {// 队列为空,直接返回// 队列为空,继续出队列就会出现阻塞this.wait();}String ret = data[head];head++;size--;if (head == data.length) {head = 0;}// 这个notify用来唤醒put中的waitthis.notify();return ret;}}
}
异常唤醒的
wait还可能被interrupt唤醒,interrupt会中断wait
处理异常唤醒的情况:
加上volatile
实现生产者消费者模型
- 用阻塞队列来实现生产者消费者模型
// 不写作泛型了,直接让队列存储String
class MyBlockingQueue{// 此处也可以使用构造方法,来指定数组的最大长度private String[] data = new String[1000];// 队列的起始位置// 都加上volatile,防止出现内存可见性问题private volatile int head = 0;// 队列的结束位置的下一个位置private volatile int tail = 0;// 队列中的有效元素个数private volatile int size = 0;// 入队列和出队列public void put(String elem) throws InterruptedException {synchronized (this) {while (size == data.length) {// 满了,再插入就阻塞了// 普通队列,直接returnthis.wait();}data[tail] = elem;tail++;// 如果自增之后来到了数组末尾,让它回到数组的开头,环形队列if (tail == data.length) {tail = 0;}size++;// 这个notify用来唤醒take中的waitthis.notify();}}public String take() throws InterruptedException {synchronized (this) {while (size == 0) {// 队列为空,直接返回// 队列为空,继续出队列就会出现阻塞this.wait();}String ret = data[head];head++;size--;if (head == data.length) {head = 0;}// 这个notify用来唤醒put中的waitthis.notify();return ret;}}
}public class Demo23 {public static void main(String[] args) {// 生产者和消费者分别用一个线程来表示(也可以用多个)MyBlockingQueue queue = new MyBlockingQueue();// 消费者Thread t1 = new Thread(()->{while(true){try {String result = queue.take();System.out.println("消费元素 = " + result);Thread.sleep(500);// 先不sleep,利用sleep控制生产和消费的速度} catch (InterruptedException e) {throw new RuntimeException(e);}}});// 生产者Thread t2 = new Thread(()->{int num = 1;while(true) {try {queue.put(num + " ");System.out.println("生产元素 = " + num);num++;// Thread.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
}