java基础之ReentrantLock锁
Lock锁的公平性和非公平性
1、lock锁项目使用
在项目中的使用方式:
public class AQSTestOne {
// 使用公平锁来进行测试
private static final Lock LOCK = new ReentrantLock(true);
public static void main(String[] args) {
LOCK.lock();
try {
System.out.println("so something ");
}catch (Exception e){
System.out.println("do Exception something............");
}finally {
LOCK.unlock();
}
}
}
因为对于对象来说,对于成员变量LOCK锁来说,会在堆内存中,任何一个线程进来的时候执行了对应的方法,都会执行到lock锁上来进行排队。
每个线程都会来使用lock锁,那么lock.lock()方法是如何保证多线程环境下,在JVM中在某一个时刻,只有一个线程占用锁呢?
2、AQS继承体系
对于ReentrantLock类中,存在AbstractQueuedSynchronizer类以及对应的子类Sync和Sync的两个子类:FairSync和NonfairSync
对应的是公平同步锁和非公平同步锁。
3、构造函数
首先从构造函数来讲起,因为非公平锁的效率高,所以推荐使用的是非公平锁。
从构造函数中可以看到对应的结构:
private final Sync sync;
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 默认采用非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 继承体系图
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
}
4、加锁流程
lock锁是如何保证多线程能够保证线程安全呢?
那么看一下lock锁又是如何来进行加锁的。
首先来看这行代码到底做了什么?
lock.lock();
进源码查看:
public void lock() {
sync.lock();
}
那么这里看公平锁的实现方式:
final void lock() {
acquire(1);
}
重点就来到了acquire方法,看看对应的代码实现:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
通过代码可以看到首先会来尝试获取。
可以根据代码来画一幅流程图来描述是如何来获取得到锁的:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 判断当前锁是否被其他线程持有!利用一个int类型的变量来进行修饰
int c = getState();
if (c == 0) {
// 这里判断是无锁状态之后,并不是直接获取得到锁,还需要来进行判断CLH队列中是否有线程在排队
// 因为这里是公平锁,公平锁就需要保证如果CLH队列中有在排队的线程,那么让他们先获取得到
if ( !hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
// 设置为独占
setExclusiveOwnerThread(current);
return true;
}
}
// 如果是当前的线程,那么表示的是可重入锁。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
看一下是如何查看队列中是否是有线程在排队的:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
}
在这里来组成一个双向链表,又称之为CLH队列。head指向队头,tail指向队尾。
如果h!=t,表示的是CLH队列中是有线程排队的,后面的两个判断只要有一个判断是成功的,那么就说明队列中是有值的。
两个判断:1、如果头结点的下一个节点为空;2、头结点的下一个节点不为空并且不为当前线程;
如果返回true的话,那么表示CLH队列中是有线程在排队的;
如果是false的话,那么表示的是CLH队列中是没有线程在排队的。
这里就是直接判断是否存在首节点,head的节点的下一个节点(首节点)是有是有值的,如果有值,那么说明CLH队列中是有值的。
如果队列中没有线程在等待锁,可以看到利用CAS来获取得到锁,然后设置锁被哪个线程获取得到;
如果已经有线程持有了锁,那么判断是否是当前的线程,如果是,那么进行再次加锁;
如果队列中有线程在排队等待锁并且不是当前线程,那么直接获取得到锁失败;
那么对应的流程如下所示:
对应的流程如下所示:
- 1、每个线程在获取锁的时候,判断锁的状态,如果是无锁状态,那么进入到队列中查看队列中是否有其它线程,如果没有,那么去获取得到锁;如果有的话,那么获取锁事变,排队等锁;
- 2、如果当前线程检查是有锁状态,那么判断持有锁的是否是当前线程,如果是,那么锁重入次数+1;
- 3、如果不是当前线程持有锁,那么获取得到锁失败;
4.1、加锁流程的两种情况
总结起来,获取得到锁的线程只有两种情况:
1、当前锁状态是无锁,且队列中没有线程在排队,那么获取得到锁;
2、当前锁状态是有锁,且持有锁的线程是自己,那么这个时候锁是可重入的;
5、线程没有抢到锁之后需要排队
没有抢到锁的线程需要进行排队,继续看下源码:
public final void acquire(int arg) {
// 没有抢到锁,返回false,取反为true,那么执行后面的逻辑
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先会创建线程等待者,当前是独占模式:
private Node addWaiter(Node mode) {
// 创建节点保存当前的线程
Node node = new Node(Thread.currentThread(), mode);
// 获取得到尾结点
Node pred = tail;
if (pred != null) {
// 如果尾结点不为空
// 1、首先将尾节点的前驱指针指针尾指针指向的节点
node.prev = pred;
// 2、比较并交换,并将尾结点指针后移一位
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 这里有两种情况存在。因为上面如果存在多线程竞争,称为不了尾结点的线程将会走到这里来。看一下入队操作
// 1、tail为null;2、比较并交换称为尾结点的节点
enq(node);
return node;
}
注意看下上面的if判断,在入队尾的时进行比较并交换时,是失败的,那么这个时候将会再次执行enq方法。
看一下enq方法:
private Node enq(final Node node) {
// 死循环
for (;;) {
Node t = tail;
// 如果tail为null,那么这种也是上面的一种的情况。这里需要来进行初始化
// 从这里也可以看到初始化的是一个空节点,不保存任何线程
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 上面的另外一种情况。和上面的addWaiter中判断是一致的
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这里一直在使用死循环,这里存在着两个作用:
-
1、初始化,让head和tail节点指向一个空的节点;
-
2、将排队的节点称为尾结点(队列特点:先进先出FIFO)
这里的for循环是为了构建CLH阻塞队列。这里是第一个死循环队列来进行构建的。
总之是为了将所有没有抢到锁的线程来进行入队,这里的图形画出来:
这里就对应着上面的for循环操作。上面在死循环中构建一个CLH队列,但是此时还没有做任何操作,比如说节点中的值还没有来得及对其进行设置。
6、CLH队列中线程先抢锁后阻塞
final boolean acquireQueued(final Node node, int arg) {
// 获取锁失败为true
boolean failed = true;
try {
// 线程中断状态
boolean interrupted = false;
for (;;) {
// 获取得到前驱节点。已经之前已经排好队
final Node p = node.predecessor();
// 如果当前节点的头结点是头结点!那么再次来尝试获取得到一次锁看看能不能成功
// 因为可能在执行到这一步的时候,线程已经将锁释放了,所以这里再次来尝试一下
if (p == head && tryAcquire(arg)) {
// 将当前节点设置为头结点
setHead(node);
// 当前的头结点没有作用了,需要GC掉
p.next = null; // help GC
// 因为抢占锁成功,所以这里标注为fasle;
failed = false;
// 不是因为中断引起的线程抢占锁中断
return interrupted;
}
// 在失败获取得到锁的时候,应该将线程进行阻塞!这是才是重点!
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
6.1、for循环
这里的for循环是为了修改每个线程对应的节点的执行状态的。只有节点状态是SINGAL的时候才会在唤醒的时候有机会获取得到线程。
而刚刚入队的节点是并不是SINGAL状态的,所以这里是在循环设置。那么看一下在失败获取得到锁之后是如何将线程进行阻塞的:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 首先获取得到每个线程排队节点的前驱节点中的等待状态!
int ws = pred.waitStatus;
// 如果是SINGAL,那么标识,就等着被唤醒来获取得到锁
if (ws == Node.SIGNAL)
return true;
// 这里标识的是线程抢锁取消,不再去抢锁了
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// 如果是其他的,那么比较并交换,将当前的接地那的waitstatus进行设置
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
因为只有当这一步为true的时候,才会将线程进行阻塞。那么为true的就只有一步
if (ws == Node.SIGNAL)
return true;
只有所有的节点中的status为Node.SIGNAL的时候,才会为true。
那么为fales的时候,将会再次走到下面的死循环中来:
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 只有当线程阻塞过程中,是因为线程中断而导致排队中的线程被唤醒,那么这里标记
// 将会被设置为true;
interrupted = true;
}
知道为true的时候,才会走到parkAndCheckInterrupt方法中来,而这一步是真正的做到将线程进行终止的操作的方法:
private final boolean parkAndCheckInterrupt() {
// 将当前线程阻塞到对象上来
LockSupport.park(this);
// 当前线程是否是以中断引起的?如果是,那么返回true;如果不是,那么返回false;
return Thread.interrupted();
}
被park住的线程,此时被阻塞了,要是想苏醒过来,必须要等到前一个线程来将其进行唤醒。
注意park()和park(this)的使用区别:
将队列中的前驱节点中的waitstatus进行修改,表示的是可以将后来的线程来进行唤醒。
7、锁释放
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
直接看尝试释放锁的代码:
protected final boolean tryRelease(int releases) {
// 这里体现出现来可重入锁的特性
int c = getState() - releases;
// 如果不是当前线程来释放锁,那么将会报错!
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 将释放标记来进行标记
boolean free = false;
// 如果c==0,那么表示的是锁能够释放。如果是可重入锁,那么这里不为0的时候,将会继续来进行设置
// 所以这里也就要求!加了多少次锁,就要释放多少次锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
// 只有c==0的时候,这里才为true
return free;
}
那么再次回到上一步,成功释放锁之后操作
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
获取得到头结点,然后判断头节点不为空以及等待状态不为0(必须是SIGNAL状态)的时候,唤醒头结点的下一个节点:
private void unparkSuccessor(Node node) {
// 获取得到头结点的状态信息
int ws = node.waitStatus;
// 如果<0,那么比较并交换,将当前节点的状态的status修改成0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取得到下一个节点
Node s = node.next;
// 如果为空或者是等待状态>0(明显为0)
if (s == null || s.waitStatus > 0) {
// 失去引用,那么会GC掉
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}
具体的执行图如下所示:
当然,这里并没有将将前驱节点断掉,而是在唤醒线程后做的操作:
那么又再次回到原始起点:
for (;;) {
// 唤醒之后,又会来到这里
final Node p = node.predecessor();
// 尝试获取得到锁。对于公平锁来说,一定会执行到这一步,公平锁一定会获取得到
if (p == head && tryAcquire(arg)) {
// 当前节点设置为头结点,并消除掉前置指针
setHead(node);
// 后置指向置为空
p.next = null; // help GC
failed = false;
// 跳出循环
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
看一下这里的setHead(node)方法:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
将当前的节点设置为head节点,然后将节点中的线程置为空,然后断掉前置指针。
最终的结果就是图如下所示:
8、线程等待状态补充说明
waitStatus 非常重要,也是关键,有下面几个枚举值:
枚举 | 含义 |
---|---|
CANCELLED | 为1,表示线程获取锁的请求已经取消了 |
SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 |
CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 |
PROPAGATE | 为-3,当前线程处在SHARED情况下,该字段才会使用 |
0 | 当一个Node被初始化的时候的默认值 |
至此公平锁的流程分析结束:
lock.lock();
xxxx;
lock.unlock();
这段代码的执行逻辑分析结束。
9、总结公平锁的获取流程
这里对应的是我自己画的一个流程图:
两个for循环的作用:
- 1、第一个for循环是排队进入阻塞队列队尾;
- 2、第二个for循环是修改每个节点的状态;
队头唤醒之后进入循环
可以看到锁在释放的时候会唤醒下一个节点,唤醒下一个节点的时候,是线程自己进入到for循环中来再次尝试获取得到锁。
对于公平锁而言,队头元素肯定是可以获取得到锁的。因为有个判断,判断前驱节点是队头的才可以。
10、非公平锁的加锁流程
直接看对应的代码:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
之所以是非公平的,上来就进行比较并交换,如果成功,就比较并设置为当前的线程,野蛮至极。
就这一步的区别,其他的也没有任何的变化了,所以总结起来只需要来画个图即可。
其实就是在线程刚刚进来的时候直接抢锁,非常类似syncronized的流程。因为syncronzied锁是非公平性的,也会尝试抢占。
11、Lock锁的特性讲解
11.1、Lock锁特性
- 阻塞等待队列
- 共享锁和独占锁(排他锁)
- 公平和非公平性
- 可重入
- 可中断
11.2、Lock锁是用变量state标识
用一个state标识来表示当前的锁是否被占有,需要注意的是state变量是用volatile关键字来进行修饰的。
能够及时让其他线程线程看到锁是否被抢占。
11.3、两种队列
- 阻塞队列
- 条件队列
阻塞对象是将没有获取得到锁的线程放到CLH队列中来进行阻塞;
条件队列是在满足条件的地方,调用condition.await方法的时候,将当前线程占有的锁释放掉,然后放入到条件队列中来;当调用condition.sign()或者是condition.sinalAll()方法的时候,将被放在条件队列中的线程追加到阻塞队列上来,让条件队列中的线程有机会获取得到锁。
注:条件队列的使用及其类似多线程中原始的wait()/notify()/notifyAll()方法的使用
示例:
/**
* @Description 等待唤醒机制 除了wait() 和notify\notifyAll方法而唤醒的
*
* 使用lock锁的condition十分类似于notify和notifyAll的机制,只是通知,但是并没有真正的将锁给释放掉
* 而await方法,是将当前线程的执行权让出去;让当前的线程陷入到阻塞中去,等待其他线程的唤醒!
*
* await在当前持有锁的阶段中释放锁,然后将自己放入到阻塞线程中去;
* sinal在持有锁阶段,将因为放到条件队列中的线程唤醒,将条件队列中的线程追加到阻塞队列上去;
* @Author liguang
* @Date 2022/03/19/10:27
*/
public class LockTestOne {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 条件队列
Condition condition = lock.newCondition();
new Thread(()->{
lock.lock();
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"---------开始处理任务");
condition.await();
System.out.println(threadName+"---------处理任务结束");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
new Thread(()->{
lock.lock();
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"---------开始处理任务");
condition.signal();
System.out.println(threadName+"---------处理任务结束");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
}
}
11.4、节点五种状态
- 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
- CANCELLED,值为1,表示当前的线程被取消;
- SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
- CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
- PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享
资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出
队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现
它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但
没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待
结点返回true,否则返回false。
12、测试Lock锁特性
12.1、可重入锁
/**
* 测试可重入性
*/
public class LockTestThree {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
method1();
}).start();
}
}
public static void method1(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"method1");
method2();
}finally {
lock.unlock();
}
}
private static void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"method2");
method3();
}finally {
lock.unlock();
}
}
private static void method3() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"method3");
}finally {
lock.unlock();
}
}
}
每次去获取得到锁的时候,都会发现占用的锁的是当前的线程,所以会在state基础之上+1。
问题:加了多少次锁,就要释放多少次锁。不能多一次,否则将会导致一把锁一直被一个线程一直占用。
示例:
public class ThreadLockCount {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
for (int i = 0; i < 5; i++) {
new Thread(()->{
lock.lock();
System.out.println("hello,world");
}).start();
}
}
}
这里正是因为一个线程一直持有锁(state),一直不为0,那么所有的线程都将会阻塞在队列中,无法继续向下继续运行。
12.2、可中断性
线程被唤醒有两种情况
- 锁释放,唤醒后续节点
- 线程中断
可以在源码中的if判断中,因为中断而唤醒的,会有对应的中断标记表示的是因为中断而引起的线程中断。
而给线程打上标记之后,在外部的某个地方,可以通过判断线程状态,来获取得到对应的。对应的源码体现:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
// 如果是因为中断引起的,那么会清除中断标记并返回true
parkAndCheckInterrupt())
// 然后会给这个标识修改为true,表示是以为线程中断引起的,外部可以做另外操作。
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
然后将标记暴露给外部:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
对应的状态:
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
在外部程序可以检测到这个异常信息。
来写个代码表示一下:
/**
*
* 100个线程来进行自增操作,但是其中一个线程在排队时,发送中断标记,告知该线程不应该继续操作
* 终止其当前线程正在运行的动作
* @author liguang
* @date 2022/7/28 10:00
*/
public class Test2 {
private static final ReentrantLock lock = new ReentrantLock(false);
private static int i = 0;
public static void main(String[] args) {
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
reentrantLock();
});
threadList.add(thread);
thread.start();
}
try {
// 发一个中断信号,将其进行唤醒
threadList.get(90).interrupt();
Thread.sleep(2000);
System.out.println("最终确定的值是:"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void reentrantLock(){
try {
lock.lock();
// 如果线程中断了,就不应该再来进行后续的任务了
while (Thread.currentThread().isInterrupted()){
// 擦除掉中断标记,然后返回,后面的事情不做了
boolean interrupted = Thread.interrupted();
System.out.println("线程标记装填清除了"+interrupted);
System.out.println(Thread.currentThread().isInterrupted());
}
i++;
} catch (Exception e) {
System.out.println(e);
System.out.println("线程中断了"+Thread.currentThread().getName());
// 说明有一个线程是因为线程中断唤醒的!所以需要将其进行替换掉
System.out.println("当前线程状态是:"+Thread.currentThread().isInterrupted());
}finally {
lock.unlock();
}
}
}
多运行几次,会出现以下效果。因为线程运行过程中,如果中断了,碰上了Thread.sleep的话,会导致异常出现。
因为没有将线程睡眠一会儿,所以线程中断可能在线程运行完成之后,也可能是在线程运行之前。
线程标记装填清除了true
false
最终确定的值是:100
也有另外一个方法
lock.lockInterruptibly();
看看其实现原理:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果是因为线程中断引起,那么直接抛出异常
throw new InterruptedException();
}
} finally {
// 抛出异常之前,这里执行取消对应的排队节点
if (failed)
cancelAcquire(node);
}
}
具体实例如下所示:
/**
*
* 100个线程来进行自增操作
* @author liguang
* @date 2022/7/28 10:00
*/
public class Test1 {
private static final ReentrantLock lock = new ReentrantLock(false);
private static int i = 0;
public static void main(String[] args) {
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
reentrantLock();
});
threadList.add(thread);
thread.start();
}
try {
threadList.get(90).interrupt();
Thread.sleep(1000);
System.out.println("最终确定的值是:"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void reentrantLock(){
try {
lock.lockInterruptibly();
Thread.sleep(10);
i++;
} catch (Exception e) {
System.out.println("线程中断了,抛出异常了");
}finally {
lock.unlock();
}
}
}
12.3.1、立即失败
/**
* 尝试获取得到锁,这里是立即失败,不管是公平锁还是非公平锁,都是理解返回的状态
*/
public class LockTestFive {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
System.out.println("线程t1启动");
if (!lock.tryLock()){
System.out.println("线程t1没有获取得到锁,返回");
return;
}
try {
System.out.println("获取得到了锁!");
}finally {
// 将锁释放
lock.unlock();
}
}, "lig");
// main线程
lock.lock();
try {
System.out.println("main线程获取得到了锁");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
}
}
12.3.2、超时失败
/**
* 在指定的时间内没有获取得到锁之后,失败
*/
public class LockTestSix {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
System.out.println("线程t1启动");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)){
System.out.println("线程t1没有获取得到锁,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
// 获取得到锁之后,下面的就不需要再来进行执行了
return;
}
try {
System.out.println("获取得到了锁!");
}finally {
// 将锁释放
lock.unlock();
}
}, "lig");
// main线程
lock.lock();
try {
System.out.println("main线程获取得到了锁");
t1.start();
try {
// 休眠两秒钟来进行测试
Thread.sleep(2000);
System.out.println("main线程释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
}
}
12.4、公平锁和非公平锁
/**
* 尝试获取得到锁,这里是立即失败,不管是公平锁还是非公平锁,都是理解返回的状态
*/
public class LockTestSeven {
public static void main(String[] args) {
// 尝试公平锁和非公平锁
// ReentrantLock lock = new ReentrantLock(true);
ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 5000; i++) {
new Thread(()->{
lock.lock();
try {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程"+Thread.currentThread().getName()+" is running.........");
}finally {
lock.unlock();
}
},"t"+i).start();
}
// 休眠之后再次去抢锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5000; i++) {
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" is running.........");
}finally {
lock.unlock();
}
},"强行抢锁"+i).start();
}
}
}
让控制台交替打印,可以看到非公平锁是可以来交替打印对应的线程Name的。
12.5、条件变量
调用 Condition.await() 方法使线程等待,其他线程调用Condition.signal() 或 Condition.signalAll() 方法唤醒等待的线程。
注意:调用Condition的await()和signal()方法,都必须在lock保护之内
/**
* 条件变量:模拟生产者和生产者!这是这里明显,可以有多个条件,可以利用多个条件来进行操作
*/
public class LockTestEight {
private static Lock lock = new ReentrantLock();
private static Condition cigCon = lock.newCondition();
private static Condition takeCon = lock.newCondition();
private static boolean hasCig;
private static boolean hasTakeOut;
public void cigratee(){
lock.lock();
try {
while (!hasCig){
try {
System.out.println("没有烟了,歇一会儿");
cigCon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("有烟了,开始干活...........");
}finally {
lock.unlock();
}
}
/**
* 送外卖
*/
public void takeOut(){
lock.lock();
try {
while (!hasTakeOut){
try {
System.out.println("饭还没有好,歇一会儿");
takeCon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("有饭了,吃完饭开始干活...........");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockTestEight lockTestEight = new LockTestEight();
new Thread(()->{
lockTestEight.cigratee();
}).start();
new Thread(()->{
lockTestEight.takeOut();
}).start();
new Thread(()->{
lock.lock();
try {
hasCig = true;
cigCon.signal();
}finally {
lock.unlock();
}
}).start();
new Thread(()->{
lock.lock();
try {
hasTakeOut = true;
takeCon.signal();
}finally {
lock.unlock();
}
}).start();
}
}
这个具体分析会在后面给列出来。