Java【多线程】(2)线程属性与线程安全
- 软件开发
- 2025-09-13 02:15:01

目录
1.前言
2.正文
2.1线程的进阶实现
2.2线程的核心属性
2.3线程安全
2.3.1线程安全问题的原因
2.3.2加锁和互斥
2.3.3可重入(如何自己实现可重入锁)
2.4.4死锁(三种情况)
2.4.4.1第一种情况
2.4.4.2第二种情况
2.4.4.3第三种情况
2.4.5避免死锁
3.小结
1.前言
哈喽大家好吖,今天继续来给大家分享线程相关的内容,介绍一部分线程的核心属性,后一部分主要为线程安全部分,当然一篇博文无法讲解完全,会在后续接着为大家讲解。
2.正文2.1线程的进阶实现
上一篇关于线程的博文我们通过Thread类或实现Runnable接口来达到了多线程的实现,接下来给大家一个最推荐的实现方式:lambda表达式实现。
Thread类的构造函数接受一个Runnable接口类型的参数,而Runnable接口有一个run方法。因此,我们可以通过lambda表达式来实现这个接口,并将其传递给Thread构造器。
public class test { public static void main(String[] args) { // 使用lambda表达式创建线程 Thread thread = new Thread(() -> { // 线程执行的代码 for (int i = 0; i < 5; i++) { System.out.println("线程正在运行: " + i); try { Thread.sleep(1000); // 模拟线程工作1秒 } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); // 启动线程 } }详解:
Runnable接口:Runnable接口包含一个run方法,定义了线程要执行的任务。Lambda表达式:()->{}部分是Lambda表达式,它实现了Runnable接口的run方法。这个方法中包含了线程要执行的代码。Thread对象:使用Thread类创建一个新线程,并传入Runnable的实现(即Lambda表达式)。thread.start():调用start()方法来启动线程。线程开始执行Lambda表达式中的run方法。2.2线程的核心属性
线程有不同的生命周期状态,主要包括以下几种:
NEW:线程被创建,但还未启动。RUNNABLE:线程正在执行或等待操作系统分配CPU时间片。就绪状态分为俩种: 随时可以到cpu上去工作。在cpu上正在工作。 BLOCKED:线程因为竞争资源(如同步锁)而被阻塞,无法执行。WAITING:线程正在等待另一个线程的通知。TIMED_WAITING:线程正在等待一个特定的时间段,直到超时或被唤醒。(例如线程的join方法会使线程进入此状态)TERMINATED:线程执行完毕,已终止。附上别的大佬总结很详细的图片。
2.3线程安全
再将这个板块之前,先给大家一个案例来引入线程安全这个概念。我们当下有这么一个场景:
public class demo2 { public static int count = 0; public static void main(String[] args) { Thread t1 = new Thread(()->{ for (int i = 0;i < 500;i++){ count++; } }); Thread t2 = new Thread(()->{ for (int i = 0;i < 500;i++){ count++; } }); t1.start(); t2.start(); System.out.println(count); } }我们可以看到,我们希望通过俩个线程来完成count自增到1000的操作,打没输出结果并不是我们想要的。
原因是线程刚启动,可能还没有分配到cpu上开始执行,count便被打印出来。
我们这样处理后:
public class demo2 { public static int count = 0; public static void main(String[] args) { Thread t1 = new Thread(()->{ for (int i = 0;i < 500;i++){ count++; } }); Thread t2 = new Thread(()->{ for (int i = 0;i < 500;i++){ count++; } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(count); } }发现可以出来希望的结果:
那如果我们只让一个线程加上join呢?会发现结果开始变得随机起来:
因此我们可以知道,上述有线程产生的“bug”即没有输出想要的结果,就被称为线程安全问题,相反,如果在多线程并发的情况下,输出理想结果就叫做“线程安全”。
2.3.1线程安全问题的原因 【根因】随即调度,抢占执行(上文例子就是如此)多个线程同时修改一个变量修改操作不是原子性的(意思是某些操作如count++是由多个线程组成完成的)内存可见性(意思是某些变量的访问不一定直接访问到内存,而是有可能访问到寄存器当中)不当锁的使用(下文细讲) 2.3.2加锁和互斥如何处理这些线程安全问题呢,这里我们要引入加锁的概念与synchronized关键字。
加锁是一种同步机制,用于控制多个线程访问共享资源的顺序。 当一个线程获得了锁时,其它线程必须等待该线程释放锁后才能继续访问共享资源。
加锁的特点:
串行化访问: 同一时刻只有一个线程可以访问被加锁的资源。 防止数据竞争: 确保共享资源的操作是原子性的(不会被其他线程中断)。 提升数据一致性: 确保共享资源不会因为多个线程同时操作而引发不一致问题。加锁的过程:
加锁(Locking): 一个线程试图获取资源的锁,若获取成功,进入临界区;若失败,则阻塞或等待。解锁(Unlocking): 线程释放锁,允许其他线程获取锁并继续执行互斥(Mutual Exclusion,缩写为 Mutex)是加锁的目的之一,强调同一时刻只能有一个线程访问某个共享资源,达到线程之间的互斥访问。
如何实现加锁呢,继续拿上文来举例子:
public class demo2 { private int count = 0; // 同步实例方法 public synchronized void increment() { count++; } public int getCount() { return count; } public static void main(String[] args) { demo2 demo = new demo2(); Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { demo.increment(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { demo.increment(); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final count: " + demo.getCount()); } }运行结果:
2.3.3可重入(如何自己实现可重入锁)什么叫可重入呢,我们用一段代码来引入这个概念:
class Counter { private int count = 0; public void add() { synchronized (this) { count++;//第一次加锁 } } public int get() { return count; } } public class demo3 { public static void main(String[] args) { Counter counter = new Counter(); Thread t1 = new Thread(()->{ for(int i = 0;i < 100;i++){ synchronized (counter){ counter.add();//第二次加锁 } } }); t1.start(); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count = " + counter.get()); } }上面代码我们可以看到(如果没有可重入这个概念):
第一次加锁操作,能够成功(锁没人使用)。第二次进行加锁,此时意味着,锁对象已经是被占用的状态,第二次加锁就会出现阻塞等待。要想解除阻塞,只能往下执行才可以,要想往下执行,就需要等到第一次锁被释放,这样就叫做出现了死锁。
为了解决上述问题,Java中的synchronized引入了可重入的概念:
可重入锁是一种允许同一线程多次获取同一把锁的同步机制,解决了嵌套调用或递归场景下线程自我阻塞的问题,是避免死锁的重要设计。
所以多个锁递归,只有最外层的锁涉及真正的加锁与解锁。
那我们如何自己实现一个可重入锁呢,抓住下面核心就有头绪了:
可重入锁的核心机制
锁计数器:
每个锁对象内部维护一个计数器,记录被同一线程获取的次数。
首次获取锁时计数器=1,每次重入加1,释放时减1,归零后其他线程可竞争锁。
持有线程标识:
锁对象记录当前持有锁的线程,确保仅持有线程可重入。
下面附上示例:
public class MyLock { private Thread ownerThread; // 当前持有锁的线程 private int lockCount = 0; // 锁计数器 // 获取锁 public synchronized void lock() throws InterruptedException { Thread currentThread = Thread.currentThread(); // 若锁已被其他线程持有,则当前线程等待 while (ownerThread != null && ownerThread != currentThread) { wait(); } // 锁未被持有或当前线程重入,更新计数器和持有线程 ownerThread = currentThread; lockCount++; } // 释放锁 public synchronized void unlock() { Thread currentThread = Thread.currentThread(); // 只有持有锁的线程可以释放锁 if (ownerThread != currentThread) { throw new IllegalMonitorStateException("当前线程未持有锁!"); } lockCount--; // 锁计数器归零时完全释放锁 if (lockCount == 0) { ownerThread = null; notify(); // 唤醒一个等待线程 } } } 2.4.4死锁(三种情况) 2.4.4.1第一种情况一个线程,一个锁,被加锁多次。想必这个上文刚讲过,就不多言了,着重讲后文。
2.4.4.2第二种情况两个线程,两个锁,互相尝试获得对方的锁。可能直接这样讲不是很好懂,附上代码与注释就可以了:
public class Demo20 { public static void main(String[] args) throws InterruptedException { // 创建两个锁对象,用于线程同步 Object locker1 = new Object(); Object locker2 = new Object(); // 创建线程 t1 Thread t1 = new Thread(() -> { // 获取 locker1 的锁 synchronized (locker1) { try { // 线程休眠 1 秒,模拟耗时操作 Thread.sleep(1000); } catch (InterruptedException e) { // 如果线程被中断,抛出异常 throw new RuntimeException(e); } // 尝试获取 locker2 的锁 synchronized (locker2) { // 如果成功获取到 locker2 的锁,打印消息 System.out.println("t1 线程两个锁都获取到"); } } }); // 创建线程 t2 Thread t2 = new Thread(() -> { // 获取 locker1 的锁 synchronized (locker1) { try { // 线程休眠 1 秒,模拟耗时操作 Thread.sleep(1000); } catch (InterruptedException e) { // 如果线程被中断,抛出异常 throw new RuntimeException(e); } // 尝试获取 locker2 的锁 synchronized (locker2) { // 如果成功获取到 locker2 的锁,打印消息 System.out.println("t2 线程两个锁都获取到"); } } }); // 启动线程 t1 和 t2 t1.start(); t2.start(); // 主线程等待 t1 和 t2 执行完毕 t1.join(); t2.join(); } }线程 t1:
先获取 locker1 的锁,然后休眠 1 秒。
接着尝试获取 locker2 的锁。
线程 t2:
同样先获取 locker1 的锁,然后休眠 1 秒。
接着尝试获取 locker2 的锁。
问题:此时死锁就出现了
t1 持有 locker1 并等待 locker2。
t2 持有 locker1 并等待 locker2。
两个线程互相等待对方释放锁,导致程序无法继续执行。
2.4.4.3第三种情况死锁的第三种情况,即n个线程和m把锁,这里就要引入一个很著名的问题,哲学家就餐问题:
哲学家就餐问题(Dining Philosophers Problem) 是计算机科学中经典的同步与死锁问题,由 Edsger Dijkstra 提出,用于演示多线程环境中的资源竞争和死锁风险。
1. 问题描述
场景:5 位哲学家围坐在圆桌旁,每人面前有一碗饭,相邻两人之间放一支筷子(共 5 支筷子)。
行为:
哲学家交替进行 思考 和 就餐。
就餐时需要 同时拿起左右两边的筷子。
完成就餐后放下筷子,继续思考。
核心问题:如何设计算法,使得所有哲学家都能公平、高效地就餐,且避免死锁。
2. 死锁的产生
如果所有哲学家 同时拿起左边的筷子,会发生以下情况:
每个哲学家都持有左边的筷子,等待右边的筷子。
右边的筷子被其他哲学家持有,形成 循环等待。
所有哲学家无法继续,导致 死锁。
3. 解决思路
核心思想:为所有资源(筷子)定义一个全局顺序,要求哲学家必须按固定顺序获取资源。
实现方式:
将筷子编号为 0 到 4。
每位哲学家必须先拿编号较小的筷子,再拿编号较大的筷子。
效果:
破坏循环等待条件(不可能所有人同时等待右侧筷子)。
保证至少一位哲学家可以拿到两只筷子。
2.4.5避免死锁上述讲完了死锁出现的场景,这里可以总结死锁出现的四个必要条件:
锁是互斥的。(一个线程拿到锁之后,另一个线程再尝试获取锁,必须要阻塞等待)锁是不可抢占的。(即线程1拿到锁, 线程2也尝试获取这个锁,线程2 必须阻塞等待2而不是线程2直接把锁抢过来)请求和保持。(一个线程拿到锁1之后,不释放锁1 的前提下,获取锁2)循环等待。(多个线程, 多把锁之间的等待过程,构成了"循环",即A 等待 B, B 也等待 A 或者 A 等待 B,B 等待 C,C等待 A)既然我们知道死锁是如何产生的,那么解决死锁的思路就有啦:
打破3条件,可以把嵌套的锁改成并列的锁。打破4条件,加锁的顺序进行约定。 3.小结今天的分享到这里就结束了,喜欢的小伙伴不要忘记点点赞点个关注,你的鼓励就是对我最大的支持,加油!
Java【多线程】(2)线程属性与线程安全由讯客互联软件开发栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“Java【多线程】(2)线程属性与线程安全”