java.util.concurrent.locks 包下的常用类
interface Lock
1 | public interface Lock { |
获取锁
- lock()
- tryLock()
- tryLock(long time, TimeUnit unit)
- lockInterruptibly()
释放锁
- unlock()
lock() 获取锁最常用的一个方法,如果获取的锁已被其它线程获取,则进行等待
。采用 Lock,必须主动释放锁,并且发生异常时,不会自动释放锁,因此 Lock 必须在 try catch 块中进行,并且释放锁的操作放在 finally 块中进行
,以保证锁一定被释放,防止死锁的发生。
1 | Lock lock = ...; |
tryLock() 方法是有返回值的
,表示尝试获取锁,如果获取成功,返回 true,如果获取失败(即锁已被其它线程获取),则返回 false,这个方法无论如何都会立即返回,在拿不到锁时不会一直等待
。
tryLock(long time, TimeUnit unit) 和 tryLock() 类似,也有返回值,区别在于这个方法拿不到锁时会等待一定时间
,在这个时间内如果还拿不到,则返回 false,如果一开始或者等待期间拿到了锁,就返回 true。
1 | Lock lock = ...; |
lockInterruptibly() 方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断
。比如:当两个线程同时通过 lockInterruptibly() 获取某个锁时,假如线程 A 获取到了锁,而线程 B 只能等待,那么对线程 B 调用 Thread.interrupt() 方法能够中断线程 B 的等待过程。
由于 lockInterruptibly() 的声明中抛出了异常,所以 lockInterruptibly() 必须放在 try catch 块中或者在调用 lockInterruptibly() 的方法外声明抛出 InterruptedException。
1 | public void method throws InterruptedException { |
tips: 当一个线程获取了锁之后,是不会被 interrupt() 方法中断的,单独调用 interrupt() 只能中断阻塞过程中的线程,不能中断正在运行过程中的线程。
因此通过 lockInterruptibly() 方法获取某个锁时,如果获取不到,只能等待,在进行等待的情况下,是可以响应中断的。
用 synchronized 修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待。
class ReentrantLock
ReentrantLock 可重入锁,它是唯一实现了 Lock 接口的类,并且提供了更多的方法。
lock()
1 | public class LockTest { |
tryLock()
1 | public class TryLockTest { |
lockInterruptibly()
1 | class MyThread extends Thread { |
interface ReadWriteLock
1 | public interface Lock { |
ReadWriteLock 接口定义了两个方法,一个来获取读锁,一个来获取写锁,也就是说将文件的读写操作分开,分成两个锁来分配给线程,从而使得多个线程可以同时进行读操作
。
class ReentrantReadWriteLock
ReentrantReadWriteLock 提供了丰富的方法,最主要的方法有两个,用来获取读锁和写锁。
- readLock()
- writeLock()
多个线程同时读操作,synchronized 的效果
1 | public class ReadReadTest { |
多个线程同时读操作,ReentrantReadWriteLock 的效果
1 | public class ReentrantReadWriteLockTest { |
注意:
- 如果一个线程占用了读锁,其它线程申请写锁,申请线程会一直等待释放读锁。
- 如果一个线程占用了写锁,其它线程申请读锁或写锁,申请线程会一直等待释放写锁。
Lock 和 synchronized 的选择
- Lock 是一个接口,而 synchronized 是 Java 中的关键字,是内置的语言实现。
- synchronized 在发生异常时,会自动释放线程占有的锁,因此不会发生死锁现象;而 Lock 在发生异常时,如果没有主动通过 unlock() 释放锁,则很可能发生死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
- Lock 可以让等待锁的线程响应中断 lockInterruptibly(),而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
- 通过 Lock() 可以知道有没有获取到锁,synchronized 不行。
- Lock 可以提高多个线程进行读操作的效率。
在性能来说,如果竞争资源不激烈,二者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同步竞争),此时 Lock 的性能要远远由于 synchronized,所以说,在具体使用时要根据适当情况选择。
锁的相关概念介绍
乐观锁 vs 悲观锁
乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。
悲观锁: 对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
在 Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁
。
乐观锁: 认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
在 Java 中, 乐观锁是通过使用无锁编程来实现,最常采用的是 CAS 算法(Compare And Swap),Java 原子类中的递增操作就通过 CAS 自旋实现的
。
自旋锁 vs 适应性自旋锁
阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程 “稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁
。
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是 CAS
,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启
,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
无锁 vs 偏向锁 vs 轻量级锁 vs 重量级锁
这四种锁是指锁的状态,专门针对 synchronized 的。
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
上面我们介绍的 CAS 原理及应用即是无锁的实现,无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁,其目标就是在只有一个线程执行同步代码块时能够提高性能。
偏向锁在 JDK 6 及以后的 JVM 里是默认启用的
。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
轻量级锁
指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
若当前只有一个等待线程,则该线程通过自旋进行等待,但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
重量级锁
升级为重量级锁时,锁标志的状态值变为 “10”,此时 Mark Word 中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
可重入锁 vs 非可重入锁
如果锁具备可重入性,则称作为可重入锁,sychronized 和 ReentrantLock 都是可重入锁
。
可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
举个栗子,当一个线程执行到某个 synchronized 方法时,比如 method_1,而 method_1 中会调用另一个 synchronized 方法 method_2,此时线程不必重新去申请锁,而是可以直接执行方法 method_2。
1 | class MyClass { |
上面类的两个方法都被 synchronized 修饰了,假如某一时刻,线程 A 执行到了 method_1,此时线程获取了这个对象的锁,而由于 method_2 也是 synchronized 方法,假如 synchronized 不具备可重入性,此时线程 A 需要重新申请锁。但是这就会造成一个问题,因为线程 A 已经持有了该对象的锁,现在又要获取该对象的锁,这样线程 A 就会一直等待永远获取不到锁。
由于 synchronized 和 Lock 都具备可重入性,所以不会发生上述现象。
可中断锁 vs 非可中断锁
可中断锁,顾名思义,就是可以响应中断的锁。
线程 A 获取了锁正在执行代码,线程 B 正在等待获取锁,由于等待时间过长不想等了,想先处理其它事情,如果可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
在 Java 中,synchronized 是不可中断锁,Lock 是可中断锁
。
Lock 中 lockInterruptibly() 方法体现了可中断性。
公平锁 vs 非公平锁
公平锁:尽量以线程请求锁的顺序来分配锁,比如同时有多个线程在等待一个锁,当这个锁被释放时,等待最久的线程会获得该锁,这就是公平锁。
非公平锁:无法保证以线程请求锁的顺序来分配锁,可能导致某个或者一些线程永远获取不到锁。
在 Java 中,synchronized 就是非公平锁
,它无法保证等待的线程获取锁的顺序。
ReentrantLock 和 ReentrantReadWriteLock,默认是非公平锁,可以设置为公平锁
。
在 ReentrantLock 中定义了 2 个静态内部类,一个是 NotFairSync,一个是 FairSync ,分别用来实现非公平锁和公平锁。
ReentrantLock 类中定义了很多方法,比如:
- isFair() 判断锁是否公平锁
- isLocked() 判断锁是否被某线程获取了
- isHeldByCurrentThread() 判断锁是否被当前线程获取了
- hasQueuedThreads() 判断是否有线程在等待该锁
tips: 在 ReentrantReadWriteLock 中也有类似的方法,同样也可以设置为公平锁和非公平锁,不过要记住,ReentrantReadWriteLock 并未实现 Lock 接口,它实现的 是ReadWriteLock 接口。
读写锁
读写锁将对一个资源的访问分成了 2 个锁,一个读锁和一个写锁。因为有了读写锁,使得多个线程之间的读操作不会发生冲突。
ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口
。
通过 readLock() 获取读锁,通过 writeLock() 获取写锁。