Java中的锁
当数据竞争存在的时候,最简单的解决办法就是加锁。锁机制限制在同一时间只允许一个线程访问产生竞争的数据的临界区。Java语言中的 synchronized关键字可以为一个代码块或是方法进行加锁。任何Java对象都有一个自己的监视器,可以进行加锁和解锁操作。当受到 synchronized关键字保护的代码块或方法被执行的时候,就说明当前线程已经成功的获取了对象的监视器上的锁。当代码块或是方法正常执行完成或是发生异常退出的时候,当前线程所获取的锁会被自动释放。一个线程可以在一个Java对象上加多次锁。同时JVM保证了在获取锁之前和释放锁之后,变量的值是与主存中的内容同步的。
Java线程的同步
在有些情况下,仅依靠线程之间对数据的互斥访问是不够的。有些线程之间存在协作关系,需要按照一定的协议来协同完成某项任务,比如典型的生产者-消费者模式。这种情况下就需要用到Java提供的线程之间的等待-通知机制。当线程所要求的条件不满足时,就进入等待状态;而另外的线程则负责在合适的时机发出通知来唤醒等待中的线程。Java中的java.lang.Object类中的wait/notify/notifyAll方法组就是完成线程之间的同步的。
在某个Java对象上面调用wait方法的时候,首先要检查当前线程是否获取到了这个对象上的锁。如果没有的话,就会直接抛出java.lang.IllegalMonitorStateException异常。如果有锁的话,就把当前线程添加到对象的等待集合中,并释放其所拥有的锁。当前线程被阻塞,无法继续执行,直到被从对象的等待集合中移除。引起某个线程从对象的等待集合中移除的原因有很多:对象上的notify方法被调用时,该线程被选中;对象上的notifyAll方法被调用;线程被中断;对于有超时限制的wait操作,当超过时间限制时;JVM内部实现在非正常情况下的操作。
wait/notify/notifyAll 的时候,当前线程已经获得了所需要的锁。当对于某个对象的等待集合中的线程数目没有把握的时候,最好使用 notifyAll而不是notify。notifyAll虽然会导致线程在没有必要的情况下被唤醒而产生性能影响,但是在使用上更加简单一些。由于线程可能在非正常情况下被意外唤醒,一般需要把wait操作放在一个循环中,并检查所要求的逻辑条件是否满足。典型的使用模式如下所示:
private Object lock = new Object();
synchronized (lock) {
while (/* 逻辑条件不满足的时候 */) {
try {
lock.wait();
} catch (InterruptedException e) {}
}
//处理逻辑
}
上述代码中使用了一个私有对象lock来作为加锁的对象,其好处是可以避免其它代码错误的使用这个对象。
Monitor 详解
Java 使用名为监视器(Monitor)的同步机制来调节多个线程的活动和共享数据,如图 4 所示。Java 中,每个对象都对应一个监视器,即使在多线程的情况下,该监视器监视的区域同一时间只会被一个线程执行。一个线程想要执行监视区域的代码,唯一的途径就是获得该区域对应的监视器。当一个线程到达了一个监视区域的开始处(Entry Point),它就会被置入该监视器的请求集(Entry Pool)。如果此时没有其他线程在请求集中等待,并且也没有其它线程正持有该监视器(Monitor Hold),那么该线程就可以获得监视器,并继续执行监视区域中的代码。当这个线程执行完监视区域代码后,它就会退出并释放该监视器(Release Monitor)。如果线程在执行监视区域代码的过程中,执行了 wait() 方法,那么该线程会暂时释放该监视器,并被置入等待集(Wait Pool),等待被唤醒;如果线程在这个过程中执行了 notify() 方法,那么在等待集中的某个线程就会被标记为即将苏醒,这个即将苏醒的线程将在某个监视器空闲的时刻获取该监视器,并执行该监视区域代码,具体哪个线程被唤醒视虚拟机的实现方式而定。notifyAll() 方法会将等待集中的所有线程标记为即将苏醒,并接受虚拟机的调度。一个线程只有在它已经持有监视器时才能执行 wait() 方法,并且它只能通过再次成为监视器的持有者才能离开等待集。
根据上述对 Java 线程同步机制的分析,如果能够在执行 wait()/notify()/notifyAll() 方法的前后分别获取其监视器等待集的状态快照并加以比较,就可以得出是哪个或者是哪些线程被唤醒,而这些线程就是这次隐式线程切换的被操作线程。这样,就可以确定线程切换的三元组 < 操作线程,动作,被操作线程 > 信息。
此外,由于一个线程可能同时拥有多个对象监视器,因此必须对每个监视器上的等待集进行分析,以确定当前执行的 wait()/notify()/notifyAll() 方法所真正作用的监视器。
中断线程
通过一个线程对象的interrupt()方法可以向该线程发出一个中断请求。中断请求是一种线程之间的协作方式。当线程A通过调用线程B的interrupt()方法来发出中断请求的时候,线程A 是在请求线程B的注意。线程B应该在方便的时候来处理这个中断请求,当然这不是必须的。当中断发生的时候,线程对象中会有一个标记来记录当前的中断状态。通过isInterrupted()方法可以判断是否有中断请求发生。如果当中断请求发生的时候,线程正处于阻塞状态,那么这个中断请求会导致该线程退出阻塞状态。可能造成线程处于阻塞状态的情况有:当线程通过调用wait()方法进入一个对象的等待集合中,或是通过sleep()方法来暂时休眠,或是通过join()方法来等待另外一个线程完成的时候。在线程阻塞的情况下,当中断发生的时候,会抛出java.lang.InterruptedException,代码会进入相应的异常处理逻辑之中。实际上在调用wait/sleep/join方法的时候,是必须捕获这个异常的。中断一个正在某个对象的等待集合中的线程,会使得这个线程从等待集合中被移除,使得它可以在再次获得锁之后,继续执行java.lang.InterruptedException异常的处理逻辑。
通过中断线程可以实现可取消的任务。在任务的执行过程中可以定期检查当前线程的中断标记,如果线程收到了中断请求,那么就可以终止这个任务的执行。当遇到 java.lang.InterruptedException的异常,不要捕获了之后不做任何处理。如果不想在这个层次上处理这个异常,就把异常重新抛出。当一个在阻塞状态的线程被中断并且抛出java.lang.InterruptedException异常的时候,其对象中的中断状态标记会被清空。如果捕获了java.lang.InterruptedException异常但是又不能重新抛出的话,需要通过再次调用interrupt()方法来重新设置这个标记。