volatile变量能保证线程安全性吗?为什么?
在谈及线程安全时,常会说到一个变量——volatile。在《java并发编程实战》一书中是这么定义volatile的——java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。书中的这句话说明了两点:①volatile变量是一种稍弱的同步机制;②volatile能够确保将变量的更新操作通知到其他线程——可见性。这两点和我们探讨“volatile变量是否能够保证线程安全性”息息相关。
什么是同步机制?在并发程序设计中,各进程对公共变量的访问必须加以制约,这种制约称为同步(该定义源于百度百科)。也就是说,同步机制即为对共享资源的一种制约。《java并发编程实战》中为什么说volatile是一种“稍弱的同步机制”呢?这和volatile能够确保可见性这一重要作用相关:
volatile能够保证字段的可见性:volatile变量,用来确保将变量的更新操作通知到其他线程。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
可见性和“每个线程都有自己的缓存(或叫“线程的工作内存”)”有关系:
①操作没有用volatile来修饰字段时,各个线程都是先从主内存(堆)中复制一份数据到自己的工作内存中,然后操作自己工作内存中的数据,最后再更新到主内存中。
②当字段被volatile修饰后,各个线程操作该字段时,都是直接在主内存中进行操作的。
因为volatile能够确保可见性,所以,在一些特定情形下可以使用 volatile 变量替代锁,例如:在直接修改变量(不需先判断再修改)的情况下,多个线程同时去修改某个变量,一旦某个线程操作成功了,其他线程对这个变量的修改就立刻建立在最新的变量值上再进行修改,这样一来就避免了线程安全问题。但是,要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:①对变量的写操作不依赖于当前值;②该变量没有包含在具有其他变量的不变式中;否则依旧会出现线程安全问题。我们用代码来做说明:
class window implements runnable { private volatile int ticket = 100; public void run() { for (;;) { //通过下面的①②两个步骤我们可以发现:当不能满足“对变量的操作不依赖与当前值”,自然就会有线程安全问题。 if (ticket > 0) { try { thread.sleep(100);//①多个线程同时判断到“ticket>0”,然后挂起了 } catch (interruptedexception e) { e.printstacktrace(); } //②多个线程同时醒来,同时进行“ticket--”操作: system.out.println(thread.currentthread().getname() + ":" + ticket--); } else { break; } } } }
public class a03usevolatileisnotthreadsafe { public static void main(string[] args) { window w = new window(); thread t1 = new thread(w); thread t2 = new thread(w); thread t3 = new thread(w); t1.setname("窗口1"); t2.setname("窗口2"); t3.setname("窗口3"); t1.start(); t2.start(); t3.start(); } }
测试结果:(1)出现了大量的重复数字; (2)最后还输出了 “-1”;==》说明变量即使用volatile修饰了但依旧出现了线程安全问题。
代码解析:
出现问题(1)的原因:线程存在“先检查后执行”的竞态条件。可能有两个线程同时拥有cpu的执行权(机器是双核的),它们判断到做“if (ticket > 0)”,并同时做“ticket--”操作。
出现问题(2)的原因:
①当ticket==1时,两个或多个线程同时通过了“if (ticket > 0)”的判断,并进入了判断框中去执行代码;
②然后它们执行到“thread.sleep(100);”就睡了;
③睡醒后总有一个线程会先抢到cup的执行权,然后执行“ticket--”操作,并将最新的ticket数值推送告知到每个线程;
④此时那些在判断框中的其他的线程并不会再次做“if (ticket > 0)”的判断,而是直接拿最新的ticket并做“ticket--”操作。
就算线程在“ticket--”之前每次都做“if (ticket > 0)”的判断,也依旧会有线程安全问题,因为又可能出现①那种同时通过判断的状态。