编写线程安全的代码的核心在于,对对象状态访问的控制与管理,特别对共享的、可变的状态。
一般地讲,一个对象的状态就是它所包含的数据,存储在状态变量中,比如实例域或静态域。一个对象的状态可能还来自于它所依赖的其他对象,比如HashMap的状态一部分是存储在自己的对象空间之中的,但另一部分存储在许多的Map.Entry对象之间。所以一个对象的状态是指那些可被外界访问的方法所影响(改变)的数据。
我们讨论的线程安全性好像是关于代码的,但是我们真正要做的,是在不可控制的并发访问中如何保护共享数据。
一个对象是否应该是线程安全的,这取决于它是否会被多个线程访问。
Java中首要的同步机制是synchronized关键字,它提供了独占锁。除此之外,还有volatile变量、显示锁(Lock)、原子变量的使用。
在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患,有3种方法来修复它:
1、不要跨线程共享变量;
2、如果确实要共享,则使状态变量不可变;或
3、只能在任何访问状态变量的时候使用同步。
设计线程安全的类时,有三种技术很好的做到安全:封装(即尽量降低域的可访问能力)、不可变、明确的规范。
无状态的对象永远是线程安全的。
Servlet是多线程共享的,所以我们在设计Servlet不要设计状态,否则要确保这些共享的状态域的线程安全。
i++,自增操作不是原子性的,它包括三个动作:获取当前值,加1,写回新值。
Atomic类型的变量只能保证变量本身一条语句的原子性,不能这个变量多条语句一起执行的原子性。
synchronized方法虽然解决了线程安全性问题,但同时可能带来线程性能上的问题。
synchronized所获取的锁与ReentrantLock锁都是可重入锁,重入锁的实现是通过为每个锁关联一个请求计数和一个占有它的线程。当计数为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM将记录锁的占有者,并具将请求计数器置为1。如果同一线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减,直到计数器达到0,锁才被释放。
即然同步可以避免竞争条件,为什么不将每个方法都声明为synchronized类型?如同Vector一样,仅仅同步它每个方法,并不足以确保在Vector上执行的复合操作是原子的:
if(!vector.contains(element))
vecotr.add(element);
虽然同步方法确保了不可分割操作的原子性,但是把多个操作整合到一个复合操作时,还是需要额外的锁。
锁定整个方法会导致弱并发的问题,并发性能会大大下降,幸运的是,我们可能通过缩小synchronized块的范围来维护线程安全性,很容易提升并发性。但你就应该谨慎地控制synchronized块不要过小,因你你不可以将一个原子操作分解到多个synchronized块中。不过你应该尽量从synchronized块中分离耗时的且不影响共享状态的操作。这样即使在耗时操作的执行过程中,也不会阻止其他线程访问共享状态。
请求与释放锁的操作需要开销,所以将synchronized埠分解得过于琐碎是不合理的。在分成多个同步块时要将耗时但又不影共享变量的操作放在同步块外调用,特别是要注意I/O与线程阻塞方法的调用,一般不要将其放在块里调用。