最近学习java多线程,碰巧项目中用到AtomicReference类,在网上搜罗了一堆资料,记录一下阅读笔记和一些文章网址供以后复习。这些文章都和volatile有关,通过学习volatile可以知道jdk1.5引入java.util.concurrent.atomic包的原因。
第一篇文章是“javaeye问答”llade回答问题“加不加volatile看不出有什么效果”的答案:
package linyumin.test.thread;
/**
* @author llade
*/
public class VolatileObjectTest {
/**
* 相信绝大多数使用JAVA的人都没试出volatile变量的区别。献给那些一直想知道volatile是如何
* 工作的而又试验不出区别的人。
* 成员变量boolValue使用volatile和不使用volatile会有明显区别的。
* 本程序需要多试几次,就能知道两者之间的区别的。
* @param args
*/
public static void main(String[] args) {
final VolatileObjectTest volObj=new VolatileObjectTest();
Thread t2=new Thread(){
public void run(){
System.out.println("t1 start");
for(;;){
volObj.waitToExit();
}
}
};
t2.start();
Thread t1=new Thread(){
public void run(){
System.out.println("t2 start");
for(;;){
volObj.swap();
}
}
};
t1.start();
}
//加上volatile 修饰的时候,程序会很快退出,因为volatile 保证各个线程工作内存
//的变量值和主存一致。所以boolValue == !boolValue就成为了可能。
boolean boolValue;
public void waitToExit() {
//非原子操作,理论上应该很快会被打断。实际不是,因为此时的boolValue在线
//程自己内部的工作内存的拷贝,因为它不会强制和主存区域同步,线程2修改
//了boolValue很少有机会传递到线程一的工作内存中。所以照成了假的“原子现象”。
if(boolValue == !boolValue)System.exit(0);
}
public void swap() {
//不断反复修改boolValue,以期打断线程1.
boolValue = !boolValue;
}
}
这份代码说明了两个问题:1、线程间共享的变量会拷贝到线程的工作内存,如果不能及时写回到主存,将造成线程间共享变量不同步;2、加上volatile修饰符使得线程间共享变量同步会引起a == !a之类的逻辑错误,这是因为我们没有对非原子性操作boolValue == !boolValue加锁。所以可以修改这份代码来修正这两个错误:
public class VolatileObjectTest {
public static void main(String[] args) {
final VolatileObjectTest volObj=new VolatileObjectTest();
Thread t2=new Thread(){
public void run(){
System.out.println("t1 start");
for(;;){
volObj.waitToExit();
}
}
};
t2.start();
Thread t1=new Thread(){
public void run(){
System.out.println("t2 start");
for(;;){
volObj.swap();
}
}
};
t1.start();
}
volatile boolean boolValue;
// 加锁
public synchronized void waitToExit() {
if(boolValue == !boolValue) {
System.out.println("exit...");
System.exit(0);
}
}
// 加锁
public synchronized void swap() {
boolValue = !boolValue;
}
}
习题一:用java.util.concurrent.atomic.AtomicBoolean来替代boolean,达到上面这份代码的效果。
第一篇文章中的代码很好的演示了volatile修饰符的作用和局限:保证共享变量在线程间的可见性,无法保证共享变量的非原子操作的互斥性。第二篇文章Java theory and practice: Managing volatility进一步展示如何正确使用volatile修饰符。
正确使用volatile需要满足两个条件:
Writes to the variable do not depend on its current value.
The variable does not participate in invariants with other variables.
这两个条件其实都是针对变量非原子性操作提出来的,第一个条件表明写volatile变量不能依赖其当前值,比如i++,i+=i之类的操作,因为这些操作涉及“读-改-写”一系列动作;第二个条件表明volatile变量不能与其他变量共同决定某个状态,比如对上下限(a,b)中的上限和下限分别赋值,这可能导致下限大于上限。
// 违反条件一,为value加上volatile修饰符也可能出现value值少于预期值
public class CheesyCounter {
private int value;
public int getValue() { return value; }
public int increment() {
return value++;
}
}
// 违反条件二,为lower,upper加上volatile修饰符可能出现lower > upper的情况
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
引入volatile修饰符的原因有两个:使用简单,效率高,如果能够在恰当的时机使用它对编写高效的代码很有益,有五种模式:
1. Status flags
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
在多线的情况下,某个线程调用shutdown函数,volatile修饰符保证变量shutdownRequested的更新值能够被及时写入主存供其他线程调用。
2. Onetime safe publication
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;
public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}
public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}
这个模式的用法与Double-Checked Locking的问题有关,详细描述请见The "Double-Checked Locking is Broken" Declaration。在执行“theFlooble = new Flooble()”时,虚拟机可能会对构造函数和赋值语句的执行序列进行重组优化,也就是说先为Flooble的实例分配内存,并将这个内存地址赋予theFlooble,再调用Flooble的构造函数。为对象引用theFlooble加上volatile修饰符能保证虚拟机在调用Flooble的构造函数之后才将其地址赋予theFlooble,其他线程就能操作被成功构造的对象。
值得注意的是,这个模式有一个限制条件, initInBackground语义表明希望其他线程操作initInBackground之后的对象,initInBackground函数中如果还有其他更改theFlooble对象成员变量的操作将违反volatile修饰符的使用条件一:非原子性操作。
3. Independent observations
public class UserManager {
public volatile String lastUser;
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}
这其实是模式二的一个延伸。
4. The “volatile bean” pattern
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
这个模式的名字暗指类似于bean,变量的setter和getter加上volatile修饰符可以保证线程安全,这些getter,setter不能包含对变量的逻辑操作,假如被修饰的是对象引用,所引用的对象的成员变量必须是一些不变值,因为volatile修饰的是引用本身,对引用的对象并不起作用。
5. The cheap read-write lock trick
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}
这个模式是对那些非原子性操作加上同步锁,而原子性操作则保留了volatile变量的效率。
整篇文章在教我们如何正确使用volatile,实际上还是颇多陷阱,这也是引入java.util.concurrent.atomic包的原因。比如执行getAndDecrement这类操作,使用atomic包远比模式五要简单。
最后收藏一个网址http://www.cs.umd.edu/~pugh/java/memoryModel/,这个网址记录了很多java虚拟机内存模型的资料。