引入
对象的状态: 是指存储在状态变量(例如实例或静态域)中的数据
共享意味着变量可被多个线程访问
一个对象是否需要是线程安全的,取决于会不会被多个线程访问
什么是线程安全性?
一个类当被多个线程访问时,还能表现出正确的行为。
1:有状态对象与无状态对象
有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。其实就是有数据成员的对象。特别的是如果操作中有修改其他对象的存储变量的话也会时有状态的。
无状态就是一次操作,不能保存数据,也不会修改数据。无状态对象(Stateless Bean),就是没有实例变量的对象。不能保存数据,是不变类,是线程安全的。具体来说就是只有方法没有数据成员的对象,或者有数据成员但是数据成员是可读的对象。
无状态类一定是线程安全的。
2:竞态条件
由于不正确的执行时序而产生不正确的结果叫做竞态条件。
最常见的竞态条件是:先检查后执行。
- 就是说你按照当前检查的结果去执行某一项操作,但是执行操作之前,这个结果已经被其它线程修改了,先前的观察结果变得无效,那么你的执行结果也不会正确。
- 基于可能失效的观察结果来做出判断或者执行某个计算
读取-修改-写入也是竞态条件
但是是不是只要有竞态条件就一定会发生错误呢。当然不是,还需要一个不恰当的执行时序才会发生。所以还是看运气。
如果能把有竞态条件的代码以原子方式执行,就不会有这些情况和顾虑发生了。
3:复合操作
复合操作包含了一组必须以原子方式执行的操作来确保线程的安全性。
有竞态条件的一组操作就是复合操作,应该按照原子方式执行。
4:加锁机制
有多个状态变量的时候,并不是只要单个状态变量保持原子性就够了,需要做到能够在单个原子操作中跟新所有相关的状态变量。
用Java提供的锁机制可以很好的支持原子性操作。也就是同步代码块
4.1:同步代码块
两部分:
- 一个是锁的对象引用(内置锁)
- 另一个是锁保护的代码块
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。
以synchronized修饰的方法是横跨整个方法体的同步代码块,锁就是调用该方法的对象。如果是静态方法那么锁是该类的Class对象。
synchronized(lock){
}
线程在进入同步代码块之前会获得锁,离开同步代码块会释放锁,而且只有一个线程可以获得这个锁,如果A线程获得了锁,那么B线程就不能获得,B线程也就暂时不能执行同步代码块,只有等A线程执行完成,释放锁之后,B线程如果有机会获得了锁,才能执行同步代码块中的代码。
也就是说,当一个线程在执行同步代码块时,其他线程时没有机会进入该同步代码块的,同步代码块内操作的原子性因此得到了保证。
Java的内置锁是一种互斥锁,互斥锁就是意味着最多只有一个线程能够获得该锁。
4.2:重入
重入就是一个线程试图获取一个自己已经持有的锁,那么这个请求会成功。
重入的一种实现方法:锁设置一个计数器和所有者线程,如果计数器为0标识没有线程持有该锁。如果一个线程获得该锁后,计数器加一,并且将所有者线程设置为当前线程。如果这个线程还要重入,计数器再加一。当线程退出同步代码块时,计数器就相应的递减。当计数器为0时,这个锁就被释放了。
重入很重要,尤其是在继承递归当中
public class A{
public synchronized void DoSomething(){
......
}
}
public class B{
public synchronized void DoSomething(){
super.DoSomething();
}
}
在B的dosomething方法中,如果没有重入的机制,那么就会发生死锁,线程将永远停顿下去。
4.3用锁来保护对象
- 多个线程共同读取的可变变量也需要加锁。
- 不是一个类的每个方法都加锁就安全了,多个方法之前的复合操作可能会发生竞态条件。