JUC系列(三) | Lock 锁机制详解 代码理论相结合

简介: JUC系列(三) | Lock 锁机制详解 代码理论相结合

微信截图_20220524220130.png

本章内容涵盖Lock的使用讲解,可重入锁、读写锁。Lock和Synchronized的对比等。 多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!


沉下去,再浮上来,我想我们会变的不一样的。


阳光正好,家给人的感觉真的很舒服


JUC系列

正在持续更新中...


一、什么是 Lock


Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。


二、锁类型


可重入锁:在执行对象中所有同步方法不用再次获得锁


可中断锁:在等待获取锁过程中可中断


公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利


读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写


三、Lock接口


public interface Lock {
    void lock(); //获得锁。
    /**
    除非当前线程被中断,否则获取锁。
  如果可用,则获取锁并立即返回。
  如果锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下两种情况之一:
    锁被当前线程获取; 
    要么其他一些线程中断当前线程,支持中断获取锁。
  如果当前线程:
    在进入此方法时设置其中断状态; 
    要么获取锁时中断,支持中断获取锁,
    */
    void lockInterruptibly() throws InterruptedException; 
    /**
    仅在调用时空闲时才获取锁。
  如果可用,则获取锁并立即返回值为true 。 如果锁不可用,则此方法将立即返回false值。
  */
    boolean tryLock();
    //比上面多一个等待时间 
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  
    // 解锁
    void unlock(); 
    //返回绑定到此Lock实例的新Condition实例。
    Condition newCondition();  。
}


下面讲几个常用方法的使用。


3.1、lock()、unlock()


lock()是最常用的方法之一,作用就是获取锁,如果锁已经被其他线程获得,则当前线程将被禁用以进行线程调度,并处于休眠状态,等待,直到获取锁。


如果使用到了lock的话,那么必须去主动释放锁,就算发生了异常,也需要我们主动释放锁,因为lock并不会像synchronized一样被自动释放。所以使用lock的话,必须是在try{}catch(){}中进行,并将释放锁的代码放在finally{}中,以确保锁一定会被释放,以防止死锁现象的发生。


unlock()的作用就是主动释放锁。


lock接口的类型有好几个实现类,这里是随便找了个哈。


Lock lock = new ReentrantLock();
try {
    lock.lock();
    System.out.println("上锁了");
}catch (Exception e){
    e.printStackTrace();
}finally {
    lock.unlock();
    System.out.println("解锁了");
}


3.2、newCondition


关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类 也可以实现等待/通知模式。 用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以 进行选择性通知, Condition 比较常用的两个方法:


  • await():会使当前线程等待,同时会释放锁,当等到其他线程调用signal()方法时,此时这个沉睡线程会重新获得锁并继续执行代码(在哪里沉睡就在哪里唤醒)。


  • signal():用于唤醒一个等待的线程。


注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关 的 Lock 锁,调用 await()后线程会释放这个锁,在调用singal()方法后会从当前 Condition对象的等待队列中,唤醒一个线程,后被唤醒的线程开始尝试去获得锁, 一旦成功获得锁就继续往下执行。


在这个地方我们举个例子来用代码写一下哈:


这里就不举例synchronized 实现了,道理都差不多。


例子:我们有两个线程,实现对一个初始值是0的number变量,一个线程当number = =0时 对number值+1,另外一个线程当number = = 1时对number-1。


class Share {
    private Integer number = 0;
    private ReentrantLock lock = new ReentrantLock();
    private Condition newCondition = lock.newCondition();
    // +1 的方法
    public void incr() {
        try {
            lock.lock(); // 加锁
            while (number != 0) {
                newCondition.await();//沉睡
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            newCondition.signal(); //唤醒另一个沉睡的线程 
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    // -1 的方法
    public void decr() {
        try {
            lock.lock();
            while (number != 1) {
                newCondition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            newCondition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public class LockDemo2 {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(()->{
            for (int i=0;i<=10;i++){
                share.incr();
            }
        },"AA").start();
        new Thread(()->{
            for (int i=0;i<=10;i++){
                share.decr();
            }
        },"BB").start();
        /**
         * AA::1
         * BB::0
         * AA::1
         * BB::0
         * .....
         */     
    }
}


四、ReentrantLock (可重入锁)


ReentrantLock,意思是“可重入锁”。ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更 多的方法。


可重入锁:什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。


package com.crush.juc02;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    System.out.println("第1次获取锁,这个锁是:" + lock);
                    for (int i = 2;i<=11;i++){
                        try {
                            lock.lock();
                            System.out.println("第" + i + "次获取锁,这个锁是:" + lock);
                            try {
                                Thread.sleep(new Random().nextInt(200));
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        } finally {
                           lock.unlock();// 如果把这里注释掉的话,那么程序就会陷入死锁当中。
                        }
                    }
                } finally {
                    lock.unlock();
                }
            }
        }).start();
    new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          lock.lock();
                    System.out.println("这里是为了测试死锁而多写一个的线程");
        } finally {
          lock.unlock();
        }
      }
    }).start();
    }
}
/**
 * 第1次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
 * 第2次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
 * 第3次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
 * ...
 */


死锁的话,程序就无法停止,直到资源耗尽或主动终止。


微信截图_20220524220409.png


代码中也稍微提了一下死锁的概念,在使用Lock中必须手动解锁,不然就会可能造成死锁的现象。


五、ReadWriteLock (读写锁)


ReadWriteLock 也是一个接口,在它里面只定义了两个方法:


public interface ReadWriteLock {
    // 获取读锁
    Lock readLock();
  // 获取写锁
    Lock writeLock();
}


分为一个读锁一个写锁,将读写进行了分离,使可以多个线程进行读操作,从而提高了效率。


ReentrantReadWriteLock 实现了 ReadWriteLock 接口。里面提供了更丰富的方法,当然最主要的还是获取写锁(writeLock)和读锁(readLock)。


5.1、案例


假如多个线程要进行读的操作,我们用Synchronized 来实现的话。


public class SynchronizedDemo2 {
    public static void main(String[] args) {
        final SynchronizedDemo2 test = new SynchronizedDemo2();
        new Thread(()->{
            test.get(Thread.currentThread());
        }).start();
        new Thread(()->{
            test.get(Thread.currentThread());
        }).start();
    }
    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在进行读操作");
        }
        System.out.println(thread.getName()+"读操作完毕");
    }
}
/**
 * 输出
 * Thread-0正在进行读操作
 * Thread-0读操作完毕
 * Thread-1正在进行读操作
 * Thread-1正在进行读操作
 * Thread-1正在进行读操作
 * ....
 * Thread-1读操作完毕
 */


改成读写锁之后


public class SynchronizedDemo2 {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    public static void main(String[] args) {
        final SynchronizedDemo2 test = new SynchronizedDemo2();
        new Thread(()->{
            test.get2(Thread.currentThread());
        }).start();
        new Thread(()->{
            test.get2(Thread.currentThread());
        }).start();
    }
    public void get2(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操作");
            }
            System.out.println(thread.getName()+"读操作完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}
/**
 * 输出
 * Thread-0正在进行读操作
 * Thread-0读操作完毕
 * Thread-1正在进行读操作
 * Thread-1读操作完毕
 */


结论:改用读写锁后 线程1和线程2 同时在读,可以感受到效率的明显提升。


注意:


  1. 若此时已经有一个线程占用了读锁,此时其他线程申请读锁是可以的,但是若此时其他线程申请写锁,则只有等待读锁释放,才能成功获得。


  1. 若此时已经有一个线程占用了写锁,那么此时其他线程申请写锁或读锁,都只有持有写锁的线程释放写锁,才能成功获得。


六、Lock 与的 Synchronized 区别


类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是一个接口
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待
锁的释放 1、当 synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用 (不需要手动释放锁)2、若线程执行发生异常,jvm会让线程释放锁 在finally中必须释放锁,不然容易造成线程死锁现象 (需要手动释放锁)
锁状态 无法判断 可以判断
锁类型 锁类型 可重入 可判断 可公平(两者皆可)
性能 前提:大量线程情况下 同步效率较低 前提:大量线程情况下 同步效率比synchronized高的多


Lock可以提高多个线程进行读操作的效率。


七、自言自语


最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。


正在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。


你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。


希望与君共勉😁


待我们,别时相见时,都已有所成


目录
相关文章
|
Java 编译器
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
|
1月前
|
安全 Java 开发者
Lock锁和AQS之间的关系与区别:基于第一原理的深入分析
【11月更文挑战第26天】在Java并发编程中,锁(Lock)和队列同步器(AbstractQueuedSynchronizer,简称AQS)是两个核心概念。它们为多线程环境下的同步和互斥提供了强大的支持。本文将从第一原理出发,深入探讨Lock锁和AQS之间的关系与区别,同时分析它们的相关概念、业务场景、历史背景、功能点以及底层原理,并使用Java代码进行模拟实现。
24 1
|
3月前
|
缓存 Java 编译器
JAVA并发编程volatile核心原理
volatile是轻量级的并发解决方案,volatile修饰的变量,在多线程并发读写场景下,可以保证变量的可见性和有序性,具体是如何实现可见性和有序性。以及volatile缺点是什么?
|
3月前
|
Java
JAVA并发编程ReentrantLock核心原理剖析
本文介绍了Java并发编程中ReentrantLock的重要性和优势,详细解析了其原理及源码实现。ReentrantLock作为一种可重入锁,弥补了synchronized的不足,如支持公平锁与非公平锁、响应中断等。文章通过源码分析,展示了ReentrantLock如何基于AQS实现公平锁和非公平锁,并解释了两者的具体实现过程。
|
3月前
|
安全 Java API
Java线程池原理与锁机制分析
综上所述,Java线程池和锁机制是并发编程中极其重要的两个部分。线程池主要用于管理线程的生命周期和执行并发任务,而锁机制则用于保障线程安全和防止数据的并发错误。它们深入地结合在一起,成为Java高效并发编程实践中的关键要素。
35 0
|
7月前
|
安全 算法 关系型数据库
线程安全--深入探究线程等待机制和死锁问题
线程安全--深入探究线程等待机制和死锁问题
229 1
|
7月前
|
算法 安全 Java
Java多线程基础-14:并发编程中常见的锁策略(一)
乐观锁和悲观锁是并发控制的两种策略。悲观锁假设数据容易产生冲突,因此在读取时即加锁,防止其他线程修改,可能导致效率较低。
66 0
|
7月前
|
安全 Java 调度
Java多线程基础-14:并发编程中常见的锁策略(二)
这段内容介绍了互斥锁和读写锁的概念以及它们在多线程环境中的应用。互斥锁仅允许进入和退出代码块时加锁和解锁,而读写锁则区分读和写操作,允许多个线程同时读但写时互斥。
61 0
|
7月前
|
Java 大数据 程序员
Java并发编程中的锁机制探究
传统的Java并发编程中,锁机制一直是保证多线程程序安全性的重要手段之一。本文将深入探讨Java中常见的锁机制,包括synchronized关键字、ReentrantLock类以及java.util.concurrent包下的各种锁实现,分析它们的特点、适用场景以及性能表现,帮助开发者更好地理解和应用这些锁机制。
40 1
|
7月前
|
安全 Java 开发者
Java并发编程中的线程安全性探究
在Java编程中,线程安全性是一个至关重要的问题,涉及到多线程并发访问共享资源时可能出现的数据竞争和不一致性问题。本文将深入探讨Java并发编程中的线程安全性,介绍常见的线程安全性问题以及解决方法,帮助开发者更好地理解和应对在多线程环境下的挑战。