找回密码
 注册
搜索
查看: 145|回复: 0

6年的java被刚入门的小弟弟提的问题难住了?

[复制链接]
发表于 2024-7-17 18:24:05 | 显示全部楼层 |阅读模式
作者:奔跑的毛球

顺便吆喝一声,技术大厂捞人,前/后端or测试←感兴趣
要求学历:全日制统招本科
--加班偶尔较多,但周末加班两倍工资。
--综合薪酬15-35K,工资在一线城市属于一般,但二线城市很可以。



问题
有个亲戚的孩子,非要入行计算机学java,这现在还来学java不就是49年入国军吗。非要学,给了几套课程让自学,让有不懂的来问我,这不,带着新的问又来了。
也就是下面这行代码为什么执行之后不能完结,就一直卡在那里?我们来瞅瞅这个代码,基本就是演示了代码演示了 ReentrantLock 的基本使用和线程之间的竞争。

  1. public class Domain {
  2.     public static void main(String[] args) throws InterruptedException {
  3.         ReentrantLockTest t = new ReentrantLockTest();
  4.         new Thread(t).start();
  5.         new Thread(t).start();
  6.         new Thread(t).start();

  7.     }
  8. }
  9. class ReentrantLockTest implements Runnable {
  10.     ReentrantLock r = new ReentrantLock();
  11.     int t = 2;

  12.     @Override
  13.     public void run() {
  14.         while (true) {
  15.             r.lock();
  16.             if (t > 0) {
  17.                 t -= 1;
  18.             } else {
  19.                 r.unlock();
  20.                 break;
  21.             }
  22.         }
  23.     }
  24. }
复制代码



演示
执行一下


果然是卡住了,不结束执行也不继续执行。
这得先分析分析,卡哪了?

思路 1 日志大法

先上日志大法:
给run()方法添加日志如下
  1. @Override
  2. public void run() {
  3.     log.info("Thread.currentThread().getName() = {} 在循环外准备开始循环", Thread.currentThread().getName());
  4.     while (true) {
  5.         log.info("Thread.currentThread().getName() = {} 准备动手拿锁", Thread.currentThread().getName());
  6.         r.lock();
  7.         log.info("Thread.currentThread().getName() = {} 拿到了锁", Thread.currentThread().getName());
  8.         if (t > 0) {
  9.             log.info("Thread.currentThread().getName() = {} 在执行 {}", Thread.currentThread().getName(), t);
  10.             t -= 1;
  11.         } else {
  12.             log.info("Thread.currentThread().getName() = {} 解锁,退出循环", Thread.currentThread().getName());
  13.             r.unlock();
  14.             break;
  15.         }
  16.     }
  17.     log.info("Thread.currentThread().getName() = {} 执行完毕", Thread.currentThread().getName());
  18. }
复制代码


然后执行


可以看到线程0,1,2都准备拿锁,然后线程0拿到了锁,执行完毕之后,线程1和2还是在准备拿锁。这就奇怪了,线程0都释放锁了,线程1,2怎么不去获取锁而是一直卡着呢?因为抢锁打起来,打伤了???

思路 2 增加监测线程,检测线程状态

那我们在主线程起起一个检测线程状态的线程看看。
  1. public static void main(String[] args) throws InterruptedException {
  2.     ReentrantLockTest t = new ReentrantLockTest();
  3.     new Thread(t).start();
  4.     new Thread(t).start();
  5.     new Thread(t).start();
  6.     while (true){
  7.         Thread[] threads = new Thread[Thread.activeCount()];
  8.         Thread.enumerate(threads);
  9.         for (Thread thread : threads) {
  10.             log.info("thread = {}-{}", thread.getName(), thread.getState());
  11.         }
  12.         Thread.sleep(2000);
  13.     }
  14. }
复制代码


执行之后:


可以看到,在线程0拿到锁之后,线程1和线程2的状态一直是WAITING状态,ps(这是我多次尝试的结果哈,其实哪个线程拿到锁是不一定的,为了和上边的结果一致,刷了几次刷到了线程0拿锁。)

那么问题来了,为啥一直处于WAITING状态呢。

思路 3 尝试解锁
我们去掉检测线程状态的逻辑,在while循环最后增加一个解锁的操作:

  1. @Override
  2. public void run() {
  3.     log.info("ThreadName():{} 在循环外准备开始循环", Thread.currentThread().getName());
  4.     while (true) {
  5.         log.info("ThreadName():{} 准备动手拿锁", Thread.currentThread().getName());
  6.         r.lock();
  7.         log.info("ThreadName():{} 拿到了锁", Thread.currentThread().getName());
  8.         if (t > 0) {
  9.             log.info("ThreadName():{} 在执行 {}", Thread.currentThread().getName(), t);
  10.             t -= 1;
  11.         } else {
  12.             log.info("ThreadName():{} 解锁,退出循环", Thread.currentThread().getName());
  13.             r.unlock();
  14.             break;
  15.         }
  16.         r.unlock();//增加这个
  17.     }
  18.     log.info("ThreadName():{} 执行完毕", Thread.currentThread().getName());
  19. }
复制代码


执行后,可以看到在每次执行完之后解了锁,那么等待拿锁的线程都是有机会拿到锁并且执行的。


那么上面的为啥不行呢

思路 4 粗暴尝试
可以分析到,是加锁多次,然后解锁了一次,应该是这个行不通导致的。
那我们尝试一下。直接粗暴的解锁三次,看看是不是这儿的问题。



执行,可以看到在线程0执行完解锁之后,其余线程也都执行了,报错可以先忽略,之后会解释下。


那么就可以肯定,就是这里加锁多次,解锁只解了一次导致的。那么 就沿着这个方向继续看看。

思路 5 找到方向,查资料
这里肯定就牵扯ReentrantLock的一些机制了,查查资料:

ReentrantLock是一个可重入锁。这意味着同一个线程可以多次获取同一个锁,并且每次获取锁时,锁的重入计数器会增加;每次释放锁时,锁的重入计数器会减少。当重入计数器减为零时,锁才会真正被释放。

思路 6 看源码
哇。还有这个,去看看源码:

这里这个state就是加锁的次数,在每次执行lock()的时候,state的值会加1.
然后我们看看解锁的源码:


懂了懂了,在执行解锁的时候,先给state减1,然后判断是不是减到0了,要是没有到0那就啥都不干。可是要是到0了就检查当前节点是否为空,以及下一个节点是否为空。如果下一个节点的状态不是0(表示有线程在等待),则调用 s.getAndUnsetStatus(WAITING) 来重置节点的等待状态。使用 LockSupport.unpark(s.waiter) 来唤醒等待的线程。

总结
就是ReentrantLock在使用的时候,加锁几次,就需要解锁几次。这时候上面那个报错就很好理解了,当前线程不持有锁了,解锁当然是报错的。
相关代码这里了:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×
高级模式
B Color Image Link Quote Code Smilies

本版积分规则

Archiver|手机版|小黑屋|52RD我爱研发网 ( 沪ICP备2022007804号-2 )

GMT+8, 2024-11-23 02:20 , Processed in 0.116052 second(s), 19 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表