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学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。


希望与君共勉😁


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


目录
相关文章
|
9月前
|
缓存 安全 Java
JUC并发编程-共享模型无锁
CAS 与 volatile CAS概述 CAS的全称是: Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。 在JUC( java.util.concurrent )包下实现的很多类都用到了CAS操作 AbstractQueuedSynchronizer(AQS框架) AtomicXXX类 例子: 我们基于JMM内存模型进行说明 线程1与线程2都从主内存中获取变量int a = 100,同时放到各个线程的工作内存中 一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修
27 0
|
10月前
|
Java 编译器
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
|
2月前
|
安全 Java 调度
Java多线程基础-14:并发编程中常见的锁策略(二)
这段内容介绍了互斥锁和读写锁的概念以及它们在多线程环境中的应用。互斥锁仅允许进入和退出代码块时加锁和解锁,而读写锁则区分读和写操作,允许多个线程同时读但写时互斥。
29 0
|
2月前
|
算法 安全 Java
Java多线程基础-14:并发编程中常见的锁策略(一)
乐观锁和悲观锁是并发控制的两种策略。悲观锁假设数据容易产生冲突,因此在读取时即加锁,防止其他线程修改,可能导致效率较低。
32 0
|
2月前
|
安全 算法 Java
Java多线程基础-15:Java 中 synchronized 的优化操作 -- 锁升级、锁消除、锁粗化
`synchronized`在Java并发编程中具有以下特性:开始时是乐观锁,竞争激烈时转为悲观锁;从轻量级锁升级至重量级锁;常使用自旋锁策略;是不公平且可重入的;不支持读写锁。
30 0
|
2月前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
28 0
|
2月前
|
存储 安全 Java
【亮剑】Java并发编程涉及`ThreadLocal`、`Volatile`、`Synchronized`和`Atomic`四个关键机制
【4月更文挑战第30天】Java并发编程涉及`ThreadLocal`、`Volatile`、`Synchronized`和`Atomic`四个关键机制。`ThreadLocal`为每个线程提供独立变量副本;`Volatile`确保变量可见性,但不保证原子性;`Synchronized`实现同步锁,保证单线程执行;`Atomic`类利用CAS实现无锁并发控制。理解其原理有助于编写高效线程安全代码。根据业务场景选择合适机制至关重要。
|
2月前
|
Java 大数据 程序员
Java并发编程中的锁机制探究
传统的Java并发编程中,锁机制一直是保证多线程程序安全性的重要手段之一。本文将深入探讨Java中常见的锁机制,包括synchronized关键字、ReentrantLock类以及java.util.concurrent包下的各种锁实现,分析它们的特点、适用场景以及性能表现,帮助开发者更好地理解和应用这些锁机制。
19 1
|
11月前
|
存储 Java
【一文读懂】 Java并发 - 锁升级原理
Java对象头,锁升级的原因,重量级锁、轻量级锁、偏向锁的原理
221 0
【一文读懂】 Java并发 - 锁升级原理
|
存储 SpringCloudAlibaba 安全
JUC并发编程(四):synchronized底层原理和锁升级优化
`synchronized`翻译过来是**同步**的意思,它是Java中一个关键字,是JVM层面提供的同步锁机制,用于保证多线程访问同一资源的可见性、互斥性。即当一个线程已经获取资源锁时,其他试图获取的线程只能等待或者阻塞在那里。
73 0
JUC并发编程(四):synchronized底层原理和锁升级优化