Java并发不得不说的“锁”事

锁是并发中非常非常重要的部分,从最开始学并发常用的synchronized或者Lock到更进一步了解并发编程,会发现锁非常的多,概念也很多,不容易区分。

在较为全面的了解了之后决定先写下这篇博客打个底,并在后期的学习中进一步完善我的锁的知识体系

快速到达看这里->

  • Lock接口
      • 简介
      • 为什么需要Lock
      • 方法介绍
      • 可见性保证
      • Synchronized和ReetrantLock的区别
  • 锁的分类
      • 乐观锁和悲观锁
            • 为什么会诞生非互斥同步锁(乐观锁)
            • 什么是乐观锁和悲观锁
            • 典型例子
            • 开销对比
            • 使用场景
      • 可重入锁和非可重入锁
      • 公平锁和非公平锁
            • 什么是公平和非公平
            • 为什么要有非公平锁
            • 公平的情况(以ReentrantLock 为例)
            • 不公平的情况(以ReentrantLock 为例)
            • 特例
            • 对比非公平和公平的优缺点
      • 共享锁和排它锁
            • 什么是共享锁和排它锁
            • 读写锁的作用
            • 读写锁的规则
            • ReetrantReadWriteLock的具体用法
            • 读锁插队策略
            • 升降级策略
      • 自旋锁和阻塞锁
            • 为什么需要自旋锁
            • 自旋锁缺点
            • 代码演示
            • 自旋锁的适用场景
      • 可中断锁和不可中断锁
  • 写代码时如何优化锁并提高并发性能

Lock接口

简介

  • 锁时一种工具,用于控制对共享资源的访问
  • Lock和synchronized是最常见的两个锁,他们都能够达到线程安全的目录,但是使用和功能上又有较大的不同
  • Lock接口最常见的实现类是ReentrantLock
  • 通常情况下Lock只允许一个线程访问共享资源,特殊情况也允许并发访问,如ReadWriteLock的ReadLock

为什么需要Lock

  • synchronized不够用!!
    • 效率低:锁释放的情况少、不支持尝试锁
    • 不够灵活(比不上读写锁):加锁和释放锁时机单一,每个锁只有个一个条件,不够用
    • 无法知道是否成功获得锁

方法介绍

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

  • lock()
    • 最普通的获取锁,如果所被其他线程获得了,进行等待
    • Lock不会像synchronized一样在异常时自动释放锁
    • 使用时,一定要在finally中释放锁
    • lock不能被中断,一旦死锁就会永久等待
		lock.lock();
        try {
            //获取本锁保护的资源
            System.out.println(Thread.currentThread().getName()+"开始执行任务");
        }finally {
            lock.unlock();
        }
  • tryLock()

    • 尝试获取锁,如果当前锁没有被占用,则获取成功,否则获取失败
    • 可以根据是否获取到锁决定后续程序的行为
    • 该方法立刻返回,即使拿不到也不会等
  • tryLock(long time,TimeUnit unit)

    • 加超时时间的尝试获取锁,一段时间内等待锁,超时就放弃
    • tryLock()避免死锁案例代码
/**
 * 〈用trylock避免死锁〉
 *
 * @author Chkl
 * @create 2020/3/11
 * @since 1.0.0
 */
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(800, TimeUnit.MILLISECONDS)) {

                        try {
                            System.out.println("线程2获取到了锁1");
                            Thread.sleep(new Random().nextInt(1000));

                            if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
                                try {
                                    System.out.println("线程2获取到了锁2");
                                    System.out.println("线程2获取到了两把锁");
                                    break;
                                }finally {
                                    lock1.unlock();
                                }
                            }else {
                                System.out.println("线程2获取锁2失败");
                            }

                        } finally {
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • lockInterruptibly()
    • 相当于把tryLock(long time,TimeUnit unit)的超时时间设置为无限长,在等待锁的过程中,线程可以中断
/**
 * 〈验证尝试获取锁期间可中断线程〉
 *
 * @author Chkl
 * @create 2020/3/11
 * @since 1.0.0
 */
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();
        }
        //线程启动2秒后,一个线程获得锁并处于睡眠,另一个线程处于等待锁状态
        thread1.interrupt();

    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "尝试获取锁");
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName()+"获取到了锁");
                //等待5秒,期间第二个线程被中断
                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() + "等锁期间被中断");
        }
    }
}

运行结果可能如下图所示(线程先执行顺序不一定)
中断thread0

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

中断thread1

Thread-0尝试获取锁
Thread-1尝试获取锁
Thread-0获取到了锁
Thread-0睡眠期间被中断
Thread-0释放了锁
Thread-1获取到了锁
Thread-1释放了锁
  • unlock()
    • 解锁,最好每次都先把unlock写在finally内再写业务逻辑

可见性保证

  • lock符合happens-before规则,具有可见性
  • 当线程解锁,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作

Java并发不得不说的“锁”事_第1张图片

Synchronized和ReetrantLock的区别

  • Synchronized可以用在同步方法和代码块上
  • ReetrantLock是可重入锁,使用上更加灵活,需要手动的释放锁

锁的分类

根据不同的划分标准,常见的锁的划分如思维导图所示
Java并发不得不说的“锁”事_第2张图片

乐观锁和悲观锁

为什么会诞生非互斥同步锁(乐观锁)
  • 互斥同步锁(悲观锁)的劣势
    • 阻塞和唤醒带来的性能劣势
    • 永久阻塞:如果持有锁的线程被永久阻塞,如无限循环,死锁等活跃性问题,那么等待该线程释放锁的线程永远得不到执行
    • 优先级反转:阻塞的优先级高,持有锁的优先级低,导致优先级反转
什么是乐观锁和悲观锁

悲观锁:

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

乐观锁:

  • 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住操作对象
  • 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过。
    • 如果没有被改变过,就说明只有自己在操作,就正常修改数据
    • 如果数据与最初拿到的不一致,说明其他人在这段时间内修改过数据,就会执行放弃、报错或重试等策略
  • 乐观锁的实现通常是利用CAS算法,典型例子是原子类,并发容器

案例演示:实现累加器

public class PessimismOptimismLock {
    int a;

    //悲观锁
    public synchronized void testMethod(){
        a++;
    }
    public static void main(String[] args) {
        //乐观锁
        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.incrementAndGet();
        //悲观锁
        new PessimismOptimismLock().testMethod();
    }
}
典型例子

Git:Git是乐观锁的典型应用,当我们向远程仓库push的时候,git会检查远程仓库的版本是不是领先我们现在的版本,

  • 如果远端版本和本地版本不一致,表明远端代码被人修改过了,提交就失败
  • 如果版本一直,才能顺利提交到远程仓库

数据库:

  • select for update就是悲观锁
  • 用version控制就是乐观锁
    • 添加一个字段lock_version
    • 更新操作前先查出这条数据的version 记为mversion
    • 进行更新操作时:update set num = 2 , version = vsersion+1 where version = mversion and id = 5
    • 如果version更新了不等于查询出来的值了,更新就无效
开销对比
  • 悲观锁的原始开销要高于乐观锁,但是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
  • 乐观锁一开始的开销比悲观锁小,如果自旋时间很长或者不停重试,name消耗的资源也会越来越多
使用场景
  • 悲观锁:适合于并发写入多的情况,适合于临界区持锁时间较长的情况,悲观锁可以避免大量的无用自旋锁等消耗
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈
  • 乐观锁:适合并发写入少,大部分都是读取的场景,不加锁能让读取性能大幅提高

可重入锁和非可重入锁

非可重入锁就是最常见的锁,一旦锁被使用,如果没有释放,就不能再使用这个锁了

可重入锁以ReentrantLock为例进行展开

  • 什么是可重入:再次获取同一把锁时不需要释放之前的锁
  • 代码演示1,反复调用:
	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
  • 代码演示2:递归调用
	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) {
        new RecursionDemo().accessResource();
    }
}

运行结果:

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

从结果可以看出获得锁之后是可以重复获得锁再最后释放的,这就是可重入锁

  • 可重入锁的好处
    • 避免了死锁
    • 提高了封装性

公平锁和非公平锁

什么是公平和非公平
  • 公平:指按照线程请求的顺序来分配锁
  • 非公平:不完全按照请求的顺序,在合适的时机下,可以插队
为什么要有非公平锁
  • 为了提高效率(大多数都默认采用非公平锁)
  • 避免唤醒带来的空档期
    Java并发不得不说的“锁”事_第3张图片Java并发不得不说的“锁”事_第4张图片
公平的情况(以ReentrantLock 为例)
  • 如果创建ReentrantLock 对象时,参数填写为true,那么这个锁就是公平锁

演示案例:模拟打印机打印任务,有两个类,一个是打印作业Job类,一个是打印队列PrintQueeue 类,一个打印任务包含两次打印,两次获得锁。在main方法中创建10个线程执行Job,当锁使用公平锁时:

/**
 * 〈演示公平锁和不公平锁〉
 *
 * @author Chkl
 * @create 2020/3/11
 * @since 1.0.0
 */
public class FairLock {

    private static ReentrantLock lock = new ReentrantLock();

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


    }
}

class Job implements Runnable {
    PrintQueeue printQueeue;

    public Job(PrintQueeue printQueeue) {
        this.printQueeue = printQueeue;
    }

    @Override
    public void run() {
        System.out.println(
                Thread.currentThread().getName() + "开始打印");
        printQueeue.printJob(new Object());
        System.out.println(
                Thread.currentThread().getName() + "打印结束");

    }
}


class PrintQueeue {
    //公平锁
    private Lock queueLock = new ReentrantLock(true);
    //非公平锁
//    private Lock queueLock = new ReentrantLock();

    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);
        } finally {
            queueLock.unlock();
        }
    }
}

使用公平锁进行打印操作,每个锁会依次执行,一定是一个锁结束之后另一个锁开始打印,不会出现插队。一次运行结果如下,因为每次打印后需要休眠n秒模拟打印耗时,休眠时间足够所有的线程依次启动,所以执行顺序一定是线程0-9执行第一个打印后线程0-9执行第二次打印,顺序一定不会变

Thread-0开始打印
Thread-0正在打印,需要时间1
Thread-0正在打印,需要时间2
Thread-0打印结束
Thread-1开始打印
Thread-1正在打印,需要时间9
Thread-2开始打印
Thread-3开始打印
Thread-4开始打印
Thread-5开始打印
Thread-6开始打印
Thread-7开始打印
Thread-8开始打印
Thread-9开始打印
Thread-2正在打印,需要时间9
Thread-3正在打印,需要时间1
Thread-4正在打印,需要时间8
Thread-5正在打印,需要时间3
Thread-6正在打印,需要时间5
Thread-7正在打印,需要时间2
Thread-8正在打印,需要时间6
Thread-9正在打印,需要时间2
Thread-1正在打印,需要时间4
Thread-1打印结束
Thread-2正在打印,需要时间6
Thread-2打印结束
Thread-3正在打印,需要时间6
Thread-3打印结束
Thread-4正在打印,需要时间7
Thread-4打印结束
Thread-5正在打印,需要时间8
Thread-5打印结束
Thread-6正在打印,需要时间1
Thread-6打印结束
Thread-7正在打印,需要时间1
Thread-7打印结束
Thread-8正在打印,需要时间3
Thread-8打印结束
Thread-9正在打印,需要时间5
Thread-9打印结束
不公平的情况(以ReentrantLock 为例)

修改PrintQueeue 中的锁为非公平锁

	//非公平锁
    private Lock queueLock = new ReentrantLock();

一次运行结果如下,从结果可以看到,打印顺序并没有再按照0-9、0-9执行了,线程2的第一次打印结束后马上又开始了第二次打印,这就是非公平锁的好处了,线程2执行完第一个打印之后,线程3准备打印,但是准备的空窗期线程2干脆一次性把第二次打印也完成了,不影响线程3打印的正常运行,同理下面的线程56789都是这种情况,提高了效率,充分利用了空窗期

Thread-0开始打印
Thread-0正在打印,需要时间1
Thread-1开始打印
Thread-2开始打印
Thread-3开始打印
Thread-4开始打印
Thread-5开始打印
Thread-6开始打印
Thread-7开始打印
Thread-8开始打印
Thread-9开始打印
Thread-1正在打印,需要时间4
Thread-2正在打印,需要时间5
Thread-2正在打印,需要时间5
Thread-3正在打印,需要时间8
Thread-2打印结束
Thread-3正在打印,需要时间9
Thread-3打印结束
Thread-4正在打印,需要时间8
Thread-5正在打印,需要时间10
Thread-5正在打印,需要时间5
Thread-5打印结束
Thread-6正在打印,需要时间2
Thread-6正在打印,需要时间10
Thread-6打印结束
Thread-7正在打印,需要时间2
Thread-7正在打印,需要时间5
Thread-7打印结束
Thread-8正在打印,需要时间5
Thread-8正在打印,需要时间9
Thread-8打印结束
Thread-9正在打印,需要时间8
Thread-9正在打印,需要时间6
Thread-9打印结束
Thread-0正在打印,需要时间5
Thread-0打印结束
Thread-1正在打印,需要时间6
Thread-1打印结束
Thread-4正在打印,需要时间1
Thread-4打印结束
特例
  • trylock()方法不准守公平规则,自带插队属性
  • 当trylock()执行时,一旦有线程释放了锁,就一定被使用trylock()的线程获得,即使现在这个锁的等待队列里有线程在等待
对比非公平和公平的优缺点

Java并发不得不说的“锁”事_第5张图片

共享锁和排它锁

以ReetrantReadWriteLock读写锁为例

什么是共享锁和排它锁
  • 排它锁:又称独占锁,独享锁
  • 共享锁:又称为读锁,获得共享锁后,可以查看但是无法修改和删除数据,其他线程此时也可以蝴蝶共享锁,同样无法修改和删除数据
  • 共享锁和排它锁的典型就是读写锁ReetrantReadWriteLock,其中读锁是共享锁,写锁是排它锁
读写锁的作用
  • 在没有读写锁之前,假设我们使用ReetrantLock,虽然保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率
读写锁的规则
  • 当一个线程占用读锁时,其他线程可以申请读锁,不能申请写锁
  • 当一个线程占用写锁时,其他线程读锁写锁都不可以申请
  • 总结:要么多读,要么一写
ReetrantReadWriteLock的具体用法

创建4个线程,前两个获取读锁,后两个获取写锁
运行后可以看到读锁可以同时获取,写锁必须获取释放了才能再获取

public class CinemaReadWrite {
    private static ReentrantReadWriteLock
            reentrantReadWriteLock = new ReentrantReadWriteLock();
    //读锁
    private static ReentrantReadWriteLock.ReadLock
            readLock = reentrantReadWriteLock.readLock();
    //写锁
    private static ReentrantReadWriteLock.WriteLock
            writeLock = reentrantReadWriteLock.writeLock();


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

    }
    
    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()
                    + "得到了读锁,正在读取ing");
            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()
                    + "得到了写锁,正在写入ing");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName()
                    +"释放写锁");
            writeLock.unlock();
        }
    }
}

运行结果:

Thread1得到了读锁,正在读取ing
Thread2得到了读锁,正在读取ing
Thread1释放读锁
Thread2释放读锁
Thread3得到了写锁,正在写入ing
Thread3释放写锁
Thread4得到了写锁,正在写入ing
Thread4释放写锁
读锁插队策略
  • 公平锁:不允许插队
  • 非公平锁:
    • 写锁可以随时插队
    • 读锁仅在等待队列头节点不是想要获取写锁的线程的时候可以插队(有写锁马上要执行了就不允许插队)

不能插队的代码演示:将上面的案例的调用进行修改,顺序为w,r,r,w,r

线程2和线程3执行读的操作的时候,线程5不能插队,因为等待队列头的线程4是写锁

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得到了写锁,正在写入ing
Thread1释放写锁
Thread2得到了读锁,正在读取ing
Thread3得到了读锁,正在读取ing
Thread3释放读锁
Thread2释放读锁
Thread4得到了写锁,正在写入ing
Thread4释放写锁
Thread5得到了读锁,正在读取ing
Thread5释放读锁
升降级策略
  • 为什么需要升降级
    • 方法在执行过程中不同时间段的操作不同,如果只有最开始需要写锁,后面大部分时间都只需要读锁,如果一直保持写锁效率不高,浪费资源
    • 支持锁的降级,不支持升级
    • 为什么不支持锁的升级?
      如果升级需要等所有的读锁都释放了才能升级,否则会造成死锁

代码演示:

public class CinemaReadWrite {
    private static ReentrantReadWriteLock
            reentrantReadWriteLock = new ReentrantReadWriteLock();
    //读锁
    private static ReentrantReadWriteLock.ReadLock
            readLock = reentrantReadWriteLock.readLock();
    //写锁
    private static ReentrantReadWriteLock.WriteLock
            writeLock = reentrantReadWriteLock.writeLock();


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



    }

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()
                    + "得到了读锁,正在读取ing");
            Thread.sleep(1000);
            writeLock.lock();
            System.out.println("在不释读锁情况下,获取写锁,升级成功");

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }


    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()
                    + "得到了写锁,正在写入ing");
            Thread.sleep(1000);
            readLock.lock();
            System.out.println("在不释放写锁情况下,获取读锁,降级成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName()
                    + "释放写锁");
            writeLock.unlock();
        }
    }
    
}

运行结果如下,降级成功了,而升级发生了阻塞

Thread1得到了写锁,正在写入ing
在不释放写锁情况下,获取读锁,降级成功
Thread1释放写锁
Thread2得到了读锁,正在读取ing

自旋锁和阻塞锁

为什么需要自旋锁
  • 阻塞或者唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态装换需要耗费处理器时间
  • 如果同步代码块中的内容过于简单,状态转换消耗的时间可能比用户代码执行的时间还长
  • 同步资源锁定时间很短的场景,线程挂起和恢复现场的花费可能会让系统得不偿失
  • 为了让当前线程“稍微等一下”,需要让当前线程自旋,如果自旋完成后前面锁定同步资源的线程已经释放锁了,那么当前线程可以不必阻塞而是直接获取同步资源,从而避免线程切换的开销,这就是自旋锁
  • 阻塞锁和自旋锁相反,阻塞锁如果没有拿到锁,会直接把线程阻塞,直到被唤醒
自旋锁缺点
  • 如果锁被占用时间很长,那么自旋的线程只会白白浪费CPU资源
  • 虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间增长,开销随线性增长
代码演示
/**
 * 〈演示自旋锁〉
 *
 * @author Chkl
 * @create 2020/3/12
 * @since 1.0.0
 */
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();

    }

}
自旋锁的适用场景
  • 自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
  • 自旋锁适用于临界区较短的情况

可中断锁和不可中断锁

  • Java中,synchronized就是不可中断锁,而Lock是可中断锁,因为trylock(time)lockInterruptibly都可以响应中断

  • 如果某个线程A正在执行锁中的代码,另一个线程B正在等待获取该锁,可能由于等待时间太长了,线程B不相等了,可以去处理其他事情,把它中断,这就是中断锁

写代码时如何优化锁并提高并发性能

  • 缩小同步代码块,只锁关键代码
  • 尽量不要锁住方法,避免方法扩展造成消耗增大
  • 减少请求锁的次数
  • 避免人为制造“热点”
  • 避免锁中包涵锁,嵌套锁容易造成死锁
  • 选择合适的锁类型或合适的工具类

本文参考了:《玩转Java并发工具》


更多Java面试复习笔记和总结可访问我的面试复习专栏《Java面试复习笔记》,或者访问我另一篇博客《Java面试核心知识点汇总》查看目录和直达链接

你可能感兴趣的:(#,多线程考点)