什么是线程安全性?
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,就称这个类是线程安全的。
无状态对象一定是线程安全的。大多数Servelt都是无状态的。
原子性
原子操作避免竞态条件
在没有同步的情况下统计已处理请求数量的Servlet
++count:并不是原子性的,读取-修改-写入
没有同步的情况下,会由于不恰当的执行时序出现不正确的结果:竞态条件
竞态条件
当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。
最常见的竞态条件类型
先检查后执行
解析:首先观察到某个条件(例如文件X不存在)为真,任何根据这个结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之前,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏等)
竞态条件举例
递增命中计数器
延迟初始化
该程序包含了一个竞态条件:两个线程同时执行getInstance,如果时序未知,可能出现两个不同的实例。
原子变量类
AtomicLong
在java.util.concurrent.atomic包中,包含了一些原子变量类,用于实现在数值和对象引用上的原子状态转换。
在实际情况中,应尽可能使用现有的线程安全对象(例如AtomicLong)来管理类的状态
当在无状态的类中添加一个状态时(如count),如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。
加锁机制
内置锁
- java提供了一种内置的锁机制来支持原子性:同步代码块
- 同步代码块包括2部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。
- 以关键字synchronized来修饰的方法就是同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法以Class对象作为锁。
- 每个java对象都可以用作一个实现同步的锁,这些锁称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁。
重入
内置锁是可以重入的,如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。
重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数器为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将计数器置为1。如果同一个线程再次获取这个锁时,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。