织梦动漫网站模板,中国住房和城乡建设部网站,成都便宜网站建设公司哪家好,聚诚网站建设线程安全 问题 一. 线程不安全的典型例子二. 线程安全的概念三. 线程不安全的原因1. 线程调度的抢占式执行2. 修改共享数据3. 原子性4. 内存可见性5. 指令重排序 一. 线程不安全的典型例子
class ThreadDemo {static class Counter {public int count 0;void increase() {cou… 线程安全 问题 一. 线程不安全的典型例子二. 线程安全的概念三. 线程不安全的原因1. 线程调度的抢占式执行2. 修改共享数据3. 原子性4. 内存可见性5. 指令重排序 一. 线程不安全的典型例子
class ThreadDemo {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);}
}
多次执行的结果 两个线程各 加了 50000 次 但最终结果都不是我们预期的 100000, 并且相差甚远。
二. 线程安全的概念
操作系统调度线程是随机的抢占式正因为这样的随机性就可能导致程序的执行出现一些 bug。
如果多线程环境下代码运行的结果是符合我们预期的即在单线程环境应该的结果则说这个程序是线程安全的否则就是线程不安全的。
三. 线程不安全的原因
1. 线程调度的抢占式执行
线程是抢占式执行线程间的调度就充满随机性。 这是线程不安全的万恶之源但是我们无可奈何无法解决。
2. 修改共享数据
多个线程针对同一变量进行了修改操作假如说多个线程针对不同变量进行修改则没事多个线程针对相同变量进行读取也没事。
上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改. 此时这个 counter.count 是一个多个线程都能访问到的 “共享数据” counter.count 这个变量就是在堆上. 因此可以被多个线程共享访问.
3. 原子性
针对变量的操作不是原子的通过加锁操作可以把多个操作打包成一个原子操作。
一条 java 语句不一定是原子的也不一定只是一条指令 比如上面的 count其实是由三步操作组成的 1从内存把数据 count 读到 CPU 2进行数据更新 count count 1 3把更新后的数据 count 写回到 内存
所以说导致上面那段代码线程不安全的原因就是 只要 t2 是在 t1 线程 save 之前读的, t2 的自增就会覆盖 t1 的自增, 那么两次加 1 的效果都相当于只加了 1 次. 所以上面的代码的执行结果 在 5w ~ 10w并且大多数是靠近 5w 的。 (小于 5w 的是非常少见的, 这种情况就是 t2 线程覆盖了 t1 线程的多次 自增操作, 也就是说 t2 线程的 load 与 save 之间跨度很大的情况.)
解决加锁 ! 打包成原子操作。 最常见的加锁方式就是 使用 synchronized
class ThreadDemo {static class Counter {public int count 0;synchronized 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);}
}在自增之前加锁自增之后再解锁。 当一个线程加锁成功时其他线程再尝试加锁就会阻塞直到占用锁的线程将锁释放。
那这样不就是串行执行了嘛那多线程又有什么用 ? 实际开发中一个线程要执行的任务很多可能只有其中的很少一部分会涉及到线程安全问题才需要加锁而其他地方都能并发执行。
注意 加锁是要明确指出对哪个对象进行加锁的如果两个线程对同一个锁对象进行加锁才会产生锁竞争阻塞等待如果对不同的对象进行加锁那么不会产生锁竞争阻塞等待。 上面代码中 synchronized 加在方法上那么就是对 Counter 对象加锁对应到代码中就是两个线程就是对 counter 这个实例对象加锁。
4. 内存可见性
可见性指, 一个线程对共享变量值的修改能够及时地被其他线程看到.
Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型. 目的是屏蔽掉各种硬件和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的并发效果. 线程之间的共享变量存在 主内存 (Main Memory).每一个线程都有自己的 “工作内存” (Working Memory) .当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法. 所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存.
由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程 1 的工作内存中的值, 线程 2 的工作内存不一定会及时变化.
举一个栗子 针对同一个变量 一个线程进行循环读取操作另一个线程在某个时机进行了修改操作。 比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的比寄存器慢 3-4 个数量级.
但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问
内存了. 效率就大大提高了. 编译器优化的结果解决 使用 synchronized synchronized 不仅能保证原子性同时能保证内存可见性 被 synchronized 修饰的代码编译器不会轻易优化。 使用 volatile 关键字 volatile 和原子性无关但是能保证内存可见性禁止编译器优化每次都要从内存中读取变量。
5. 指令重排序
什么是代码重排序 举个栗子 一段代码是这样的
1. 去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递如果是在单线程情况下JVM、CPU指令集会对其进行优化比如按 1-3-2的方式执行也是没问题可以少跑一次前台提高效率。这种叫做指令重排序。
编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.
解决: 使用 volatile 关键字 volatile 除了能保证内存可见性之外还能防止指令重排序。
好啦 以上就是对 线程安全 问题的讲解希望能帮到你 评论区欢迎指正 !