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

2,312 阅读6分钟

本章内容涵盖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]
 * ...
 */

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

在这里插入图片描述

代码中也稍微提了一下死锁的概念,在使用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. 若此时已经有一个线程占用了读锁,此时其他线程申请读锁是可以的,但是若此时其他线程申请写锁,则只有等待读锁释放,才能成功获得。
  2. 若此时已经有一个线程占用了写锁,那么此时其他线程申请写锁或读锁,都只有持有写锁的线程释放写锁,才能成功获得。

六、Lock 与的 Synchronized 区别

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

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


七、自言自语

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

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

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

希望与君共勉😁

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