1 第一章 简介
2 第二章 线程安全性
本章介绍如何通过同步来避免多个线程在同一时刻访问相同的数据
2.1 什么是线程安全
当多个线程访问某个方法时,不论线程按照什么顺序交替执行,这个方法的最终结果都是正确的,可以说这个方法是线程安全的。
2.2 保证线程安全有哪些方法
- synchronized关键字
- Lock显式锁
- volatile类型变量
- 原子变量
无状态对象一定是线程安全的
2.3 竞态条件
当弍计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。如:先检查后执行
2.4 重入
2.4.1 概念
当某个线程请求其他线程持有的锁时,发出请求的线程就会阻塞。
当某个线程请求一个由自己持有的锁,请求就会成功
获取锁的操作粒度是线程,而不是调用
2.4.2 一种实现方法
每个锁关联一个获取计数值和一个所有者线程,计数值为0意味着锁没有被任何线程持有。
当线程请求一个未被持有的锁时,JVM几下锁的持有者,计数值为1
同线程再次获取这个锁,计数值加一
释放锁时,计数值减一
2.4.3 重入锁样例
public class Widget{
public synchronized void doSomething(){}
}
public class LogWidget extends Widget{
public synchronized void doSomething(){
super.doSomething();
}
}
若锁不可以重入,上面的代码将会产生死锁。super.doSomething()再次请求Widget的锁,而此时该锁已经被线程占用。
2.5 关于性能和简单性
性能和简单性有着相互制约的关系。
当执行时间比较长的计算或操作时(如网络IO和控制台IO)一定不要持有锁
3 第三章 对象的共享
本章介绍如何共享和发布对象,从而使他们能够安全地由多个线程同时访问。
个人理解书中提到的状态是指对象中的变量
3.1 可见性
我们不希望某个线程使用对象状态而另一个线程在同时修改该状态。
我们希望一个线程修改对象状态后其他线程能够看到发生的状态变化。
在JVM中,变量存放在主存中,每个线程拥有自己的线程,当线程对变量操作的时候,先将变量从主存拿到自己的内存中再进行操作,结束后将变量存回主存中。
但在没有控制的情况下,无法判断什么时候才会将结果放回主存中。
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;
}
}
ReaderThread里的函数可能
- 永远循环下去------线程在内存中进行ready=true后没有将其放到主存中(个人猜测 或者就绪状态后没调用?)
- 输出0-----发生了重排序,ready赋值发生在number赋值之前,ready赋值后读线程获取到ready,并输出没有赋值的number
- 输出42
3.1.1 失效数据
public class SynchronizedInteger{
private int value;
public int get(){return value;}
public void set(int value){this.value=value;}
}
- get和set都不用synchronized同步
- 写线程将value赋值后还没放回主存,读线程将value从主存拿到线程内存,读到失效的值(未改前的值)
- 只对get进行synchronized同步
- get和set都同步
(这上面三条解释不确定,都是猜的)
3.1.2 非原子的64位操作
线程未同步的情况下读取变量,可能得到失效值,但这个值至少是之前某个线程设置的值,而不是随机值。这种安全性保证也被成为最低安全性。
非volatile类型的64位数值变量不一定有最低安全性。
JVM中,变量读取和写入都是原子操作。
对于非volatile的long和double变量,JVM允许64位的读或写操作分为两个32位的操作。造成读一个值获得两个值的高低32位。
即使不考虑失效数据的问题,多线程中使用共享可变的long、double等非volatile类型的64位变量是不安全的。
(两个32位一起刷到主存中吗)
(用volatile声明他们,或者用锁保护起来,会避免数据失效问题吗)
3.1.3 加锁与可见性
访问某个共享且可变的变量时要求所有线程在同一个锁上同步。确保写入变量的值对于其他线程都是可见的。
(之前的get和set是在一个锁上吗)
3.1.4 Volatile变量
- volatile,稍弱的同步机制,将变量的更新操作通知到其他线程
(怎么通知)
- 编译器注意到被volatile声明的前两是共享的,不会将该变量上的操作与其他内存操作仪器重排序。
volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方
(这里需要看看其他的书和解释)
- 读取volatile类型变量总会返回最新写入的值
(操作和刷回主存是原子操作)
- 当volatile变量能简化代码实现同步策略的验证时,可以使用。
若验证正确性时需要对可见性进行复杂判断,不要使用volatile变量
(这句没法体会)
- volatile变量的正确使用方式
- 确保自身状态的可见性
- 确保所引用对象状态的可见性
- 表示重要程序生命周期事件的发生
- (没法体会,遇到再说)
- 读取volatile变量的开销只比读取非volatile变量开销略高一些
- 满足下面所有的条件才能使用volatile变量
- 对变量的写入操作不依赖变量的当前值,或确保只有单个线程更新变量的值
- 该变量不会与其他状态变量一起纳入不变性条件中 (没法体会)
- 访问变量时不需要加锁
3.2 发布和逸出
3.2.1 概念
发布一个对象是指:使对象能够在当前作用域之外的代码中使用(引用保存在可访问地,非私有方法返回,引用传递到其他类)
问题:
- 有时需要确保对象及其内部状态不被发布
- 有时需要发布对象后确保线程安全性,可能需要同步
- 发布内部状态可能破坏封装性,例如在对象构造完成前就发布对象,会破坏线程安全性
逸出是指:某个不该发布的对象被发布
3.2.2 造成逸出的一些情况
public static Set<Secret> knownSecrets;
public void initialize(){
knownSecrets = new HashSet<Secret>();
}
将Secret对象添加到集合中,发布上面的整个对象时,集合中的对象也会被发布。
class UnsafeStates{
private String[] states =new String[]{""};
public String[] getStates(){return states;}
}
从非私有方法中返回一个引用,造成该引用的对象发布。
-
发布一个对象时,该对象非私有域中引用的所有对象都会被发布 -
public class ThisEscape{
public ThisEscape(EventSource source){
source.registerListener{
new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
}
}
}
}
发布一个内部的类的实例。当ThisEscape发布EventListener时,也隐含地发布了ThisEscape实例本身。因为这个内部类的实例中包含了对ThisEscape实例的隐含引用。 (为啥?不懂?)
(个人理解,相当于传入引用后,该引用能操控内部构造,相当于类构造的线程和引用所在的线程同时操作构造函数)
- 当且晋档对象的构造函数返回时,对象才处于可预测的和一致的状态。当从对象的构造函数中发布对象时,只是发布了一个尚未完成构造的对象。
不要在构造过程中使用this引用逸出 ?
-
构造过程使this引用逸出的常见错误是 :在构造函数中启动一个线程时,无论显示创建(将它传给构造函数)还是隐式创建(由于Thread或Run那边了是该对象的一个内部类),this引用都会被创建新创建的线程共享。在对象尚未完全构造前,新的线程就可以看到他。 -
构造函数中创建线程最好不要立即启动,通过start或initialize方法启动。 -
构造函数中调用可以改写的实例方法,同样导致this引用在构造过程中逸出
3.2.3 在构造函数中注册事件监听器或启动线程
使用公共的工厂方法,避免不正确的构造过程
public class SafeListener{
private final EventListener listener;
private SafeListener(){
listener=new EventListener(){
public viod onEvent(Event e){
doSomething(e);
}
}
}
public static SafeListener newInstance(EventSource source){
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
3.3 线程封闭
3.3.1 概念
仅在单线程内访问数据就不需要同步,这种技术被称为线程封闭。当对象封闭在一个线程中时,这种方法自动实现线程安全性。
如JDBC的Connection对象。在典型的服务器应用程序中,线程从连接池获取Connection对象,处理请求,再讲对象返回给连接池。由于大多数请求由单线程采用同步方式处理,并且在Connection对象返回之前,连接池不会将它分配给其他线程,因此隐含地将Connection对象封闭在线程中。
3.3.2 Ad-hoc线程封闭
指维护线程封闭性的职责完全由程序实现来承担。
缺点:十分脆弱,没有一种语言特性能将对象封闭到目标线程上。
在volatile变量上存在特殊的线程封闭,只要能确保只有单个线程对共享的volatile变量执行写入操作,就可以安全地在这些共享的volatile变量上执行“读取-修改-写入”的操作。相当于将修改操作封闭在单个线程中以防止发生竞态条件。
(意思是可以有多个线程在这个变量上尽心读取-修改,单个线程进行写入吗?但是不对啊,volatile变量会将修改后的结果立即放到主存中,修改和写入是原子操作吧。可能我之前volatile没太了解)?
3.3.3 栈封闭
更强的线程封闭技术,是线程封闭的一种特例。
在栈封闭中,只能通过局部变量才能访问对象。
局部变量的固有属性是封闭在执行线程中,位于执行线程的栈中,其他线程无法访问。对于基本类型的局部变量,其没有引用,不会破坏栈的封闭性。对于对象的引用,发布对象的引用或者对象内部数据的引用,都会破坏封闭性,导致对象逸出。
好处:比Ad-hoc线程封闭更容易维护,更加健壮。
ThreadLocal类,更加规范的方法,维持线程封闭性
|