目录
一、可见性简介
1、可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到。
2、共享变量:如果一个变量在多个线程的工作内存中都存有副本,那么这个变量就是共享变量。
3、java内存模型(JMM)
所有的变量都存在主内存中,每个线程都有自己的工作内存,里面保存着该线程使用到的变量副本(就是主内存中该变量的一份拷贝)。
Java内存模型有两条规定:
1、线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接对主内存中进行读写
2、不同线程之间无法访问其他线程工作内存中的变量,线程间变量的值传递都需要通过主内存来完成。
二、synchronized实现可见性原理
synchronized可以实现互斥锁,可以保证在同一个时刻都只有一个线程在执行锁里面的代码。
synchronized能够实现:原子性(同步)、可见性
(一)JMM关于synchronized的两条规定
1、线程解锁前,必须把共享变量的最新值刷新到主内存中
2、线程加锁时,将情况工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
(二)线程执行互斥代码的过程:
-
获得互斥锁
-
清空工作内存
-
从主内存拷贝变量的最新副本到线程工作内存
-
执行代码
-
将更改后的最新的共享变量的值刷新到主内存
-
释放互斥锁
(重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性而做的优化)
as-if-serial:无论如何重排序,程序执行的结果都应该和代码顺序执行的结果一致。
试着用synchronized关键字写一个读写的demo:
package com.jqs.sync;
public class SynchronizedDemo {
//共享变量
private int result = 0;
private boolean ready = false;
private int num = 1;
//写操作
public synchronized void write() { //synchronized在此可以保证共享变量的内存可见性,从而线程安全
ready = true;
num = 2;
}
//读操作
public synchronized void read() { //synchronized在此可以保证共享变量的内存可见性,从而线程安全
if (ready)
result = num * 3;
System.out.println("result中的结果为:" + result);
}
private class ReadWriteThread extends Thread {
private boolean flag;
public ReadWriteThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag)
write();
else
read();
}
}
public static void main(String args[]) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.new ReadWriteThread(true).start();
try {
Thread.sleep(1000);
/*
* synchronized可以保证线程之间不会交叉执行,但是仍有可能因为读线程先执行run方法导致结果为0。(即是写线程不是一定就会在读线程执行之前先执行完)
* 所以在写线程启动之后加入主线程的休眠语句,这样写线程就可以基本在读线程执行之前执行完!!!正确结果应该是6
* */
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.new ReadWriteThread(false).start();
}
}
注:当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将会被阻塞
三、volatile保证可见性
volatile关键字:能够保证volatile变量的可见性,不能保证volatile变量复合操作的原子性
如何实现内存可见性:通过加入内存屏障和禁止重排序优化来实现
简单的说就是,volatile变量在每次被线程访问时,都强迫从主内存中读取该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存,这样任何时刻,不同的线程总能看到该变量的最新值。
注意:volatile不能保证volatile变量复合操作的原子性
(原子性: 原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1;):
private volatile int number=1;
number++;
//不是原子操作
//分为三步:1、读number值 2、number值加1 3、写入number的值
试着写一个demo
package com.jqs.sync;
public class VolatileDemo {
private volatile int number = 0;
public int getNumber() {
return this.number;
}
public void increase() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.number++;
}
public static void main(String args[]) {
final VolatileDemo demo = new VolatileDemo();
for (int i = 0; i < 500; i++) {
new Thread(new Runnable() {
@Override
public void run() {
demo.increase();
}
}).start();
}
/*
* 如果还有子线程还在执行,主线程就让出cpu资源
* 直到所有的子线程都运行完了,主线程再往下执行
* */
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("number:" + demo.getNumber());
}
//由于number++不是原子操作,对应的三步操作有可能会有线程交叉执行,最终导致结果可能不为500
}
存在这种情况的原因,就是线程A读取到了num值,但是执行时被B占用了cup,num被B加一并且写入了主内存,但是此时A线程中的num值还没有加一,之后A再加一将值写入,就不对了。
想要解决的话,就把number++替换为
synchronized (this){
this.number++;
}
或者用到lock,具体看代码
Lock lock=new ReentrantLock();
lock.lock();
try {
this.number++;
}finally {
lock.unlock();
}
四、volatile适用场景
1、对变量的写入操作不依赖其当前值
不满足的:number++、count=count*5
满足的:boolean变量、温度记录变量等
2、该变量没有包含在具有其他变量的不变式中
如 low<up
五、synchronized和volatile比较
1、volatile不需要加锁,比synchronized更轻量级,不会阻塞线程
2、从内存可见性的角度讲,volatile读相当于加锁,volatile写相当于解锁
3、synchronized既能保证可见性,又可保证原子性,而volatile只能保证可见性,不能保证原子性。总结来说,就是synchronized可以保证线程安全,但是volatile没办法保证线程安全
六、总结
如果既想要线程安全又想性能好的话,synchronized和volatile要结合具体情况使用。
synchronized并不是万能的,在加锁时能够锁局部就不要锁住整体,能够锁变量就不要锁方法。在保证线程安全的前提下,加锁的范围越小越好。
参考:《Java并发编程实践》
如有错误,欢迎指出,以上。