线程安全
- 要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享和可变状态的访问。
- 从非正式的意义来讲,对象的状态是指存储在状态变量(例如实例或静态域)中的数据,可能包含其它依赖对象的域。
- 一个对象是否需要实现线程安全,取决于它是否会被多个线程访问。要使得对象是线程安全的,需要采取同步机制来协同对对象可变状态的访问。
Java同步机制:关键字synchronized、volatile类型的变量、显式锁(Lock)、原子变量。
无状态的对象一定是线程安全的。
原子性
竞态条件(Race Condition):计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。例如“读取-修改-写入”操作和“先检查后执行”操作。
- “读取-修改-写入”操作:最经典的就是自增操作。例如count++操作,该操作是非原子性的,实际上它包含三个操作:读取count的值,将值加一,将计算出的结果写入count。如果此时多个线程都访问count并++,那么不能保证最后结果正确。
- “先检查后执行”操作:经典的例子就是单例模式。
复合操作:要避免竞态条件问题就要保证在某个线程修改变量时,通过某种方式阻止其他线程使用该变量。“读取-修改-写入”操作和“先检查后执行”操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。
加锁机制是Java中用于确保原子性的内置机制。
内置锁Synchronized
同步代码块(Synchronized Block)。内置锁可以支持原子性和可见性。同步代码块包含两部分:
- 一个作为锁的对象引用;
- 一个作为由这个锁保护的代码块;
其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以class对象作为锁。
Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能够持有这种锁。
重入:
当某个线程请求一个其他线程持有的锁时,就会阻塞。因为内置锁是可重入的,如果某个线程试图获得一个已经被自己占有的锁,就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用“。
重入的一种实现方法是为每一个锁设置一个计数器,同一个线程再次获取这个锁,计数值加一,而当线程退出同步代码块时,计数值减一。
如果内置锁是不可重入的,下面的代码将会死锁:
public class Widget{public synchronized void doSomeThing(){ ... }
}public class LoggingWidget extends Widget{public synchronized void doSomeThing(){//如果是非重入的锁,获取Widget上的锁时就会发生死锁super.doSomeThing();...}
}
锁的使用规则:
- 对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁。这种情况也可以成为该变量是由这个锁保护的。
- 每个共享的和可变的状态变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
- 对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。
注意:
对象的内置锁和对象的状态之间没有内在的关联。当获取对象关联的锁时,并不能阻止其他线程访问该对象,只能阻止其他线程获取同一个锁。
可以使用@GuardBy标签标注使用的是哪一个锁。
对象锁和类锁
synchronized关键字修饰不同的位置含义不同:
- 修饰一个类:作用的对象是这个类的所有对象,即它是一个类锁
- 修饰一个方法:被修饰的方法称为同步方法,作用的对象是调用这个方法的对象,它是对象锁
- 修改一个静态的方法:作用的对象是这个类的所有对象,它是一个类锁
- 修饰一个代码块:其作用的范围是大括号{}括起来的代码, 作用的对象是调用这个代码块的对象,是一个对象锁
对象锁和类锁是不同的锁,多个线程同时执行这2个不同锁的方法时,是异步的。
下面用伪代码简单的说明一下类锁和对象锁的区别:
public class Test{public synchronized static methodA{}public synchronized static methodB{}public synchronized methodC{}
}
类Test中有两个类锁,一个对象锁。如果现在我们创建三个线程thread1、thread2、thread3,分别执行对象test的methodA、methodB、method3,那么结果可能是什么?
最后执行的顺序可能是:
线程A开始
线程C开始
线程C结束
线程A结束
线程B开始
线程B结束
即AB肯定是顺序(同步)执行,C和AB是异步执行。