线程锁及锁的升降级

目录:

1、Lock 简介、地位、作用

2、Lock 方法

3、锁

4、共享锁和排它锁

5、锁的升降级

 6、自旋锁和阻塞锁

7、可中断锁

8、如何使用锁

 

第三章 线程锁

1、Lock 简介、地位、作用

1、锁是一种工具,用于控制对共享资源的访问;

2、Lock 和 synchronized,这两个是最常见的锁,他们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同;

3、Locak 并不是用来代替 synchronized 的,而是当使用 synchronized 不合适或不足以满足要求的时候,来提供高级功能的;

4、Lock 接口最常见的实现类是 ReentrantLock;

5、通常情况下,Lock只允许一个线程来访问这个共享资源。不过有的时候,一些特殊的实现可允许并发访问,比如 ReadWriteLock 里面的 ReadLock;

6、为什么 synchronized 不够用?

  1. 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程(当进入 synchronized 中的线程,如果发生异常,JVM 会让其释放锁);
  2. 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的;
  3. 无法知道是否成功获取到锁。

 

2、Lock 方法

1、locak()

  1. lock() 就是最普通的获取锁。如果锁以被其他线程获取,则进行等待;
  2. Lock 不会像 synchronized 一样在异常时自动释放锁;
  3. 因此最佳实践是,在 finally 中释放锁,以保证发生异常时锁一定被释放;

 

2、tryLock()

  1. tryLock() 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,并返回 true,否则返回 false;
  2. 该方法会立即返回,不会像 lock() 一样,拿不到锁就阻塞在那里;
  3. tryLock(long time , TimeUnit unit):超时时间内如果还没有获取到锁,返回 false,如果能获取到锁,就获取锁,并返回 true。

 

3、lockInterruptibly()

  1. lockInterruptibly() 相当于 tryLock(long time , TimeUnit unit) 把超时时间设置为无限。在等待锁的过程中,线程可以被中断。

 

4、unlock():解锁

 

5、interrupt()方法:

  1. 其作用是中断此线程,它可以中断使用 lockInterruptibly() 获取锁,并且正在运行的线程,也可以中断使用 lockInterruptibly() 等待获取锁的线程。但是 interrupt()不能中断使用 lock() 等待获取锁的线程,也不能中断使用 lock() 获取锁,并正在运行的线程,除非,这个线程运行到了 Thread.sleep(),他会抛出异常,而执行 finally 中的 unlock() 释放锁。

 

6、interrupted()方法:

  1. 作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除,将返回一个false。

 

7、isInterrupted()方法:

  1. 作用是测试此线程是否被中断 ,不清除中断状态。

3、锁

1、锁的分类如图所示:

 

2、乐观锁和悲观锁

乐观锁可以称为非互斥同步锁,悲观锁可以称为互斥同步锁。

 

  1)乐观锁是什么?

  1. 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象;
  2. 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过:如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据;
  3. 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略;
  4. 乐观锁的实现一般都是利用 CAS 算法来实现的;
  5. 乐观锁的典型例子就是原子类、并发容器等。

 

  2)悲观锁是什么?

  1. 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失;
  2. Java 中悲观锁的实现就是 synchronized 和 Lock 相关类。

 

  3)两种锁的使用场景

  1. 悲观锁:适合并发写入多的情况,用用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗。典型情况:
    1. 临界区有 IO 操作;
    2. 临界区代码复杂或者循环量大;
    3. 临界区竞争非常激烈。
  2. 乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。

 

3、可重入锁和非可重入锁,以 ReentrantLock 为例(重点)

  1)可重入锁

    1、同一个线程没有释放锁的情况下,重复获取自己所持有的锁。

    1. 获取锁时先判断,如果当前线程就是已经占有锁的线程,则 status 值 +1,并返回 true。
    2. 释放锁时也是先判断当前线程是否已占有锁的线程,然后在判断 status。如果 status 等于 0,才真正的释放锁。

 

  2)非可重入锁

    1、非重入锁是直接尝试获取锁,如果已经持有锁,不能获再次取锁;

    2、释放锁时也是直接将 status 置为0。

 

  3)可重入锁和非可重入锁区别:

    1、可重入锁在使用的时候一般是一个类当中有AB两个方法,而A和B都是有统一的一把锁,当实施A方法的时候就可以获得锁,但在A办法的所还没有全部释放的时候也可以直接使用B方法,而在这个时候也是可以获得这个锁的。
    2、不可重入锁也是指的是A和B两个方法,A和B可以获得统一的一把锁,而在A方法还没有释放的时候是没有办法使用B方法的,也就是说必须要等A释放之后才可以使用B方法。

  

  6)可重入锁演示:

一:

public class LockDemo3 {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        System.out.println(lock.getHoldCount());//打印获取了几次锁
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.lock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }
}

 

二:

public class RecursionDemo {
    private static ReentrantLock lock = new ReentrantLock();

    public static void accessResource() {
        try{
            lock.lock();
            System.out.println(Thread.currentThread().getName()+":进入access并获取了锁");
            agin();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void agin() {
        try{
            lock.lock();
            System.out.println(Thread.currentThread().getName()+":进入agin并获取了锁");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        accessResource();
    }
}

 

 

  5)ReentrantLock 的其他方法介绍

    1、isHeldByCurrentThread 可以看出锁是否被当前线程持有;

    2、getQueueLength 可以返回当前正在等待这把锁的队列有多长,一般这两个方法是开发和调试时候使用,上线后用到的不多。

 

4、公平锁和非公平锁

  1)公平的情况(以 ReentrantLock 为例)

    1、创建 ReentrantLock 时传入 true 参数即可;

    2、在线程1执行 unlock() 释放锁之后,由于此时线程2的等待时间最久,所以线程2先得到执行,然后是线程3、线程4。

  2)不公平情况(以 ReentrantLock 为例)

    1、创建  ReentrantLock 时传入 false 参数或者不传即可;

    2、如果在线程1释放锁的时候,线程5恰好去执行 lock(),就是插队了;

    3、线程5可以插队,直接拿到这把锁,也是 ReentrantLock 默认的公平策略,也就是“不公平”

 

5、演示公平锁

/**
 * 公平锁
 */
public class FairLock {
    //公平
    private static Lock lock = new ReentrantLock(true);
    private static Thread[] threads = new Thread[5];
    public static void fairLockTest(){
        try{
            lock.lock();
            System.out.println(Thread.currentThread().getName()+":获取到锁,正在执行业务");
            Random random = new Random();
            int i = random.nextInt(8);
            Thread.sleep(i * 1000);
            System.out.println(Thread.currentThread().getName()+":执行业务完毕,耗时:"+i);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                fairLockTest();
            });
        }
            for (int i = 0; i < 5; i++) {
                threads[i].start();
            }
    }
}

 

运行结果:

 

6、演示非公平锁

/**
 * 非公平锁
 */
public class FairLock {
    //非公平
    private static Lock lock = new ReentrantLock();
    private static Thread[] threads = new Thread[5];
    public static void fairLockTest(){
        try{
            lock.lock();
            System.out.println(Thread.currentThread().getName()+":获取到锁,正在执行业务");
            Random random = new Random();
            int i = random.nextInt(8);
            Thread.sleep(i * 1000);
            System.out.println(Thread.currentThread().getName()+":执行业务完毕,耗时:"+i);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                fairLockTest();
            });
        }
            for (int i = 0; i < 5; i++) {
                threads[i].start();
            }
    }
}

 

运行结果:

 

7、特例

  1)针对 tryLock(),他是很猛的,他不遵守设定的公平的规则;

  2)例如,当有线程执行 tryLock()的时候,一旦有线程释放了锁,那么这个正在 tryLock() 的线程就能获取到锁,即使在他之前已经有其他现在在等待队列里了。

 

8、对比公平和非公平的优缺点

  优势 劣势
公平锁 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 更慢,吞吐量更小
不公平锁 更快,吞吐量更大 有可能产生线程饥饿,也就是某些线程长时间内,始终得不到执行

 

 

4、共享锁和排它锁

1、什么是共享锁和排它锁

  1)共享锁

    1. 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据。

  2)排它锁

    1. 排它锁,又称独占锁、独享锁。

  3)共享锁和排它锁的典型是读写锁 ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁。

 

2、读写锁的作用

  1)在没有读写锁之前,我们假设使用 ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题;

  2)在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率。

 

3、读写锁的规则

  1)多个线程只申请读锁,都可以申请到;

  2)如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁;

  3)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁;

  4)一句话总结要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写)。

 

4、ReentrantReadWriteLock(boolean b) 具体用法

ReentrantReadWriteLock(boolean b)可以设置参数为公平或非公平锁,默认为非公平锁。

简单演示:

public class CinemaReadWrite {
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    private static void read(){
        readLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("释放读锁");
            readLock.unlock();
        }
    }

    private static void write(){
        writeLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在读取");
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(() ->{
            read();
        },"线程一").start();
        new Thread(() ->{
            read();
        },"线程二").start();
        new Thread(() ->{
            write();
        },"线程三").start();
        new Thread(() ->{
            write();
        },"线程四").start();
        new Thread(() ->{
            read();
        },"线程五").start();
    }
}

 

运行结果:

 

5、读写锁插队策略

  1)公平锁:不允许插队;

  2)非公平锁:

    1、获取写锁的线程可以随时插队。

    2、如果等待队列中有线程要获取写锁,则其他想要获取读锁的线程不能插要获取写锁线程的队。举个例子:假设等待队列中,一个写锁前面有三个读锁,后面又十个读锁,前面的三个读锁可以互相插队,后面的十个读锁也能互相插队,但是不能插到写锁前面。

    3、原因:因为读锁是可以共享的,一旦获取到了读锁,写锁就不可被获取,如果读锁可以一直插队的话,写锁将一直阻塞。

示例:

public class NonfairBargeDemo {
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(false);
    //创建读锁
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    //创建写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    private static void read(){
            System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
            readLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(500);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write(){
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "得到写锁");
            Thread.sleep(700);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(() -> write(),"Thread1").start();
        new Thread(() -> read(),"Thread2").start();
        new Thread(() -> read(),"Thread3").start();
        new Thread(() -> write(),"Thread4").start();
        new Thread(() -> read(),"Thread5").start();
        new Thread(() -> {
            Thread thread[] = new Thread[10];
            for (int i = 0; i < 10; i++) {
                thread[i] = new Thread(() -> read(),"子线程创建的Thread"+i);
            }
            for (int i = 0; i < 10; i++) {
                thread[i].start();
            }
        }).start();
    }
}

 

 

运行结果:

 

6、共享锁和排它锁总结

  1)ReentrantReadWriteLock 实现了 ReadWriteLock 接口,最主要的有两个方法:readLock() 和 writeLock() 用来获取读锁和写锁;

 

  2)锁申请和释放策略:

    1. 多个线程只申请读锁,都可以申请到;
    2. 如果一个线程已经占用了读锁,则其他线程如果要申请写锁,则申请写锁的线程会一直等待释放写锁。如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

  

  3)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁;

 

  4)插队策略:为了防止饥饿,读锁不能插写锁的队;

 

  5)升降级策略:读锁不能升级,写锁能降级;

  

  6)适用场合:相比与 ReentrantLock 适用于一般场合,ReentrantReadWriteLock 适用于读多写少的情况,合理使用,可以进一步提高并发效率。

5、锁的升降级

1、锁的升降级介绍:

  1)升级(以读锁升级到写锁为例)

  1. 首先声明,读锁是不支持升级到写锁的。
  2. 当一个线程拥有一个读锁,此时该线程想要获取写锁来做一些事情,但是又不想释放手上的读锁,否则下次再获取读锁时就要排队。
  3. 如何实现锁的升级?
    1. 获取读锁成功后,在释放读锁前,获取写锁,然后在做相关逻辑,最后在释放锁。
  4. 为什么读锁不支持升级到写锁?
    1. 容易造成死锁。假设有A、B两个线程,他们都拿着读锁,然后他们都想升级为写锁,但是,因为写锁不能与读锁并存,必须要先释放完读锁后才能获取写锁,所以导致A等待B释放读锁,然后A在升级为写锁,B的情况也是一样,等待A释放读锁,然后升级为写锁,由此造成了死锁;
    2. 如果强行升级为写锁,程序会一直阻塞在获取写锁上。

示例:

public class CinemaReadWrite {
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

    private static void read(){
        readLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "得到了读锁");
            Thread.sleep(800);
            System.out.println(Thread.currentThread().getName()+"尝试升级为写锁");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName()+"升级成功");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("释放读锁");
            readLock.unlock();
        }
    }

    private static void write(){
        writeLock.lock();
        try{
            System.out.println(Thread.currentThread().getName() + "得到了写锁");
            Thread.sleep(800);
            System.out.println(Thread.currentThread().getName() + "尝试降级为读锁");
            readLock.lock();
            System.out.println(Thread.currentThread().getName() + "降级成功");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        //升级
        //new Thread(() -> read(),"Thread01").start();
        //降级
        new Thread(() -> write(),"Thread02").start();
    }
}

 

降级结果:

 

升级结果:

 结论:读锁升级为写锁,程序会一直阻塞获取写锁(所以读锁不支持升级),写锁可以降级为读锁。

 

 6、自旋锁和阻塞锁

 1、什么是自旋锁?

  1)为了让当前线程 “稍等一下” ,我们需要让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

 

2、什么是阻塞锁?

  1)阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒。

 

3、自旋锁的缺点

  1)如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源;

  2)在自旋的过程中,一直消耗 cpu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的。

 

4、自旋锁的适用场景

  1)自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高;

  2)另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(现场一旦拿到锁,很久以后才会释放)。那也是不合适。

 

5、原理和源码分析

  1)在 java1.5 以上的并发框架 atmoic 包下的类基本都是自旋锁的实现;

  2)AtomicInteger 的实现:自旋锁的实现原理是 CAS,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自选操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在 while 里死循环,直至修改成功。

6、模拟自旋锁

示例:

public class SpinLock {
    private AtomicReference<Thread> sign = new AtomicReference<>();
    public void lock() {
        Thread current = Thread.currentThread();
        while(!sign.compareAndSet(null,current)){

        }
    }
    public void unlock(){
        Thread current = Thread.currentThread();
        sign.compareAndSet(current,null);
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
                spinLock.lock();
                System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                try{
                    Thread.sleep(1000);
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

 

7、可中断锁

1、在 java中,synchronized 就不是可中断锁,而 Lock 是可中断锁,因为 tryLock(time) 和 lockInterruptibly() 都能响应中断(上面有详细介绍).

 

8、如何使用锁

1、缩小同步代码块;

2、尽量不要锁住方法;

3、减少请求锁的次数;

4、锁中尽量不要再包含锁;

5、选择合适的锁类型或合适的工具类。

 

posted @ 2021-09-16 20:20  nicechen  阅读(582)  评论(0编辑  收藏  举报