不积跬步,无以至千里;不积小流,无以成江海。要沉下心来,诗和远方的路费真的很贵!
多线程——保证线程安全
含义
线程安全:指在多线程对一个共享资源同时进行操作的时候,所得到的结果每次都是一样的。
如何保证线程安全
方法:要保证线程安全,就必须保证线程同步
。保证线程的可见性
,有序性
和原子性
。
- 线程同步
线程同步的含义和字面意义相反。同步其实是线程“排队”的意思。就是让线程按照一定的顺序执行,
每一时刻,只有一个线程,对共享资源进行操作。
- 可见性
一个线程对共享资源的操作,所有的线程都可以看见。
以具体实例来说明。就好比一个线程修改了一个数据,其他线程立马知道该数据被修改了。
即就是在线程和主存之间有一个缓存,线程修改数据,是在缓存中修改,还需要在主存修改,而可见性就是立刻在主存中修改,防止其他线程读取时,发生数据错误。
- 有序性
就是代码的执行顺序是有序的,执行的顺序不会发生改变。
- 原子性
顾名思义,原子是一个不可分的整体。就是一个代码块,要么全部执行,要么全部不执行,只要其执行了,就无法被任何事务打断。
具体方法
volatile关键字
作用:保证线程可见性和禁止指令重排序。
保证可见性
实现原理:当一个变量被volatile修饰,一旦其发生改变,那么根据缓存一致性协议,其他线程的缓存都会失效,需要重新从内存中获取数据,就可以保证数据的准确性。就好比这个数据修改了,其他线程缓存失效,就知道了它被修改了,就要重新获取,即可见的含义。
不加volatile关键字
package com.hnu;
public class Main2 {
// static 静态变量 全局
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
//未停止,不知道已修改
public void run() {
while(true) {
if(flag) {
break;
}
}
System.out.println("变量变化");
};
}).start();
//主线程修改flag值
Thread.sleep(1000);
flag = true;
}
}
线程先启动,启动后修改变量,未加关键字修饰,子线程不知道其变量值已经发生变化,即不可见,所以死循环无法停止。
加volatile关键字
package com.hnu;
public class Main2 {
// static 静态变量 全局
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
//停止,知道已修改
public void run() {
while(true) {
if(flag) {
break;
}
}
System.out.println("变量变化");
};
}).start();
//主线程修改flag值
Thread.sleep(1000);
flag = true;
}
}
加入关键字,首先先死循环,然后修改其值,知道其修改完成,然后调用,停止死循环,输出文字。可以证明其保证了可见性。
禁止重排序
实现原理:通过jvm给指令的前后加上内存屏障,屏障两边的指令不可以重排序,保证有序。
例子:单例模式
禁止重排序的经典案例就是单例模式的创建过程中的双重检测锁。
package com.hnu;
public class Main2 {
//自己对象,禁止重排序
private volatile static Main2 main = null;
//构造方法
private Main2() {
}
//创建自己
public static Main2 getInstance() {
if(main == null) {
//类锁,双重检测锁
synchronized(Main2.class) {
if(main == null) {
main = new Main2();
}
}
}
return main;
}
public static void main(String[] args) {
Main2 m1 = getInstance();
Main2 m2 = getInstance();
Main2 m3 = getInstance();
System.out.println(m1 == m2);
System.out.println(m1 == m3);
System.out.println(m2 == m3);
}
}
可知三个对象,都是同一个对象,内存地址相同。
synchronized关键字
作用:利用线程互斥来实现线程同步,即通过同一时刻只有一个线程可以访问(互斥),来实现线程的原子性,全部执行完,才能换线程执行,线程顺序执行(同步)。
synchronized
最主要的就是保持原子性
,保持原子性,最主要的就是线程同步
,同步最基本的方法就是加锁
,加锁最直接的就是加synchronized关键字
。
效率:synchronized在早期是一把重量级锁
,但是随着java
发展,如今的效率已经很高。例如i++不是原子操作,它分为三步:1.获取i的值 2.修改i的值 3.将修改的值赋予i 。如果在其外面加入synchronized
关键字,保证了每次只有一个线程可以修改i,那么就保证了i++在并发环境下的安全性。
保证原子性
上面的双重检测锁也使用了synchronized关键字,加同一个锁的线程同步互斥,里面的代码块在同一时刻,只有一个线程可以访问,所以保证了唯一实例。
防止死锁
原因
两个线程相互请求对方持有的资源,都不释放自己持有的资源,相互阻塞,导致死锁。
后果
至少有两个线程相互阻塞等待对方的资源。
检查死锁
使用jdk
工具jconsole
查看线程的状态。
解决方法
- 资源一次性分配
- 当线程满足条件时释放自己已占有的资源
- 资源有序分配