背景
在并发编程中,为了保证线程的原子执行,需要使用锁,jvm 内 可以使用 synchronized 和 ReentrantLock,如果是集群部署,我们可以使用Redis 分布式锁 其他的锁后面再介绍。
ReentrantLock 和 synchronized
1、ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与synchronized(1.8之后性能得到提升)会被JVM自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁
2、ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁
简述
成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法
Lock 在 java.util.concurrent.locks 下
ReentrantLock 实现了 Lock 接口,拥有下面几个方法:
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;public interface Lock {//执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经
被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁void lock();//如果当前线程未被中断,获取锁void lockInterruptibly() throws InterruptedException;//如果锁可用, 则获取锁, 并立即返回 true, 否则返回 falseboolean tryLock();//如果锁在给定等待时间内没有被另一个线程保持,则获取该锁并返回 true,否则返回 false。boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程
并不持有锁, 却执行该方法, 可能导致异常的发生void unlock();//条件对象,获取等待通知组件Condition newCondition();
}
初始化 锁
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class LockTest {Lock lock = new ReentrantLock();Condition condition = lock.newCondition();
}
说明
1、同一时刻只有一个线程能获取独占锁(lock.lock()),持有了这个锁的人才能调用condition.await()和 condition.sign()方法。
2、调用 condition.await()方法会释放独占锁(lock.unlock),并且将自身加入到condition队列中。
3、Thread-signal-1线程获取lock 后,condition.signal()方法不会立即唤醒await中的线程,而是将condition队列中的线程转移到AQS队列中。
4、当持锁线程释放锁时,AQS队列的线程再抢到则会被唤醒。
线程池中使用锁
1、我们使用线程池 来演示 如何使用 ReentrantLock,首先创建一个 固定的线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
2、发起10个线程的并发处理,这里使用for 循环来提交 线程到线程池
3、每个线程 实现 Runnable 接口的run 方法
4、run方法中尝试获取锁,等待2秒,获取到锁后,休眠10秒,然后调用await 方法,在调用await方法时,当前线程会把自身加入到condition中,同时释放 lock 锁(当然这里也有部分线程获取不到锁)。
5、run 方法执行完成后,本次请求结束。
6、请求unlock路由,这时线程会去尝试获取 lock , 然后 调用 condition.signal() 方法 来唤起刚刚通过 condition.await() 加入到condition 队列中的线程,当lock.unlock() 后,会执行刚刚加入condition队列中线程的剩余代码并打印 "signal-> await 执行剩余的代码:pool-4-thread-1"。
package com.yd.controller.user;import com.yd.entity.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;@RestController
@RequestMapping("/admin/user")
public class LockTest {Lock lock = new ReentrantLock();Condition condition = lock.newCondition();@GetMapping("exec")public void execs() {ExecutorService executor = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executor.execute(new Runnable() {@Overridepublic void run() {try {if (lock.tryLock(2, TimeUnit.SECONDS)) {System.out.println("获取到锁:" + Thread.currentThread().getName());TimeUnit.SECONDS.sleep(10);condition.await(); //此处会将当前线程放到队列condition,并释放lock锁,其余线程才能获取到锁,但下面的代码不会被执行,只有当signal() 方法被调用时,才会打印下面的代码System.out.println("signal-> await 执行剩余的代码:" + Thread.currentThread().getName());lock.unlock();} else {System.out.println("未获取锁" + Thread.currentThread().getName());}} catch (InterruptedException e) {throw new RuntimeException(e);}}});}executor.shutdown();}@GetMapping("unlock")public ResponseEntity<String> unlock() {lock.lock();condition.signalAll();lock.unlock();return ResponseEntity.success("yes");}
}
启动springboot 后,访问
curl -X GET http://127.0.0.1:8089/admin/user/exec
执行结果如下,只有 thread-1 获取到锁,其余线程均未获取到锁
获取到锁:pool-4-thread-1
未获取锁pool-4-thread-2
未获取锁pool-4-thread-7
未获取锁pool-4-thread-4
未获取锁pool-4-thread-5
未获取锁pool-4-thread-6
未获取锁pool-4-thread-10
未获取锁pool-4-thread-3
未获取锁pool-4-thread-8
未获取锁pool-4-thread-9
接下来访问
curl http://127.0.0.1:8089/admin/user/unlock
执行结果如下,会将上面 thread-1 加入到condition 队列中的剩余代码执行完毕。
signal-> await 执行剩余的代码:pool-4-thread-1
注意事项
在单元测试中,想要看到锁的效果,需要在代码后面加一个 睡眠提醒(Thread.sleep(10000),主线程结束后,锁的运行逻辑表现不出来。
在springboot中,主线程默认是后台运行的,没有影响。
Semaphore 信号量
Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信
号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来
构建一些对象池,资源池之类的,比如数据库连接池。
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与
release()方法来获得和释放临界资源。
在springboot中使用单元测试,直接上代码:
@Testpublic void testSem() throws InterruptedException {Semaphore semaphore = new Semaphore(5);ExecutorService executor = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executor.execute(new Runnable() {@Overridepublic void run() {try {semaphore.acquire();Thread.sleep(4000);//DateFormatUtils 使用时,需要引入common-lang3 包System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss") + "获取到锁");} catch (InterruptedException e) {throw new RuntimeException(e);} finally {semaphore.release();}}});}executor.shutdown();//让主线程挂起,否则程序直接退出Thread.sleep(20000);}
执行结果如下, 开启10个线程来执行,每次最多5个线程获取到锁
2023-08-29 19:50:52获取到锁
2023-08-29 19:50:52获取到锁
2023-08-29 19:50:52获取到锁
2023-08-29 19:50:52获取到锁
2023-08-29 19:50:52获取到锁
2023-08-29 19:50:56获取到锁
2023-08-29 19:50:56获取到锁
2023-08-29 19:50:56获取到锁
2023-08-29 19:50:56获取到锁
2023-08-29 19:50:56获取到锁