[Java并发与多线程](十七)不可不说的“锁”事

1、Lock接口

1.1 简介、地位、作用

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

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

  1. 效率低:锁的释放情况少(两种方式)、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程;
  2. 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的;
  3. 无法知道是否成功获取到锁

1.3 方法介绍

在Lock中声明了四个方法来获取锁:
lock()tryLock()tryLock(long time, TimeUnit unit)lockInterruptibly()
四个方法的区别

1.3.1 lock()

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

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

1.3.2 tryLock()

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

1.3.3 tryLock(long time, TimeUnit unit)

超时就放弃

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

1.3.4 lockInterruptibly

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

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() + "获得锁期间被中断");
        }
    }
}

1.3.5 unlock()

解锁。注意,要写在finally里面,获取锁之后的第一件事就是把unlock放到finally里面,在写业务代码。

1.4 可见性保证

指的是线程与线程之间并不是随时能看到对方最新的动态的;
happens-before原则:我们这件事发生了,如果其他线程一定能看到我们做的其他修改的话,就代表拥有happens-before;Lock的加解锁和synchronized有同样的内存语义,也就是说,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作

2、锁的分类

这些分类是从各种不同角度出发去看的;这些分类并不是互斥的,也就是多个类型可以并存:有可能一个锁,同时属于两种类型;比如:ReentrantLock既是互斥锁,又是可重入锁,说明了这个锁又多个属性。
在这里插入图片描述

2.1 乐观锁和悲观锁(*)

2.1.1 为什么会诞生非互斥同步锁(乐观锁)?—互斥同步锁(悲观锁)的劣势

互斥同步锁的劣势

  1. 阻塞和唤醒带来的性能劣势:悲观锁锁住之后就是独占的,其他线程如果还想获得相同的资源,就必须等待,带来的最大问题就是性能问题;乐观锁最主要解决的就是性能问题,乐观锁不需要把线程挂起
  2. 悲观锁可能陷入永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行;
  3. 优先级反转:如果被阻塞的线程优先级比较高,而持有锁的线程优先级比较低,这就会导致优先级反转;

2.1.2 什么是乐观锁和悲观锁(*)

从人的性格分析:
乐观锁:遇到什么事情都很开心,认为事情总是不大会失败的,出错是小概率;所以先肆无忌惮的做一些事情,如果说真的遇到问题,有则改之无则加勉;
悲观锁:总是担惊受怕,认为出错是一种常态,无论事无巨细,都考虑的面面俱到,滴水不漏,保证万无一失。

从是否锁住资源的角度分析
悲观锁:如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失;在Java最典型的悲观锁实现就是synchronized和Lock相关类;具体流程一个线程获取到锁,另一个必须等待,直到该线程释放才可以获取
乐观锁:认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象;在更新的时候,去对比在我修改的期间,数据有没有被其他人改变过:如果没有被改变,就说明真的是只有我自己在操作,那我就正常去修改数据;如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略。
乐观锁的实现一般都是利用CAS算法来实现的;CAS的核心思想就是我可以在一个原子操作内,把你这个数据对比并且交换;那么在此期间是不会有人能打断我的。

2.1.3 乐观锁操作

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.1.4 典型例子

悲观锁synchronized和lock接口
乐观锁:典型例子就是原子类、并发容器等;
Git:Git就是乐观锁的典型例子,当我们往远端仓库push的时候,git会检查远端仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地不一样,就表示有其他人修改了远端代码了,我们这次提交就失败,如果远端和本地版本号一致,我们就可以顺利提交版本到远端仓库。
数据库
a、select for update就是悲观锁
b、用version控制数据库就是乐观锁

2.1.5 开销对比

悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越长,也不会对互斥锁的开销造成影响;相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多

2.1.6 两种锁各自的使用场景

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

2.2 可重入锁和非可重入锁(*)

2.2.1 可重入锁

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

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

2.2.2 什么是可重入?

可重入锁也叫递归锁,指的是同一个线程可以多次获取同一把锁synchronizedReentrantLock都是可重入锁;
好处可以避免死锁;提升封装性;

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

2.2 公平锁和非公平锁(*)

2.2.1 什么是公平和非公平?

公平:指的是按照线程请求的顺序,来分配锁;
非公平:指的是,不完全按照请求的顺序,在一定情况下,可以插队;
注意:非公平也同样不提倡 插队 行为,这里的公平,指的是在 合适的时机插队,而不是盲目插队。

2.2.2 为什么要有非公平锁?

假设有三个线程A、B、C,线程A持有这把锁,线程B请求这把锁;由于锁已经被A持有,那么B需要去休息;假设A此时被释放,那么B就要被唤醒并且拿到这把锁;假设这时C突然来请求这把锁,由于C一直处于唤醒状态,是可以立刻执行的;所以很有可能在B被唤醒之前就已经获得了并且使用完释放了这把锁,这就行成了一种双赢局面。
为了提高效率,避免唤醒带来的空档期;主要就是因为在唤醒的开销比较大,为了增加吞吐量把这个等待期间利用出去,这就是非公平设计的根本原因。
公平的情况:
在这里插入图片描述
在这里插入图片描述
不公平的情况:
在这里插入图片描述
在这里插入图片描述

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

public class FairLock {

    public static void main(String[] args) throws InterruptedException {
        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();
            Thread.sleep(100);
        }
    }
}

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 {
    //true——公平 false——非公平
    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();
        }
    }
}

2.2.4 特例

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

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

在这里插入图片描述

2.3 共享锁和排它锁(*)

2.3.1 什么是共享锁和排他锁?

排他锁,又称为独占锁、独享锁:排他锁获取了这个锁之后,既能读又能写;但是此时其他线程再也没有办法获取这个排他锁了,只能由它本人去修改数据;保证了线程安全;比如:synchronized
共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,
也可以查看但无法修改和删除数据。
共享锁和排他锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁(可以有多个线程同时持有),写锁是独享锁(最多有一个线程持有)。

2.3.2 读写锁的作用

在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题;在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率。

2.3.3 读写锁的规则

  1. 多个线程只申请读锁,都可以申请到
  2. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则=申请写锁的线程会一直等待释放读锁。
  3. 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
    一句话总结:要么是一个或多个线程同时有读锁要么是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写)

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

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

}

2.3.5 读锁和写锁的交互方式

  • 选择规则
  • 读线程插队
  • 升降级:读锁和写锁不是平等的,写锁相对读锁要高级

ReentrantReadWriteLock:

  • 插队不允许读锁插队
  • 升降级允许降级不允许升级

2.3.6 读锁插队策略(*)

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

现在有两种策略:
策略1:线程5可以插队;好处:读可以插队,效率高;缺点:容易造成饥饿;
策略2避免饥饿,不可以插队

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

总结
公平锁不允许插队
非公平锁写锁可以随时插队;读锁仅在等待队列头节点不是想获取写锁的线程的时候可以插队
CinemaReadWrite.java NonfairBargeDemo.java

public class NonfairBargeDemo {

    //false——允许插队,true——公平锁,不允许——完全按照排队的
    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() + "得到读锁,正在读取");
            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();
    }
}

2.3.7 锁的升降级(*)

为什么需要升降级?
有一个任务,上来先写日志,写完之后需要读取一些文件;先写后读由于只是开始先写,后面都是读;如果一直持有写锁这样就非常浪费资源;如果支持降级的话,我直接从写锁拿到读锁,再把写锁释放掉,这样就可以提高整个的效率

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

public class Upgrading {

    //false——允许插队,true——公平锁,不允许——完全按照排队的
    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() + "得到读锁,正在读取");
            try {
                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() + "得到写锁,正在写入");
            try {
                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();
    }
}

为什么不支持锁的升级?死锁
线程支持多线程读,但是只能支持一个线程写;并且不能同时有读和写
如果我们想升级,就必须等所有读锁都释放,才可以升级;如果所有的读锁都释放完毕,就可以升级为写锁;
假设一种情况,有线程A、B、C,且都已经获取到读锁;如果A想升级为写,要求B、C都释放读锁;
如果B、C都是主动释放读锁,A就可以成功升级到写锁;

另一种情况:
假设有两个线程A、B,它们都在读;并且A、B都想升级;二者等待对方释放读锁,这样就陷入了死锁。

2.3.8 共享锁和排它锁总结

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

2.4 自旋锁和阻塞锁(*)

2.4.1 什么叫自旋锁?

如果不使用自旋锁,那么就需要阻塞或唤醒一个Java线程,需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间;如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长;为了应对 同步资源的锁定时间很短的场景,不必要为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
如果物理机有多个处理器的话,我们可以让两个或以上的线程同时并行执行,在这种情况下呢,后面请求锁的线程就不放弃CPU的执行时间,它会不停的检测,是不是释放了,如果释放了,我就来拿到;这样一来CPU没有释放,一直在检测,就避免了切换的过程。
为了让当前线程去检测,也就是说让我"稍等一下",我们让当前线程进行自旋,如果自旋完成后,前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒。

2.4.2 自旋锁缺点

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

2.4.3 原理和源码分析

1、在java1.5版本及以上的并发框架java.util.concurrent的atomic包下的类基本都是自旋锁的实现;
2、AtomicInteger的实现:自旋锁的实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,直至修改成功。

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();
        //把当前线程置为null就代表没有任何人持有这把锁——解锁
        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();
    }
}

2.4.4 适用场景

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

2.5 可中断锁:顾名思义就是可以响应中断的锁

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

2.6 锁优化

2.6.1 自旋锁和自适应锁

在锁比较短小的情况下,使用自旋锁,可以有效提高效率。自适应的概念就是尝试了几次之后发现尝试不到,于是就会转为阻塞锁———自适应;可以在JVM的参数中修改默认自旋参数

2.6.2 锁消除

有一些场景下,不需要加锁;比如说一段代码是在方法内部,并且所有同步的东西都是在方法内部,那么在这种情况下根本不可能有外人来访问到我里面的东西,此时虚拟机就会分析出来认为它们是私有的,无需加锁,于是就会把锁给消除了。

2.6.3 锁粗化

我们的JVM,会动态监测,如果发现前后相邻的synchronized代码块,使用的是同一个锁对象,那么它就会把这几个合为一个较大的;这样一来,在执行的时候就无须反复的申请和释放锁,只要申请和释放锁一次就能执行完全部的代码块,也提高了性能。

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

1、缩小同步代码块
2、尽量不要锁住方法
3、减少请求锁的次数
4、避免人为制造热点:某些数据是共享的,使用它就会加锁;避免人为去制造那些使用锁的场景;
5、锁中尽量不要再包含锁:容易死锁
6、选择合适的锁类型或合适的工具类

下一章: 第十八章 atomic包

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值