Java并发14:干变万化的锁

89 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情

学习MOOC视频记录的笔记

1.Lock接口

1.1 简介、地位、作用

  • 锁是一种工具,用于控制对共享资源的访问
  • Locksynchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同
  • Lock并不是用来代替 synchronized 的,而是当使用 synchronized 不合适或不足以满足要求的时候,来提供高级功能的 (有各自适用的场合)
  • Lock 接口最常见的实现类是 ReentrantLock
  • 通常情况下,Lock 只允许一个线程来访问这个共享资源。不过有的时候,一些特殊的实现也可允许并发访问,比如 ReadWriteLock 里面的 ReadLock

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

  1. 效率低:锁的释放情况少【一个线程在获取synchronized锁之后只能执行里面的代码,其他线程只能在外面等待获取锁的线程释放锁,而释放锁只有两种情况,第一种是将代码完全执行完毕,第二种是发生异常,那么JVM会将这个锁释放】、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
  2. 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件,(某个对象),可能是不够的
  3. 无法知道是否成功获取到锁

1.3 方法介绍

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

  • lock()

    • lock就是最普通的获取锁。如果锁已被其他线程获取,则进行等待
    • Lock不会像synchronized一样在异常时自动释放锁
    • 因此最佳实践是,finally 中释放锁,以保证发生异常时锁一定被释放
    • lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock()就会陷入永久等待
  • tryLock()

    • tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败
    • 相比于lock,这样的方法显然功能更强大了,我们可以根据是否能获取到锁来决定后续程序的行为
    • 该方法会立即返回,即便在拿不到锁时不会一直在那等
  • tryLock(long time, TimeUnit unit)

    • 超时就放弃
  • lockInterruptibly()

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

    • 解锁 写在finally里面
  • 那么这四个方法有何区别呢?

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;
        r2.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();
                }
            }
        }
    }
}

运行结果:

线程2获取到了锁2
线程1获取到了锁1
线程1获取锁2失败,已重试
线程2获取到了锁1
线程2成功获取到了两把锁
线程1获取到了锁1
线程1获取到了锁2
线程1成功获取到了两把锁

锁中断实例

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();
        }
        thread0.interrupt();
        // 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() + "获取锁期间被中断了");
        }
    }
}

线程0被中断,运行结果:

Thread-0尝试获取锁
Thread-1尝试获取锁
Thread-0获取到了锁
Thread-0睡眠期间被中断了
Thread-0释放了锁
Thread-1获取到了锁
Thread-1释放了锁

线程1被中断,运行结果:

Thread-0尝试获取锁
Thread-1尝试获取锁
Thread-0获取到了锁
Thread-1获取锁期间被中断了
Thread-0释放了锁

1.4 可见性保证

happens-before原则:这件事发生了,如果其他线程一定能看到之前所做的修改的话,就代表它们拥有happens-before

Lock 的加解锁和 synchronized 有同样的内存语义,也就是说下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作

可见性1

假设线程A先获取到锁并执行完代码,那么等线程B获取到锁的时候,线程A所做的操作线程B都可以看到

可见性2

2.锁的分类

这些分类,是从各种不同角度出发去看的

这些分类并不是互斥的,也就是多个类型可以并存:有可能一个锁,同时属于两种类型

比如 ReentrantLock 既是互斥锁,又是可重入锁

好比一个人可以同时是男人,又是军人,这个不冲突

image-20221204014700661

3.乐观锁和悲观锁

乐观锁也叫非互斥同步锁,悲观锁也叫互斥同步锁

3.1 为什么会诞生非互斥同步锁——互斥同步锁的劣势

互斥同步锁的劣势 锁住之后是独占的,其他线程如果想获取相同的资源就必须等待,性能问题

  • 阻塞和唤醒带来的性能劣势
  • 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行
  • 优先级反转:优先级低的线程拿到了锁之后不释放或者释放比较慢,这样即便另外的线程优先级很高也没用,因为必须等待线程释放锁才能执行

3.2 什么是乐观锁和悲观锁

是否锁住资源的角度分类

乐观锁思想:事情总是不太可能会失败的,出错是小概率事件,所以先肆无忌惮的去做一些事情,如果真的遇到了一些问题那么有则改之无则加勉

悲观锁思想:总是担惊受怕,认为出错是一种常态,总是事无巨细,考虑得滴水不漏,保证万无一失

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

悲观锁

悲观锁2

乐观锁:

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

乐观锁1

乐观锁2

乐观锁3

乐观锁4

乐观锁5

3.3 典型例子

  • 悲观锁:synchronizedlock 接口
  • 乐观锁的典型例子就是原子类、并发容器
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会检查远端仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其他人修改了远端代码了,我们的这次提交就失败;如果远端和本地版本号一致,我们就可以顺利提交版本到远端仓库

Git不适合用悲观锁,否则公司倒闭 【我在写代码的时候,你是不能往里面提交的,因为我已经把整个远端仓库锁住了,这段时间内只能我来提交】

数据库:

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

添加一个字段 lock version

先查询这个更新语句的version: select * from table

然后 update set num = 2, version = version + 1 where version 1 and id = 5;

如果version被更新了等于2,不一样就会更新出错,这就是乐观锁的原理

3.4 开销对比

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

3.5 两种锁各自的使用场景

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

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

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

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

4.1 用法

普通用法1:预定电影院座位

ReentrantLock使用案例

假设拿到电影院这把锁才能选座位

ReentrantLock使用案例2

此时线程1拿到锁,其他线程不能选座位,进入等待状态

ReentrantLock使用案例3

/**
* 演示多线程预定电影院座位
*/
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();
    }
}

普通用法2:打印字符串

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

如果不使用锁,会出现顺序错乱的情况

大师兄
悟空
悟空大师
兄
悟空

4.2 可重入性质

什么是可重入

  • 摇号的故事

  • 同一个线程可以多次获取同一把锁

好处

  • 避免死锁
  • 提升封装性

演示可重入

已获得的锁的数量

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

输出:

0
1
2
3
2
1
0

递归:

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

可重入:

已经对资源进行了处理
1
已经对资源进行了处理
2
已经对资源进行了处理
3
已经对资源进行了处理
4
已经对资源进行了处理
4
3
2
1

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

可重入锁和不可重入锁源码对比

4.3 ReentrantLock的其他方法介绍

  • isHeldByCurrentThread 可以看出锁是否被当前线程持有

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

5. 公平锁和非公平锁

5.1 什么是公平和非公平

公平指的是按照线程请求的顺序,来分配锁;非公平指的是不完全按照请求的顺序,在一定情况下,可以插队。

注意:非公平也同样不提倡”插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队。

什么是合适的时机呢?

火车票被插队的例子

5.2 为什么要有非公平锁

  • 联想到自己买票时候被插队的情况,更是怒火中烧,凭什么默认策略是非公平,我难道前面的线程白白排队了吗!Java的设计者是不是没有素质!

  • 实际情况并不是这样的,Java设计者这样设计的目的,是为了提高效率

  • 避免唤醒带来的空档期

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

如果在创建ReentrantLock对象时,参数填写为true,那么这就是个公平锁

后续等待的线程会到wait queue里,按照顺序依次执行

公平锁1

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

公平锁2

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

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

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

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

非公平锁

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

/**
* 演示公平和不公平两种情况
*/
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(false);
 
    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 * 100);
        } 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 * 100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
    }
}

公平锁效果:

Thread-0开始打印
Thread-0正在打印,需要7
Thread-1开始打印
Thread-2开始打印
Thread-3开始打印
Thread-4开始打印
Thread-5开始打印
Thread-6开始打印
Thread-7开始打印
Thread-8开始打印
Thread-9开始打印
Thread-1正在打印,需要5
Thread-2正在打印,需要2
Thread-3正在打印,需要10
Thread-4正在打印,需要7
Thread-5正在打印,需要8
Thread-6正在打印,需要5
Thread-7正在打印,需要9
Thread-8正在打印,需要8
Thread-9正在打印,需要6
Thread-0正在打印,需要3秒
Thread-0打印完毕
Thread-1正在打印,需要1秒
Thread-1打印完毕
Thread-2正在打印,需要8秒
Thread-2打印完毕
Thread-3正在打印,需要8秒
Thread-3打印完毕
Thread-4正在打印,需要1秒
Thread-4打印完毕
Thread-5正在打印,需要9秒
Thread-5打印完毕
Thread-6正在打印,需要9秒
Thread-6打印完毕
Thread-7正在打印,需要3秒
Thread-7打印完毕
Thread-8正在打印,需要2秒
Thread-8打印完毕
Thread-9正在打印,需要3秒
Thread-9打印完毕

非公平锁效果:

 

5.6 特例

  • 针对tryLock()方法,它是很猛的,它不遵守设定的公平的规则
  • 例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他现在在等待队列里了

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

image-20221204152112390

5.8 源码分析

对比公平锁和非公平锁源码

6.共享锁和排它锁:以ReentrantReadWriteLock读写锁为例(重点)

6.1 什么是共享锁和排它锁

排他锁,又称为独占锁、独享锁

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

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

6.2 读写锁的作用

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

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

6.3 读写锁的规则

  • 多个线程只申请读锁,都可以申请到
  • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
  • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

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

换一种思路更容易理解:读写锁只是一把锁,可以通过两种方式锁定:读锁定和写锁定。读写锁可以同时被一个或多个线程读锁定,也可以被单一线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁定。

这里是把“获取写锁”理解为“把读写锁进行写锁定”,相当于是换了一种思路,不过原则是不变的,就是要么是一个或多个线程同时有读锁(同时读锁定),要么是一个线程有写锁(进行写锁定),但是两者不会同时出现

6.4 ReentrantReadWriteLock具体用法

电影院升级

之前的情况,未使用读写锁

ReentrantReadWriteLock具体用法 1

现在用了读写锁,线程1和线程2可以同时用读锁,提高了效率:

ReentrantReadWriteLock具体用法 2

当线程1和线程2都释放了锁以后,线程3和线程4就可以写入了,但是只能有一个线程持有写锁

ReentrantReadWriteLock具体用法 3

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

运行结果:

Thread1得到了读锁,正在读取
Thread2得到了读锁,正在读取
Thread1释放读锁
Thread2释放读锁
Thread3得到了写锁,正在写入
Thread3释放写锁
Thread4得到了写锁,正在写入
Thread4释放写锁

6.5 读锁和写锁的交互方式

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

插队 不允许读锁插队

升降级 运行降级不允许升级

读锁插队策略:公平读写锁不允许插队

非公平:假设线程2和线程4正在同时读取,线程3想要写入,拿不到锁,于是进入等待队列,线程5不在队列里,现在过来想要读取

此时有2种策略:

策略1:让线程5插队

  • 读可以插队,效率高
  • 容易造成饥饿

读锁插队策略 1

策略2:

避免饥饿

读锁插队策略 2

image-20221204154716764

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

6.6 总结

读锁插队策略

  • 公平锁:不允许插队
  • 非公平锁
    • 写锁可以随时插队
    • 读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队 【队列头节点不是想获取写锁的线程那就可以插队】

源码:

static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}
 
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }
    final boolean readerShouldBlock() {
        /* As a heuristic to avoid indefinite writer starvation,
         * block if the thread that momentarily appears to be head
         * of queue, if one exists, is a waiting writer.  This is
         * only a probabilistic effect since a new reader will not
         * block if there is a waiting writer behind other enabled
         * readers that have not yet drained from the queue.
         */
        // 队列第一个是不是想获取写锁的线程
        return apparentlyFirstQueuedIsExclusive();
    }
}

代码:

public class CinemaReadWrite {
    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(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(() -> write(), "Thread1").start();
        new Thread(() -> read(), "Thread2").start();
        new Thread(() -> read(), "Thread3").start();
        new Thread(() -> write(), "Thread4").start();
        new Thread(() -> read(), "Thread5").start();
    }
}

结果:读不插队

Thread1得到了写锁,正在写入
Thread1释放写锁
Thread2得到了读锁,正在读取
Thread3得到了读锁,正在读取
Thread3释放读锁
Thread2释放读锁
Thread4得到了写锁,正在写入
Thread4释放写锁
Thread5得到了读锁,正在读取
Thread5释放读锁

读实际上可以插队(非公平)

public class NonfairBargeDemo {
    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() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            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() + "得到写锁,正在写入");
            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();
    }
}

锁的升降级

  • 为什么需要升降级
  • 支持锁的降级,不支持升级:代码演示
  • 为什么不支持锁的升级?死锁
public class Upgrading {
 
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
 
    private static void readUpgrading() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000);
            System.out.println("升级会带来阻塞");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "获取到了写锁,升级成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }
 
    private static void writeDowngrading() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000);
            readLock.lock();
            System.out.println("在不释放写锁的情况下,直接获取读锁,成功降级");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        System.out.println("先演示降级是可以的");
        Thread thread1 = new Thread(() -> writeDowngrading(), "Thread1");
        thread1.start();
        thread1.join();
        System.out.println("------------------");
        System.out.println("演示升级是不行的");
        Thread thread2 = new Thread(() -> readUpgrading(), "Thread2");
        thread2.start();
    }
}

结果:

先演示降级是可以的
Thread1得到了写锁,正在写入
在不释放写锁的情况下,直接获取读锁,成功降级
Thread1释放写锁
------------------
演示升级是不行的
Thread2得到了读锁,正在读取
升级会带来阻塞

共享锁和排它锁总结

  1. ReentrantReadWriteLock实现了ReadWriteLock接☐,最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁
  2. 锁申请和释放策略
    • 多个线程只申请读锁,都可以申请到
    • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
    • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
    • 要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现
  3. 插队策略:为了防止饥饿,读锁不能插队
  4. 升降级策略:只能降级,不能升级
  5. 适用场合:相比于ReentrantLock适用于一般场合ReentrantReadWriteLock适用于读多写少的情况,合理使用可以进一步提高并发效率

7.自旋锁和阻塞锁

7.1 概念

  • 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
  • 如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
  • 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失

如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁

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

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

7.2 缺点

  • 如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
  • 在自旋的过程中,一直消耗cpu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的
  • 在java1.5版本及以上的并发框架java.util.concurrent的atmoic包下的类基本都是自旋锁的实现
  • AtomicInteger的实现:自旋锁的实现原理是CAS;AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,直至修改成功

7.3 原理和源码分析

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 
    return var5;
}

自己实现的自旋锁:

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() + "释放了自旋锁");
                }
            }
        };
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}

7.4 适用场景

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

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

在Java中,synchronized就不是可中断锁,而Lock是可中断锁因为tryLock(time)和lockInterruptibly都能响应中断。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以中断它,这种就是可中断锁

9.锁优化

9.1 Java虚拟机对锁的优化

  • 自旋锁和自适应:自旋锁转为阻塞锁
  • 锁消除:虚拟机认为无需加锁
  • 锁粗化:反复加锁解锁被合并为一个较大的

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

  1. 缩小同步代码块

  2. 尽量不要锁住方法

  3. 减少请求锁的次数 多个操作合并为一个

  4. 避免人为制造“"热点”

  5. 锁中尽量不要再包含锁

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