预见猿份
主题
首页面试题在线工具关于我们老苗一对一私教学员评价
实战项目
项目前置基础创新WMS项目Java微服务框架与实战云岚到家项目闪聚支付项目学成在线项目青橙电商项目JVM原理与实战调优分布式事务专题Java高频面试题MySQL从入门到精通Java数据结构与算法Java 并发编程(JUC)实战课程老苗一对一私教学员评价blog
blog
  • Java 并发编程(JUC)实战课程

    • 课程介绍
    • 01 多线程入门
    • 02 线程的常用方法
    • 03 线程池
    • 04 synchronized教程
    • 05 ReentrantLock教程
    • 06 读写锁
    • 07 JMM教程
    • 08 CAS教程
    • 09 wait&notify
    • 10 常用的并发集合
    • 11 异步任务控制
    • 12 常用并发工具







----- 到底线了 -----

第5章 ReentrantLock教程 ​

2.2 ReentrantLock ​

2.2.1 ReentrantLock入门 ​

ReentrantLock介绍 ​

synchronized存在的不足:

1、编程不灵活

synchronized作为关键字存在,获取锁及释放锁的过程由JVM控制,程序员无法控制。

获取锁和释放锁的流程是:进入临界区获取锁,执行临界区完成释放锁。

2、等待锁的过程无法中断

线程等待synchronized锁时进入阻塞队列此时它是不允许被中断的,这个特性可能会引起死锁。

死锁就是两个线程都需要对方的锁而对方还不释放锁,就这样僵持下去。

ReentrantLock不仅可以解决上述的不足还具有很多优势:

ReentrantLock和synchronized一样都具有锁的可重入性(参考:”synchronized可重入性“知识点),相比synchronized它的功能更加强大,具有如下功能:

1、灵活控制锁的获取和释放

ReentrantLock是一个类型,实现了Lock接口,使用ReentrantLock获取锁及释放锁的过程可以由程序员控制。

ReentrantLock可以显示定义锁、获取锁、释放锁。

2、等待锁的过程可被打断。

线程等待ReentrantLock锁时可以被中断 的,线程等待锁时可被中断可以有效的解决死锁。

3、尝试获取锁。

ReentrantLock提供尝试获取锁功能,支持阻塞与非阻塞,可立即返回结果 也可等待一定的超时时间。

4、支持公平锁。

公平锁则是按照先到先得的规则 来获取锁,ReentrantLock是支持公平锁的。

5、支持多个条件变量。

ReentrantLock支持多个等待队列,通过条件变量可灵活控制由哪个等待队列 的线程去获取锁,ReentrantLock的条件变量特性在后面线程协作章节讲解。

ReentrantLock的基本语法 ​

ReentrantLock实现了Lock接口,Lock是jdk5提供的接口,可以显示定义锁、获取锁、释放锁,Lock接口定义如下:

摘自jdk8(中文api):

image-20201203223255160

从api定义上可以看出使用Lock可以手动获取锁、释放锁, 基本语法 如下:

//定义锁对象
ReentrantLock reentrantLock = new ReentrantLock() ;
//获取锁
reentrantLock.lock();
try {
	//临界区代码...
} finally {
	//释放锁
	reentrantLock.unlock();
}
1
2
3
4
5
6
7
8
9
10

一定要注意添加finally块释放锁,一旦忘记会导致锁泄漏造成危害。

ReentrantLock例子 ​

下边将线程安全的例子由synchronized的实现改为使用ReentrantLock实现,100个线程执行sum++操作。

代码如下:

package com.yjoffer.javase.thread2.safe;

import com.yjoffer.javase.config.Logger;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock测试
 * @author 预见猿份(www.yjoffer.com)
 * @version 1.0
 **/
public class ReentrantLockTest {

    private static Logger logger = Logger.getLogger(ReentrantLockTest.class);

    static int sum=0;

    public static void test_reentrantLock_first(){
        ExecutorService threadPool = Executors.newCachedThreadPool();
        ReentrantLock lock = new ReentrantLock();
        for (int i = 0; i < 100; i++) {
            threadPool.execute(()->{
                lock.lock();
                try {
                    sum++;
                }finally {
                    lock.unlock();
                }
            });
        }
        //如果所有任务没有完成继续等待
        shutdown(threadPool);
        logger.debug("sum="+sum);
    }

    private static void shutdown(ExecutorService threadPool) {
        //线程池结束
        threadPool.shutdown();
        while (!threadPool.isTerminated()) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            sum=0;
            test_reentrantLock_first();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

经过测试每次都输出正确的结果,如下:

sum=100
1

通过例子掌握了ReentrantLock的基本使用方法。

2.2.2 ReentrantLock获取锁中断 ​

什么是获取锁中断 ​

线程执行临界区代码去获取锁,由于锁已被其它线程获取则进入阻塞等待状态,在等待锁的过程中是可以被中断的,这叫获取锁中断 。

image-20220224154710295

synchronized不具有获取锁中断的特性,Reentrantlock则具有获取锁中断的特性,获取锁中断可以有效的解决死锁,关于死锁稍后会详细介绍。

synchronized无法获取锁中断 ​

下边先测试synchronized无法获取锁中断:

启动两个线程,t1线程先获取到了锁,t2线程获取锁需要等待,等待过程中中断 t2没有反映。

package com.yjoffer.javase.thread.safe;

import com.yjoffer.javase.config.Logger;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;


/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {

	private static Logger logger = Logger.getLogger(ThreadsafeTest.class);
//synchronized获取锁无法中断
	public static void test_sync_interrupt(){
		//锁对象
		Object obj = new Object();
		Thread t1 = new Thread(() -> {
			logger.info("申请获取锁");
			synchronized (obj){
				logger.info("获取到锁");
				try {
					TimeUnit.SECONDS.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			logger.info("释放锁");
		},"t1");
		Thread t2 = new Thread(() -> {
			logger.info("申请获取锁");
			synchronized (obj){
				logger.info("获取到锁");
				try {
					TimeUnit.SECONDS.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			logger.info("释放锁");
		},"t2");
		t1.start();
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		t2.start();
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		logger.debug("向t2请求中断");
		t2.interrupt();
	}
	public static void main(String[] args) throws InterruptedException {
		test_sync_interrupt();
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

运行程序,输出 :

17:55:48.646 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t1] - 申请获取锁
17:55:48.667 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t1] - 获取到锁
17:55:50.646 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t2] - 申请获取锁
17:55:52.646 FINE com.yjoffer.javase.thread.safe.ThreadsafeTest[main] - 向t2请求中断
17:55:58.667 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t2] - 获取到锁
17:55:58.667 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t1] - 释放锁
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.yjoffer.javase.thread.safe.ThreadsafeTest.lambda$test_sync_interrupt$32(ThreadsafeTest.java:1299)
	at java.lang.Thread.run(Thread.java:748)
17:55:58.670 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t2] - 释放锁
1
2
3
4
5
6
7
8
9
10
11
12
13

t2申请获取锁失败,中断 t2没有反映,等t1执行完成t2获取了锁 。

Reentrantlock获取锁中断测试 ​

下边用Reentrantlock测试获取锁中断,将synchronized锁换成Reentrantlock,申请获取锁如果可被中断需要 使用reentrantLock.lockInterruptibly()方法。

代码如下:

//reentrantlock获取锁可中断
	public static void test_reentrantlock_interrupt(){
		//锁对象
		ReentrantLock reentrantLock = new ReentrantLock();

		 //创建两个线程
        Thread t1 = new Thread(() -> {
            logger.debug("申请获取锁");
            try {
                reentrantLock.lockInterruptibly();//申请获取一个可以被打断的锁
                try {
                    logger.debug("获取到锁");
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }finally {
                    logger.debug("释放锁");
                    reentrantLock.unlock();
                }
            } catch (InterruptedException e) {
                logger.debug("获取锁被打断");
                e.printStackTrace();
            }

        },"t1");

        Thread t2 = new Thread(() -> {
            logger.debug("申请获取锁");
            try {
                reentrantLock.lockInterruptibly();//申请获取一个可以被打断的锁
                try {
                    logger.debug("获取到锁");
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }finally {
                    logger.debug("释放锁");
                    reentrantLock.unlock();
                }
            } catch (InterruptedException e) {
                logger.debug("获取锁被打断");
                e.printStackTrace();
            }
        },"t2");

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

	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

运行程序,输出 :

10:36:56.320 FINE com.yjoffer.javase.thread.basic.ThreadsafeTest[t1] - 申请获取锁
10:36:56.344 FINE com.yjoffer.javase.thread.basic.ThreadsafeTest[t1] - 获取到锁
10:36:58.322 FINE com.yjoffer.javase.thread.basic.ThreadsafeTest[t2] - 申请获取锁
10:37:00.321 FINE com.yjoffer.javase.thread.basic.ThreadsafeTest[main] - 向t2发起中断
10:37:00.322 FINE com.yjoffer.javase.thread.basic.ThreadsafeTest[t2] - 获取锁被打断
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.yjoffer.javase.thread.basic.ThreadsafeTest.lambda$test_reentrantLock_interrupt$22(ThreadsafeTest.java:711)
	at java.lang.Thread.run(Thread.java:748)
10:37:06.351 FINE com.yjoffer.javase.thread.basic.ThreadsafeTest[t1] - 释放锁
1
2
3
4
5
6
7
8
9
10
11
12

t2申请获取锁被中断,t1正常执行完成释放锁。

2.2.3 ReentrantLock尝试获取锁 ​

尝试获取锁介绍 ​

ReentrantLock有一个尝试获取锁的方法叫tryLock(),该方法为非阻塞方法,返回结果为布尔型,false表示无法获取锁,true表示获取到锁,更加提高了编程的灵活性。另外此方法支持设置超时时间,等待一定的时间如果还没有获取到锁则返回结果。API如下:

image-20210926111544044

尝试获取锁测试 ​

下边程序启动t1和t2,t1先获取到了锁,执行时间为10秒,t2尝试获取锁,如果没有获取到锁直接 终止方法,代码如下:

package com.yjoffer.javase.thread.safe;

import com.yjoffer.javase.config.Logger;

import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;


/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {

	private static Logger logger = Logger.getLogger(ThreadsafeTest.class);
//测试reentrantLock的尝试获取锁
    public static void test_reentrantLock_trylock(){
        ReentrantLock lock = new ReentrantLock();

        //t1
        Thread t1 = new Thread(()->{
            //尝试获取锁
            logger.debug("尝试获取锁");
            boolean b = lock.tryLock();
            if(!b){
                logger.debug("获取锁失败");
                return ;
            }
            try {
                //临界区
                logger.debug("获取到锁");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }finally {
                lock.unlock();
            }

        },"t1");

        //t2
        Thread t2 = new Thread(()->{
            //尝试获取锁
            logger.debug("尝试获取锁");
			boolean b = lock.tryLock();

            if(!b){
                logger.debug("获取锁失败");
                return ;
            }
            try {
                //临界区
                logger.debug("获取到锁");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }finally {
                lock.unlock();
            }

        },"t2");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

    }
	public static void main(String[] args) throws InterruptedException {
		test_reentrantLock_trylock();
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

运行程序,输出 如下:

18:14:42.213 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t1] - 尝试获取锁
18:14:42.237 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t1] - 获取到锁
18:14:44.213 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t2] - 尝试获取锁
18:14:44.213 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t2] - 获取锁失败
18:14:52.238 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t1] - 释放锁
1
2
3
4
5

从输出 可以看出t2获取锁失败直接 退出方法。

尝试获取锁超时 ​

下边使用tryLock(long timeout, TimeUnit unit)测试,代码如下:

尝试获取锁的超时时间设置为11秒,t1的执行时间为10秒,正确情况是t2可以获取到锁并执行完成。

//测试reentrantLock的尝试获取锁
    public static void test_reentrantLock_trylock(){
        ReentrantLock lock = new ReentrantLock();

        //t1
        Thread t1 = new Thread(()->{
            //尝试获取锁
            logger.debug("尝试获取锁");
            boolean b = lock.tryLock();
            if(!b){
                logger.debug("获取锁失败");
                return ;
            }
            try {
                //临界区
                logger.debug("获取到锁");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }finally {
                lock.unlock();
            }

        },"t1");

        //t2
        Thread t2 = new Thread(()->{
            //尝试获取锁
            logger.debug("尝试获取锁");
//            boolean b = lock.tryLock();
            boolean b = false;
            try {
               b= lock.tryLock(11,TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(!b){
                logger.debug("获取锁失败");
                return ;
            }
            try {
                //临界区
                logger.debug("获取到锁");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }finally {
                lock.unlock();
            }

        },"t2");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

运行程序,输出 :

18:16:02.691 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t1] - 尝试获取锁
18:16:02.713 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t1] - 获取到锁
18:16:04.691 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t2] - 尝试获取锁
18:16:12.713 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t1] - 释放锁
18:16:12.714 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t2] - 获取到锁
18:16:22.714 INFO com.yjoffer.javase.thread.safe.ThreadsafeTest[t2] - 释放锁
1
2
3
4
5
6

从输出 可以看出t2在超时时间内获取到了锁并执行完成。

注意:尝试获取锁超时方法支持中断。

2.2.4 ReentrantLock公平锁 ​

公平锁与非公平锁 ​

公平锁与非公平锁是当锁释放后由哪个线程来获取锁的规则 。

公平锁:

处在阻塞队列中等待获取锁的线程,当锁释放后按照先进先出的规则来获取锁。

如:t1获取了锁,t2进入队列等待,t3进入队列等待,当t1释放了锁,由于t2先进入队列等待所以由t2获取锁,t3由于后于t2进入队列所以t3需后于t2获取锁。

非公平锁:

非公平锁并不是按照先进先出的规则获取锁。

synchronized是非公平锁,ReentrantLock支持非公平锁也支持公平锁。

ReentrantLock源码解析 ​

下边通过解析ReentrantLock的源码求证公平锁与非公平锁的区别 。

ReentrantLock默认为非公平锁,查看它的无参构造方法如下:

    public ReentrantLock() {
        sync = new NonfairSync();
    }
1
2
3
4

new NonfairSync()表示创建 一个非公平锁。

ReentrantLock也支持公平锁,使用有参构造方法即可实现,有参构造方法源码如下:

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
1
2
3

new FairSync()表示创建 一个公平锁。

所以,下边的代码创建 一个公平锁:

ReentrantLock reentrantLock = new ReentrantLock(true);
1

下边分析FairSync和NonfairSync的关键区别 。

通过上边的分析可知:

公平锁时,新线程获取锁前会判断是否有线程在等待,如果没有线程在等待则通过CAS更新状态,如果更新成功则占有锁。

非公平锁时,新线程获取锁前不再判断是否有线程在等待,直接通过CAS方式更新状态,如果更新成功则占有锁。

注意:CAS是一种乐观锁的操作方法,在JMM章节会详细介绍。

公平锁与非公平锁测试 ​

编写 一个测试方法:

t1线程获取锁休眠10秒,释放锁后立即再次获取锁。

t1启动后2秒后启动t2

t2启动后2秒后启动t3

t3启动后2秒后启动t4

测试代码如下:

    //测试公平锁非公平锁
    public static void test_reentrantlock_FairSync(boolean fair){
        ReentrantLock lock = new ReentrantLock(fair);

        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                logger.debug("执行");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }finally {
                lock.unlock();
            }
            lock.lock();
            try {
                logger.debug("执行");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }finally {
                lock.unlock();
            }
        },"t1");
        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                logger.debug("执行");
            }finally {
                lock.unlock();
            }
        },"t2");
        Thread t3 = new Thread(() -> {
            lock.lock();
            try {
                logger.debug("执行");
            }finally {
                lock.unlock();
            }
        },"t3");
        Thread t4 = new Thread(() -> {
            lock.lock();
            try {
                logger.debug("执行");
            }finally {
                lock.unlock();
            }
        },"t4");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t3.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t4.start();

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

测试:

如果是公平锁执行顺序 如下:

t1
t2
t3
t4
t1
1
2
3
4
5

由于t1释放锁后立即获取锁所以t1之后继续由t1获取锁,t2、t3、t4每次执行顺序 可能不一致。

t1
t1
t2
t3
t4
1
2
3
4
5
建议 ​

非公平锁可能出现阻塞队列中有些线程长期获取不到锁,造成线程饥饿问题,公平锁可以解决线程饥饿问题。

根据上边的源码分析可知:公平锁总是按照先进先出的规则去获取锁,阻塞队列中的线程都有机会获取锁,而非公平锁则会首先尝试获取锁,如果获取锁成功则立即返回,所以公平锁的并发性没有非公平锁高。

生产中没有特殊要求使用非公平锁即可。

2.2.5 死锁 ​

什么是死锁 ​

两个或两个以上的线程去竞争一个锁,一个线程在等待锁,另一个线程拥有锁却永不释放,此时就产生了死锁。存在死锁的线程在无外力的作用下将永远阻塞下去。

下图描述了死锁的现象:

ScreenShot_2026-09-20_103352_084.png

1、线程1拥有R1资源锁,线程2拥有R2资源锁。

2、线程1请求R2资源锁,由于已被线程2占用,阻塞并等待锁释放。

3、线程2请求R1资源锁,由于已被线程1占用,阻塞并等待锁释放。

线程1和线程2都在等待对方释放锁。

常见的死锁的情况:

1、交叉式锁可能导致程序死锁

线程1持有R1的锁等待获取R2锁,线程2持有R2的锁等待获取R1的锁。

2、内存不足导致死锁

多线程并发请求系统的可用内存,如果此时系统内存不足,可能会出现死锁的情况。比如:两个线程A和B,执行某个任务,其中A已经获取到了10MB内存,B获取到了20MB内存,如果每一个线程执行单元都需要30MB,但是剩余的可用内存刚好5MB,那么两个线程有可能都在等待彼此能够释放内存资源。

  1. 死循环引起的死锁

程序由于代码原因进入了死循环,CPU的占有率居高不下,程序不工作,这种死锁称之为假死,是一种最致命也是最难排查的死锁现象。

演示死锁 ​

测试程序如下:

package com.yjoffer.javase.thread.safe;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {
	// 定义锁对象
	static Object R1 = new Object() ;
	static Object R2 = new Object() ;
	//测试死锁
	public static void test_deadLock() {
		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		threadPool.execute(()->{
				synchronized (R1) {
					logger.debug( "获取到了R1锁"+R1+",申请R2锁");
					synchronized (R2) {
						logger.debug( "获取到了R1锁,获取到了R2锁");
					}

				}
		});
		threadPool.execute(()->{
			synchronized (R2) {
				logger.debug( "获取到了R2锁"+R2+",申请R1锁");
				synchronized (R1) {
					logger.debug( "获取到了R2锁,获取到了R1锁");
				}

			}
		});


		//结束线程池
		shutdown(threadPool);
	}
	public static void main(String[] args) {
		test_deadLock();
	}


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

多次运行测试,发现存在线程无法运行结束的现象,此时即发生的死锁:

pool-1-thread-1获取到了R1锁,申请R2锁
pool-1-thread-2获取到了R2锁,申请R1锁
1
2
死锁诊断 ​

当程序出现了死锁现象可以使用jdk自带的工具进行诊断,jconsole.exe和 jstack.exe都可以进行诊断。

此工具在jdk的bin目录下

首先看jstack.exe如何诊断死锁:

1、首先运行上边程序,当前程序处于死锁状态。

2、cmd进入命令行状态

运行jps找到当前java程序的进程id :23000

image-20201207132731946

(注意每个人电脑上进程id不一样)

使用jstack工具可以查看线程状态,根据线程状态进行死锁诊断。

按如下步骤进行诊断:

image-20201207133119700

从上图可以看出pool-1-thread-2和pool-1-thread-1处于阻塞状态,都在等待一个锁。

...

下图显示找到一个死锁,并且显示是哪几个线程存在死锁。

image-20201207133043353

下边再看jconsole.exe诊断死锁的方法:

打开jconsole.exe工具,找到要监控的java进程

点击连接

image-20210927154745248

点击“不安全的连接”进入监控界面

找进线程标签,点击“检测死锁”

找到死锁后将出现死锁的线程信息列了出来

image-20210927154940589

死锁处理 ​

死锁发生一般需要外力进行处理,比如: 重启进程。

暂时停止系统的运行势必会影响用户的使用,所以对于死锁问题需要学习避免发生的死锁的方法。

发生死锁的条件由以下4个条件组成,4个条件缺一不可:

1)锁的互斥性,一个线程获取了该资源的同步锁后其它线程必须等待该锁被释放后方可再获取锁。

2)拥有锁且还在等待锁,至少有一个任务当前持有锁并且正在等待被其它任务获取的锁。

3)不可强制剥夺,一个任务获取了锁是无法被强制剥夺,必须等待该任务释放锁。

4)循环等待,任务A等待任务B释放锁,任务B等待任务A释放锁。

4个条件只要有一个条件不满足死锁将不会发生,一般是从第4个条件入手来避免死锁。上边例子中两个线程获取R1、R2出现了循环等待,只要固定获取这两个锁的顺序即可避免死锁,经如:要获取R2锁必须拥有R1的锁,修改代码如下:

1)更改R2锁的获取方法,添加getR2()方法

	//拥有R1的锁方可获取R2
	public static Object getR2() {
		synchronized (R1) {
			return R2;
		}
	}
1
2
3
4
5
6

2、测试程序

package com.yjoffer.javase.thread.safe;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


/**
 * 	线程安全测试
 * @author 预见猿份(www.yjoffer.com)
 *
 */
public class ThreadsafeTest {

	//测试死锁处理
	public static void test_deadLock2() {

		//线程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		threadPool.execute(()->{
			String name = Thread.currentThread().getName();
			synchronized (R1) {
				System.out.println(name + "获取到了R1锁,申请R2锁"+R1);
				synchronized (getR2()) {
					System.out.println(name + "获取到了R1锁,获取到了R2锁"+R2);
				}

			}
		});
		threadPool.execute(()->{
			String name = Thread.currentThread().getName();
			synchronized (getR2()) {
				System.out.println(name + "获取到了R2锁,申请R1锁"+R2);
				synchronized (R1) {
					System.out.println(name + "获取到了R2锁,获取到了R1锁"+R1);
				}

			}
		});

		//结束线程池
		shutdown(threadPool);
	}
	public static void main(String[] args) {
		test_deadLock2();
	}


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

多次测试程序,不会出现死锁的现象,输出:

pool-1-thread-1获取到了R1锁,申请R2锁
pool-1-thread-1获取到了R1锁,获取到了R2锁
pool-1-thread-2获取到了R2锁,申请R1锁
pool-1-thread-2获取到了R2锁,获取到了R1锁
1
2
3
4

2.2.6 哲学家死锁案例 ​

哲学家案例描述 ​

5位哲学家围绕一张圆桌,哲学家要么思考问题,要么吃饭,每两位哲学家中间放了一根筷子,哲学家就餐时需要同时拿起左边和右边的筷子,思考问题时将两根筷子放回原处。如下图所示:

问题:如何保证每位哲学家有序吃饭,不能出现互相等待的问题,及永远吃不上饭的问题。

synchronized实现 ​

下边使用synchronized锁来实现:

1、创建5个线程表示5位哲学家。

2、创建5个对象表示5根筷子,并且标上序号,使用synchronized获取对象锁。

代码如下:

//哲学家就餐问题
	public static void test_philosopher(){
		//定义5根筷子对象
		Object obj1 = new Object();
		Object obj2 = new Object();
		Object obj3 = new Object();
		Object obj4 = new Object();
		Object obj5 = new Object();

		//哲学家1
		Thread t1 = new Thread(() -> {
			while (true) {
				synchronized (obj1){
					synchronized (obj2){
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}

		},"t1");
		//哲学家2
		Thread t2 = new Thread(() -> {
			while (true) {
				synchronized (obj2) {
					synchronized (obj3) {
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}
		},"t2");
		//哲学家3
		Thread t3 = new Thread(() -> {
			while (true) {
				synchronized (obj3) {
					synchronized (obj4) {
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}
		},"t3");
		//哲学家4
		Thread t4 = new Thread(() -> {
			while (true) {
				synchronized (obj4) {
					synchronized (obj5) {
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}
		},"t4");
		//哲学家5
		Thread t5 = new Thread(() -> {
			while (true){
				synchronized (obj5){
					synchronized (obj1){
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}

		},"t5");
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

运行程序,过一会发现控制台没有输出内容。

用jconsole.exe工具检测发现死锁。

解决死锁 ​

根据前边学习的死锁的解决方案,只要固定获取锁的顺序即可避免死锁,将第5位哲学家获取锁的顺序更改如下:

			synchronized (obj1){
					synchronized (obj5){
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
1
2
3
4
5
6
7
8
9
10

完整代码如下:

//解决哲学家就餐死锁问题,出现新问题:线程饥饿
	public static void test_philosopher2(){
//定义5根筷子对象
		Object obj1 = new Object();
		Object obj2 = new Object();
		Object obj3 = new Object();
		Object obj4 = new Object();
		Object obj5 = new Object();

		//哲学家1
		Thread t1 = new Thread(() -> {
			while (true) {
				synchronized (obj1){
					synchronized (obj2){
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}

		},"t1");
		//哲学家2
		Thread t2 = new Thread(() -> {
			while (true) {
				synchronized (obj2) {
					synchronized (obj3) {
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}
		},"t2");
		//哲学家3
		Thread t3 = new Thread(() -> {
			while (true) {
				synchronized (obj3) {
					synchronized (obj4) {
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}
		},"t3");
		//哲学家4
		Thread t4 = new Thread(() -> {
			while (true) {
				synchronized (obj4) {
					synchronized (obj5) {
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}
		},"t4");
		//哲学家5
		Thread t5 = new Thread(() -> {
			while (true){
				synchronized (obj1){
					synchronized (obj5){
						logger.debug("吃饭");
						try {
							TimeUnit.MILLISECONDS.sleep(200);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
			}

		},"t5");
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

运行程序死锁问题解决,但是发现t5线程长期吃不了饭,产生了线程饥饿问题。

解决线程饥饿问题 ​

造成线程饥饿问题的原因是有些线程在长期阻塞,我们可以使用ReentrantLock的尝试获取锁的方法来问题。

将1号哲学家获取锁的代码更改如下:

if(obj1.tryLock()){
					try {
						if(obj2.tryLock()){
							try {
								logger.debug("吃饭");
								try {
									TimeUnit.MILLISECONDS.sleep(200);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
							}finally {
								obj2.unlock();
							}
						}
					}finally {
						obj1.unlock();
					}

				}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

通过上边的代码可知当第2个筷子无法获取时则释放第1根筷子,避免长期占有筷子。

完整代码如下:

//解决哲学家就餐死锁问题以及线程饥饿
	public static void test_philosopher3(){
		//定义5根筷子对象
		ReentrantLock obj1 = new ReentrantLock();
		ReentrantLock obj2 = new ReentrantLock();
		ReentrantLock obj3 = new ReentrantLock();
		ReentrantLock obj4 = new ReentrantLock();
		ReentrantLock obj5 = new ReentrantLock();

		//哲学家1
		Thread t1 = new Thread(() -> {
			while (true) {
				if(obj1.tryLock()){
					try {
						if(obj2.tryLock()){
							try {
								logger.debug("吃饭");
								try {
									TimeUnit.MILLISECONDS.sleep(200);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
							}finally {
								obj2.unlock();
							}
						}
					}finally {
						obj1.unlock();
					}

				}
			}

		},"t1");
		//哲学家2
		Thread t2 = new Thread(() -> {
			while (true) {
				if(obj2.tryLock()){
					try {
						if(obj3.tryLock()){
							try {
								logger.debug("吃饭");
								try {
									TimeUnit.MILLISECONDS.sleep(200);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
							}finally {
								obj3.unlock();
							}
						}
					}finally {
						obj2.unlock();
					}

				}
			}
		},"t2");
		//哲学家3
		Thread t3 = new Thread(() -> {
			while (true) {
				if(obj3.tryLock()){
					try {
						if(obj4.tryLock()){
							try {
								logger.debug("吃饭");
								try {
									TimeUnit.MILLISECONDS.sleep(200);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
							}finally {
								obj4.unlock();
							}
						}
					}finally {
						obj3.unlock();
					}

				}
			}
		},"t3");
		//哲学家4
		Thread t4 = new Thread(() -> {
			while (true) {
				if(obj4.tryLock()){
					try {
						if(obj5.tryLock()){
							try {
								logger.debug("吃饭");
								try {
									TimeUnit.MILLISECONDS.sleep(200);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
							}finally {
								obj5.unlock();
							}
						}
					}finally {
						obj4.unlock();
					}

				}
			}
		},"t4");
		//哲学家5
		Thread t5 = new Thread(() -> {
			while (true){
				if(obj1.tryLock()){
					try {
						if(obj5.tryLock()){
							try {
								logger.debug("吃饭");
								try {
									TimeUnit.MILLISECONDS.sleep(200);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
							}finally {
								obj5.unlock();
							}
						}
					}finally {
						obj1.unlock();
					}

				}
			}

		},"t5");
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

运行程序,解决了线程饥饿问题。

优化代码 ​

虽然程序功能正常但代码冗余严重,现在将上边的多线程代码进行优化。

1、创建哲学家类

static class PhilosopherThread implements Runnable{
		private ReentrantLock obj1;
		private ReentrantLock obj2;
		public PhilosopherThread(ReentrantLock obj1,ReentrantLock obj2){
			this.obj1 = obj1;
			this.obj2 = obj2;
		}
		@Override
		public void run() {
			while (true){
				if(obj1.tryLock()){
					try {
						if(obj2.tryLock()){
							try {
								logger.debug("吃饭");
								try {
									TimeUnit.MILLISECONDS.sleep(200);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
							}finally {
								obj2.unlock();
							}
						}
					}finally {
						obj1.unlock();
					}

				}
			}
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

2、测试代码

//解决哲学家就餐死锁问题以及线程饥饿,优化代码
	public static void test_philosopher4(){
		//定义5根筷子对象
		ReentrantLock obj1 = new ReentrantLock();
		ReentrantLock obj2 = new ReentrantLock();
		ReentrantLock obj3 = new ReentrantLock();
		ReentrantLock obj4 = new ReentrantLock();
		ReentrantLock obj5 = new ReentrantLock();

		//创建5位哲学家对象
		Thread t1 = new Thread(new PhilosopherThread(obj1, obj2),"t1");
		Thread t2 = new Thread(new PhilosopherThread(obj2, obj3),"t2");
		Thread t3 = new Thread(new PhilosopherThread(obj3, obj4),"t3");
		Thread t4 = new Thread(new PhilosopherThread(obj4, obj5),"t4");
		Thread t5 = new Thread(new PhilosopherThread(obj1, obj5),"t5");

		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

运行程序,一切正常。

← 04 synchronized教程06 读写锁 →








如果发现文档内容有错误或排版错乱,请及时联系站长老苗修改,不胜感激。联系我们
关于我们 | 隐私政策 | 豫ICP备2026003386号-4 | 豫公网安备41010202004008号
目录

本页无章节