Java中线程同步机制包括锁、volatile
关键字、final
关键字、static
关键字以及一些API,比如Object.wait()
Object.notify()
.
锁
线程访问共享数据前必须获得锁,一旦获得了锁,就是这个锁的持有线程,一个锁只能被一个线程所有,这种锁叫做排他锁或者互斥锁。访问结束后线程必须释放锁。获得锁到释放锁直接执行的代码叫临界区。
所以临界区在任何时间只能被一个线程执行。
Java中的锁包括内部锁和显式锁。
内部锁:synchronized
关键字
任何一个对象都有唯一一个与之关联的锁,称为监视器或者内部锁。这是一种排他锁,可以保证原子性、可见性、有限性。
内部锁通过synchronized
关键字实现,可以用来修饰方法或者代码块。
修饰方法
例如:
|
|
被synchronized
修饰的方法叫做同步方法,该方法确保任意时间该方法只能被一个线程执行。
修饰代码块
|
|
其中this表示锁句柄,也叫做锁,表示这个代码段以当前对象作为引导锁。上面同步方法中的synchronized
没有声明锁句柄,因为同步方法总是以当前对象为引导锁。
如果是同步静态方法,则以当前类的Class对象为锁句柄。如InnerLock.class
。
因为在使用synchronized
关键字时,获得锁、释放锁由JVM代为执行,所以叫做内部锁,这种方法实现线程同步语法简单,易于实现。
内部锁的调度
JVM会给每一个内部锁分配一个入口集Set,用于记录等待获得相应内部锁的线程。
当多个线程申请该内部锁时,只有一个线程能够获得该锁,其余线程会被置为BLOCKED状态且放入这个Set中再次等待获得锁的机会。
当之前获得锁的线程完成了数据访问,释放了锁之后,JVM会从Set中选取一个线程唤醒,选择算法时不确定的,因此开发的时候不能依赖选择算法。
显式锁:Lock
接口
显式锁是另一种排他锁,作用与内部锁相同,但有一些自己的特性。
|
|
ReentrantLock
类是Lock
接口的默认实现类,使用步骤:
- 创建一个Lock接口实例
- 申请lock
- 数据访问
- 释放锁
为了保证锁一定在最后能被释放(考虑临界区的代码出现异常导致线程死亡),临界区的代码最好放在try
中,然后释放锁的代码放在finally
中,这样就能确保一定能够释放锁而不会造成锁泄漏。
显式锁的调度
ReentrantLock
及支持非公平锁也支持公平锁,默认是非公平锁,如果需要公平锁,使用构造器ReentrantLock(boolean)
总的来说使用公平锁的开销比非公平锁大。
内部锁和显示锁的比较
内部锁的优点:
- 简单易用
- 不会造成锁泄漏
内部锁的缺点:
- 不够灵活。获得锁,释放锁只能在同方法或者同代码段。
显式锁基于对象,可以充分发挥OOP的优势,灵活,但是难度也大,容易造成锁泄漏。
改进型锁:读写锁java.util.concurrent.locks.ReadWriteLock
上面讲的锁对读写都是排他的,但是线程安全问题往往是由写操作引起的,所以如果读操作是共享的,那么能够大大提升系统的并发性。
读写锁就是读锁+写锁。
读写锁允许多个线程同时读取共享变量,但是只能同时允许最多一个线程更新共享变量。
任何线程读的时候,其他线程不能写;一个线程写的时候,其他线程不能读。ReentrantReadWriteLock
是读写锁接口的实现,其中包含两个内部类ReadLock``WriteLock
分别表示读锁和写锁,都实现了Lock
接口。
锁使用的场景
- check-then-act
- read-modify-write
- 多个线程对多个共享数据进行更新
CAS
使用锁来保障原子性的开销有点大,volatile
虽然开销小但是不能保证自增这种操作(读+写)的原子性。
CAS原理就是在更新一个变量之前,先检查这个变量的旧值有没有被其他线程修改过,如果没有被修改,那么会将旧值改为新值;如果被修改过了,那么更新失败。
如果要用CAS实现线程同步的话,比如自增操作的同步,利用上面的规则保证了数据的更改不会丢失,然后要做的是,在更新失败后,重新取值、计算、更新,直至更新成功,这就是CAS的基本算法模板。
原子变量类 Atomics
分组 | 类 |
---|---|
基础数据型 | AtomicInteger AtomicLong AtomicBoolean |
数组型 | AtomicIntegerArray AtomicLongArray AtomicReferenceArray |
字段更新器 | AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicReferenceFieldUpdater |
引用型 | AtomicReference AtomicStampedReference AtomicMarkableReference |
以上类可以被看作是增强型的volatile
变量,volatile
只能保证单次独写的原子性,但是不能保证如自增自减操作的原子性。在AtomicInteger
类中,可以通过incrementAndGet
方法实现自增。
在没有使用原子变量类之前,开1000个线程会产生读写冲突,最终nums
并没有加到1000:
改用AtomicInteger
,可以实现自增的原子操作: