线程同步机制
从广义上说,Java平台提供的线程同步机制包括锁、volatile关键字、final关键字和一些相关的API,如Object.wait( )/.notify( )等
锁
定义:锁具有排他性,即一个锁一次只能被一个线程持有。因此,这种锁被称为排他锁或者互斥锁。还有另外一种锁--读写锁,它可以被看作排他锁的一种相对改进。
作用:锁能够保护共享数据以实现线程安全,其作用包括保障原子性,保障可见性和保障有序性。
临界区:锁的持有线程在其获得锁之后和释放锁之前的这段时间内所执行的代码被称为临界区。
在java平台中,锁的获得隐含着刷新处理器缓存这个动作,这使得读线程在执行临界区代码前(获得锁之后)可以将写线程对共享变量所做的更新同步到该线程执行处理器的高速缓存中;而锁的释放隐含着冲刷处理器缓存这个动作,这使得写线程对共享变量所做的更新能够被“推送”到该线程执行处理器的高速缓存中,从而对读线程可同步。
锁对可见性、原子性和有序性的保障是有条件的,需要保证以下两点得以满足:
1. 这些线程在访问同一组共享数据的时候必须使用同一个锁。
2. 这些线程中的任意一个线程,即使其仅仅是读取这组共享数据而没有对其进行更新的话,也需要在读取时持有相应的锁。
调度的根因在于资源的排他性,及资源有限。
所有的调度策略中都应考虑公平策略和非公平策略。
内部锁:
内部锁属于非公平锁,而显式锁即支持公平锁又支持非公平锁。
java平台中的任何一个对象都有唯一一个与之关联的锁。这种锁被称为监视器或者内部锁。内部锁是一种排他锁。
java虚拟机对锁的实现,通过synchronized关键字实现。
作为锁句柄的变量通常采用final修饰,这是因为锁句柄变量的值一旦改变,会导致执行同一个同步块的多个线程实际上使用不同的锁,从而导致竞态。所以,通常我们会使用private修饰作为句柄的变量。
内部锁的调度:
- JVM会给每个内部锁分配一个入口集(Entry Set),用于记录等待获得相应内部锁的线程。
- 入口集中的线程被称为内部锁的等待线程
- 调度:当锁被持有的线程释放的时候,该锁的入口集中的任意一个线程将会被唤醒,从而得到再次(上一次申请失败,才会出现在入口集中)申请锁的机会;被唤醒的线程等待占用处理器运行时可能还有其他新的活跃线程与该线程抢占这个被释放的锁;即这是一种非公平的内部锁调度策略。
显式锁:
- 显式锁提供了一些内部锁不具备的特性,但并不是内部锁的替代品。
- java中通过java.util.concurrent.locks.Lock接口的实现类实现
- 公平锁保障锁调度的公平性往往是以增加了线程的暂停和唤醒的可能性,即增加了上下文切换为代价的。因此,公平锁适合于锁被持有的时间相对长或者线程申请锁的平均间隔时间相对长的情形。
读写锁:
- 允许多个线程可以同时读取共享变量,但是一次只允许一个线程对共享变量进行更新
- 适用场景:
- 只读操作比写操作要频繁的多
- 读线程持有锁的时间比较长
锁的适用场景
- check-then-act操作
- read-modify-writ