1.写在最前面
第二章中可以使用同步来<b>避免</b>多个线程在同一时刻访问相同的数据(使用了同步访问共享数据就需要排队,同一时刻只有一条线程有访问权),这一节主要介绍如何安全的共享及发布对象来使得多个线程可以安全的同时访问。
<b>synchronized不止能保证原子性,而且能保证可见性</b>
线程间对数据的更改如何才能达到可见性?
- 线程a在工作内存更新之后需要同步到主存
- 将主存中的变化同步到其他现成的工作内存副本中(或者强制要求线程访问该变量都从主存读取)
<b>现在还有很多虚拟机可能会允许对非volatile的long和double的64位数据类型的读或者写操作分为两次32位操作执行,会出现奇怪的东东</b>
java数据可见性保障实现途径:
- synchronized
JMM对synchronized的两条规定:线程解锁前,必须把共享变量的最新值刷新到主内存中(在退出synchronized代码块的时候,共享变量的最新值已经刷新到主内存中);线程加锁时,将清空工作内存中共享变量的值,使在使用共享变量的时候必须重新在主内存中加载最新的值(注意,加锁解锁必须用同一把锁)。这样保证了线程可见性。
- volatile(仅能保证数据可见性,不能保证原子性,弱同步机制)
以<b>内存屏障</b>方式实现数据可见性!!!!
<b>what is 内存屏障????</b>
内存屏障(memory barrier) 是一个CPU指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障, 相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会 把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
<b>volatile是怎么使用内存屏障保证可见性的?</b>
a)在写操作后插入一个<b>写屏障</b>指令,在读操作前插入一个<b>读屏障</b>指令!!!!!!
这样能保证在每次写入后都会将更新的最新值同步到其他所有相关的工作内存,每次读取前工作内存都已经是写操作刷新后的值。
2.volatile变量使用的前提
- 对变量的更新操作不依赖当前值(如果能保证一定只有一条线程进行更新操作也可以)
- 该变量不会与其他变量一起纳入不变性条件中:
private volatile int a;
private volatile int b;
public void add(){
if(a+b < 20){ //两个变量一起纳入不变性条件(可以考虑使用不可变对象包装)
a=10;
}
}
- 访问变量时没有锁
很简单,都有锁控制了,还需要volatile干什么
3.对象的发布
-
如果发布一个对象,对象将自己的属性都暴露出去,那么它的属性牵扯的类也跟着被发布了,就会被多线程共享,所以一定要做到良好的封装,不要使不该被发布的对象逸出!!!
-
构造函数导致this逸出(对象在构造过程中,自身的状态还没初始化完成,就被发布出去了)
a) 在构造函数中实例化内部类
//代码3.7
public class ThisEscape{
public ThisEscape (EventSource source){
source.registerListener{
new EventListener(){
public void onEvent(Event e){
// 如果有其他线程在构造函数还没执行完就执行了这里,就会出问题
doSomething(e);
}
}};
}
修复方法
//代码3.8
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;
}
}
为什么上面两段代码有这么大的差别?
在3.7中构造方法 {最后一行注册对象//在此时注册对象实际上已经把this发布出去了}()在3.8中构造方法()注册对象()//此时才把this发布出去区别就在于,你是在构造方法内注册对象,还是在注册方法外注册对象。即使按顺序好像是差不多的。对于3.7。你在构造方法内生成一个对象本身是没问题的,把它赋给另一个对象也是没问题的。麻烦的是你注册的是一个匿名局部类对象,这类对象本身是一定有它的enclosing object的引用的,也就是构造方法中的this。所以this逸出了。如果你这个对象有其他未初始化的状态,而被你注册的对象,其实有其他线程在跑(你当然不知道有没有,不能假设),那就很麻烦很麻烦。对于3.8。此时的函数构造过程完全是线性的:
SafeListener safe=new SafeListener();//这一行跑完,保证safe句柄是一个完整对象
source.registerListener(safe.listener);
b) 在构造函数中启动执行线程
public class Test {
private boolean isIt;
public Test() throws InterruptedException {
new Thread(new Runnable() {
public void run() {
//这里肯定会打印false,因为对象还没有构造完成。
System.out.println(isIt);
}
}).start();
Thread.sleep(2000L);
isIt = true;
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
}
}
c) 调用一个可改写的实例方法也会导致this逸出(没有懂!!!!!!!!!)
4.线程封闭 (局部变量,threadlocal)
5.不可变对象(一定是线程安全的)
不可变对象一旦构造完成,就无法再修改了,无论它在多少个线程中共享,它的内部结构都不会改变!!
tips:理解不可变对象的线程安全性时,不要把用volatile修饰不可变对象实现弱同步等东西联系在一起想,不可变对象的线程安全性是指对象本身的线程安全,不是指对象赋值对象的线程安全。如:
public class Test {
/*
*ImmutableObject为一个不可变对象
*这里object实例被直接发布,当多个线程访问时,object并不是线程安全的(由于 volatile的修饰,可以保证它的可见性而已),
*所谓的线程安全是指new ImmutableObject(1)构造出来这个实例是线程安全的,当reset方法被调用
*时,object的值已经变成了一个全新的另外一个不可变对象。
*所以,“java并发编程实战”书中的分解因数缓存使用volatile+不可变对象实现的缓存是基本没什么作用的,
*他只能缓存最后一次执行因数分解的值和结果对!他能保证两个需要进行的原子操作能通过不可变对象保持一致。
*它并不能保证缓存结果的真正的安全同步。
*/
public volatile ImmutableObject object = new ImmutableObject(1);
public void reset(){
this.object = new ImmutableObject(2);
}
}
- 如何保证一个对象是不可变对象?
a) 他的所有状态一旦构造完成就不能再更改(不提供setter或其他更改的渠道)
b) 他的所有属性都必须使用final修饰(jvm碰到final关键字时,不会在实例化类时为被修饰的属性赋予默认值,而且能保证类的实例对外可用时,他的所有属性都已经初始化完成)
c) 类必须被安全的构造(避免this逸出, 上文已经提到,构造函数里的内部类,启动的线程等都会导致this逸出)
6.安全发布对象的常用模式
<b>安全发布只是保证“发布当时”的!!!状态可见性!!!</b>----这句话特别注意,是理解发布与安全发布的前提!
安全发布对象目的:<b>确保对象对其他线程可见(所有线程能看到对象已经处于发布状态,不然可能会导致 this.n == this.n得到false)</b>,对象的引用和对象的状态必须<b>!!!!同时!!!!</b>对所有线程可见(就是要么其他线程看到该对象为空,要么看到该对象有值,而且他的值已经完全构造完成),所以安全发布的前提是:对象必须被正确的构造(有this逸出的任何对象都难以安全的发布)!
-
静态初始化函数中初始化一个对象的引用,静态块和静态属性都是类初始化阶段执行完成的。
最简单的方式就是静态初始化器,jvm在类初始化阶段执行,且jvm有自己的同步机制;public static Test a = new Test();
-
用AutomicReference或者volatile修饰(他们自身的实现机制就能保证数据的可见性)
-
将对象的引用用锁保护起来(锁也能达到数据可见性嘛)
将对象放入线程安全容器就是满足这条安全发布模式!
- 将对象引用保存在某个正确构造对象的final域中(final由jvm保证安全构造)
7.事实不可变对象
虽然不满足严格不可变对象的定义,但是事实上一般这个对象构造后也不会去修改它。
8.对象可变性决定它需要如何去发布
- 不可变对象,任意方式
- 事实不可变对象, 安全发布模式
- 可变对象,安全发布模式,后续需要以锁等方式保护起来
网友评论