深入探究Java线程不安全的原因与解决
作者:淡沫初夏Zz
一、什么是线程安全
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的
二、线程不安全的原因
1、修改共享数据
static class Counter { public int count = 0; void increase() { count++; } } public static void main(String[] args) throws InterruptedException { final 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); }
上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改.此时这个 counter.count 是一个多个线程都能访问到的 “共享数据”
2、原子性
原子性就是 提供互斥访问,同一时刻只能有一个线程对数据进行操作,有时也把这个现象叫做同步互斥,表示操作是互相排斥的
不保证原子性会给多线程带来什么问题 如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。 这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大
3、内存可见性
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.
Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型. 目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.
private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while (count == 0) { } System.out.println(Thread.currentThread().getName() + "执⾏完成"); }); t1.start(); Scanner scanner = new Scanner(System.in); System.out.print("->"); count = scanner.nextInt(); }
4、指令重排序
一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
编译器对于指令重排序的前提
“保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价
三、解决线程安全方案
- volatile解决内存可见性和指令重排序
代码在写入 volatile 修饰的变量的时候:
改变线程⼯作内存中volatile变量副本的值,将改变后的副本的值从⼯作内存刷新到主内存
- 直接访问工作内存,速度快,但是可能出现数据不⼀致的情况
- 加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了
代码示例:
/** * 内存可见性 * 线程1没感受到flag的变化,实际线程2已经改变了flag的值 * 使用volatile,解决内存可见性和指令重排序 */ public class ThreadSeeVolatile { //全局变量 private volatile static boolean flag = true; public static void main(String[] args) { //创建子线程 Thread t1 = new Thread(() ->{ System.out.println("1开始执行:" + LocalDateTime.now()); while(flag){ } System.out.println("2结束执行" + LocalDateTime.now()); }); t1.start(); Thread t2 = new Thread(() ->{ //休眠1s try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("修改flag=false"+ LocalDateTime.now()); flag = false; }); t2.start(); } }
volatile的缺点
volatile 虽然可以解决内存可见性和指令重排序的问题,但是解决不了原子性问题,因此对于 ++ 和 --操作的线程非安全问题依然解决不了
- 通过synchronized锁实现原子性操作
JDK提供锁分两种:
①一种是synchronized,依赖JVM实现锁,因此在这个关键字作用对象的作用范围内是同一时刻只能有一个线程进行操作;
②另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性的是ReentrantLock。
- synchronized 会起到互斥效果, 某个线程执行到某个对象的synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
synchronized修饰的对象有四种:
(1)修饰代码块,作用于调用的对象
(2)修饰方法,作用于调用的对象
(3)修饰静态方法,作用于所有对象
(4)修饰类,作用于所有对象
// 修饰一个代码块: 明确指定锁哪个对象 public void test1(int j) { synchronized (this) { } } // 修饰一个方法 public synchronized void test2(int j) { } // 修饰一个类 public static void test1(int j) { synchronized (SynchronizedExample2.class) { } } // 修饰一个静态方法 public static synchronized void test2(int j) { }
到此这篇关于深入探究Java线程不安全的原因与解决的文章就介绍到这了,更多相关Java线程不安全内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!