xiaobaoqiu Blog

Think More, Code Less

Java Concurrency in Practice 3 : 共享对象

第三章:共享对象 编写正确的并发程序关键在于对共享的可变状态进行管理

1. 可见性

没用同步的情况下共享变量的例子,不能保证主线程写入的值对于读线程是可见的,读线程可能会一致等待,也可能读出0(因为主线程在写入number之前已经写入ready,重排序现象)

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
public class NoVisibility {
    public static int number = 0;
    public static boolean ready = false;

    /**
     * 负责读的线程
     */
    private static class ReadThread implements Runnable {
        @Override
        public void run() {
            while(!ready){
                System.out.println("Waiting...");
                Thread.yield();
            }
            System.out.println("Number = " + number);
        }
    }

    /**
     * 主线程, 写
     * @param args
     */
    public static void main(String[] args){
        new Thread(new ReadThread()).start();
        number = 100;
        ready = true;
    }
}

结论:只要数据被跨线程共享,就需要进行恰当的同步.

上面例子的一种情况是,读线程读取的是一个过期数据.但更坏的情况是:过期即不是发生在全部变量上,也不是不过期,而是部分变量过期.

最低限的安全性(out-of-thin-air safety):当线程在没有同步的情况下读取变量,它可能会的得到过期值.但是它至少看到的是某个线程设置的真实值,而不是凭空而来的值.

最低限安全性应用于所有的变量,除了:没有声明为volatile的64位的数值变量(long,double),JVM允许将64位的读或写划分为两个32位的操作.

为了保证所有的线程能够看到共享的可变变量的最新值,读取和写入都必须使用公共的锁进行同步.

Volatile提供了同步的一种弱形式.

当一个变量声明为volatile类型后,编译与运行时候会见识这个变量,而且对它的操作不会被重排序.

volatile变量不会缓存在缓存器或者缓存在对其他处理器隐藏的地方.所以,读一个volatile类型变量时候总会返回由某一个线程写入的最新值.

访问volatile变量的操作不会加锁,也就不会引起执行线程的阻塞,这使得volatile变量相对于sychronized而言,只是轻量级的同步机制.通常被当作标记完成,中断或某个状态的标记使用.

注意:加锁可以保证可见性和原子性,volatile变量只能保证可见性.比如volatile不足以保证自增操作(count++)原子化.

2. 发布和逸出

发布(publish)一个对象的意思是使它能够被当前范围之外的代码锁使用.

逸出(Escape):一个对象尚未准备号时候就将它发布.

下面是一个发布的例子,init()声明了一个HashSet,并将它存储到公共的静态域中.

1
2
3
4
5
6
7
public class Publish {
    public static Set<Secret> knownSecret;

    public void init(){
        knownSecret = new HashSet<Secret>();
    }
}

类似的,非私有的方法中返回引用,也能发布返回的对象,如:

1
2
3
4
5
6
7
public class Publish {
    private String[] country = {"USA", "China"};

    public String[] getCountry() {
        return country;
    }
}

这种发布会有问题,任何一个调用者都能修改它的内容.

3. 线程封闭

实现线程安全最简单的方式之一就是线程封闭.

一种维护线程限制的方式是使用ThradLocal,它提供了get和set访问器,为每个使用它的线程维护了一份单独的拷贝,所有get总是返回当前执行线程通过set方法设置的最新值.简单示例如下:

1
2
3
4
5
6
7
8
9
10
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
    @Override
    protected Connection initialValue() {
        return super.initialValue();
    }
};

public static Connection getConnection(){
    return connectionHolder.get();
}

4. 不可变性

为例满足同步的需要,另外一种方法是使用不可变对象.不可变对象即创建后状态不能被修改的对象.

不可变对象并不适简单的等于将对象的所有域都声明为final类型,所有域都是final类型的对象仍然可以是可变的,因为final域可以获得一个可变对象的引用.

5. 安全发布

在并发程序中,使用和共享对象的一些最有效的策略如下:

  1. 线程限制:一个线程限制对象,通过限制在线程中,而被线程独占,且只能被占有它的线程修改.
  2. 共享只读:一个共享的只读对象,在没有额外同步的情况下啊,可以被多个线程并发的访问,但是任何线程都不能修改它.
  3. 共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无需额外的同步,就可以通过公共接口随意访问它.
  4. 被守护的:一个被守护的对象只能通过特定的锁来访问.