一、可见性
当读操作和写操作在不同的线程进行的时候,并不能保证读的线程可以读到写线程最新的更改。如果要确保内存对写入操作的可见性,就必须使用同步。
处理器还会对程序中的操作进行重排序。重排序保证在单线程的执行情况下,和不重排序得到的结果一样,但是多线程的话,就不一定了。
一个简单的方法避免所有的问题就是:只要有数据共享,就是用正确的同步。
1.失效数据 看下面的程序:
如果不对set和get进行同步,肯定是不能保证可见性的。但是仅对set加锁,仍然可能会看到失效的数据。因为set同步后把数据刷新到主内存,但是get的时候,还是使用的工作内存,并没有去主内存取值。
2.Volatile变量
volatile是一种弱同步机制,可以保证可见性,但是不能保证原子性。
volatile变量的正确使用方式是:
- 确定他们自身状态的可见性
- 确保他们所引用对象的状态的可见性
- 标志一些重要的生命周期事件的发生(初始化,关闭)
- 对变量的更新操作不依赖于变量得当前值,或者只有单线程修改变量的值
- 该变量不应该与其他状态变量一起纳入不变性条件
- 访问该变量时,不需要加锁
三、线程封闭
一种避免使用同步的方式,就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术称为线程封闭。三种方式:ad-hoc,栈封闭,ThreadLocal。
1.栈封闭
就是可重入代码,方法内只包含局部变量(形参和方法内定义的变量),那么这段代码就一定是线程安全的。因为不能获得基本类型变量的引用,所以java可以保证基本类型的局部变量封闭在线程中。
在维持对象引用的封闭性时,比如上面的animals对象,要确保栈封闭性,就要确保animals不会被发布到外面去。那么这段代码也是栈封闭的。
3.ThreadLocal类
ThreadLocal常用与防止对可变的单实例变量或全局变量进行共享。当某个频繁的操作需要一个临时对象,比如说缓冲区,而同时又希望避免在每次执行时,都重新分配该临时对象,就可以使用这种技术。
ThreadLocal类类似于全部变量会,降低代码的可重用性。
四、不变性
如果某个对象创建后,其状态就不能修改,我们称之为不可变对象。不可变对象一定是线程安全的。
当满足以下条件时,对象才是可变的:
- 对象创建以后,状态就不能改变
- 对象的所有域都是final类型的
- 对象是正确的创建(对象创建期间,没有this引用逸出)
不可变的对象并不是不可变的对象引用。
2.final域
final类型的域中可以保存对可变对象的引用。final域能够确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时,不需要使用同步。
除非需要某个域是可变的,否则应该将其声明为final域,是一个比较好的编程习惯。
当访问和更新多个相关变量时出现竞态条件,可以通过把这些变量都保存在一个不可变对象中来消除。
当需要更新这个变量的时候,在重新new一个不可变对象就好了。
五、安全发布
1.不正确的发布:正确的对象被破坏
在对象被完全构造出来之前,将其发不出去。
2.不可变对象与初始化安全性
我们知道:即使某个对象的引用对其他线程来说是可见的,也不意味着对象状态对于该线程一定是可见的。为了使对象状态呈现出一致性视图,必须使用同步。
即使再发布不可变对象时,没有使用同步,也可以保证这种可见性。可以延伸到final类型的域中。
3.安全阀不得常用模式
- 在静态初始化函数中初始化一个对象引用
- 将对象的引用保存到volatile或者atomicreference中
- 将对象的引用保存到某个,被正确构造的final类型的域中
- 将对象的引用保存到一个锁保护的域中
4.实事不可变对象
如果对象从技术上来说是可变的,但是从它发布以后,就不再改变。这种对象称为事实不可变对象。不需要额外同步的情况下,任何线程都可以安全的访问实事不可变对象。
5.可变对象
要安全的共享可变对象,这些对象就必须被安全的发布,并且必须是线程安全或者某个锁保护起来。
对象的发布需求取决于它的可变性:
不可变对象可通过任意机制发布。
实事不可变对象必须通过安全方式发布。
对象就必须被安全的发布,并且必须是线程安全或者某个锁保护起来可变。
6.安全的共享对象
当发布一个对象的时候,必须明确地说明对象的访问方式。
线程封闭
只读共享
线程安全共享
保护对象。