在并发编程中,为了保证线程的原子执行,需要使用锁,jvm 内 可以使用 synchronized 和 ReentrantLock,如果是集群部署,我们可以使用Redis 分布式锁 其他的锁后面再介绍。
1、ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与synchronized(1.8之后性能得到提升)会被JVM自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁
2、ReentrantLock 相比 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, 否则返回 false
boolean 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() {
@Override
public 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 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 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与
release()方法来获得和释放临界资源。
在springboot中使用单元测试,直接上代码:
@Test
public 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() {
@Override
public 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获取到锁