JAVA并发编程--4 理解Lock

前言:
在多线程并发的编程中,我们已经知道使用synchronized关键字来对临界资源进行加锁保护,那么为什么还要用lLock?

1 背景:
synchronized局限性:
1.1 使用synchronized,其他线程只能等待直到持有锁的线程执行完释放锁(synchronized释放锁有且仅有两种情况)
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,但是获取锁的线程释放锁只会有两种情况:
释放synchronized同步锁的第一种情况:获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
释放synchronized同步锁的第二种情况:线程执行发生异常,此时JVM会让线程自动释放锁(tip:这也是synchronized的一个好处,不会造成死锁)。
因此,可以看到,其他线程只能等待直到持有锁的线程执行完释放锁。

1.2 :使用synchronized,非公平锁使一些线程处于饥饿状态
synchronized实现的锁,只能是非公平的强制锁,对于一些线程,可能长久无法抢占到锁,导致处于饥饿状态,对于某些特定的业务场景,必须要使用公平锁,这时,synchronized同步锁无法满足要求。
synchronized 为什么是非公平的:
1、加锁过程是不断地尝试加锁,实在不行了才放入队列里,而且还是插入队列头的位置,最后才挂起自己。
2、想象一种场景:现在A线程持有锁,B线程在队列里等待,在A释放锁的时候,C线程刚好插进来获取锁,还未等B被A唤醒,C就获取了锁,B苦苦等待那么久还是没有获取锁。B线程不排队的行为造成了不公平竞争锁。
3、再想象另一种场景:还是A线程持有锁,B线程在队列里等待,此时C线程也要获取锁,因此要进入队列里排队,此处进入的是队列头,也就是在B的前面排着。当A释放锁后,唤醒队列里的头节点,也就是C线程。C线程插队的行为造成了不公平竞争锁。
4、综合1、2、3点可知,因为有走后门(不排队)\、插队(插到队头)、重量级锁是不公平锁

1.3 使用synchronized,多个线程都只是进行读操作时,线程之间也会发生冲突
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:即使多个线程都只是进行读操作,当一个线程在进行读操作时,其他线程也只能等待无法进行读操作。

基于synchronized的这些缺陷所以Lock 应运而生,Lock对于三个问题的解决/Lock相对于synchronized的三个优点(有限等待、公平锁、读写锁)。

2 Lock 的使用:
在这里插入图片描述

对于Lock接口类结构示意图的解释:Lock接口是所有的父接口,ReentrantLock类是Lock接口的实现,有三个组合类,Sync类、FairSync类、NonfairSync类 ;
2.1 Lock 的定义:

public interface Lock {
    // 阻塞当前线程直到获取锁
    void lock();
    // 可以响应线程中断的获取锁,如果当前线程因为没有获取锁而发生阻塞,
    // 则该线程可以使用interrupt() 方法来中断改阻塞线程,
    // lockInterruptibly 响应中断并抛出InterruptedException 
    void lockInterruptibly() throws InterruptedException;
    // 尝试获取锁,如果获取到锁则立即返回true,如果获取不到锁则立即返回false
    boolean tryLock();
    // 在有限的时间内获取锁,
    // 情况1:如果获取一次就获取了锁则立即返回true
    // 情况2:没有立即获取锁则在指定的时间内尝试去获取锁(阻塞当前线程),
    // 时间结束,如果获取到锁则返回true,否则返回false
    // 如果时间设置为0则等价于 tryLock() 只会获取一次锁,非阻塞立即返回获取锁的结果
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 释放锁
    void unlock();
    Condition newCondition();
}

2.2 ReentrantLock 使用:
ReentrantLock 类实现Lock接口和Serializable接口,其实现了Lock 定义的5个接口

public class ReentrantLock implements Lock, java.io.Serializable {}

2.2.1 Lock接口的lock()使用:

Lock lock = new ReentrantLock();
lock.lock();
 try{
     //处理任务
 }catch(Exception ex){

 }finally{
     lock.unlock();   //释放锁
 }

tip1:为什么使用lock.lock()放在try块外面?
避免未加锁但是解锁的情况。
解释1:lock.lock()放在try块外面,因为如果在获取锁时发生了异常,异常抛出的同时,会导致锁无故被释放(即如果将lock.lock()写到try块中,lock.unlock()写到finally块,可能出现未加锁成功却释放锁的情况);
tip2:为什么临界代码要放在必须在try块中,lock.unlock()必须放在finally块中
为了避免死锁。
解释:对于Lock锁机制释放锁,正常情况下不会自动释放锁,在发生异常时也不会自动释放锁。
所以,必须由程序员主动去释放锁,所以临界代码必须放在try块中,lock.unlock()必须放在finally块中,保证只要通过lock.lock()获得锁后,无论正常执行还是发生异常,锁一定会得到释放,最终目的是为了避免死锁。

2.2.2 Lock接口的tryLock() + tryLock(long time, TimeUnit unit) 使用:
tryLock()方法: 非阻塞的获取锁,立即返回获取锁的结果,成功为true,失败为false:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

Lock lock = ...;
if(lock.tryLock(60, TimeUnit.SECONDS)) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

2.2.3 Lock接口的lockInterruptibly():
lockInterruptibly()方法:如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
在调用lockInterruptibly()的方法外声明抛出InterruptedException

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

tip: 当通过lock.lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,才可以响应中断。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

3 Lock和synchronized的对比:
ReentrantLock是Lock的实现类,是一个互斥的同步器,在多线程高竞争条件下,ReentrantLock比synchronized有更加优异的性能表现。
3.1 两者均是可重入锁,但是锁的实现方式不一样:
(1):synchronized是Java语言的内置的关键字,而Lock不是Java语言内置的;Lock是一个接口,通过这个接口实现类可以实现同步访问;
(2):ReentrantLock和synchronized都是可重入锁,但是ReentrantLock是API层面的互斥锁,synchronized是原生语法层面的互斥锁;
3.2 解锁方式不同:
(1):采用synchronized不需要用户去手动释放锁,当synchronized方法执行异常或者synchronized代码块执行完之后,系统会自动让线程释放锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
(2):synchronized在发生异常时,会自动释放线程占有的锁,所以不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unlock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
3.3 使用层面不同:
(1)Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块;
(2)Lock 中提供的方法,可以更加灵活的获取锁,可以在有限的时间内获取锁,获取锁的线程可以响应中断,synchronized在获取不到锁的时候只能一直等待无法被打断;
(3)获取锁成功标志:通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
3.4 获取锁的公平性性不同:
(1)synchronized中的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过构造方法ReentrantLock(true)来要求使用公平锁(底层由Condition的等待队列实现)。
(2)ReentrantLock具备尝试非阻塞地获取锁的特性:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁。
3.5 锁可以绑定多个条件(Lock接口中的newCondition()方法保证)
ReentrantLock对象可以同时绑定多个Condition对象(条件变量或条件队列),而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含条件,但如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无需这么做,只需要多次调用newCondition()方法即可。而且我们还可以通过绑定Condition对象来判断当前线程通知的是哪些线程(即与Condition对象绑定在一起的其它线程)

4 ReentrantReadWriteLock :
4.1 ReentrantReadWriteLock 介绍:
​ ReentrantLock虽然可以灵活地实现线程安全,但是他是一种完全互斥锁,即某一时刻永远只允许一个线程访问共享资源,不管是读数据的线程还是写数据的线程,这就造成了都是读线程也需要去等待的获取锁,从而影响效率;ReentrantReadWriteLock 中将读锁和写锁进行分离,以应对读多写少的情况;
在这里插入图片描述
ReadWriteLock:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

4.2 ReentrantReadWriteLock 的使用:
​ ReentrantReadWriteLock中维护了读锁和写锁。允许线程同时读取共享资源;但是如果有一个线程是写数据,那么其他线程就不能去读写该资源。即会出现三种情况:读读共享,写写互斥,读写互斥。

// ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true); // 公平锁
 ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();// 非公平锁
 Lock readLock = reentrantReadWriteLock.readLock();
 Lock writeLock = reentrantReadWriteLock.readLock();
 // writeLock.lock(); // 写锁加锁
 readLock.lock(); // 读锁加锁
  try {
      // 业务处理
   } catch (Exception e) {
   } finally {
       // writeLock.unlock();// 写锁释放锁
       readLock.unlock();// 读锁释放锁
   }

tip:
(1)WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能;
(2) .WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。
(3)由于读锁和写锁的互斥性,所以在读多写少,非公平锁的清下就可能出现,写线程迟迟无法竞争到锁定而一直处于等待状态。

4.3 ReentrantLock 与 ReentrantReadWriteLock 的对比:
(1) 实现类不同:
ReentrantLock 实现了Lock 接口;ReentrantReadWriteLock 实现了ReadWriteLock (提供读锁和写锁)接口;
(2) 使用层面上:
都是适应于代码块层面,ReentrantLock 有的功能ReentrantReadWriteLock 也都有;ReentrantReadWriteLock 细化读锁和写锁,而ReentrantLock 并没有细化锁;

5 StampedLock:
5.1 StampedLock介绍:
使用ReentrantReadWriteLock 将读锁和写锁分离,非公平锁在读多写少的情况下,写线程会因为一直获取不到锁而一直阻塞,造成写线程的“饥饿”,ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞,而StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。

5.2 StampedLock 使用:

package org.lgx.bluegrass.bluegrasscoree.util.lockUtil;

import java.util.concurrent.locks.StampedLock;

/**
 * @Description TODO
 * @Date 2022/10/14 16:59
 * @Author lgx
 * @Version 1.0
 */
public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

// 获取 / 释放悲观读锁示意代码
 void read() {
        long stamp = sl.readLock();

        try {
            /**
             * 省略业务相关代码
             */
        } finally {
            sl.unlockRead(stamp);
        }
    }

    void move(double deltaX, double deltaY) { // an exclusively locked method
        // 获取写锁
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            // 释放写锁
            sl.unlockWrite(stamp);
        }
    }

    double distanceFromOrigin() { // A read-only method
        // 乐观读
        long stamp = sl.tryOptimisticRead();
        double currentX = x, currentY = y;

        if (!sl.validate(stamp)) {
            // 如果在读取期间其他线程修改了值,则悲观读
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                // 释放读锁
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    void moveIfAtOrigin(double newX, double newY) { // upgrade
        // Could instead start with optimistic, not read mode
        // 获取读锁
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                // 获取写锁
                long ws = sl.tryConvertToWriteLock(stamp);
                if (ws != 0L) {
                    // 获取成功
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 释放读锁
                    sl.unlockRead(stamp);
                    // 获取写锁
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

tip:如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。 所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly();

5.3 ReentrantReadWriteLock 与StampedLock比较:
(1)实现方式不一样ReentrantReadWriteLock 实现 ReadWriteLock ,StampedLock没有实现锁的接口锁的方法是由类内部实现的;
(2)ReentrantReadWriteLock 是可重入锁,但是StampedLock它是不可重入的,如果一个线程已经持有了写锁,同线程再去获取写锁的话就会造成死锁,所以一定避免在同一个线程中不释放锁的前提下调用多次去获取写锁;
(3)ReentrantReadWriteLock 读写冲突可能造成线程饥饿,StampedLock 提供了乐观读的方式,可以进一步提高程序吞吐量;

7 扩展
7,1 可重入锁:
不论是多少个同步代码或同步方法,只要是同一个同步锁对象,这个线程就只要加锁一次,基于线程分配而不是基于方法调用分配;

class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {
         
    }
}

如果一个线程执行到一个synchronized方法method1,而在method1中又调用了另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2;
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法。假如synchronized不具备可重入性,此时线程A需要重新申请锁。因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待,永远不会获取到的锁。而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

7,2 公平锁和非公平锁:
非公平锁定义:不保存请求锁的顺序,即无法保证锁的获取是按照请求锁的顺序进行的,可能会导致某个或者一些线程永远获取不到锁。
公平锁定义:排成队列,保存请求锁的顺序,以请求锁的顺序来获取锁。即同时有多个线程在等待一个同步锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值