转载请注明出处 http://www.paraller.com
原文排版地址 点击获得更好阅读体验
核心知识点
- 同步不仅能够保证原子性; 还能实现内存可见性: 当一个线程修改了对象状态后,其他线程能够看到发生的状态变化
- 如何让多个线程安全的访问可变状态 ?
可见性
重排序
class Test{
private static boolean visible;
private static int number;
static class Reader extends Thread{
void run(){
while(!visible){
Thread.yield();
}
system.out.println(number);
}
}
static void main(String[] args){
new Reader().start();
number = 12;
visible = true;
}
}
Reader线程可能看到了visible的值,却没有看到number的,这种现象称为重排序
Jvm为了充分利用现代多核处理器,可能对操作的执行顺序进行调整
如何避免 : 只要有数据在线程之间共享,就使用正确的同步
Volatile
- 变量不会被缓存在寄存器或者其他处理器不可见的地方,在读取valatile变量时总会返回最新写入的值
- 变量声明为 volatile,编译器和运行时都会注意到这个变量是共享的,不会将该变量的操作与其他内存操作一起
重排序
使用场景:
检查某个状态标记以判断是否退出循环:(因为所有线程修改对其他线程都是可见)
while(!asleep){
// TODO
}
发布与逸出
核心知识点
- 发布的概念: 指对象能够在当前作用域之外的代码中使用.
- 发布内部的状态,会破坏封装性,会影响线程安全
- 逸出: 某个不应该被发布的对象被发布,就成为逸出(Escape)
- 当对象在其构造函数中创建一个线程时,this引用都会被新创建的线程共享;解决方案:在构造函数中创建但不是马上启动它,而是通过一个方法来启动。
- 避免逸出: 要返回一个对象A的时候,拷贝对应的值创建一个对象B,然后返回对象B。(可能在拷贝的时候A对象发生了改变,造成数据不一致,取决于需求是否这样使用)
逸出常见场景1: 将对象保存在共有的静态变量.
public static Set<Object> hello;
public void init(){
hello = new HashSet<Object>()
}
逸出常见场景2: 非私有方法中返回一个引用.
class A{
private Object obj = new Object()''
public get Obj(){
return this.obj;
}
}
线程封闭
核心知识点
- 避免使用同步的方式就是
不共享
数据: 如果仅在单线程内访问数据,就不需要同步。 - 线程封闭: 在数据封闭在一个线程中不共享
栈封闭
定义:栈封闭?其实就是把同步变量写在局部方法中,当然就线程安全了,根本不会共享变量。线程私有。
注意事项:确保栈内对象不会逸出
ThreadLocal
概念:ThreadLocal 提供了get /set等访问接口或方法,这些方法为每个使用该变量的线程都保存一份独立的副本, 因此Get方法总是返回由当前执行线程在调用set
时设置的最新值。
使用ThreadLocal,能使线程中的某个值所有改变只在该当前线程中变动,不会被其他线程操作,保证了当前线程的数据不共享
,避免竞态条件的发生。
场景: Spring中的事务上下文(Transaction Context),需要对当前线程进行 回滚、提交 操作,为了当前事务不会和其他线程串了,就通过将事务上下文与某个执行线程关联起来。
缺点: 耦合性会更强,并且降低代码的可重用性
不变性
核心知识点
- 如果某个对象被创建之后就不能被改变(不变性条件是由构造函数创建的),就一定是线程安全的。
- 即使对象中所有的域都是final类型的,这个对象仍然可能是可变的, 因为final类型的域可以保存对 可变对象的引用
- 不可变对象需满足的条件,代码示例符合以下三个条件:
1、对象创建以后其所有状态都不能修改。(没有修改状态的方法)
2、对象的所有域都是final类型的。
3、对象是正确创建的(创建期间,this引用没有逸出)。
class Demo{
private final Object demoObj;
public Demo(Object obj){
demoObj = obj;
}
public Object getObj(){
return demoObj;
}
}
- 保存在不可变对象中的程序状态仍然可以更新,即通过将一个保存新状态的实例来"替换"原有的不可变对象
class ImmutableObject{
private final BigDecimal bd;
ImmutableObject(String val){
bd = new BigDecimal(val);
}
}
...
ImmutableObject io = new ImmutableObject("2");
io = new ImmutableObject("3");
io是不可变对象,但它的程序状态 bd仍然可以更新, 因为我创建了一个保存新状态的实例来替换原有的不可变对象io;
Final域
- final类型的域是不能被修改的(但是所引用的对象可能是可变的)。
- 除非需要更高的可见性,否则应该将所有的变量声明为私有的。
- 除非需要某个域是可变的,否则变量应该声明为final类型的
- 对于在访问和更新 多个相关变量时出现的竞态条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。
final类型的域初始化之后就不能更改赋值
。但如果引用的对象是可变的,比如:
public static final Set<String> se = new HashSet();
se.add("hello");
se.add("world");
安全的发布
前面讨论的三种方式 栈封闭、ThreadLocal、对象中不可变final变量。前两种都是为了使对象不发布不共享,保存在单个线程中。现在要讨论如何安全的共享变量。
错误示例:即使在构造函数中正确的创建不可变对象,也很难保证其他线程获取的时候构造函数没有执行完成
class Demo{
public Object obj;
Demo(){
obj = new Object("Hello");
}
}
安全发布常用模式
要安全的发布一个对象,对象的引用和对象的状态必须同时对其他线程可见
,(安全的发布
重点在于发布的过程中不会存在竞态条件,发布之后还是要有相应的同步机制 !) 以下是安全发布的方式:
- 在静态初始化函数中,初始化一个对象引用 :
静态初始化器由JVM在类的初始化阶段执行,JVM内部存在着同步机制,所以任何对象都可以被安全的发布。 - 将对象的引用保存到 volatile 类型的域 或者 AtomicReference对象中。
- 将对象的引用保存到 某个
正确构造对象
(没有逸出)的 final 域中。(对象不可变) - 将对象的引用保存到 一个由锁保护的域中。
- 线程安全的容器("装饰器设计模式")满足上述最后一条需求。例如:Vetor / synchronizedList / Hashtable / BlockingQueue / ConcurrentMap .e.g ;
- 类库中的其他数据传递机制(例如Future和Exchanger)同样能实现安全发布。
- 事实不可变对象:对象在技术中可以改变,但是业务中其状态在发布后不会被改变。只读
总结
对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意方式发布
- 事实不可变对象发布需要通过安全方式
- 可变对象必须通过安全方式发布,发布后的改变需要同步机制
重点
:在并发程序中使用和共享对象,可以采用以下策略
- 线程封闭:栈封闭、ThreadLocal
- 只读共享:将对象修改为不可变对象。(包括事实不可变对象)
- 线程安全共享: 线程安全的对象在其内部实现同步,多个线程通过公有接口访问,比如线程安全类
- 保护对象: 使用同步机制来保证访问。(包括封装在线程安全对象中的对象)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。