1.线程安全的定义
多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程如何交替运行,并且在主掉代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全的。
2.如何编写线程安全的代码
要编写线程安全的代码,其核心在于对状态访问操作进行管理,特别是对共享的(shared)和可变的(mutable)状态的访问。共享意味着可以由多个线程同时访问,可变意味着它的值在生命周期内可以发生变化。
一个对象是否需要时线程安全的,取决于他是否被多个线程访问,要使得他是线程安全的,需要采用同步机制来协同对对象可变状态的访问。Java中主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,当然还有volatile等。
1.原子性
原子操作是指:对于访问同一个状态的所有操作来说,这个操作是一个以原子方式执行的的操作。假设有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。
以++count
为栗子。++count
看起来只是一个操作,其实他包含了3个独立的操作,它并不会作为一个不可分割的操作来执行。包步骤括:读取count的值,将值+1,然后将计算结果写入count;即:读取–>修改–>写入,它的操作结果依赖于之前的状态。
因此因此有可能出现:线程A和线程B同时读取了count的值(假设初始值为0),并且都做+1操作,最后的结果都是返回1.两个线程操作一个数返回同一个结果,而不是返回2,不符合预期的正确行为。
2.加锁机制
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
Java提供了一种内置的锁机制来支持原子性:同步代码块(synchronized block)。同步代码块包括两部分:一个对位锁的对象引用,一个作为这个锁保护的代码块。其中同步代码块的锁就是方法调用所在的对象。代码如下:
synchronized(lock){
//访问或修改由锁保护的共享状态
}
Java的每个对象都可以用作一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程进入同步代码块之前获得锁,并且在退出同步代码块时自动释放锁(这个退出包括正常退出和异常退出)。
Java的内置锁是一种互斥锁,最多只有一个线程能持有这种锁。即如果A线程线想获取B线程持有的锁,A就必须等待或者阻塞,直到B释放。
内置锁是可以重入的,如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。好处:避免死锁。比如,子类改写了父类的synchronized方法,然后调用父类中的方法,如果没有重入的锁那么将会产生死锁。
3.tips
- 如果多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有3中方式可以修复:
- 不在线程间共享该状态变量;
- 将该状态变量修改为不可变的变量;
- 在访问状态变量时使用同步。
- 无状态对象一定是线程安全的;
- 当获取与对象关联的锁时,并不能阻止其它线程访问该对象(该对象又没被同步代码块包住),某个线程获得对象的锁后,只能阻止其它线程获得同一个锁。
- 当执行时间比较成的计算或者可能无法快速完成的操作时,一定不要持有锁。