一. 指令重排序
例子如下:
- public class Visibility1 {
- public static boolean ready;
- public static int number;
- }
- public class ReaderThread extends Thread {
- @Override
- public void run() {while (!Visibility1.ready){
- Thread.yield();
- System.out.println(Visibility1.number);
- }
- }
- }
- public class Test1 {
- public static void main(String[] args) {
- new ReaderThread().start();
- Visibility1.number = 42;
- Visibility1.ready = true;
- }
- }
多次运行结果分别如下:
可以看到多次运行所得到三种结果, 分别为 0,42, 没有输出结果
程序一开始执行, 默认将 ready 赋值为 false,ready 默认赋值为 0, 一开始执行时, 在 ReaderThread 中符合循环条件, 进入循环, 遇到
Thread.yield();
这语句是就是说当一个线程使用了这个方法之后, 它就会把自己 CPU 执行的时间让掉, 让自己或者其它的线程运行如果自己先走出现结果为 0 的情况, 因为, 默认初始 int 的类型数据为 0, 如果让别的线程先走让这样就又回到了 Main 方法中的继续执行
- Visibility1.number = 42;
- Visibility1.ready = true;
这两条语句, 问题来了, 这两条语句会按照程序的顺序执行吗?
答案是, 并不一定的这就涉及到指令重排序问题, 也就是说
CPU 一般采用流水线来执行指令一个指令的执行被分成: 取指译码访存执行写回等若干个阶段然后, 多条指令可以同时存在于流水线中, 同时被执行
指令流水线并不是串行的, 并不会因为一个耗时很长的指令在执行阶段呆很长时间, 而导致后续的指令都卡在执行之前的阶段上
重排序的目的是为了性能
也就是说, 有时候 CPU 认为
Visibility1.ready = true; 语句执行要快, 所以让 Visibility1.ready = true; 先执行这就导致了会出现了无输出结果这就是涉及了指令重排序问题, 虽然这里如果出现了指令重排序问题,
进入生产开发会造成严重的后果的
但是要注意一点, 如下:
二. 可见性
可见性, 是指线程之间的可见性, 一个线程修改的状态对另一个线程是可见的也就是一个线程修改的结果另一个线程马上就能看到比如: 用 volatile 修饰的变量, 就会具有可见性 volatile 修饰的变量不允许线程内部缓存和重排序, 即直接修改内存
所以对其他线程是可见的但是这里需要注意一个问题, volatile 只能让被他修饰内容具有可见性, 但不能保证它具有原子性比如 volatile int a = 0; 之后有一个操作 a++; 这个变量 a 具有可见性, 但是 a++ 依然是一个非原子操作, 也就这这个操作同样存在线程安全问题
先看如下程序:
- public class Visibility {
- private static boolean bChanged;
- public static void main(String[] args) throws InterruptedException {
- new Thread(){
- @Override
- public void run() {
- for (; ; ){
- if (bChanged == true){
- System.out.println("!=");
- System.exit(0);
- }
- }
- }
- }.start();
- Thread.sleep(10);
- new Thread(){
- @Override
- public void run() {
- for (; ; ){
- bChanged = true;
- }
- }
- }.start();
- }
- }
运行结果为:
可以看到运行为死循环按道理来说应该会输出!= 结束的但是为什么会出现死循环呢?
原因是因为程序中 bChanged 如果没有手动赋值前, 默认是 false 的, 所以一开始没输出, 但是第二线程命名修改了 bChanged 的值了, 为什么还不退出呢, 因为线程是 CPU 启动的, 而 CPU 一开始从主存中取数据并没有立即将数据送到 CPU, 而是先送到了缓存中,
而, 另外一个线程修改 bChanged 的值, 是就该主存中的值, 而那个输出结果的线程并没有从主存中取 bChanged 的值, 而是去缓存中取了 bChanged 的值, 而缓存中 bChanged 的值是 false, 这就是为什么会死循环的原因如下图:
解决办法:
1. 在成员变量中加入关键字 volatile, 如下:
private volatile static boolean bChanged;
这个关键字告诉线程一定要走内存, 不要再从缓存中获取数据了只要已修改, 其他线程立马知道值已经被修改了, 因为它们都是从主存中取的数据
修改后的结果如下图:
可以看到程序立马结束并且有输出因为这个关键字让这个成员变量都可见了这就是线程的可见性
其实 volatile 关键字还有一个功能就是: 阻止指令排序但是这只是相对的阻止如下:
以前官方推荐是 volatile, 但是现在 synchronized 已经优化的很好了不要刻意去用 volatile 如果一个类加上 volatile 关键字, 那么它的成员变量也会默认加上 volatile 关键字
volatile 只能解决可见性一定程度上解决指令排序, 但是是相对的
但是 volatile 解决了可见性, 就一定可以解决问题了吗? 并不是的例子如下:
如果你只希望单一变量可见性, volatile 是可以的, 但是如果关注的不是单一变量 volatile 就不管用了
2. 用 synchronize 加锁的办法它能解决可见性, 原子性
三. 线程封闭
实现好的并发是一件困难的事情, 所以很多时候我们都想躲避并发避免并发最简单的方法就是线程封闭什么是线程封闭呢?
当访问共享变量时, 往往需要加锁来保证数据同步一种避免使用同步的方式就是不共享数据如果仅在单线程中访问数据, 就不需要同步了这种技术称为线程封闭在 Java 语言中, 提供了一些类库和机制来维护线程的封闭性, 例如局部变量和 ThreadLocal 类,
线程封闭其实就是不共享数据就行了
线程封闭方法有:
1. 不要共享变量, 在变量中加 final 关键字
2. 栈封闭: 栈封闭是我们编程当中遇到的最多的线程封闭什么是栈封闭呢? 简单的说就是局部变量多个线程访问一个方法, 此方法中的
局部变量都会被拷贝一分儿到线程栈中所以局部变量是不被多个线程所共享的, 也就不会出现并发问题所以能用局部变量就别用全局的变量, 全局变量容易引起并发问题如果了解 JVM 应该懂得
3.ThreadLocal 线程绑定 ThreadLocal 内部维护了一个 Map,Map 的 key 是每个线程的名称, 而 Map 的值就是我们要封闭的对象每个线程中的对象都对应着 Map 中一个值, 也就是 ThreadLocal 利用 Map 实现了对象的线程封闭
想要操作什么数据, 可以先把数据放在 ThreadLocal 中绑定, 用的时候才取出来
例子如下:
- public class LocalTest {
- private int num;
- public int getNum() {
- return num;
- }
- public void setNum(int num) {
- this.num = num;
- }
- }
- public class ThreadLocalDemo {
- private static ThreadLocal<LocalTest> threadLocal = new ThreadLocal<LocalTest>();
- public static void main(String[] args) throws InterruptedException{
- final LocalTest local = new LocalTest();
- new Thread(){
- @Override
- public void run() {
- for (; ; ){
- threadLocal.set(local);
- LocalTest l = threadLocal.get();
- l.setNum(20);
- System.out.println(Thread.currentThread().getName() + "---" +threadLocal.get().getNum());
- Thread.yield();
- }
- }
- }.start();
- new Thread(){
- @Override
- public void run() {
- for (; ; ){
- threadLocal.set(local);
- LocalTest l = threadLocal.get();
- l.setNum(30);
- System.out.println(Thread.currentThread().getName() + "---" +threadLocal.get().getNum());
- Thread.yield();
- }
- }
- }.start();
- }
- }
结果如下:
可以看到各个线程对应的值并没有乱
来源: https://www.cnblogs.com/huangjuncong/p/8510293.html