如果两个线程存取相同的对象,并且每个线程都调用了一个修改该对象状态的方法,根据各线程访问数据的次序,可能会产生讹误的对象。这样一个情况通常称为竞争条件。
14.5.3 锁对象
有两种机制防止代码块受并发访问的干扰。
- synchronized关键字;
- ReentrantLock类;
ReentrantLock保护代码块的基本结构如下:
private Lock myLock = new ReentrantLock(); myLock.lock(); try{ }finally{ myLock.unlock(); }
把解锁操作放在finally子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其它线程将永远被阻塞。
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数( hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都会调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
14.5.4 条件对象
线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
private Condition sufficientFunds; sufficientFunds = bankLock.newCondition(); sufficientFunds.await(); //当前线程现在被阻塞了,并放弃了锁。
等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦一个线程调用await方法,它进人该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法时为止。
当另一个线程转账时,它应该调用sufficientFunds.signalAll();
14.5.8 Volatile域
多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。
volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另外一个线程并发更新的。
volatile变量不能提供原子性。
14.5.9 final变量
还有一种情况可以安全地访问一个共享域,即这个域声明为final时。
14.5.10 原子性
假设对共享变量除了赋值之外并不完成其它操作,那么可以将这些共享变量声明为volatile。
AtomicInteger类提供了incrementAndGet和decrementAndGet将一个整数自增、自减。
大量线程访问相同的原子值,使用LongAddr和LongAccumulator。
14.5.12 线程局部变量
避免使用共享变量。
ThreadLocal
public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd")); String dateStamp = dateFormat.get().format(new Date());
14.5.14读/写锁
如果很多线程从一个数据结构读取数据儿很少线程修改其中数据的话,后者是十分有用的。
读/写锁的必要步骤:
- 构造一个ReentrantReadWriteLock对象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
- 抽取读锁和写锁:
private Lock readLock = rwl.readLock(); private Lock writeLock = rwl.writeLock();
- 对所有的获取(get)方法加读取锁:
public double getBalance(){ readLock.lock(); try { }finally { readLock.unlock(); } return 0; }
- 对所有的修改方法加写锁:
public void setBalance(){ writeLock.lock(); try { }finally { writeLock.unlock(); } }
14.5.15为什么弃用stop和suspend方法
初始的Java版本定义了一个stop方法用来终止一个线程, 以及一个suspend方法用来阻塞一个线程直至另一个线程调用resume。stop 和suspend方法有一些共同点: 都试图控制一个给定线程的行为。
stop、suspend和resume方法已经弃用。stop 方法天生就不安全,经验证明suspend方法会经常导致死锁。
首先来看看stop方法,该方法终止所有未结束的方法,包括run方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。例如,假定TransferThread在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转入目标账户,现在银行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。
当线程要终止另一个线程时,无法知道什么时候调用stop方法是安全的,什么时候导致对象被破坏。因此,该方法被弃用了。在希望停止线程的时候应该中断线程,被中断的线程会在安全的时候停止。
接下来,看看suspend方法有什么问题。与stop不同,suspend 不会破坏对象。但是,如果用suspend挂起一个持有-个锁的线程,那么,该锁在恢复之前是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。
在图形用户界面中经常出现这种情况。假定我们有一个图形化的银行模拟程序。Pause按钮用来挂起转账线程,而Rsume按钮用来恢复线程。