java并发编程基础之对象的共享
标签 : 多线程
3.1可见性
可见性指的是多线程中,一个线程对共享数据的修改是对其他线程是否可见的一种性质。
当我们多线程去执行一段读写操作的时候,读和写是在不同的线程执行,当一个线程向一个变量写入数据,另一个读线程可能看不到这个已经写入的数据。为了保证多个线程对内存读写的可见性,我们必须要采用同步机制。
举一个多个线程在没有同步情况下读写共享数据出现错误的一个例子 代码如下:
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
主线程启动读线程,并且把number设置成42,ready设置成true 。读线程在while循环条件中判断如果ready设置为true了就输入number
结果又可能是while循环一直循环下去,因为读线程没有读取到主线程修改后的值,也有可能输出的结果是0
因为number = 42;ready = true;这两句话可能会发生重排序,也就是当ready = true;完成的时候number还没有完成赋值。但是读取线程的输出语句已经执行了。
3.1.1失效数据
上面的例子已经向我们展示了一种失效数据的情况。当读取线程查看ready变量的时候,读取到了一个失效的值。失效的值就可能导致一些意外情况的发生。
在举一个有关失效值的例子 代码如下
@NotThreadSafe
public class MutableInteger {
private int value;
public int get() {
return value;
}
public void set(int value) {
this.value = value;
}
}
当一个线程调用set,另一个线程调用get 有可能获取到最新的值。也有可能获取到失效的值。我们可以通过对get,set方法进行同步,使其成为一个线程安全的类,仅仅对set方法同步是不够的。调用get方法还是会活得失效的值。代码如下:
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this") private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
}
3.1.2 非原子的64位操作。
在没有同步机制的情况下多线程读写数据可能会得到一个失效的值,但是这个值起码是一个“旧值”而不是一个随机的值。这种安全性叫做“最低安全性”。
最低安全性适用于绝大多数变量,但是非volatile64位变量Long double除外,因为jvm允许将64位的读写操作分为两个32位进行。所有有读写线程同时给64位变量写值和读值,就有可能读到其中的32位部分。
因为在编写java虚拟股规范的时候,很多处理器价格不能有效的提供64位数值的原子操作
所以就算不考虑数值失效的问题,在多线程环境中使用long double这样的64位数值也是线程不安全的,除非用volatile关键字修饰他们或者用锁保护起来。
3.1.3加锁与可见性。
加锁的含义不仅仅是局限于互斥行为,还包括内存可见性。为了所有线程都能看到共享变量的最新值,所有操作这个变量的读写操作必须在同一个锁上同步。
现在我们也可以进一步的理解为什么访问某个共享变量的所有操作都需要在一个锁上同步,就是为了一个线程对其修改对于其他线程来说是可见的。否则的话一个线程持有一个不正确的锁读取一个变量,有可能读到的就是失效的值。
3.1.4 Volatile变量
java语言提供了一种弱同步机制就是volatile变量。被volatile修饰的变量发生修改的时候,对其他线程是可见的,并且不会将这个变量的操作与其他内存操作重排序。那么这种机制原理上是怎么实现的呢
当把变量声明成volatile的时候,编译和运行时都会主要到这个变量是共享的。不会将该变量上的操作与其他内存操作进行重排序。volatile变量不会缓存在寄存器或者其他对于处理器来说不可见的地方。因此在读取volatile的时候总会返回最新写入的值。volatile是不需要加锁的一个轻量级的同步机制。
但是不建议依赖使用volatile变量提供的可见性,这通常比用锁更脆弱,也难以理解。
volatile变量的正确使用方式是:
1. 确保他们自身状态的可见性
2. 确保他们所引用对象的可见性
3. 标识一些重要的程序生命周期时间的发生(例如初始化或者关闭)
4. 发生64位赋值的变量
如下代码,给出了volatile变量的一种典型用法
volatile boolean asleep;
....
while(!asleep)
countSomeSheep();
检查某个共享变量的标记来判断时候继续循环。此时因为变量是volatile修饰的,另一个线程如果对这个变量进行修改会马上看到。用锁也可以实现,但是会使代码变得复杂。所以使用volatile变量。
volatile变量使用很方便但是也具有一定的局限性,通常来说就是用来当做一些状态的标示,就像如上的这个程序。在volatile表示其他状态信息的时候要小心,比如volatile不能保证 count++操作的原子性。
加锁机制既可以保证可见性和原子性,volatile只能保证可见性
所以当且仅当如下情况的的时候可以使用valatile变量
1. 当变量的写入操作不依赖当前变量的值,或者能确保只有单个线程更新变量的值
2. 该变量不会与其他变量一起纳入不变性条件中
3. 在访问变量的时候不需要加锁
3.2发布和逸出
简单来说发布对象指的就是,让对象能够在当前作用域之外使用。例如把对象传递到其他类或者方法中,或者吧对象的引用保存到其他代码可以访问的地方。发布对象可能会发布一个未构造完成的对象就会造成线程安全问题。
逸出就是当某个不应该发布的对象发布出去了就是逸出。
下面举发布和溢出的例子。
public static Set<Secret> konwnSecrets;
public void initialize(){
konwnSecrets = new HashSet<Secret>();
}
当发布某个对象的时候可能间接的会发布其他对象。比如在这个set中添加一个Secret对象,那么这Secret对象也会被发布。外部可以遍历集合拿到Secret的引用。
当一个公有方法返回了一个私有对象或者属性的时候就可能会导致逸出,代码如下
class UnsafeStates {
private String[] states = new String[]{
"AK", "AL" /*...*/
};
public String[] getStates() {
return states;
}
}
当某些对象的私有属性是不想被外部访问的。但是由于一些原因可能会被发布出去(实际情况要比和这个例子要复杂)这样外部就能获取到这个私有的属性,这样就是一种逸出。发布出去后,你并不知道有什么线程会对这个属性做什么操作,所以存在这不安全性。
最后一种发布对象或者内部状态的机制就是发布一个内部类的实例代码如下:
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);//调用外部实例的方法,隐式的逸出this引用。
}
});
}
这个程序在发布EventListener的时候也隐式的发布了ThisEscape本身,因为在内部类的实力中包含这对ThisEscape实例的引用。这样会是this引用在构造函数中逸出。这种情况只有构造函数构造完成返回的时候,才能确保对象的状态是正确的可以使用的。所以在构造函数中发布对象有可能发布出去的是一个为构造完成对象。如果this引用在构造过程中逸出,这种对象就是一种不正确的构造。
不要在构造过程中使this引用逸出
下面来看看正确的做法
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
利用工厂方法创建一个SafeListener。然后再注册这个SafeListener。这样可以保证在对象构造的过程中this引用不会逸出。
3.3 线程封闭
多线程访问共享数据通常要使用同步,还有一种不使用同步的方式就是线程封闭。线程封闭就是不共享数据每个线程都在线程内部访问数据。即使一个线程不安全的对象封闭在线程中,这个对象也是线程安全的。
3.3.1 Ad-hoc线程封闭
这种方式不推荐使用,这种方式的线程封闭的责任完全交给程序去维护。这种方式是非常脆弱的。
3.3.2 栈封闭
局部变量的特性就是封闭在线程的执行栈中,每个线程都有各自的栈保存个各自的局部变量。所以线程之间不能互相访问各自的局部变量。所以线程之间不会操作共享变量,避免了线程安全问题,这种方式要比Ad-hoc线程封闭更加健壮 。
对于基本类型局部变量,是无论如何都不会破坏线程的封闭性,因为在java中任何方法都不能获取基本类型的引用,这种语义就会确保基本数据类型的局部变量会始终封闭在线程内,而对于引用类型的局部变量,我们要确保这个引用类型变量不会逸出。代码示例如下。
public int loadTheArk(Collection<Animal> candidates){
SortSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;
//animals被封闭在方法中,不要让它逸出!
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for(Animal a: animals){
//.....
}
return numPairs;
}
3.3.3 ThreadLocal类
ThreadLocal类提供了一种维持线程封闭的解决方案,这个类提供了get set 方法等,调用set 可以让变量绑定到当前线程上,每个线程都存有一个独立的副本,所以再调用get方法的时候可以获取到当前线程set的值。让我们看看ThreadLocal内部是怎么实现的。
public void set(T value) {
Thread t = Thread.currentThread();
//从当前线程获取存储值的map
ThreadLocalMap map = getMap(t);
//如果没有则新建一个map。有就直接设置值。
if (map != null)
//这里的Key就是ThreadLocal对象本身
map.set(this, value);
else
createMap(t, value);
}
那么getmap(t)方法试怎么获取到当前线程的map呢,看如下代码
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
这段代码可以看出来,getMap会返回当前线程的threadLocals这个变量,接下来我们看下在Thread类中这个变量的定义
public class Thread implements Runnable {
//省略其他属性和方法
//是ThreadLocal 中的一个静态内部类ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
}
此时我们可以懂了。在Thread中保存这这样的一个map可以供我们存取值。并且调用get方法就会获取到当前线程set过的值。
ThreadLocal类的应用,通常防止对一个可变的单利的实力变量或者全局变量进行共享,举个例子比如一个全局的数据库连接,在单线程程序中可能就会维持一个全局的数据库连接,并且在程序启动的时候初始化这个全局的连接,从而避免调用每个方法都需要传递一个Connection对象。但是在多线程应用中由于JDBC的连接对象不一定是线程安全的,此时如果多线程操作一个共享的线程不安全的对象就会出现线程安全问题。这时候我们就可以用ThreadLocal,这样每个线程就能拿到自己的连接。代码示例如下:
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
当线程终止后,放在ThreadLocal中的这个线程的值会被垃圾回收
ThreadLocal适合多线程应用程序中,如果频繁的使用一个对象,但是这个对象不是线程安全的不能共享访问,并且不想频繁的创建这个对象。这时候你就可以使用ThreadLocal。
这是一个应用ThreadLocal很好的例子
当然也有很多程序员经常乱用ThreadLocal,例如将全局参数都作为ThreadLocal对象,或者为了减少方法传参,从而会隐藏参数。同时ThreadLocal变量会造成类之间的耦合性。要小心使用。
3.4 不变性
如果某个对象在创建后不能被修改,那这个对象就称之为不可变对象。之前提到过很多多线程访问一个可变状态会发生很多问题。但是如果这个对象本身就是一个不可变对象,那么分析起来要简单多了。不可变对象内部的属性只会初始化一次并且由构造函数完成。可以放心的发布,不用担心被修改,或者破坏。
那么如何构造一个不可变的对象呢,仅仅吧所有的属性都用final修饰是不够的,因为final域中可以保存一个可变对象的引用。所以要正确的构造一个不可变对象需要满足以下条件。
- 对象创建后其状态就不能被修改。
- 对象所有的域都需要是final类型
- 对象是正确创建的(对象构造期间,this引用没有逸出)
之前我们提过final域中可以保存一个可变对象的引用。我们可以用可变对象来构造这个不可变对象,但是要保证构造完成后,这个对象是不能被修改的。代码示例如下
@Immutable
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
}
我们可以看出来尽管Set对象是可变的但是在Set对象在构造完成后是无法被修改的。同时我们要注意不要使this引用逸出
3.4.1 Final域