Java【多线程】(2)线程属性与线程安全

打印 上一主题 下一主题

主题 878|帖子 878|积分 2634



目次
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构造器。
  1. public class test {
  2.     public static void main(String[] args) {
  3.         // 使用lambda表达式创建线程
  4.         Thread thread = new Thread(() -> {
  5.             // 线程执行的代码
  6.             for (int i = 0; i < 5; i++) {
  7.                 System.out.println("线程正在运行: " + i);
  8.                 try {
  9.                     Thread.sleep(1000); // 模拟线程工作1秒
  10.                 } catch (InterruptedException e) {
  11.                     e.printStackTrace();
  12.                 }
  13.             }
  14.         });
  15.         thread.start();  // 启动线程
  16.     }
  17. }
复制代码
详解
  

  • 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线程安全

再将这个板块之前,先给各人一个案例来引入线程安全这个概念。我们当下有这么一个场景:
  1. public class demo2 {
  2.     public static int count = 0;
  3.     public static void main(String[] args) {
  4.         
  5.         Thread t1 = new Thread(()->{
  6.             for (int i = 0;i < 500;i++){
  7.                 count++;
  8.             }
  9.         });
  10.         Thread t2 = new Thread(()->{
  11.             for (int i = 0;i < 500;i++){
  12.                 count++;
  13.             }
  14.         });
  15.         t1.start();
  16.         t2.start();
  17.         System.out.println(count);
  18.     }
  19. }
复制代码
我们可以看到,我们希望通过俩个线程来完成count自增到1000的操作,打没输出结果并不是我们想要的。

缘故原由是线程刚启动,可能还没有分配到cpu上开始执行,count便被打印出来。
我们这样处理处罚后:
  1. public class demo2 {
  2.     public static int count = 0;
  3.     public static void main(String[] args) {
  4.         Thread t1 = new Thread(()->{
  5.             for (int i = 0;i < 500;i++){
  6.                 count++;
  7.             }
  8.         });
  9.         Thread t2 = new Thread(()->{
  10.             for (int i = 0;i < 500;i++){
  11.                 count++;
  12.             }
  13.         });
  14.         t1.start();
  15.         t2.start();
  16.         try {
  17.             t1.join();
  18.             t2.join();
  19.         } catch (InterruptedException e) {
  20.             e.printStackTrace();
  21.         }
  22.         System.out.println(count);
  23.     }
  24. }
复制代码
发现可以出来希望的结果:

那如果我们只让一个线程加上join呢?会发现结果开始变得随机起来:


因此我们可以知道,上述有线程产生的“bug”即没有输出想要的结果,就被称为线程安全题目,相反,如果在多线程并发的情况下,输出抱负结果就叫做“线程安全”。
2.3.1线程安全题目的缘故原由

   

  • 【根因】随即调理,抢占执行(上文例子就是云云)
  • 多个线程同时修改一个变量
  • 修改操作不是原子性的(意思是某些操作如count++是由多个线程构成完成的)
  • 内存可见性(意思是某些变量的访问不一定直接访问到内存,而是有可能访问到寄存器当中)
  • 不妥锁的使用(下文细讲)
  2.3.2加锁和互斥

如那里理处罚这些线程安全题目呢,这里我们要引入加锁的概念与synchronized关键字。
   加锁是一种同步机制,用于控制多个线程访问共享资源的顺序。
当一个线程获得了锁时,其它线程必须期待该线程释放锁后才能继承访问共享资源。
  加锁的特点:
  

  • 串行化访问

    • 同一时间只有一个线程可以访问被加锁的资源。

  • 防止数据竞争

    • 确保共享资源的操作是原子性的(不会被其他线程中断)。

  • 提拔数据一致性

    • 确保共享资源不会因为多个线程同时操作而引发不一致题目。

  加锁的过程:
  

  • 加锁(Locking): 一个线程试图获取资源的锁,若获取乐成,进入临界区;若失败,则壅闭或期待。
  • 解锁(Unlocking): 线程释放锁,允许其他线程获取锁并继承执行
  互斥(Mutual Exclusion,缩写为 Mutex)是加锁的目的之一,强调同一时间只能有一个线程访问某个共享资源,到达线程之间的互斥访问
  
  怎样实现加锁呢,继承拿上文来举例子:
  1. public class demo2 {
  2.     private int count = 0;
  3.     // 同步实例方法
  4.     public synchronized void increment() {
  5.         count++;
  6.     }
  7.     public int getCount() {
  8.         return count;
  9.     }
  10.     public static void main(String[] args) {
  11.         demo2 demo = new demo2();
  12.         Thread t1 = new Thread(() -> {
  13.             for (int i = 0; i < 1000; i++) {
  14.                 demo.increment();
  15.             }
  16.         });
  17.         Thread t2 = new Thread(() -> {
  18.             for (int i = 0; i < 1000; i++) {
  19.                 demo.increment();
  20.             }
  21.         });
  22.         t1.start();
  23.         t2.start();
  24.         try {
  25.             t1.join();
  26.             t2.join();
  27.         } catch (InterruptedException e) {
  28.             e.printStackTrace();
  29.         }
  30.         System.out.println("Final count: " + demo.getCount());
  31.     }
  32. }
复制代码
运行结果:
  

  2.3.3可重入(怎样本身实现可重入锁)

什么叫可重入呢,我们用一段代码来引入这个概念:
  1. class Counter {
  2.     private int count = 0;
  3.     public void add() {
  4.         synchronized (this) {
  5.             count++;//第一次加锁
  6.         }
  7.     }
  8.     public int get() {
  9.         return count;
  10.     }
  11. }
  12. public class demo3 {
  13.     public static void main(String[] args) {
  14.         Counter counter = new Counter();
  15.         Thread t1 = new Thread(()->{
  16.             for(int i = 0;i < 100;i++){
  17.                 synchronized (counter){
  18.                     counter.add();//第二次加锁
  19.                 }
  20.             }
  21.         });
  22.         t1.start();
  23.         try {
  24.             t1.join();
  25.         } catch (InterruptedException e) {
  26.             e.printStackTrace();
  27.         }
  28.         System.out.println("count = " + counter.get());
  29.     }
  30. }
复制代码
上面代码我们可以看到(如果没有可重入这个概念):
   

  • 第一次加锁操作,能够乐成(锁没人使用)。
  • 第二次进行加锁,此时意味着,锁对象已经是被占用的状态,第二次加锁就会出现壅闭期待。
  要想解除壅闭,只能往下执行才可以,要想往下执行,就必要等到第一次锁被释放,这样就叫做出现了死锁。
  
   为相识决上述题目,Java中的synchronized引入了可重入的概念:
  可重入锁是一种允许同一线程多次获取同一把锁的同步机制,解决了嵌套调用或递归场景下线程自我壅闭的题目,是克制死锁的告急设计。
  以是多个锁递归,只有最外层的锁涉及真正的加锁与解锁
  那我们怎样本身实现一个可重入锁呢,捉住下面焦点就有头绪了:
   可重入锁的焦点机制
  

  • 锁计数器

    • 每个锁对象内部维护一个计数器,记录被同一线程获取的次数。
    • 初次获取锁时计数器=1,每次重入加1,释放时减1,归零后其他线程可竞争锁。

  • 持有线程标识

    • 锁对象记录当前持有锁的线程,确保仅持有线程可重入。

  下面附上示例:
  1. public class MyLock {
  2.     private Thread ownerThread;  // 当前持有锁的线程
  3.     private int lockCount = 0;   // 锁计数器
  4.    
  5.     // 获取锁
  6.     public synchronized void lock() throws InterruptedException {
  7.         Thread currentThread = Thread.currentThread();
  8.         // 若锁已被其他线程持有,则当前线程等待
  9.         while (ownerThread != null && ownerThread != currentThread) {
  10.             wait();
  11.         }
  12.         // 锁未被持有或当前线程重入,更新计数器和持有线程
  13.         ownerThread = currentThread;
  14.         lockCount++;
  15.     }
  16.    
  17.     // 释放锁
  18.     public synchronized void unlock() {
  19.         Thread currentThread = Thread.currentThread();
  20.         // 只有持有锁的线程可以释放锁
  21.         if (ownerThread != currentThread) {
  22.             throw new IllegalMonitorStateException("当前线程未持有锁!");
  23.         }
  24.         lockCount--;
  25.         // 锁计数器归零时完全释放锁
  26.         if (lockCount == 0) {
  27.             ownerThread = null;
  28.             notify(); // 唤醒一个等待线程
  29.         }
  30.     }
  31. }
复制代码
2.4.4死锁(三种情况)

2.4.4.1第一种情况

一个线程,一个锁,被加锁多次。想必这个上文刚讲过,就不多言了,偏重讲后文。
2.4.4.2第二种情况

两个线程,两个锁,互相尝试获得对方的锁。可能直接这样讲不是很好懂,附上代码与注释就可以了:
  1. public class Demo20 {
  2.     public static void main(String[] args) throws InterruptedException {
  3.         // 创建两个锁对象,用于线程同步
  4.         Object locker1 = new Object();
  5.         Object locker2 = new Object();
  6.         // 创建线程 t1
  7.         Thread t1 = new Thread(() -> {
  8.             // 获取 locker1 的锁
  9.             synchronized (locker1) {
  10.                 try {
  11.                     // 线程休眠 1 秒,模拟耗时操作
  12.                     Thread.sleep(1000);
  13.                 } catch (InterruptedException e) {
  14.                     // 如果线程被中断,抛出异常
  15.                     throw new RuntimeException(e);
  16.                 }
  17.                 // 尝试获取 locker2 的锁
  18.                 synchronized (locker2) {
  19.                     // 如果成功获取到 locker2 的锁,打印消息
  20.                     System.out.println("t1 线程两个锁都获取到");
  21.                 }
  22.             }
  23.         });
  24.         // 创建线程 t2
  25.         Thread t2 = new Thread(() -> {
  26.             // 获取 locker1 的锁
  27.             synchronized (locker1) {
  28.                 try {
  29.                     // 线程休眠 1 秒,模拟耗时操作
  30.                     Thread.sleep(1000);
  31.                 } catch (InterruptedException e) {
  32.                     // 如果线程被中断,抛出异常
  33.                     throw new RuntimeException(e);
  34.                 }
  35.                 // 尝试获取 locker2 的锁
  36.                 synchronized (locker2) {
  37.                     // 如果成功获取到 locker2 的锁,打印消息
  38.                     System.out.println("t2 线程两个锁都获取到");
  39.                 }
  40.             }
  41.         });
  42.         // 启动线程 t1 和 t2
  43.         t1.start();
  44.         t2.start();
  45.         // 主线程等待 t1 和 t2 执行完毕
  46.         t1.join();
  47.         t2.join();
  48.     }
  49. }
复制代码
  

  • 线程 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.小结

今天的分享到这里就结束了,喜欢的小伙伴不要忘记点点赞点个关注,你的鼓励就是对我最大的支持,加油!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

万有斥力

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表