Java 多线程8 - locks 锁

发布于 2024-06-11 23:12:20 字数 10862 浏览 19 评论 0

任何一个新引入的知识都是为了解决以往系统中出现的问题,否则新引入的将变得毫无价值,如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁。

但当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象,通过 Lock 就可以实现。

Lock 接口, 提供了与 synchronized 一样的锁功能。虽然它失去了像 synchronize 关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性,Lock 必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

一个线程获取多少次锁,就必须释放多少次锁。这对于内置锁也是适用的,每一次进入和离开 synchronized 方法(代码块),就是一次完整的锁获取和释放。

锁的分类

  • 悲观锁:悲观锁,每次去请求数据的时候,都认为数据会被抢占更新(悲观的想法);所以每次操作数据时都要先加上锁,其他线程修改数据时就要等待获取锁。适用于写多读少的场景,synchronized 就是一种悲观锁
  • 乐观锁:在请求数据时,觉得无人抢占修改。等真正更新数据时,才判断此期间别人有没有修改过(预先读出一个版本号或者更新时间戳,更新时判断是否变化,没变则期间无人修改);和悲观锁不同的是,期间数据允许其他线程修改
  • 自旋锁:一句话,魔力转转圈。当尝试给资源加锁却被其他线程先锁定时,不是阻塞等待而是循环再次加锁
    在锁常被短暂持有的场景下,线程阻塞挂起导致 CPU 上下文频繁切换,这可用自旋锁解决;但自旋期间它占用 CPU 空转,因此不适用长时间持有锁的场景

lock

Lock

public interface Lock {

//获取锁。如果锁已被其他线程获取,则进行等待
void lock();


//通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过 lock.lockInterruptibly() 想获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有在等待,那么对线程 B 调用 threadB.interrupt() 方法能够中断线程 B 的等待过程
void lockInterruptibly() throws InterruptedException;

//tryLock() 方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回 true,如果获取失败(即锁已被其他线程获取),则返回 false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待
boolean tryLock();

//这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回 false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

//释放锁
void unlock();

//获取与 lock 绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回
Condition newCondition();
}

一般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被被释放,防止死锁的发生():

锁【lock.lock】必须紧跟 try 代码块,且 unlock 要放到 finally 第一行。

ReentrantLock

可重入锁, 支持重入性,表示能够对共享资源重复加锁,即当前线程获取该锁再次获取不会被阻塞。ReentrantLock 实现了 Lock 接口的,并且 ReentrantLock 提供了更多的方法

JVM 允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁

public class ReentrantLockTest1 {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock();

public static void main(String[] args) {
final LocksTest test = new LocksTest();

new Thread(() -> test.insert(Thread.currentThread())).start();

new Thread(() -> test.insert(Thread.currentThread())).start();
}

public void insert(Thread thread) {
lock.lock();
try {
System.out.println(thread.getName() + "得到了锁");
for (int i = 0; i < 5; i++) {
arrayList.add(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(thread.getName() + "释放了锁");
lock.unlock();
}
}

}

一般情况下通过 tryLock 来获取锁时是这样使用的

public class ReentrantLockTest1 {
//......
public void insert(Thread thread) {
if(lock.tryLock()) {
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
} else {
System.out.println(thread.getName()+"获取锁失败");
}
}
}


//打印
//Thread-0 得到了锁
//Thread-1 获取锁失败
//Thread-0 释放了锁

由于 lockInterruptibly() 的声明中抛出了异常,所以 lock.lockInterruptibly() 必须放在 try 块中或者在调用 lockInterruptibly() 的方法外声明抛出 InterruptedException

public class InterruptTest {

private Lock lock = new ReentrantLock();
public static void main(String[] args) {
InterruptTest test = new InterruptTest();
MyThread thread1 = new MyThread(test);
MyThread thread2 = new MyThread(test);
thread1.start();
thread2.start();

try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}

public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将 InterruptedException 抛出
try {
System.out.println(thread.getName()+"得到了锁");
long startTime = System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) {
break;
}
//插入数据
}
}
finally {
System.out.println(Thread.currentThread().getName()+"执行 finally");
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}

}

class MyThread extends Thread {
private InterruptTest test;
public MyThread(InterruptTest test) {
this.test = test;
}
@Override
public void run() {

try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中断");
}
}
}


ReadWriteLock

ReadWriteLock 也是一个接口,只有两个方法:

public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();

/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作

使用 ReadWriteLock 时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改, 适合读多写少的场景

ReentrantReadWriteLock

ReentrantReadWriteLock 实现了 ReadWriteLock 接口,并添加了可重入的特性

如果在系统中,读操作次数远远大于写操作,则读写锁就可以发挥最大的功效,提升系统的性能

Lock 和 synchronized 的选择

  总结来说,Lock 和 synchronized 有以下几点不同:

  1)Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;

  2)synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock() 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;

  3)Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;

  4)通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

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

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。所以说,在具体使用时要根据适当情况选择

Condition

它用来替代传统的 Object 的 wait()、notify() 实现线程间的协作,相比使用 Object 的 wait()、notify(),使用 Condition 的 await()、signal() 这种方式实现线程间协作更加安全和高效。使用 Condition 可以实现等待/唤醒,并且能够唤醒制定线程

Condition 可以替代 wait 和 notify;Condition 对象必须从 Lock 对象获取

LockSupport

LockSupport 是一个工具类,可以让线程在任意位置阻塞,也可以在任意位置唤醒,它的内部其实两类主要的方法:park(停车阻塞线程)和 unpark(启动唤醒线程):

// 暂停当前线程
public static void park(Object blocker);

// 暂停当前线程,不过有超时时间的限制
public static void parkNanos(Object blocker, long nanos);
public static void parkNanos(long nanos);

// 暂停当前线程,直到某个时间
public static void parkUntil(Object blocker, long deadline);
public static void parkUntil(long deadline);

// 无期限暂停当前线程
public static void park();


// 恢复当前线程
public static void unpark(Thread thread);

//blocker 的作用是在 dump 线程的时候看到阻塞对象的信息
public static Object getBlocker(Thread t);

//java14 新增了设置 blocker 的方法
public static void setCurrentBlocker(Object blocker);
public class LockSupportTest {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");

public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super(name);
}
@Override public void run() {
synchronized (u) {
System.out.println(Thread.currentThread() +"in " + getName());
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread() +"被中断了");
}
System.out.println(Thread.currentThread() + "继续执行");
}
}
}

public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000L);
t2.start();
Thread.sleep(3000L);
t1.interrupt();
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}

LockSuport 主要是针对 Thread 进进行阻塞处理,可以指定阻塞队列的目标对象,每次可以指定具体的线程唤醒。Object.wait() 是以对象为纬度,阻塞当前的线程和唤醒单个(随机) 或者所有线程

park 和 unpark 可以实现类似 wait 和 notify 的功能,但是并不和 wait 和 notify 交叉,也就是说 unpark 不会对 wait 起作用,notify 也不会对 park 起作用

StampedLock

之前的锁或多或少都存在一些缺点,比如 synchronized 不可中断等,ReentrantLock 未能读写分离实现,虽然 ReentrantReadWriteLock 能够读写分离了,但是对于其写锁想要获取的话,就必须没有任何其他读写锁存在才可以,这实现了悲观读取。而且如果读操作很多,写很少的情况下,线程有可能遭遇饥饿问题。

饥饿问题:ReentrantReadWriteLock 实现了读写分离,想要获取读锁就必须确保当前没有其他任何读写锁了,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,因为当前有可能会一直存在读锁。而无法获得写锁

所以 java8 引入了新的锁 StampedLock,这个类没有直接实现 Lock 或者 ReadWriteLock 方法,源码中是把他当作一个单独的类来实现的。相比于普通的 ReentranReadWriteLock 主要多了一种乐观读的功能。当然,一个 StampedLock 可以通过 asReadLock,asWriteLock,asReadWriteLock 方法来得到全部功能的子集

AbstractQwnableSynchronizer

抽象拥有同步器,简称 AOS

AbstractQueuedSynchronizer

提供了一个基于 FIFO 队列,可以用于构建锁或者其他相关同步装置的基础框架, 简称 AQS

AbstractQueuedLongSynchronizer

扩展自 AbstractQueuedSynchronizer

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

依 靠

暂无简介

0 文章
0 评论
24 人气
更多

推荐作者

我们的影子

文章 0 评论 0

素年丶

文章 0 评论 0

南笙

文章 0 评论 0

18215568913

文章 0 评论 0

qq_xk7Ean

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文