一.指令重排序

例子如下:

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(); } }

结果如下:

可以看到各个线程对应的值并没有乱。

Java并发编程-线程可见性&线程封闭&指令重排序的更多相关文章

  1. java并发编程的艺术(二)---重排序与volatile、final关键字

    本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...

  2. Java并发编程系列-(2) 线程的并发工具类

    2.线程的并发工具类 2.1 Fork-Join JDK 7中引入了fork-join框架,专门来解决计算密集型的任务.可以将一个大任务,拆分成若干个小任务,如下图所示: Fork-Join框架利用了 ...

  3. 关于volatile的可见性和禁止指令重排序的疑惑

    在学习volatile语义的可见性和禁止指令重排序的相关测试中,发现并不能体现出禁止指令重排序的特性 实验代码如下 package com.aaron.beginner.multithread.vol ...

  4. 【java并发编程实战】-----线程基本概念

    学习Java并发已经有一个多月了,感觉有些东西学习一会儿了就会忘记,做了一些笔记但是不系统,对于Java并发这么大的"系统",需要自己好好总结.整理才能征服它.希望同仁们一起来学习 ...

  5. Java并发编程:进程和线程的由来(转)

    Java多线程基础:进程和线程之由来 在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程.当然,Java并发编程涉及到很多方面的内容,不是一朝一夕就能够融会贯通 ...

  6. 【Java并发编程一】线程安全和共享对象

    一.什么是线程安全 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用代码代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的 ...

  7. 【Java并发编程六】线程池

    一.概述 在执行并发任务时,我们可以把任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程,只要池里有空闲的线程,任务就会分配一个线程执行.在线程池的内部,任务被插入一个阻塞队列(Blo ...

  8. Java并发编程扩展(线程通信、线程池)

    之前我说过,实现多线程的方式有4种,但是之前的文章中,我只介绍了两种,那么下面这两种,可以了解了解,不懂没关系. 之前的文章-->Java并发编程之多线程 使用ExecutorService.C ...

  9. Java并发编程(五)JVM指令重排

    我是不是学了一门假的java...... 引言:在Java中看似顺序的代码在JVM中,可能会出现编译器或者CPU对这些操作指令进行了重新排序:在特定情况下,指令重排将会给我们的程序带来不确定的结果.. ...

随机推荐

  1. TP手册学习第一天

    调试执行的sql语句 User::get(1); echo User::getLastSql(); 方法直接返回当前的查询SQL而不执行fetchSql echo User::fetchSql()-& ...

  2. awk的批量replace功能

    awk的批量replace功能 需求 现在需要替换一个文本 文本内容如下 $cat file MD_D1TS_1_060_I MD_D1TS_1_061_F MD_D1TS_1_062_U MD_D1 ...

  3. all,any函数

    all函数:当矩阵全为非零元素时返回1,否则(存在零元素),返回0: any函数:当矩阵中存在非零      1     1     1     1      1     1     1     1 ...

  4. jquery中ajax跨域提交的时候会有2次请求

    我们平时在同域中请求页面什么的时候不会有这种情况,这种情况大多发生在移动端的跨域请求中发生的. 解决方法就是在服务端中加一层过滤HTTP请求的类型,把OPTION等不用的类型过滤掉.就是当请求为非 H ...

  5. JAVA设计模式之---工厂模式

    1.引言 工厂模式可以分为类: 1)简单工厂模式(Simple Factory) 2)工厂方法模式(Factory Method) 3)抽象工厂模式(Abstract Factory)  这种模式从上 ...

  6. python生成随机图形验证码

    使用python生成随机图片验证码,需要使用pillow模块 1.安装pillow模块 pip install pillow 2.pillow模块的基本使用 1.创建图片 from PIL impor ...

  7. Yii框架里用grid.CGridView调用pager扩展不显示最后一页按钮的解决

    有如下一例,调用zii.widgets.grid.CGridView显示Blog信息,代码如下: $this->widget('zii.widgets.grid.CGridView', arra ...

  8. web项目,ftl文件中的路径引入问题

    路径问题的引入方式:绝对路径.相对路径 区别如图: =================================================

  9. BZOJ 3239: Discrete Logging [BGSG]

    裸题 求\(ind_{n,a}b\),也就是\(a^x \equiv b \pmod n\) 注意这里开根不能直接下取整 这个题少了一些特判也可以过... #include <iostream& ...

  10. Eclipse多平台编译(armeabi, armeabi-v7a, x86, mips)

    Jni目录下新增Application.mk,加入 APP_ABI := armeabi armeabi-v7a x86 mips 上面的平台可加可减,全编的话可以写为 APP_ABI := all ...