多线程之二:锁

目录

第三部分——锁

一、 锁的分类

1.1. 可重入锁、不可重入锁

1.2. 乐观锁、悲观锁

1.3. 公平锁、非公平锁

1.4. 互斥锁、共享锁

二、深入synchronized

2.1 类锁、对象锁

2.2 synchronized的优化

2.3 synchronized实现原理

2.4 synchronized的锁升级

2.5 重量锁底层ObjectMonitor

三、深入ReentrantLock

3.1 ReentrantLock和synchronized的区别

3.2 AQS概述

3.3 加锁流程源码剖析

3.3.1 加锁流程概述

​编辑

3.3.2 三种加锁源码分析

3.3.2.1 lock方法

3.3.2.2 tryLock方法

3.3.2.3 lockInterruptibly方法

3.3.2.4 总结:Lock 接口的常用方法、区别、使用

3.4 释放锁流程源码剖析

3.5 AQS中常见的问题

3.6 ConditionObject

3.6.1 ConditionObject的介绍&应用

 3.6.2 好文

四、深入ReentrantReadWriteLock

读写锁的锁降级 

简洁明了的ReentrantReadWriteLock总结_reentrantreadwritelock 锁降级-CSDN博客

JAVA并发编程——ReentrantReadWriteLock锁降级和StampedLock

五、死锁


 

第三部分——锁

一、 锁的分类

下面的几种分类都是一些常见的分类。

1.1. 可重入锁、不可重入锁

        synchronized,ReentrantLock,ReentrantReadWriteLock都是可重入锁。

1.2. 乐观锁、悲观锁

        synchronized,ReentrantLock,ReentrantReadWriteLock都是悲观锁。Java中提供的CAS操作,就是乐观锁的一种实现。

悲观锁:获取不到锁资源时,会将当前线程挂起(进入BLOCKED、WAITING),线程挂起会涉及到用户态和内核态的切换,而这种切换是比较消耗资源的。

  • 用户态:JVM可以自行执行的指令,不需要借助操作系统执行。

  • 内核态:JVM不可以自行执行,需要操作系统才可以执行。

乐观锁:获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源。

Atomic原子性类中,就是基于CAS乐观锁实现的。

1.3. 公平锁、非公平锁

        synchronized只能是非公平锁。ReentrantLock,ReentrantReadWriteLock可以实现公平锁和非公平锁。

公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,锁被A持有,同时线程B在排队。直接排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。

非公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,先尝试竞争一波

  • 拿到锁资源:开心,插队成功。

  • 没有拿到锁资源:依然要排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。

1.4. 互斥锁、共享锁

        synchronized、ReentrantLock是互斥锁。ReentrantReadWriteLock有互斥锁也有共享锁。

互斥锁:同一时间点,只会有一个线程持有者当前互斥锁。(写操作互斥锁更好)

共享锁:同一时间点,当前共享锁可以被多个线程同时持有。(读操作共享锁更好)

【多线程系列】终于懂了 Java 中的各种锁-腾讯云开发者社区-腾讯云 (tencent.com)

二、深入synchronized

2.1 类锁、对象锁

synchronized的使用一般就是同步方法和同步代码块。

synchronized的锁是基于对象实现的。

a) 如果使用同步方法

  • static:此时使用的是当前类.class作为锁(类锁)

  • 非static:此时使用的是当前对象做为锁(对象锁)

调用下面a()方法和b()方法时,不存在互斥性,因为不是同一把锁。即:用synchronized修饰静态方法时锁的是当前类,synchronized修饰非静态方法时锁的是当前对象this。

问:用synchronized修饰静态方法时锁的是当前类,为什么说也是基于对象实现的呢?

答:static修饰时是类锁【类.class】,这个对象是JVM提前加载好的,全局只有一个;非static时,对象是自己new的。

public class MiTest {
    public static void main(String[] args) {
        // 锁的是,当前Test.class
        Test.a();

        Test test = new Test();
        // 锁的是new出来的test对象
        test.b();
    }
}

class Test{
    public static synchronized void a(){
        System.out.println("1111");
    }
    public synchronized void b(){
        System.out.println("2222");
    }
}

 b) 如果采用同步代码块

     如果采用同步代码块,就会以代码块中括号()里设置的对象作为锁。

2.2 synchronized的优化

在JDK1.5的时候,Doug Lee推出了ReentrantLock,lock的性能远高于synchronized,所以JDK团队就在JDK1.6中,对synchronized做了大量的优化——锁消除、锁膨胀、锁升级。

锁升级:ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差。(在2.4详细讲)

2.3 synchronized实现原理

synchronized是基于对象实现的。先要对Java中对象在堆内存的存储有一个了解。(视频42)

展开MarkWord

MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、重量级锁。

2.4 synchronized的锁升级

锁默认情况下,开启了偏向锁延迟。

整个锁升级状态的转变:

 Lock Record以及ObjectMonitor存储的内容:

2.5 重量锁底层ObjectMonitor

三、深入ReentrantLock

3.1 ReentrantLock和synchronized的区别

核心区别:

  • ReentrantLock是个类,synchronized是关键字,当然都是在JVM层面实现互斥锁的方式

效率区别:

  • 如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级概念。而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。

底层实现区别:

  • 实现原理是不一样,ReentrantLock基于AQS实现的,synchronized是基于ObjectMonitor

功能上的区别:

  • ReentrantLock的功能比synchronized更全面。

    • ReentrantLock支持公平锁和非公平锁

    • ReentrantLock可以指定等待锁资源的时间。

选择哪个:如果你对并发编程特别熟练,推荐使用ReentrantLock,功能更丰富。如果掌握的一般般,使用synchronized会更好。

3.2 AQS概述

AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。

首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。

    /**
     * The synchronization state.
     */
    private volatile int state;

其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;

        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;


        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread; 
}

 AQS内部结构和属性:

3.3 加锁流程源码剖析

3.3.1 加锁流程概述

非公平锁的流程:

3.3.2 三种加锁源码分析
public class ReentrantLock implements Lock, java.io.Serializable {

3.3.2.1 lock方法

lock方法的使用:(默认是非公平锁)

import java.util.concurrent.locks.ReentrantLock;

public class test05 {
    private final ReentrantLock lock = new ReentrantLock();//默认是非公平锁(效率高,推荐)
    private final ReentrantLock lock2 = new ReentrantLock(true);// 公平锁
    // ...
    public void method(){
        lock.lock();
        try {
            // ... method body
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
    }
}
    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

lock方法的源码分析:★★★

lock()方法内部都调用了acquire()方法,acquire()方法内部包含tryAcquire方法、addWaiter方法、acquireQueued方法,详细分析见讲义&视频。

3.3.2.2 tryLock方法

boolean tryLock() ——CAS抢一下;

boolean tryLock(long timeout, TimeUnit unit)——略。

3.3.2.3 lockInterruptibly方法

3.3.2.4 总结:Lock 接口的常用方法、区别、使用

在Lock接口中声明了4种方法来获取锁,这4种方法具体有什么区别呢?

1. lock() 

在线程获取锁时如果锁已被其他线程获取,则进行等待,是最初级的获取锁的方法。与此同时,lock() 方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock() 就会陷入永久等待,所以一般用 tryLock() 等其他更高级的方法来代替 lock()。示例代码如下:

Lock lock = ...;
lock.lock();
try{
    //获取到了被本锁保护的资源,处理任务
    //捕获异常
}finally{
    lock.unlock();   //释放锁
}

2. tryLock()

用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回 true,否则返回 false,代表获取锁失败。相比于 lock(),这样的方法显然功能更强大,可以根据是否能获取到锁来决定后续程序的行为。因为该方法会立即返回,即便在拿不到锁时也不会一直等待,所以通常情况下,用 if 语句判断 tryLock() 的返回结果,根据是否获取到锁来执行不同的业务逻辑,典型使用方法如下:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则做其他事情
}

创建 lock() 方法之后使用 tryLock() 方法并用 if 语句判断它的结果,如果 if 语句返回 true,就使用 try finally 完成相关业务逻辑的处理,如果 if 语句返回 false 就会进入 else 语句,代表它暂时不能获取到锁,可以先去做一些其他事情,比如等待几秒钟后重试,或者跳过这个任务,有了这个强大的 tryLock() 方法便可以解决死锁问题,代码如下所示。

    public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
        while (true) {
            if (lock1.tryLock()) {
                try {
                    if (lock2.tryLock()) {
                        try {
                            System.out.println("获取到了两把锁,完成业务逻辑");
                            return;
                        } finally {
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                Thread.sleep(new Random().nextInt(1000));
            }
        }
    }

如果代码中我们不用 tryLock() 方法,那么便可能会产生死锁,比如有两个线程同时调用这个方法,传入的 lock1 和 lock2 恰好是相反的,那么如果第一个线程获取了 lock1 的同时,第二个线程获取了 lock2,它们接下来便会尝试获取对方持有的那把锁,但是又获取不到,于是便会陷入死锁,但是有了 tryLock() 方法之后,便可以避免死锁的发生,首先会检测 lock1 是否能获取到,如果能获取到再尝试获取 lock2,但如果 lock1 获取不到也没有关系,会在下面进行随机时间的等待,这个等待的目标是争取让其他的线程在这段时间完成它的任务,以便释放其他线程所持有的锁,以便后续供我们使用,同理如果获取到了 lock1 但没有获取到 lock2,那么也会释放掉 lock1,随即进行随机的等待,只有当它同时获取到 lock1 和 lock2 的时候,才会进入到里面执行业务逻辑,比如在这里会打印出“获取到了两把锁,完成业务逻辑”,然后方法便会返回。

3. tryLock(long time, TimeUnit unit)

这个方法和 tryLock() 很类似,区别在于 tryLock(long time, TimeUnit unit) 方法会有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。

这个方法解决了 lock() 方法容易发生死锁的问题,使用 tryLock(long time, TimeUnit unit) 时,在等待了一段指定的超时时间后,线程会主动放弃这把锁的获取,避免永久等待;在等待的期间,也可以随时中断线程,这就避免了死锁的发生。本方法和lockInterruptibly() 是非常类似的,来看一下 lockInterruptibly() 方法。

4. lockInterruptibly() 

这个方法的作用就是去获取锁,如果这个锁当前是可以获得的,那么这个方法会立刻返回,但是如果这个锁当前是不能获得的(被其他线程持有),那么当前线程便会开始等待,除非它等到了这把锁或者是在等待的过程中被中断了,否则这个线程便会一直在这里执行这行代码。一句话总结就是,除非当前线程在获取锁期间被中断,否则便会一直尝试获取直到获取到为止。

顾名思义,lockInterruptibly() 是可以响应中断的。相比于不能响应中断的 synchronized 锁,lockInterruptibly() 可以让程序更灵活,可以在获取锁的同时,保持对中断的响应。可以把这个方法理解为超时时间是无穷长的 tryLock(long time, TimeUnit unit),因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断,只不过 lockInterruptibly() 永远不会超时。

这个方法本身是会抛出 InterruptedException 的,所以使用的时候,如果不在方法签名声明抛出该异常,那么就要写两个 try 块,如下所示。

    public void lockInterruptibly() {
        try {
            lock.lockInterruptibly();
            try {
                System.out.println("操作资源");
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

在这个方法中首先执行了 lockInterruptibly 方法,并且对它进行了 try catch 包装,然后同样假设能够获取到这把锁,和之前一样,就必须要使用 try finally 来保障锁的绝对释放。

参考:二十四、“锁”(五)Lock 的常用方法 - 简书

Lock锁的方法使用_lock锁常用方法-CSDN博客

五十、死锁问题 - 简书

3.4 释放锁流程源码剖析

加锁的逻辑:对state加1,重入也加1 ;

释放锁的逻辑:对state减1,直到减为0才会把当前锁资源释放出来,调用的是unlock()方法。

源码分析:

public void unlock() {
    // 释放锁资源不分为公平锁和非公平锁,都是一个sync对象
    sync.release(1);
}

3.5 AQS中常见的问题

3.6 ConditionObject

3.6.1 ConditionObject的介绍&应用

synchronized提供了wait和notify的方法实现线程在持有锁时,可以实现挂起,以及唤醒的操作。

【Java多线程编程】wait与notify方法详解_wait()方法和notify()方法-CSDN博客

滑动验证页面icon-default.png?t=N7T8https://segmentfault.com/a/1190000018096174https://www.cnblogs.com/fenjyang/p/11603229.html

为什么要使用wait()方法和notify()方法?

       线程的调度是无序的,为了解决资源无序抢占问题,使得线程的执行是有序的,需要使用wait()方法来使线程执行有序。

ReentrantLock也拥有这个功能。

ReentrantLock提供了await和signal方法去实现类似wait和notify的功能。

想执行await或者是signal就必须先持有lock锁的资源。

先看一下Condition的应用

public static void main(String[] args) throws InterruptedException, IOException {
    ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    new Thread(() -> {
        lock.lock();
        System.out.println("子线程获取锁资源并await挂起线程");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("子线程挂起后被唤醒!持有锁资源");

    }).start();
    Thread.sleep(100);
    // =================main======================
    lock.lock();
    System.out.println("主线程等待5s拿到锁资源,子线程执行了await方法");
    condition.signal();
    System.out.println("主线程唤醒了await挂起的子线程");
    lock.unlock();
}

ConditionObject类实现了Condition接口,ConditionObject是AbstractQueuedSynchronizer这个抽象类的内部类。

 3.6.2 好文

Java并发编程知识点总结(十二)——Condition中await和signal通知机制_java condition的await和异步的await-CSDN博客

深入详解Condition条件队列、signal和await-腾讯云开发者社区-腾讯云

有图解有案例,我终于把Condition的原理讲透彻了-腾讯云开发者社区-腾讯云

深入详解Condition条件队列、signal和await_condition.await()-CSDN博客

四、深入ReentrantReadWriteLock

synchronized和ReentrantLock都是互斥锁,在读多写少的场景下,使用它们会降低效率。所以,ReentrantReadWriteLock入场了。

ReentrantReadWriteLock读写锁解析-腾讯云开发者社区-腾讯云 (tencent.com)

面试10000次依然会问的【ReentrantLock】,你还不会?-腾讯云开发者社区-腾讯云 (tencent.com)

【多线程系列】基于 AQS 实现的同步器源码精讲(ReentrantLock、ReentrantReadWriteLock)-腾讯云开发者社区-腾讯云 (tencent.com) 读写锁——ReentrantReadWriteLock原理详解-腾讯云开发者社区-腾讯云 (tencent.com)

基本使用: 

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 测试一:在线程1持有”写锁“时,main线程不能再获得”写锁“ 【写写互斥】
 * 测试二:在线程1持有”写锁“时,main线程不能再获得”读锁“ 【读写互斥】
 * 测试三:在线程1持有”读锁“时,main线程可以获得”读锁“ 【读读不互斥,即读读共享】
 * 测试三:在线程1持有”读锁“时,main线程不能再获得”写锁“ 【读写互斥】
 */
public class testReadWriteLock {
    static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();//获得“读锁”对象

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            readLock.lock();//也可以省略上面【获得“读锁”对象】那一行代码,直接写为:lock.readLock().lock();
            try {
                System.out.println("线程1获得了锁");
                System.out.println(Thread.currentThread());//打印当前线程的名字:Thread[线程1,5,main]
                try {
                    Thread.sleep(500000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                readLock.unlock();
            }
        },"线程1").start();

        Thread.sleep(1000);//这里是为了首先让线程1获得锁
        writeLock.lock();
        try {
            System.out.println("main线程获得了锁");
        } finally {
            writeLock.unlock();
        }
    }
}

读写锁的锁降级 



读写锁(ReentrantReadWriteLock)-腾讯云开发者社区-腾讯云

读写锁ReentrantReadWriteLock之锁降级 - 简书

简洁明了的ReentrantReadWriteLock总结_reentrantreadwritelock 锁降级-CSDN博客

JAVA并发编程——ReentrantReadWriteLock锁降级和StampedLock

五、死锁

概念,产生的原因,解决方案

java并发系列之:死锁_java 死锁_QR_adaptor的博客-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值