900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > Java多线程中出现的线程安全问题分析以及如何解决

Java多线程中出现的线程安全问题分析以及如何解决

时间:2019-08-11 23:36:18

相关推荐

Java多线程中出现的线程安全问题分析以及如何解决

文章目录

前言举个栗子分析解决第二个栗子分析解决volatile 的作用

前言

由于调度器的抢占式执行, 或者说随机性很强的调度行为, 会让我们捉摸不透程序实际中的运行模式, 特别是在多线程的模式下, 就容易出现线程安全的问题, 例如我们举一个非常经典的例子:

举个栗子

创建两个线程, 让两个线程同时对一个静态变量 cnt 各自进行自增操作 10000 次

例如: 线程 1 让 cnt 自增 10000 次, 线程2 同样让 cnt 自增 10000 次

按我们的原始想法来说, 各自自增 10000, 最终值应该为 20000 才合理

但是: 实际上 cnt 最终结果并非 20000

public class Tmp {//创建两个线程,让这俩线程同时对一个变量进行自增操作public static int cnt = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {for(int i = 0; i < 10000; i++) {cnt++;}});Thread t2 = new Thread(() -> {for(int i = 0; i < 10000; i++) {cnt++;}});t1.start();t2.start();try {t1.join(); //线程等待, 让两个线程都执行完, 在打印 cnt 最终次数t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("最终运算结果为" + cnt);}}

我们执行上述程序, 如果什么都不考虑, 那么每个线程对 cnt 分别自增 10000 次, 最终 cnt 应该是20000 , 但是实际上:

实际上: 这个值基本上都在 1w - 2w之间随机出现

分析

那么为什么会出现上面那种情况呢, 我们分析一波

上面两个线程都只执行了一个语句cnt ++;而这行代码又可以具体分成三个机械指令为了方便描述, 我给下面三个步骤起个小名① load → 从内存中读取 cnt 的值到 CPU 中准备运算② add → 在 CPU 的寄存器中完成自增操作③ write → 把寄存器中的运行结果再写回内存中

在理想状态下, 我们如果想顺利将 cnt 自增到 20000, 那么站在两个线程的角度上, 应该是按照下面这种操作方式, 让一个操作完整的完成 cnt ++ 后, 再让另外一个线程去操作

但是操作系统对线程的调度都是很随机的, 实际上每个线程的这 3 个操作在这种情况下, 根本就没法保证执行顺序 !我们举个例子 ↓

有可能出现这种情况↑ , 例如刚开始 cnt = 0;

线程 1 读取 cnt = 0, 线程 2 也读取 cnt = 0;线程 1 进行 cnt ++, 然后写入内存, 之后 cnt = 1;线程 2 再进行 cnt ++, 但是由于实际上 cnt 刚刚读取的是 0 , 所以自增完 cnt = 1, 再写入内存, 之后 cnt 还是为 1也就是说两个自增操作, cnt 仍然是 1

除了这种情况之外, 还有很多排列组合的情况, 大致如上, 不再一一列举

解决

为了解决以上问题, 我们可以使用synchronized关键字来给对象或者类加锁。加这个关键词后线程执行任务前都会上锁, 执行完任务就会解锁。 在上锁后, 其他线程需要执行任务前要上锁, 但是由于已经被锁住了, 就无法完成上锁操作, 由此进入阻塞状态, 直到锁被释放。 例如:

class Test {public int cnt = 0;public void increase() {synchronized (this) {//同步代码块t ++;}}}public class Tmp {//创建两个线程,让这俩线程同时对一个变量进行自增操作public static Test test = new Test();public static void main(String[] args) {Thread t1 = new Thread(() -> {for(int i = 0; i < 10000; i++) {test.increase();}});Thread t2 = new Thread(() -> {for(int i = 0; i < 10000; i++) {test.increase();}});t1.start();t2.start();try {t1.join(); //线程等待, 让两个线程都执行完, 在打印 cnt 最终次数t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("最终运算结果为" + t);}}

第二个栗子

创建一个静态变量 test = 1;让线程1 进入 whlie (test == 1) {} 但什么都不做的死循环, 如果循环结束, 打印"线程1结束"在线程2 中修改 test 的值为6按原始的想法来说, 在线程2中将 test 改为 6 之后, 线程1应该停下循环, 然后打印"线程1结束"

public class Tmp {public static int test = 0; //定义一个整形变量public static void main(String[] args) {Thread t1 = new Thread(() -> {while (test == 0) {//啥都不干}System.out.println("线程1执行完了"); //循环结束, 则打印这个语句});Thread t2 = new Thread(() -> {Scanner in = new Scanner(System.in);System.out.println("输入 ->"); //在线程2中偷偷改变 test 的值test = in.nextInt();});t1.start();t2.start();try {t1.join(); //线程等待t2.join();} catch (InterruptedException e) {e.printStackTrace();}}}

如上代码, 在修改test = 6之后,线程1并不会停下来:

可以看出, 线程1并没有停下, 还在一直死循环(没有打印)

分析

我们再来分析一波:

在线程1的死循环中有两个机械指令① 将 test 的值从内存中取出② 判断是否和 0 相等这种情况下, 死循环的判断频率非常高, 而 test 的值却一直没变化。但是在计算机中, 内存操作的速度比寄存器的速度相比, 会慢1000 + 倍,而不断的从内存中取出一个电脑认为没变化的 test 值, 会导致:系统或者JVM对这做出优化:将第一次读取到的数据放在寄存器中, 然后进行反复判断, 就可以直接节省大量从内存中取值的时间, 而后我偷偷摸摸在另外一个线程中改掉 test 的值, 系统也不知情, 也不会再从内存中读取 test 值, 就出现了这种现象, 也就是内存可见性

解决

为了让上述情况的操作更有可控性, 我们可以使用关键字volatile, 我们示范一次

上述代码在 test 前加上一个 volatile 后

public static volatile int test = 0;

就可以按照我们的逻辑搞定了

那么 volatile 有在上面起到了什么作用呢

volatile 的作用

通过特殊的二进制指令为这个变量增加了一个内存屏障能够让JVM在读取这个变量的时候, 知道这个变量"身份特殊",就强制每次都要从内存中读取这个变量的值因此提升了线程安全性, 还能禁止指令重排序, 这个下回有机会再讲。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。