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

2022-10-31 14:59:28 浏览数 (1)

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

总是很喜欢这样的天

JUC 系列

  • JUC系列(一)什么是JUC?
  • JUC系列(二)回顾Synchronized关键字
  • JUC系列(三)Lock 锁机制详解 代码理论相结合 正在持续更新中…

一、什么是 Lock

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

二、锁类型

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

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

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

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

三、Lock接口

代码语言:javascript复制
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接口的类型有好几个实现类,这里是随便找了个哈。

代码语言:javascript复制
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。

代码语言:javascript复制
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 提供了更 多的方法。

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

代码语言:javascript复制
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 也是一个接口,在它里面只定义了两个方法:

代码语言:javascript复制
public interface ReadWriteLock {
	
    // 获取读锁
    Lock readLock();

	// 获取写锁
    Lock writeLock();
}

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

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

5.1、案例

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

代码语言:javascript复制
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读操作完毕
 */

改成读写锁之后

代码语言:javascript复制
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 区别

类别

synchronized

Lock

存在层次

Java的关键字,在jvm层面上

是一个接口

锁的获取

假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待

分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待

锁的释放

1、当 synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用 (不需要手动释放锁)2、若线程执行发生异常,jvm会让线程释放锁

在finally中必须释放锁,不然容易造成线程死锁现象 (需要手动释放锁)

锁状态

无法判断

可以判断

锁类型

锁类型

可重入 可判断 可公平(两者皆可)

性能

前提:大量线程情况下 同步效率较低

前提:大量线程情况下 同步效率比synchronized高的多

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


七、自言自语

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

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

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

希望与君共勉

0 人点赞