关于线程安全问题的总结
1.是啥
一段代码,在多线程中,并发执行后,产生bug
2.原因
- 操作系统对于线程的调度是随机的,抢占式执行[根本]
- 多个线程同时修改一个变量
- 修改操作不是原子的
- 内存可见性 -> 编译器优化
- 指令重排序
3.解决方案
(1)加锁
synchronized(锁对象){
需要加锁的代码
}
(2)volatile
编译器优化,出bug
使用这个关键字修饰的变量,就属于"易失""易变",必须每次重新读取内存中的数据
4.死锁
一旦代码触发死锁,此时线程就卡住了
原因
-
互斥
-
不可剥脱/不可抢占
-
请求和保持
-
循环等待
解决死锁
- 避免循环嵌套 = > 打破3
- 约定加锁顺序 = >打破4
JMM
JMM是java内存模型
缓存
寄存器虽然快但空间小存放不了的多少东西,于是在cpu上另外建设了一些存储空间,称为缓存

workmemory
这是Java官方文档的术语
每个线程,有一个自己的"工作内存"(work memory),同时这些线程共享同一个"主内存"(main
memory)
工作内存可以理解为存储空间,是寄存器与缓存的综合
当一个线程循环进行上述读取变量操作的时候,就会把主内存中的数据,拷贝到该线程的工作内存
中
后续另一个线程修改,也是先修改自己的工作内存,拷贝到主内存里。
由于第一个线程仍然在读自己的工作内存,因此感知不到主内存的变化。
这与前面讲到的把读内存的操作优化成都寄存器的操作类似
wait/notify
当多个线程竞争一把锁的时候,获取到锁的线程如果释放了,其他线程也不一定能拿到锁,因为线
程调度是随机的,充满不确定性
其他线程都属于锁上阻塞状态,当前这个释放锁的线程是就绪状态,这个线程有很大概率再次拿到
这把锁
wait和notify都是Object的方法,Java中的任意对象都提供了wait和notify

Java标准库中每个阻塞的方法都会抛出这个异常,意味着随时可能会被Interrupt唤醒
wait的使用
java
public static void main(String[] args) throws InterruptedException {
Object ob=new Object();
System.out.println("wait之前");
ob.wait();
System.out.println("wait之后");
}
java
synchronized (ob){
ob.wait();
}
非法的锁状态,对于ob.wait()这个方法来说,第一件事就是先释放object对象对应的锁,能够释放
锁的前提是object对相应处于加锁状态才能释放
java
synchronized (ob){
ob.wait();
}
代码进入wait,就会先释放锁,并且阻塞等待,如果其他线程做完了必要工作,调用notify唤醒这
个wait线程,wait就会阻塞等待,重新获取锁,继续执行并返回。
注意
这里要求synchronized的锁对象必须和wait的对象是同一个
notify的使用
wait 操作必须要搭配锁来进行,wait 会先释放锁。notify 操作原则上说不涉及到加锁解锁操作,但在Java 中,也强制要求 notify 搭配 synchronized
java
synchronized (lock){
lock.notify();
}
java
public static void main(String[] args) {
Object lock=new Object();
Thread t1=new Thread(()->{
try {
Thread.sleep(1000);
System.out.println("wait之前");
synchronized (lock){
lock.wait();
}
System.out.println("wait之后");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t2=new Thread(()->{
Scanner sc=new Scanner(System.in);
System.out.println("输入任意内容,唤醒t1");
sc.next();
synchronized (lock){
lock.notify();
}
});
t1.start();
t2.start();
}
sc.next就是一个带有阻塞的操作,等待用户在控制台输入
locker.notify()这里同样需要先拿到锁,再进行notify

注意
这四处必须是相同对象
wait和notify是针对同一个对象才能生效,这两个相同对象是线程沟通的桥梁
如果不是两个相同的对象,则没有任何相互的影响和作用
同时要务必确保先wait再notify才有作用,如果先notify再wait,此时wait无法被唤醒
一个notify唤醒一个wait,当出现多个wait,notify只会随机唤醒一个
java
public static void main(String[] args) throws InterruptedException {
Object lock=new Object();
Thread t1=new Thread(()->{
try {
System.out.println("t1的wait之前");
synchronized (lock){
lock.wait();
}
System.out.println("t1的wait之后");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t1");
Thread t2=new Thread(()->{
try {
System.out.println("t2的wait之前");
synchronized (lock){
lock.wait();
}
System.out.println("t2的wait之后");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
},"t2");
Thread t3=new Thread(()->{
Scanner sc=new Scanner(System.in);
System.out.println("输入");
sc.next();
synchronized (lock){
lock.notify();
}
});
t1.start();
t2.start();
t3.start();
}

可以通过多增加notify()来唤醒另外一个线程


notifyAll
notifyAll一次唤醒所有的wait线程


wait与join的区别
join也是等,但是等另外一个线程彻底执行完才继续走,join有等待时间
wait也是等,等到另外一个线程执行notify才继续走(不需要执行完另一个线程),wait也引入了超时间等待

wait可以使用notify提前唤醒
sleep也可以使用Interrupt提前唤醒,Interrupt看起来是唤醒sleep,其实本身的作用是通知线程终
止
wait和sleep最主要的区别在于针对锁的操作
- wait必须要搭配锁,才能用wait,sleep不需要
- 如果都是在synchronized内部使用,wait会释放锁,sleep不会释放锁
java
synchronized (locker){
Thread.sleep(1000);
}
sleep是抱着锁睡,其他线程是没法获取这个锁的
多线程案例
针对不同的场景有不同的设计模式。
单例模式
单例模式是一种设计模式,单例模式强制要求一个类不能创建多个对象,在代码中,如果创建了多
个实例,直接编译失败
单例模式的实现有以下两种方式
饿汉模式和懒汉模式
饿汉模式
类加载的同时, 创建实例.
java
class Singleton{
private static Singleton singleton=new Singleton();
public static Singleton getSingleton(){
return singleton;
}
private Singleton(){
}
}
public class Demo05 {
public static void main(String[] args) {
Singleton singleton1=Singleton.getSingleton();
Singleton singleton2=Singleton.getSingleton();
System.out.println(singleton1==singleton2);
}
}
静态成员的初始化是在类加载时的阶段触发的
类加载往往就是在程序一启动就会触发
对于不同情况下,需要传入参数的话,通过创建不同的私有构造方法来实现不同参数传入
懒汉模式
懒汉模式与饿汉模式创建实例的时间相反,懒汉模式创建实例尽量晚
java
class SingletonLazy{
private static SingletonLazy singletonLazy=null;
public static SingletonLazy getSingletonLazy(){
if (singletonLazy==null){
singletonLazy=new SingletonLazy();
}
return singletonLazy;
}
private SingletonLazy(){
}
}
public class Demo06 {
public static void main(String[] args) {
}
}

懒汉模式下创建实例的时机是在第一次使用的时候,而不是在程序启动的时候
懒汉模式-多线程版
上面的懒汉模式的实现是线程不安全的
线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getSingletonLazy方法, 就可能导
致创建出多个实例
加锁可以改善这里的线程安全问题
java
public static SingletonLazy getSingletonLazy() {
synchronized (lock) {
if (singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
}
加入锁之后执行的线程就会在加锁的位置阻塞,阻塞到前一个线程解锁,当后一个线程进入的条件
的时候,前一个线程已经修改完毕,singletonLazy不再为null,就不会进行后续的new操作
这个写法相当于锁对象换成了类对象SingletonLazy.class和之前的locker相比没区别
加锁后有引入新的问题
当把实例创建好了之后,后续再调用 getSingletonLazy 都是直接执行 return,如果只是进行 if 判
定 + return是纯粹的读操作,而对于读操作,不涉及到线程安全问题.但是,每次调用上述的方法,
都会触发一次加锁操作,虽然不涉及线程安全问题了。多线程情况下,这里的加锁就会相互阻塞~
影响程序的执行效率.
java
public static SingletonLazy getSingletonLazy() {
if (singletonLazy == null) {
synchronized (lock) {
if (singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
多加的锁是涉及实例创建的,如果实例已经创建就不涉及线程安全问题;如果还没创建就涉及线程
的安全问题
这个看起来应该没啥问题了吧,但是很抱歉,还是有问题的
我们先引入一个概念
指令重排序
指令重排序是在逻辑不变的前提下,修改代码执行的顺序
懒汉模式-多线程版(改进)
多线中,两次判定之间可能存在其他线程就把if中的singletonLazy变量修改了,也就导致这里的两
次if的结论可能不同

t1线程读取singletonLazy的时候,t2进行修改,指令重排序会在这里出现问题

对于这个new操作,执行顺序是
- 申请内存空间
- 在空间上构造对象
- 内存空间的首地址,赋值给引用变量
正常来说是按1,2,3的顺序执行下去,但在指令重排序的情况下,可能成为1,3,2的顺序
(单线程环境下,1,2,3还是1,3,2其实无所谓)
多线程的背景下,执行第一个if操作时,另外线程调走可能导致拿到一个"未初始化"的对象
使用volatile
这里我们可以通过增加volatile来解决这个问题

volatile的功能有两方面
1.确保每次读取操作都是都内存--解决内存可见性问题
2.关于该变量的读取和修改操作不会触发重排序--解决指令重排序问题