活跃度失败:当一个活动进入某种它无法再继续执行的状态时,活跃度失败就发生了。(如死循环、死锁、饥饿、活锁等)
1、线程安全
一个对象是否应该是线程安全的取决于它是否会被多个线程访问。
无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。
在没有正确使用同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患。3种方法修复它:不用跨线程共享变量;使状态变量为不可变的;在任何访问状态变量的时候使用同步。
设计线程安全的类时,优秀的面向对象技术——封装、不可变性以及明确的不变约束——会给你提供诸多的帮助。
线程安全的:当多个线程访问一个类时,如果不用考虑这些线程在运行环境下的调度和交替执行,并且不需要额外的同步以及在调用方式代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
无状态对象永远是线程安全的。无状态:不包含域也没有引用其他类的域。
2、原子性
假设有操作A和B,如果执行A的线程角度看,当其他线程执行B时,要么B全部执行完成,要么一点都没有执行,这样A和B互为原子操作。一个原子操作是指:该操作对于所有操作,包括它自己,都满足前面描述的状态。
利用像AtomicLong这样已有的线程安全对象管理类的状态是非常实用的。相比于非线程安全对象,判断一个线程安全对象的可能状态和状态的转换要容易得多。这简化了维护和验证线程安全性的工作。
3、锁
3.1内部锁:
强制原子性的内置锁机制:synchronized块。一个synchronized块有两部分:锁对象的引用和这个锁保护的代码块。
内部锁在Java中扮演了互斥锁的角色,意味着至多只有一个线程可以拥有锁,当线程A尝试请求一个被线程B占有的锁时,线程A必须等待或者阻塞,直到B释放它。如果B永远不释放锁,A将永远等下去。——导致糟糕的,无法接受的响应性。
synchronized(lock对象){ ……//同步代码块 }
3.2可重进入:
内部锁是可重进入的,因此线程在试图获得它自己占有的锁时,请求会成功。重进入是通过为每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并将请求计数设置为1。如果同一线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数值将递减。直到计数器达到0时,锁被释放。
互斥锁针对于每调用,可重进入针对于每线程。
public class Widget{ public synchronized void doSomething(){ … } } public class LoggingWidget extends Widget{ public synchronized void doSomething(){ System.out.println(toString()+":calling doSomething"); super.doSomething(); } }
4、用锁来保护状态
操作共享状态的复合操作必须是原子的,以避免竞争条件,比如递增中计数器(读-改-写)或者惰性初始化(检查再运行)。
对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。
每个共享的可变变量都需要由唯一一个确定的锁保护。而维护者应该清楚这个锁。
对于每一个涉及多个变量的不变约束,需要同一个锁保护其所有的变量。
5、活跃度与性能
应尽量从synchronized块中分离耗时的且不影响共享状态的操作。这样即使在耗时操作的执行过程中,也不会阻止其他线程访问共享状态。
通常简单性与性能之间是相互牵制的。实现一个同步策略时,不要过早的为了性能而牺牲简单性(这是对安全性潜在的妥协)。
有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成。执行这些操作期间不要占有锁。