线程安全主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况,线程安全问题表现为三个方面:原子性、可见性和有序性。
2.1 原子性
原子(Atomic)就是不可分割的意思,原子操作的不可分割有两层含义:
- 访问(读、写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕,要么尚未发生,即其他线程看不到当前线程操作的中间结果;
- 访问同一组共享变量的原子操作是不能够交错的,如ATM机取款,对用户来说,要么操作成功,余额减少,要么没成功。
Java有两种方式实现原子性:
- 锁:具有排它性,保证共享变量在某一时刻只能被一个线程访问;
利用处理器的CAS(Compare and Swap)指令,看作硬件锁。
public class Test01 {public static void main(String[] args) {// MyInt myInt = new MyInt();MyIntThreadSafe myInt = new MyIntThreadSafe();int num = 2;for (int i = 1; i <= num; i++) {new Thread(() -> {while (true) {System.out.println(Thread.currentThread().getName()+ " -> " + myInt.getNum());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}).start();}}static class MyInt {int num;public int getNum() {/*自增操作实现的步骤:1. 读取num值2. num自增3. 把自增的值赋值给num变量*/return num++;}}/*** 在Java中提供了一个线程安全的AtomicInteger类,保证了操作的原子性*/static class MyIntThreadSafe {AtomicInteger num = new AtomicInteger();public int getNum() {return num.getAndIncrement();}}}
2.2 可见性
在多线程环境中,一个线程对某个共享变量进行更新之后,后续其他的线程可能无法立即读到这个更新的结果,这就是线程安全问题的另外一种形式:可见性(visibility)。
如果一个线程对共享变量更新后,后续访问该变量的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见,否则称这个线程对共享变量的更新对其他线 程不可见。
多线程程序因为可见性问题可能会导致其他线程读取到了旧数据(脏数据) ```java public class Test02 { public static void main(String[] args) throws InterruptedException {MyTask task = new MyTask();new Thread(task).start();Thread.sleep(1000);// 主线程1秒后取消主线程,可能会出现main线程对toCancel做了修改子线程看不到的情况task.cancel();
}
static class MyTask implements Runnable{
private boolean toCancel = false;@Overridepublic void run() {while (!toCancel){if (doSomething()){}}if (toCancel){System.out.println("任务被取消");} else {System.out.println("任务正常结束");}}private boolean doSomething(){System.out.println("执行某个任务……");try {Thread.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}return true;}public void cancel(){toCancel = true;System.out.println("收到取消线程的消息");}
} }
2.3 有序性
- 源代码顺序:源码指定的内存访问顺序。
- 程序顺序:处理器上运行的目标代码所指定的内存访问顺序。
- 执行顺序:内存访问操作在处理器上的实际执行顺序。
- 感知顺序:给定处理器所感知到的该处理器及其他处理器的内容访问操作的顺序 。
多线程在执行多个操作时,在不同处理器运行的执行的顺序不同,与目标代码指定的顺序可能不一样,这种现象称为重排序。可以把重排序分为指令重排序与存储子系统重排序两种。
- 指令重排序(Instruction Reorder)主要是由即时(JIT)编译器、处理器引起的指令重排,使程序顺序与执行顺序不一样。注意:javac编译器不会执行指令重排序。
- 存储子系统重排序是指高速缓存,写缓冲器引起的感知顺序与执行顺序不一致,但是并没有真正对指令执行顺序进行调整,只是从内存操作的结果来看的一种假象。
- 高速缓存(Cache)是CPU中为了区配与主内存处理速度不匹配而设计的一个高速缓存。
- 写缓冲器(Stroe buffer, Write buffer)用来提高写高速缓存操作的效率。
为保证貌似串行语义、多线程正常执行,存在数据依赖关系不能出现重排序,不存在数据依赖关系可能出现重排序,存在控制依赖关系的语句允许重排。
可以使用valatile、synchronized关键字实现有序性。
2.4 Java内存模型
见 Java虚拟机学习笔记 第一部分。
