文章目錄
  1. 1. 一、volatile关键字
  2. 2. 二、违规代码(数组)
  3. 3. 三、合规解决方案(AtomicIntegerArray和同步)
    1. 3.1. 3.1 AtomicIntegerArray
    2. 3.2. 3.2 同步
  4. 4. 四、违规代码
    1. 4.1. 4.1 可变对象
    2. 4.2. 4.2 volatile读操作,同步写操作
    3. 4.3. 4.3 合规解决方案(同步)
    4. 4.4. 4.4 违规代码示例(可变子对象)
    5. 4.5. 4.5 合规解决方案(每次调用创建一个实例/防御式复制)
    6. 4.6. 4.6 合规解决方案(同步)
    7. 4.7. 4.7 合规解决方案(ThreaedLocal存储)
  5. 5. 五、适用性
  6. 6. 附:ThreadLocal(摘自其他博客)

摘要:程序员经常做出假设的那些行为,可能会使程序得到一些与直觉相反的结果。

一、volatile关键字

 根据Java语言规范的“volatile Field”:

如果一个字段被声明为 volatile,那么以为着Java的内存模型会保证所有的线程看到的该字段的值都是一致的。

 但是,使用volatie关键字仅能保证原生类型字段和对象引用的安全发布,也就是成员对象,而此处volatie保障的可见性是针对对象引用的。对于引用所指向的真正对象(referent)已经不属于安全发布的保障范围内
 因此,把一个引用声明为volatile并不能保证,对该引用所指对象的修改会告知其他线程,这会导致线程可能无法获知另一个线程对成员字段所指对象的更改。
 此外,当引用所指对象是可变对象,且代码缺乏线程安全性时,其他线程就可以能会看到未构造完全活处于非一致状态的(临时)对象。
 尽管如此,如果引用所指对象为不可变对象,那么仅把引用声明为volatile就可以满足线程安全需求。volatile关键字只能保证原生类型字段,对象引用以及不可变对象字段的安全。
 对于volatile修饰的对象,程序员经常把对象本身的线程安全性和其成员变量的线程安全弄混。
 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。

二、违规代码(数组)

 下面违规代码声明了一个volatile的引用,该引用指向一个数组对象:

1
2
3
4
5
6
7
8
9
final class Foo{
private volatile int[] arr = new int[20];
public int getFirst(){
return arr[0];
}
public void setFirst(int n){
arr[0] = n;
}
}

 一个线程给数据添加了一个元素,比如调用setFirst()方法,但是这次方法调用对别的正在调用的getFirst()方法的线程是不可见的。因为volatile关键字只能保证数组引用的线程安全,对于数组中的实际元素是没有任何保障的。
 因此,当调用setFirst()方法的线程和调用getFirst()方法的线程之间没有先行发生(happen-before) 的关系时,代码就可能会出现问题。尽管在写volatile变量的线程和后续读取该变量的线程之间存在“先行发生”关系,但是上面代码中,setFirst()getFirst()方法每次都读取volatile变量(指向数组的引用),两者都没有修改volatile变量。

三、合规解决方案(AtomicIntegerArray和同步)

3.1 AtomicIntegerArray

 为了保证对数组元素的写操作是原子的,且写结果对其他线程可见,下面的合规解决方案使用了java.util.concurrent.atomic包中的AtomicIntegerArray类

1
2
3
4
5
6
7
8
9
final class Foo{
private final AtomicInTegerArray atomicIntegerArray = new AtomicInTegerArray() ;
public int getFirst(){
return atomicIntegerArray.get(0);
}
public void setFirst(int n){
atomicIntegerArray.set(0 , n);
}
}

3.2 同步

1
2
3
4
5
6
7
8
9
final class Foo{
private volatile int[] arr = new int[20];
public sychronized int getFirst(){
return arr[0];
}
public sychronized void setFirst(int n){
arr[0] = n;
}
}

 基于同一个锁做同步的所有线程之间因为同步操作建立一个“先行发生”的关系。在当前例子中,调用setFirst()方法的线程和后续调用getFirst()方法的线程都是以当前对象为锁进行同步的,因此可以保证代码的线程安全性。

四、违规代码

4.1 可变对象

 下面的示例声明了一个volatile修饰map字段,因为代码中有put()方法,所以map中的值是可变的。

1
2
3
4
5
6
7
8
9
10
11
12
final class Foo{
private volatile Map<String,String> map;
public Foo(){
map = new HashMap<String,String>()
}
public String get(String s){
return map.get(s);
}
public void put(String key, String value){
map.put(key,value);
}
}

 交叉使用get()方法和put()方法可能会从Map对象中获取到不一致的值;因为put()方法回修改map中的值。把map引用声明为volatile并不能消除这种数据竞争的状况。

4.2 volatile读操作,同步写操作

 下段程序想要时用“volatile读操作,同步写操作”技术。Foo类中的map字段被声明为volatile来同步对该字段的读写,另外,还给会修改map字段值的put()方法添加synchronized关键字一保证该方法执行的原子性。

1
2
3
4
5
6
7
8
9
10
11
12
final class Foo{
private volatile Map<String,String> map;
public Foo(){
map = new HashMap<String,String>();
}
public String get(String s){
return map.get(s);
}
public synchronized void put(String key,String value){
map.put(key,value);
}
}

 “volatile读操作,同步写操作”主要的作用是:既可以通过同步的方式来保证复合操作的原子性(如自增操作),又可以为只读操作提供更快的访问速度。尽管如此,此项技术并不适合用于可变对象,因为volatile提供的“线程发布安全性”只能保障字段本身(原生类型的值或对象引用),而引用所指向的对象并没有任何线程安全性的保障,对于该对象的内部成员更是如此。因此,上面的代码中,map的写操作和读操作之间没有形成“先行发生”关系。

4.3 合规解决方案(同步)

使用方法同步保证可见性。

1
2
3
4
5
6
7
8
9
10
11
12
final class Foo{
private final Map<String,String> map;
public Foo(){
map = new HashMap<String,String>();
}
public synchronized String get(String s){
return map.get(s);
}
public synchronized void put(String key,String value){
map.put(key,value);
}
}

 上面的代码已经没有必要把map字段声明为volatile,因为所有方法均为同步。字段声明为final是为了防止在引用所指的对象在初始化完成前被发布出去。

4.4 违规代码示例(可变子对象)

 下面的代码中,volatile字段fomat保存了一个指向可变对象java.text.DateFormat的引用。

1
2
3
4
5
6
final class DateHandler{
private static volatile DateFormat format = DateFormat.getDateInstance(DateFormat.MEDIUM);
public stataic Date parse(String str) throws ParseException{
return fomat.parse(str);
}
}

 因为DatFomat不是线程安全的,所以parse()方法返回的Date值可能和str参数对应的时间不一致。

4.5 合规解决方案(每次调用创建一个实例/防御式复制)

1
2
3
4
5
final class DateHandler{
public static Date parse(Stirng str) throws ParseExcption {
return DateFormat.getDateInstance(DateFormat.MEDIUM).parse(str);
}
}

4.6 合规解决方案(同步)

1
2
3
4
5
6
7
8
final class DateHandler{
private static volatile DateFormat format = DateFormat.getDateInstance(DateFormat.MEDIUM);
public stataic Date parse(String str) throws ParseException{
synchronized(format){
return fomat.parse(str);
}
}
}

4.7 合规解决方案(ThreaedLocal存储)

 使用ThreadLocal对象为每一个线程创建一个独立的DateFormat实例

1
2
3
4
5
6
7
final class DateHandler{
private static final ThreadLocal<DateFormat> fomat = new ThreadLocal<DateFormate>(){
protected DateFormat initialValue(){
return DateFormat.getDateInstance(DateFormat.MEDIUM);
}
};
}

五、适用性

 错误的假设volatile关键字对引用所指的对象或对象本身内部成员的线程安全保障,可能会导致程序观察到怪异地值。
 引用所指对象的严格不变性,比线程安全发布的基本要求要高。如果可以从设计上保证引用所指对象是线程安全的,那么也可以直接声明引用为volatile来保障线程安全。尽管如此,这种使用volatile的方式会降低程序的可维护性,应该尽量避免使用。

附:ThreadLocal(摘自其他博客)

http://www.cnblogs.com/alphablox/archive/2013/01/20/2869061.html
http://stackoverflow.com/questions/817856/when-and-how-should-i-use-a-threadlocal-variable

One possible (and common) use is when you have some object that is not thread-safe, but you want to avoid synchronizing access to that object (I’m looking at you, SimpleDateFormat). Instead, give each thread its own instance of the object.

 如果你定义了一个单实例的Java bean,它有若干属性,但是有一个属性不是线程安全的,比如说HashMap。并且碰巧你并不需要在不同的线程中共享这个属性,也就是说这个属性不存在跨线程的意义。那么你不要sychronized这么复杂的东西,ThreadLocal将是你不错的选择。
 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
 
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

  • void set(Object value)设置当前线程的线程局部变量的值。
  • public Object get()该方法返回当前线程所对应的线程局部变量。
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

 值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为:ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)T get()以及T initialValue()

 ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.test;  

public class TestNum {
// ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
public Integer initialValue() {
return 0;
}
};

// ②获取下一个序列值
public int getNextNum() {
seqNum.set(seqNum.get() + 1);
return seqNum.get();
}

public static void main(String[] args) {
TestNum sn = new TestNum();
// ③ 3个线程共享sn,各自产生序列号
TestClient t1 = new TestClient(sn);
TestClient t2 = new TestClient(sn);
TestClient t3 = new TestClient(sn);
t1.start();
t2.start();
t3.start();
}

private static class TestClient extends Thread {
private TestNum sn;

public TestClient(TestNum sn) {
this.sn = sn;
}

public void run() {
for (int i = 0; i < 3; i++) {
// ④每个线程打出3个序列值
System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
+ sn.getNextNum() + "]");
}
}
}
}

 常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个TestNum实例。运行以上代码,在控制台上输出以下的结果:

1
2
3
4
5
6
7
8
9
thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]

 考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个TestNum实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。
 但是使用ThreadLoacal你必须非常小心地使用ThreadLocal的的remove()方法清理你得到get()、set()设置任何ThreadLocals。如果当你做不清理,它保存到加载部署Web应用程序的一部分类的任何引用仍将在永久堆和将永远不会得到垃圾收集。一下是stackoverflow原文:

Since a ThreadLocal is a reference to data within a given Thread, you can end up with classloading leaks when using ThreadLocals in application servers which use thread pools. You need to be very careful about cleaning up any ThreadLocals you get() or set() by using the ThreadLocal’s remove() method.

If you do not clean up when you’re done, any references it holds to classes loaded as part of a deployed webapp will remain in the permanent heap and will never get garbage collected. Redeploying/undeploying the webapp will not clean up each Thread’s reference to your webapp’s class(es) since the Thread is not something owned by your webapp. Each successive deployment will create a new instance of the class which will never be garbage collected.

You will end up with out of memory exceptions due to java.lang.OutOfMemoryError: PermGen space and after some googling will probably just increase -XX:MaxPermSize instead of fixing the bug.

 但是Java8中已经移除了PermGen,这个是否会对ThreadLocal的使用有影响呢?这只能等待后续学习。

文章目錄
  1. 1. 一、volatile关键字
  2. 2. 二、违规代码(数组)
  3. 3. 三、合规解决方案(AtomicIntegerArray和同步)
    1. 3.1. 3.1 AtomicIntegerArray
    2. 3.2. 3.2 同步
  4. 4. 四、违规代码
    1. 4.1. 4.1 可变对象
    2. 4.2. 4.2 volatile读操作,同步写操作
    3. 4.3. 4.3 合规解决方案(同步)
    4. 4.4. 4.4 违规代码示例(可变子对象)
    5. 4.5. 4.5 合规解决方案(每次调用创建一个实例/防御式复制)
    6. 4.6. 4.6 合规解决方案(同步)
    7. 4.7. 4.7 合规解决方案(ThreaedLocal存储)
  5. 5. 五、适用性
  6. 6. 附:ThreadLocal(摘自其他博客)