一、可见性介绍
1.可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。
2.共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。
3.线程的工作内存:Java内存抽象出来的概念。
4.Java内存模型(JMM-Java Memory Model):描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。
5.所有变量都存储在主内存中;而每个线程有自己独立的工作内存,在工作内存中的保存的该线程使用的变量(此变量是主内存中变量的副本)。
6.Java内存的规定:
-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。
7.共享变量可见性实现原理:线程1-->工作内存1中变量X-->更新到主内存中-->工作内存2中的变量X得到更新-->线程2。
二、synchronized实现可见性
2.1、synchronized实现可见性原理
1、synchronized能够实现:原子性(同步)、可见性。
2、JMM关于synchronized的两条规定
a)线程解锁前,必须把共享变量的最新值刷新到主内存中。
b)线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取新的值。
线程解锁前对共享变量的修改在下次加锁时对其它线程可见。
3、线程执行互斥代码的过程
a)获得互斥锁
b)清空工作内存
c)从主内存拷贝变量的最新副本到工作内存
d)执行锁内代码
e)将更改后的共享变量值刷新到主内存
f)释放互斥锁
4、指令重排序
重排序:代码的书写顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化。
a)编译器优化的重排序(编译器优化)
b)指令级并行重排序(处理器优化)
c)内存系统的重排序(处理器优化)
指令重排序不会给单线程带来可见性问题,但是多线程的交叉执行可能会带来可见性问题。
2.2、synchronized实现可见性代码
1、原子性:由于锁的关系,线程之间不允许交叉执行;相当于给该线程(或当前运行的有且仅有一个的线程)加了一把锁,外面的线程无法进入,更别提互相交叉执行。
2、原子性+as-if-serial语义:线程不能交叉执行,重排序对于单线程不能影响运行结果。 3、可见性:共享变量的更新执行。
//共享变量private booleanready= false;private intresult= 0;private intnumber= 1;//写操作public voidwrite(){ready= true; //1.1number= 2; //1.2}//读操作public voidread(){if(ready){ //2.1result= number*3; //2.2}System.out.println("result的值为:"+ result); }//内部线程类private classReadWriteThread extendsThread {//根据构造方法中传入的flag参数,确定线程执行读操作还是写操作private booleanflag;publicReadWriteThread(booleanflag){this.flag= flag; } @Overridepublic voidrun() {if(flag){//构造方法中传入true,执行写操作write(); }else{//构造方法中传入false,执行读操作read(); } } }public static voidmain(String[] args) { SynchronizedDemo synDemo = newSynchronizedDemo();//启动线程执行写操作synDemo.newReadWriteThread(true).start();/*try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }*/ //启动线程执行读操作synDemo.newReadWriteThread(false).start(); }
三、volatile实现可见性
3.1、volatile能够保证可见性
3.1.1、volatile关键字:
1.能够保证volatile变量的可见性。
2.不能保证volatile变量复合操作的原子性。
3.1.2、volatile如何实现内存可见性:通过加入内存屏障和禁止重排序优化来实现的。
1.对volatile变量执行写操作时,会在写操作后加入一条store屏障指令。
2.对volatile变量执行读操作时,会在读操作前加入一条load屏障指令。
3.1.3、线程读/写volatile变量的过程:
-线程写volatile变量的过程:
1.改变线程工作内存的中volatile变量副本的值。
2讲改变后的副本的值从工作内存刷新到主内存。
-线程读volatile变量的过程:
1.从主内存中读取volatile变量的最新值到线程的工作内存中。
2.从工作内存中读取volatile变量的副本。
Ps:通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时候,不同的线程总能看到该变量的最新值。
3.2、volatile不能保证原子性
代码示例:
private Lock lock = new ReentrantLock();
private int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
lock.lock();
try {
this.number++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果还有子线程在运行,主线程就让出CPU资源,
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
程序分析:
1、线程A读取到的number 为 5;
2、线程B读取到的number 为 5;
3、线程B 执行加1 操作, number ++
4、线程B写入最新的 number 为 3 中的 5+ 1 = 6;
5、线程A 执行加 1 操作没有向主内存读取共享变量,故 依旧是 由 原始变量 number = 5 开始 加 1 操作,即 此时 number = 5+ 1 ;
6、线程 A 写入最新的 number 值 ,此时内存中的 只是 将 线程B 的 6 改成了 线程 A 的6 ,实际上只是同值的 覆盖,而非 递增
保证自增操作原子性的解决方案:
1.使用synchronized关键字
2.使用ReentrantLock
3.使用AtomicInteger
3.3、volatile使用注意事项
要在多线程中安全的使用volatile变量,必须同时满足:
a)对变量的写入操作不依赖其当前值
不满足:number++、count=count*5等
满足:boolean变量、记录温度变化的变量等
b)该变量没有包含在具有其他变量的不变式中
不满足:不变式low
3.4、synchronized和voiatile的比较
a)volatile不需要加锁,比synchronized更轻便,不会阻塞线程
b)从内存可见性的角度来讲,volatile的读相当于加锁,volatile的写相当于解锁
c)synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性
四、总结
保证主内存共享变量对线程工作内存的可见性,可以用sychonized、volatile、final来实现。final修饰的不可更改,syschonized效率低,但可以保证原子性。volatile效率高,但是不完全保证原子性(两种特殊情况)。
long 和double 在被jvm加载的时候,可能会分成两个32位来读取,多线程时需要加个volatile来确保原子性。
问:即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存见得到及时的更新?
答:一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快滴刷新缓存,所以一般情况下很难看到这种问题。