之前说线程安全就是对状态访问变量的管理。
状态访问变量的管理,主要是实现原子性和内存可见性。
什么是内存可见性?
我们的赋值操作,一般包含3个步骤 读取---修改---写入。
在没有同步的情况下,编译器,处理器以及在运行时的操作,都对执行顺序进行调整,这些调整往往会打乱应有的顺序,导致意想不到的问题。
通常JVM对这个顺序会有调整。比如一些常用的服务器程序。在开发调试阶段,我们会增加-server命令行。server模式的JVM比clien模式的JVM有更多的优化。导致很多在研发阶段不会出现的bug,在安装阶段却层出不穷。这就是因为不同的开发模式下,JVM的优化导致代码执行的顺序不一样。
什么是失效数据
对于在这个多线程环境下,未同步的操作,导致部分状态对象读取到错误的值,这些就是失效数据。
面向对象开发中,我们通常会将数据的获取通过get set函数进行,往往这些操作是未经过同步的。要修改这样函数调用,需要在函数名前添加synchronized关键字。
特殊的类型
long和double变量类型是64位的。JVM对于普通的long跟double型进行分解操作,分解成两个32位操作。
可想而知,当你利用这两种类型时,就存在race condition,要想解决这个问题,就必须将这两种类型的变量声明为volatile。
ps。除非处理器能够提供64位的原子操作。
加锁的意义
将原先执行的ABABAB操作,变得有序可循,我们看待代码块加锁应该要有互斥行为以及内存可见性的观念
更轻量级的锁volatile
volatile能很好的简化代码,在内存层面上的表现,volatile将变量从分内存提升到了主存中,保证了其内存可见性。任何有效更改都能及时的反映出来。
volatile的使用
原则上,volatile保证其自身的可见性。以及确保其引用的对象的可见性,以及标识一些程序生命周期的开始或者结束标识。
我们要注意volatile所标注的变量对象不会于其他状态变量被一起纳入不变性条件中。volatile不能被依赖,若有操作需要依赖这个volatile变量,那么就不能这么使用了。必须找其他的方式去实现。
volatile的意义在乎提升了变量所储存的位置。在于内存可见性。对原子性却并没有体现。
publish和escape
之所以存在线程安全问题,是因为我们胡乱的使用了其他对象内的变量\对象,publish和escape指的是两种共享行为。
何为安全的函数构造
很多文章说要慎防this的escape。
不要在构造过程中使this引用escape。这是为什么?因为在对象尚未构造完整之前就急启动它,这可能引发时许混乱。另外一个理由是this的泄漏,虽然你不用,但是依旧存在被引发的可能性。不经意的一笔可能给你带来很大的麻烦。要保持严谨性!
因此,在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过start或者inititalize启动它。
引用一段代码
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;
}
}
线程封闭
Ad-hoc线程封闭:将线程封闭对象保存的引用保存在公有变量中。要确定 !!!只有一个线程!!!对共享变量进行写入操作!!!
栈封闭:只能通过局部变量才能访问对象。局部变量有一个属性:封闭在执行线程中。其他线程无法访问这个栈。那么就是安全的。
ThreadLocal:1. 解决线程执行时要频繁访问,而不断重新分配的问题。
2. 让对象跟对象值关联起来。同时提供一个独立的副本。使得每次都get set都是安全的。
不可变的对象是线程安全的
有几种情况是线程安全的。
1.对象创建后,就无法更改的对象
2.对象所有域都是final的
3.对象是完整创建的,没有escape的
这里说的,不一定 只有final对象。如果,这个对象不能修改。也是线程安全的。
注. final类型的函数在内存上有一个特定的区域被分配。类似于一个主存上的数据
publish
未被完整publish的对象是没有完整性的是线程不安全的。
public class Holder {
private int n;
public Holder (int n) { this.n = n; }//#这里在发布n的时候,要先写入默认的值,在写入n。这样就存在race condition。一个间隔。足以导致错误
public void asserttSanity() {
if (n != n)
throw new AssertionError("This statements is wrong");
}
}
如何做到安全publish
1. 在静态初始化函数中初始化一个对象引用。类似factory方法
2. 将对象引用保存成volatile或者AtomicReferance对象中
3. 将对象引用保存成final
4. 将对象引用保存到synchronized中。有锁保护的域内就是安全的。
5. 活用jdk中现有的线程安全类:Hashtable, synchronizedMap, ConcurrentMap, Vector, CopyOnWriteArrayList, CopyOnWriteArraySet, synchronizedList, synchronizedSet,
BlockingQueue, ConcurrentLinkedQueue等等。
6. 对象被发布后根本就不可能被更改
7. 也是最重要的一条。要熟知对象的 既定规则。知道怎么用,什么时候用,就不会犯错误。