一、Lock接口简介
public interface Lock
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而JDK1.5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。
Lock的
二.接口Lock定义的方法
以及Lock的三个子类ReentrantLock , Condition , ReadWriteLock,在之后会介绍这三个类的具体作用
三.Lock的使用方式
Lock lock=new ReentrantLock();
lock.lock();
try {
//do something
}finally {
lock.unlock();
}
注意:在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
四.重入锁ReentrantLock
1.什么是重入锁
顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,而反过来说不可重入锁就是线程在未释放锁却试图再次获取该锁 会被锁阻塞。可重入的实现需要解决两个问题,
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
**2)锁的最终释放。**线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
2.synchronized如何实现重入锁
ynchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁,
代码实例
public class LockTest {
public synchronized void test(int n){
if(n>0){
System.out.println(Thread.currentThread().getName()+":"+n);
test(n-1);
}
return;
}
public static void main(String[] args) {
LockTest test=new LockTest();
for(int i=0;i<2;i++){
new Thread(new Runnable() {
@Override
public void run() {
test.test(4);
}
}).start();
}
}
}
运行结果:
Thread-0:4
Thread-0:3
Thread-0:2
Thread-0:1
Thread-1:4
Thread-1:3
Thread-1:2
Thread-1:1
Process finished with exit code 0
从代码结果来看使用synchronized,当一个线程获取到锁之后未释放还可以再次获取锁,并不会产生同步互斥的现象。
3.ReentrantLock实现重入锁
使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。
public class LockTest {
private Lock lock=new ReentrantLock();
public void methodA(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"已经进入methodA了");
this.methodB();
}finally {
lock.unlock();
}
}
public void methodB(){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"已经进入methodB了");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockTest test=new LockTest();
for(int i=0;i<2;i++){
new Thread(new Runnable() {
@Override
public void run() {
test.methodA();
}
}).start();
}
}
}
运行结果
Thread-1已经进入methodA了
Thread-1已经进入methodB了
Thread-0已经进入methodA了
Thread-0已经进入methodB了
Process finished with exit code 0
从上述代码来看,当线程进入方法A之后,在方法B中再次调用lock()方法时,该线程并没有被阻塞。这就说明ReentrantLock是支持重进入的。
之前说了实现重入锁必须解决的两个问题1.线程再次获取锁。
ReentrantLock提供了几个方法来解决这两个问题
1)、.nonfairTryAcquire方法
nonfairTryAcquire方法是ReentrantLock的静态内部类Sync中的一个方法,该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。
2)、tryRelease方法
如果该锁被获取了n次,那么前(n-1)次tryRelease(intreleases)方法必须返回false,而只有同步状态完全释放了,才能返回true,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
当用synchronized来实现重入锁,由于synchronized的特性,我们可以不用考虑锁的获取和释放,synchronized可以自动获取与释放,但是当用Lock是我们必须手动来设置锁的获取和释放【lock()/unLock()】,如果我们只是获取锁而忘记了释放锁会影响后续线程对锁的获取,现在将methodB中的lock.unLock()注释掉会发现
这时程序会被阻塞住。
五、Condition接口
任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(longtimeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。
1.Object的监视器方法与Condition接口的对比
2.Condition的使用
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。
Condition定义的一些常用的方法
Condition的使用方式比较简单,需要注意在调用方法前获取锁,使用方式如下
做一个简单地生产消费者模型
public class LockTest {
private Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
static volatile int COUNT=0;
public void addCount() throws InterruptedException {
lock.lock();
try {
COUNT++;
System.out.println(Thread.currentThread().getName()+"生产了一件商品,COUNT:"+COUNT);
while (COUNT>=10){
condition.signalAll();
condition.await();
}
}finally {
lock.unlock();
}
}
public void getCount() throws InterruptedException {
lock.lock();
try {
COUNT--;
System.out.println(Thread.currentThread().getName()+"消费了一件商品,COUNT:"+COUNT);
while (COUNT<=0){
condition.signalAll();
condition.await();
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockTest test=new LockTest();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
test.addCount();
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"生产者");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
while (true){
try {
test.getCount();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"消费者");
thread1.start();
thread2.start();
}
}
可以看到用法与Object中的wait()和notify()差不多
一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
Condition支持唤醒部分指定的线程,这是notify、notifyAll所不具备的,使用方式就是是可以创建多个Condition对象
Condition conditionA=lock.newCondition();
Condition conditionB=lock.newCondition();
就是说conditionA.signalAll()只能唤醒拥有conditionA.await()的线程,而不会唤醒拥有 conditionB.await()线程,这种可以唤醒部分指定的线程的机制有助于提升程序运行的效率
六、读写锁ReadWriteLock
1.什么是读写锁
之前提到锁基本都是排他锁,这些锁在同一时刻只允许一个线 程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读 线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写 锁,使得并发性相比一般的排他锁有了很大提升。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
2.读写锁的特性
3.关于读写锁的一些常见方法
4.读写锁的使用方式
public class ReadWriteLockTest {
private ReadWriteLock lock=new ReentrantReadWriteLock();
Lock readLock=lock.readLock();
Lock writeLock=lock.writeLock();
public void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获得读锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取写锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
1)、读读共享,获得读锁的线程之间并不会出现排斥的现象
代码示例:
public static void main(String[] args) {
ReadWriteLockTest test=new ReadWriteLockTest();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
while (true){
test.read();
}
}
},"线程1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
while (true){
test.read();
}
}
},"线程2");
thread1.start();
thread2.start();
}
运行结果:
线程2获得读锁
线程1获得读锁
线程2获得读锁
线程1获得读锁
线程1获得读锁
线程2获得读锁
线程1获得读锁
线程2获得读锁
线程1获得读锁
线程2获得读锁
Process finished with exit code -1
2)、读写互斥,获取读锁的线程和获取写锁的线程会排斥
public static void main(String[] args) {
ReadWriteLockTest test=new ReadWriteLockTest();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
while (true){
test.read();
}
}
},"线程1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
while (true){
test.write();
}
}
},"线程2");
thread1.start();
thread2.start();
}
运行结果:
线程2获取写锁
线程2获取写锁
线程2获取写锁
线程2获取写锁
线程2获取写锁
线程2获取写锁
Process finished with exit code -1
3)、写写互斥,拥有写锁的线程之间会互相排斥
public static void main(String[] args) {
ReadWriteLockTest test=new ReadWriteLockTest();
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
while (true){
test.write();
}
}
},"线程1");
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {
while (true){
test.write();
}
}
},"线程2");
thread1.start();
thread2.start();
}
运行结果:
线程1获取写锁
线程1获取写锁
线程1获取写锁
线程1获取写锁
线程1获取写锁
线程1获取写锁
Process finished with exit code -1
6.什么是降级锁
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读 锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到 读锁,随后释放(先前拥有的)写锁的过程。
**降级锁的必要性:**为了保证数据的可见性,如果 当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修 改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级 的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进 行数据更新。
为什么没有升级锁:RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的 也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了 数据,则其更新对其他获取到读锁的线程是不可见的。
七、公平锁和非公平锁
公平锁:线程按照他们请求锁的顺序来获取锁,也就是等待时间最长的线 程最优先获取锁,使用FIFO队列实现
非公平锁:当获取锁的线程请求锁的,锁的状态恰好为可用状态,则此此线程进行插队,直接获取锁,否则,将请求加入队列,队列的请求依然遵循FIFO的原则
事实上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以此作为 唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。Lock和synchronized默认使用非公平锁。
八、总结
1.Lock与synchronized的共同点
**1)、**Lock和synchronized都是可重入锁
**2)、**Lock和synchronized默认为非公平锁
**3)、**Lock和synchronized都可以实现线程之间的通信
2…Lock与synchronized的不同点
**1).**性质不同:synchronized是一个关键字,是JVM层面上实现的;Lock是一个接口,是java语言层面上实现的。
**2).**作用位置不同:synchronized可以给方法,代码块加锁;Lock只能给代码块加锁
3).锁的获取和释放机制不同:synchronized无需手动获取和释放锁,发生异常会自动解锁,不会出现死锁;Lock必须自己手动获取和释放锁,如果忘记使用unLock(),会影响后续进程的运行,出现死锁。
4).Lock提供了超时机制,超时机制可以让我们更灵活的控制程序,而不必陷入等待锁的死循环中,在一定时间内获取不到锁,线程就释放出来继续干下面的事情,而synchronized一旦尝试加锁,就会死等,所以这种情况就有可能会出现死锁。
5).Lock支持非阻塞的获取锁,Lock支持不阻塞的方式获取锁,以这种方式获取锁时会返回获取锁是否成功,当尝试获取锁不成功时,线程并不会阻塞。
6).Lock可以选择指定的线程进行等待或唤醒
synchronized只有一个等待队列,任何情况的阻塞都是放在一个队列里面的,Lock可以创建多个Condition队列,不同的Condition控制不同的条件,每个Condition有单独的一个队列。而synchronized只能随机唤醒等待的线程,或者全部唤醒,效率不高
7).Lock可以通过isLocked()判断锁的获取状态,而synchronized无法判断是否获取到锁
**8).**Lock可以自行设置是否为公平锁,而synchronized不行。
9).Lock支持线程释放锁进入等待状态可以不响应中断,而synchronized不支持
3.Lock与synchronized的应用场景
从性能上讲,在资源竞争不激烈的情况下,synchronized的性能会比lock好,竞争激烈的情况下,synchronized的性能会下降,而Lock不受影响。