JUC-各种锁详解

2,549 阅读5分钟

声明:本文是自己自学慕课网悟空老师的《玩转Java并发工具,精通JUC,成为并发多面手》的锁部分后整理而成课程笔记。

如有侵权,请私信我并第一时间删除本文。

Lock锁

1. Lock接口

1.1 简介、地位、作用

  • 锁是一种工具,用于控制对共享资源的访问。

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

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

  • Lock接口最常见的实现类是ReentrantLock

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

1.2 为什么synchronized不够用 ?为什么需要Lock ?

image-20220216170254997

1.3 方法介绍(lock,trylock,lockInterruptibly)

在Lock中声明了四个方法来获取锁

lock()、tryLock()、 tryLock(long time, TimeUnit unit)和lockInterruptibly()

那么这四个方法有何区别呢?

image-20220216173934278

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     Lock不会像synchronized一样,异常的时候自动释放锁,
 * 所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放
 */
public class MustUnlock {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try{
            //获取本锁保护的资源
            System.out.println(Thread.currentThread().getName()+"开始执行任务");
        }finally {
            lock.unlock();
        }
    }
}

image-20220216174018139

import java.util.Random;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     用tryLock来避免死锁
 */
public class TryLockDeadlock implements Runnable {


    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r1.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();

    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("线程1获取到了锁1");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程1获取到了锁2");
                                    System.out.println("线程1成功获取到了两把锁");
                                    break;
                                } finally {
                                    lock2.unlock();
                                }
                            } else {
                                System.out.println("线程1获取锁2失败,已重试");
                            }
                        } finally {
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("线程2获取到了锁2");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程2获取到了锁1");
                                    System.out.println("线程2成功获取到了两把锁");
                                    break;
                                } finally {
                                    lock1.unlock();
                                }
                            } else {
                                System.out.println("线程2获取锁1失败,已重试");
                            }
                        } finally {
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

image-20220216200351849

image-20220216201425622

package lock.lock;

import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     TODO
 */
public class LockInterruptibly implements Runnable {

    private Lock lock = new ReentrantLock();
public static void main(String[] args) {
    LockInterruptibly lockInterruptibly = new LockInterruptibly();
    Thread thread0 = new Thread(lockInterruptibly);
    Thread thread1 = new Thread(lockInterruptibly);
    thread0.start();
    thread1.start();

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread1.interrupt();
}
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "尝试获取锁");
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + "获取到了锁");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "获得锁期间被中断了");
        }
    }
}

image-20220216202712333

4.可见性保证

image-20220216204510409

image-20220216204525141

image-20220216204552555

2. 锁的分类

◆这些分类,是从各种不同角度出发去看的 ◆这些分类并不是互斥的,也就是多个类型可以并存:有可能一个锁 同时属于两种类型 ◆比如ReentrantLock既是互斥锁,又是可重入锁

image-20220216204820180

3 .乐观锁(非互斥同步锁)和悲观锁(互斥同步锁)

3.1.为什么会诞生非互斥同步锁

互斥同步锁的劣势

  • 阻塞和唤醒带来的性能劣势

  • 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无 限循环、死锁等活跃性问题,那么等待该线程释放锁的 那几个悲催的线程,将永远也得不到执行

  • 优先级反转

3.2.什么是乐观锁和悲观锁

image-20220216210733369

image-20220216210825991

image-20220216210844773

乐观锁

  • 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象

  • 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过:如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据

  • 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了, 我会选择放弃、报错、重试等策略

  • 乐观锁的实现一般都是利用CAS算法来实现的

image-20220216211537946

image-20220216211556038

image-20220216211625657

image-20220216211649118

3.3.典型例子

◆悲观锁: synchronized和lock接口

◆乐观锁的典型例子就是原子类、并发容器等

◆代码演示

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 描述:     TODO
 */
public class PessimismOptimismLock {

    int a;

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.incrementAndGet();
    }

    public synchronized void testMethod() {
        a++;
    }


}

◆Git : Git就是乐观锁的典型例子,当我们往远端仓库push的时候, git会检查远端仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其他人修改了远端代码了,我们的这次提交就失败;如果远端和本地版本号一致 ,我们就可以顺利提交版本到远端仓库

◆数据库

  • select for update就是悲观锁
  • 用version控制数据库就是乐观锁

image-20220216212156854

3.4.开销对比

◆悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响

◆相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多

3.5.两种锁各自的使用场景

◆ 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:

  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈,

乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。

4. 可重入锁和非可重入锁,已ReentrantLock为例(重点)

4.1 使用案例

4.1.1.预定电影院座位

image-20220216213003267

image-20220216213028957

image-20220216213052570

代码演示:

import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     演示多线程预定电影院座位
 */
public class CinemaBookSeat {

    private static ReentrantLock lock = new ReentrantLock();

    private static void bookSeat() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始预定座位");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "完成预定座位");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(() -> bookSeat()).start();
        new Thread(() -> bookSeat()).start();
        new Thread(() -> bookSeat()).start();
        new Thread(() -> bookSeat()).start();
    }
}

image-20220216213948230

4.1.2 打印字符串

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     演示ReentrantLock的基本用法,演示被打断
 */
public class LockDemo {

    public static void main(String[] args) {
        new LockDemo().init();
    }

    private void init() {
        final Outputer outputer = new Outputer();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.output("悟空");
                }

            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.output("大师兄");
                }

            }
        }).start();
    }

    static class Outputer {

        Lock lock = new ReentrantLock();

        //字符串打印方法,一个个字符的打印
        public void output(String name) {

            int len = name.length();
           // lock.lock();
            try {
                for (int i = 0; i < len; i++) {
                    System.out.print(name.charAt(i));
                }
                System.out.println("");
            } finally {
                //lock.unlock();
            }
        }
    }
}

运行结果

image-20220216214318255

4.2 可重入性质

什么是可重入:摇号故事

可重入的好处:

  • 避免死锁
  • 提升封装性

代码演示:

import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述: 演示可重入性质
 */
public class GetHoldCount {
    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());
    }
}

运行结果

image-20220216215342947

import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述: 演示递归
 */
public class RecursionDemo {

    private static ReentrantLock lock = new ReentrantLock();

    private static void accessResource() {
        lock.lock();
        try {
            System.out.println("已经对资源进行了处理");
            if (lock.getHoldCount()<5) {
                System.out.println(lock.getHoldCount());
                accessResource();
                System.out.println(lock.getHoldCount());
            }
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        accessResource();
    }
}

运行结果

image-20220216215615479

源码对比:可重入锁ReentrantLock以及非可重入锁 ThreadPoolExecutor的Worker类

image-20220216220008125

◆isHeldByCurrentThread可以看出锁是否被当前线程持有 ◆getQueueLength可以返回当前正在等待这把锁的队列有多长,一般这两个方法是开发和调试时候使用,上线后用到的不多

5. 公平锁和非公平锁

5.1.什么是公平和非公平

image-20220216221452335

5.2.为什么要有非公平锁

image-20220216221555170

5.3.公平的情况(以ReentrantLock为例)

如果在创建ReentrantLock对象时,参数填写为true,那么这就是个公平锁: ◆假设线程1234是按顺序调用lock()的

image-20220216221947876

image-20220216222015025

5.4.不公平的情况(以ReentrantLock为例)

如果在线程1释放锁的时候,线程5恰好去执行lock()

◆由于ReentrantLock发现此时并没有线程持有lock这把锁(线程2还没来得及获取到,因为获取需要时间)

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

image-20220216222250725

5.5.代码案例:演示公平和非公平的效果

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     演示公平和不公平两种情况
 */
public class FairLock {

    public static void main(String[] args) {
        PrintQueue printQueue = new PrintQueue();
        Thread thread[] = new Thread[10];
        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(new Job(printQueue));
        }
        for (int i = 0; i < 10; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Job implements Runnable {

    PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始打印");
        printQueue.printJob(new Object());
        System.out.println(Thread.currentThread().getName() + "打印完毕");
    }
}

class PrintQueue {

    private Lock queueLock = new ReentrantLock(true);

    public void printJob(Object document) {
        queueLock.lock();
        try {
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
            Thread.sleep(duration * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }

        queueLock.lock();
        try {
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
            Thread.sleep(duration * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
    }
}

5.6.特例

image-20220216223221219

5.7.对比公平和非公平的优缺点

image-20220216223236234

5.8.源码分析

image-20220216223253458

6. 共享锁和排它锁

注:以ReentrantReadWriteLock读写锁为例(重点)

6.1什么是共享锁和排它锁

image-20220217150903533

6.2 读写锁的作用

image-20220217151001124

6.3 读写锁的规则

image-20220217151031961

image-20220217151239408

6.4 ReentrantReadWriteLock具体用法

以买电影票为例

image-20220217151454365

image-20220217151510164

image-20220217151524491

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 描述: 演示读写锁
 */
public class CinemaReadWrite {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

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

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

    public static void main(String[] args) {
        new Thread(()->read(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->write(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
    }
}

image-20220217152206165

6.5 读锁和写锁的交互方式

◆选择规则 ◆读线程插队(比喻:男女共用厕所,男生可以插队吗? )

◆升降级

image-20220217153146233

image-20220217153741918

image-20220217154000212

image-20220217154135880

◆策略的选择取决于具体锁的实现 , ReentrantReadWriteLock的实现是选择了策略2 ,是很明智的。

◆公平锁:不允许插队

◆非公平锁

  • 写锁可以随时插队

  • 读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队

源码分析

image-20220217154816682

image-20220217154848243

代码演示:

1读不插队

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 描述:     TODO
 */
public class CinemaReadWriteQueue {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

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

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(10000);
        } catch (InterruptedException 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();
    }
}

运行结果

image-20220217161352735

如运行结果所示,Thread5并没有插队到Thread4之前。

2读实际上可以插队

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 描述:     演示非公平和公平的ReentrantReadWriteLock的策略
 */
public class NonfairBargeDemo {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            true);

    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            try {
                Thread.sleep(20);
            } catch (InterruptedException 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() + "得到写锁,正在写入");
            try {
                Thread.sleep(40);
            } catch (InterruptedException 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(new Runnable() {
            @Override
            public void run() {
                Thread thread[] = new Thread[1000];
                for (int i = 0; i < 1000; i++) {
                    thread[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
                }
                for (int i = 0; i < 1000; i++) {
                    thread[i].start();
                }
            }
        }).start();
    }
}

锁的升降级

◆为什么 需要升降级

◆支持锁的降级,不支持升级:代码演示

◆为什么不支持锁的升级?死锁

6.6总结

image-20220217213533158

image-20220217213554518

image-20220217213608903

7. 自旋锁和阻塞锁

7.1 自旋锁的概念

image-20220217214009438

image-20220217214101684

7.2 自旋锁的缺点

  • 如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
  • 在自旋的过程中,一直消耗cpu ,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的

7.3 自旋锁源码分析

image-20220217215216446

import java.util.concurrent.atomic.AtomicReference;

/**
 * 描述:     自旋锁
 */
public class SpinLock {

    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void lock() {
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null, current)) {
            System.out.println("自旋获取失败,再次尝试");
        }
    }

    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(300);
                } catch (InterruptedException 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();
    }
}

image-20220217221158853

image-20220217221133601

7.4 自旋锁的适用场景

  • 自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
  • 另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁 ,很久以后才会释放) , 那也是不合适的

8. 可中断锁:顾名思义,就是可以响应中断的锁

可中断锁

  • 在Java中, synchronized就不是可中断锁,而Lock是可中断锁 因为tryLock(time)和lockInterruptibly都能响应中断。
  • 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁可能由于等待时间过长,线程B不想等待了,想先处理其他事情 我们可以中断它,这种就是可中断锁

9. 锁优化

image-20220217221800199

我们在写代码时如何优化锁和提高并发性能:

1缩小同步代码块

2尽量不要锁住方法

3减少请求锁的次数

4避免人为制造“热点”

5锁中尽量不要再包含锁

6选择合适的锁类型或合适的工具类