synchronized:同步锁
多线程操作共享资源时,会出现三个问题:可见性、有序性以及原子性
互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。
可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享 ,变量可能是修改前的值或不一致的值,这将引发许多严重问题。(竞态条件)
volatile: 多线程环境中,可以保证变量的可见性, 能够使变量在值发生改变时能尽快地让其他线程知道(保证了可见性和有序性)
首先我们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器或者是CPU缓存上进行,最后才写入内存.而在这个过程中,变量的新值对其他线程是不可见的.当对volatile标记的变量进行修改时,会将其他缓存中存储的修改前的变量清除,然后重新读取。这里从哪读取我并不明确,一般来说应该是先在进行修改的缓存A中修改为新值,然后通知其他缓存清除掉此变量,当其他缓存B中的线程读取此变量时,会向总线发送消息,这时存储新值的缓存A获取到消息,将新值传给B。最后将新值写入内存。当变量需要更新时都是此步骤,volatile的作用是被其修饰的变量,每次更新时,都会刷新上述步骤
实例一:此实例会产生死循环
public class Task implements Runnable{
boolean running = true;
int i=0;
@Override
public void run() {
while(running){
i++;
}
}
public static void main(String[] args) throws Exception {
Task task = new Task();
Thread thread = new Thread(task);
thread.start();
Thread.sleep(10L);
task.running = false;
Thread.sleep(100);
System.out.println(task.i);
System.out.println("程序停止");
}
}
产生死循环原因:主线程修改running 变量值,子线程中running的值并未发生改变
分析:java内存模型(JMM)
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
解决方案:加volatile 修饰 volatile boolean running = true; 此时就不会产生死循环现象
可见性的特性总结为以下2点:
1. 对volatile变量的写会立即刷新到主存
2. 对volatile变量的读会读主存中的新值
volatile 变量的原子性
原子性的特别总结为2点:
1. 对一个volatile变量的写操作,只有所有步骤完成,才能被其它线程读取到。
2. 多个线程对volatile变量的写操作本质上是有先后顺序的。也就是说并发写没有问题。
这样说也许读者感觉不到和非volatile变量有什么区别,我来举个例子:
//线程1初始化User
User user;
user = new User();
//线程2读取user
if(user!=null){
user.getName();
}
在多线程并发环境下,线程2读取到的user可能未初始化完成
具体来看User user = new User的语义:
1:分配对象的内存空间
2:初始化对象
3:设置user指向刚分配的内存地址
步骤2和步骤3可能会被重排序,流程变为
1->3->2
这些线程1在执行完第3步而还没来得及执行完第2步的时候,如果内存刷新到了主存,
那么线程2将得到一个未初始化完成的对象。因此如果将user声明为volatile的,那么步骤2,3
将不会被重排序。
案例二分析:单例模型
public class Singleton {
//volatile
private static Singleton instance;
private Singleton(){};
public static Singleton getInstance(){
if(instance == null){ //步骤一
synchronized (Singleton.class) {//步骤二
if(instance == null ){//步骤三
instance = new Singleton();//步骤四
}
}
}
return instance;
}
}
这个单例模式看起来很完美,如果instance为空,则加锁,只有一个线程进入同步块完成对象的初始化,然后instance不为空,那么后续的所有线程获取instance都不用加锁,从而提升了性能。但是我们刚才讲了对象赋值操作步骤可能会存在重排序,即当前线程的步骤4执行到一半,其它线程如果进来执行到步骤1,instance已经不为null,因此将会读取到一个没有初始化完成的对象。但如果将instance用volatile来修饰,就完全不一样了,对instance的写入操作将会变成一个原子操作,没有初始化完,就不会被刷新到主存中。
volatile 和 synchronized比较
1)当线程对 volatile变量写时,java 会把值刷新到共享内存中;而对于synchronized,指的是当线程释放锁的时候,会将共享变量的值刷新到主内存中。
2)线程读取volatile变量时,会将本地内存中的共享变量置为无效;对于synchronized来说,当线程获取锁时,会将当前线程本地内存中的共享变量置为无效。
3)synchronized 扩大了可见影响的范围,扩大到了synchronized作用的代码块。