JUC并发编程(二):线程相关知识点

news2024/11/20 13:17:40

1.背景

实现编发编程的主要手段就是多线程。线程是操作系统里的一个概念。接下来先说说两者的定义、联系与区别。

1.1 进程和线程的区别

进程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。如下所示就是mac电脑后台的程序进程:

线程

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

用window系统的用户都喜欢下载电脑管家,如下所示:

这里电脑管家就是一个进程,你可以同时进行病毒查杀、垃圾清理、电脑加速等操作,这些操作就是由一个个线程去执行完成的。谈到多线程就不能说一下什么是上下文切换

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨群:Shepherd_126

1.2 什么是上下文切换

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。

  • 主动让出 CPU,比如调用了 sleep(), wait() 等。
  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换

上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁上下文切换就会影响多线程的执行速度,这也是多线程不一定就快的原因。

Thread.Sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”。竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权

1.3 用户线程和守护线程

Java 中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)

线程的daemon属性为true表示是守护线程,false表示是用户线程。JVM启动会调用 main 函数, main函数所在的线程就是一个用户线程,其实在 JVM后台还启动了很多守护线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程。两者的区别是:当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出

public class DaemonDemo {
    public static void main(String[] args) {
            Thread t1 = new Thread(() -> {
                System.out.println(Thread.currentThread().getName()+" 开始运行,"+(Thread.currentThread().isDaemon() ? "守护线程":"用户线程"));
                while (true) {

                }
            }, "t1");
            //线程的daemon属性为true表示是守护线程,false表示是用户线程
            t1.setDaemon(true);
            t1.start();
            //3秒钟后主线程再运行
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("----------main线程运行完毕");
    }
}

2.线程的生命周期和状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW: 初始状态,线程被创建出来但没有被调用 start()
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED :阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换

由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态,线程的创建主要有这几种方式:实现Runnable接口和继承Thread类,使用 FutureTask 方式,代码如下所示:

public class ThreadTest {


    public static void main(String[] args) throws Exception {
      System.out.println("main......start.....");
      // 方式1
      Thread thread = new Thread01();
      thread.start();
      System.out.println("main......end.....");

      // 方式2
      Runnable01 runnable01 = new Runnable01();
      new Thread(runnable01).start();

      // 放松3
      FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
      new Thread(futureTask).start();
      System.out.println(futureTask.get());
      System.out.println("main......end.....");
    }
  	
  	 public static class Thread01 extends Thread {
        @Override
        public void run() {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
        }
    }


    public static class Runnable01 implements Runnable {
        @Override
        public void run() {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
        }
    }


    public static class Callable01 implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
            return i;
        }
    }
  
  
 }

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,底层会调用Java本地方法``start0(), 当分配到时间片后就可以开始运行了。 start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。 但是,直接执行run()方法,会把run()方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。线程在执行完了run()`方法之后将会进入到 TERMINATED(终止) 状态。

当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。

线程等待和唤醒的方式大概有如下三种:

  • 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程

        public static void main(String[] args) {
            Object objectLock = new Object();  //同一把锁,类似资源类
    
            new Thread(() -> {
                synchronized (objectLock) {
                    try {
                        objectLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒了");
            }, "t1").start();
    
            //暂停几秒钟线程
            try {
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(() -> {
                synchronized (objectLock) {
                    objectLock.notify();
                }
            }, "t2").start();
        }
    

    Object类中的wait、notify、notifyAll用于线程等待和唤醒的方法,都必须在synchronized内部执行(必须用到关键字synchronized)

    先wait后notify、notifyall方法,等待中的线程才会被唤醒,否则无法唤醒。

  • Condition接口中的await后signal方法实现线程的等待和唤醒

    public static void main(String[] args) {
            Lock lock = new ReentrantLock();
            Condition condition = lock.newCondition();
    
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t" + "start");
                    condition.await();
                    System.out.println(Thread.currentThread().getName() + "\t" + "被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }, "t1").start();
    
            //暂停几秒钟线程
            try {
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            new Thread(() -> {
                lock.lock();
                try {
                    condition.signal();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + "通知了");
            }, "t2").start();
    
        }
    

    调用condition中线程等待和唤醒的方法的前提是,要在lock和unlock方法中,要有锁才能调用。必须先await()后signal,否则线程无法被唤醒。Condition 精准的通知和唤醒线程,而object的wait和notify机制做不到

  • LockSupport类中的park等待和unpark唤醒

    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。

    public static void main(String[] args) {
            Thread t1 = new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "\t" + System.currentTimeMillis());
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + "\t" + System.currentTimeMillis() + "---被叫醒");
            }, "t1");
            t1.start();
    
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName() + "\t" + System.currentTimeMillis() + "---unpark over");
        }
    
    

如何使用中断标识停止线程?

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("-----t1    线程被中断了,break,程序结束");
                break;
            }
            System.out.println("-----hello");
        }
    }, "t1");
    t1.start();

    System.out.println("**************" + t1.isInterrupted());
    //暂停5毫秒
    try {
        TimeUnit.MILLISECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t1.interrupt();
    System.out.println("**************" + t1.isInterrupted());
}

当对一个线程,调用 interrupt() 时:

① 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。
被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

② 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),在别的线程中调用当前线程对象的interrupt方法,
那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。

3. 线程死锁

死锁,是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

产生死锁的必要条件:

  • 互斥条件:所谓互斥就是进程在某一时间内独占资源。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁的解决方法:

  • 撤消陷于死锁的全部进程。
  • 逐个撤消陷于死锁的进程,直到死锁不存在。
  • 从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失。
  • 从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。

死锁案例如下:

public static void main(String[] args) {
        final Object objectLockA = new Object();
        final Object objectLockB = new Object();

        new Thread(() -> {
            synchronized (objectLockA) {
                System.out.println(Thread.currentThread().getName() + "\t" + "自己持有A,希望获得B");
                //暂停几秒钟线程
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objectLockB) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "A-------已经获得B");
                }
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (objectLockB) {
                System.out.println(Thread.currentThread().getName() + "\t" + "自己持有B,希望获得A");
                //暂停几秒钟线程
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objectLockA) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "B-------已经获得A");
                }
            }
        }, "B").start();

    }

使用jps -l查看当前案例的进程号,再使用`jstack 进程号会看到如下信息:

Found one Java-level deadlock:
=============================
"B":
  waiting to lock monitor 0x00007fe86e02f208 (object 0x000000076adfdf60, a java.lang.Object),
  which is held by "A"
"A":
  waiting to lock monitor 0x00007fe86e0332a8 (object 0x000000076adfdf70, a java.lang.Object),
  which is held by "B"

Java stack information for the threads listed above:
===================================================
"B":
        at com.shepherd.juc.JucExampleDemo.lambda$main$1(JucExampleDemo.java:40)
        - waiting to lock <0x000000076adfdf60> (a java.lang.Object)
        - locked <0x000000076adfdf70> (a java.lang.Object)
        at com.shepherd.juc.JucExampleDemo$$Lambda$2/2094777811.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"A":
        at com.shepherd.juc.JucExampleDemo.lambda$main$0(JucExampleDemo.java:25)
        - waiting to lock <0x000000076adfdf70> (a java.lang.Object)
        - locked <0x000000076adfdf60> (a java.lang.Object)
        at com.shepherd.juc.JucExampleDemo$$Lambda$1/1879492184.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1081339.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

比特大陆:全员工资停发!昔日的“矿机一哥”遇现金流危机?

近日&#xff0c;一则关于比特大陆暂缓发放9月份全体员工部分工资的消息在网上流传。比特大陆对员工称公司营运现金流尚未转正&#xff0c;尤其是部矿(部署矿机进矿场)进度严重不达标&#xff0c;决定暂缓发放9月份全体员工部分公司&#xff0c;10月7日后视情况发放。 脉脉上多…

Unity基础课程之物理引擎6-关于物理材质的使用和理解

每个物体都有着不同的摩擦力。光滑的冰面摩擦力很小&#xff0c;而地毯表面的摩擦力则很大。另外每种材料也有着不同的弹性&#xff0c;橡皮表面的弹性大&#xff0c;硬质地面的弹性小。在Unity中这些现象都符合日常的理念。虽然从原理上讲&#xff0c;物体的摩擦力和弹性有着更…

利用异常实现短期hook

场景1 调用目标call 需要跳过某些判断或者函数 场景2 目标call 只需要部分指令执行 大概实现技术 设置线程上下文设置drX寄存器 实现硬件执行断点 主动调用目标call 通过硬件断点获取寄存器或修改eip 以上实现不改变crc且不通过驱动实现。只对当前执行线程有效&#xff…

ubuntu离线编译安装cmake 3.22.5(could not fonud OPENSSL) and cmake-versinon查不到版本问题

1、首先去cmake官网下载压缩包,例如: cmake-3.22.5.tar.gz 2、拉到ubuntu进行解压: tar -zxcf cmake-3.22.5.tar.gz 3、cd 进入目录 cd cmake-3.22.5 4、执行configure可执行文件 ./configure 如果在编译过程中出现报错:Could NOT findOpenSSL,原因可能是缺少ssl库 按…

第四节(1):EXCEL中判断一个WORD文件是否被打开

《VBA信息获取与处理》教程(10178984)是我推出第六套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。这部教程给大家讲解的内容有&#xff1a;跨应用程序信息获得、随机信息的利用、电子邮件的发送、VBA互联网…

如何计算一个结构体的大小?(C语言)

文章目录 写在前面1. 结构体的内存对齐2. 结构体大小的计算方式&#xff08;四步法&#xff09;3. 修改默认对齐数4. 内存对齐的意义5. 结构体设计技巧 写在前面 我们知道C语言中各种内置类型定义的变量都是有大小的&#xff0c;比如 int 类型的变量占4个字节&#xff0c;而像…

对一个变速器原理的分析

背景 原本是朋友在调试一个看起来比较新的变速器驱动&#xff0c;整体来说支持两种变速模式&#xff0c;一种是进程级&#xff0c;这种用了HOOK&#xff0c;中规中矩的实现&#xff0c;原理网上都有。另一种是”系统级内核全局变速“&#xff0c;这个模式初步看了下有些特殊&a…

猫头虎带您了解CSDN1024城市开发者大会分会场报名指南(文末送30元优惠券)

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

全链路压测:保障系统稳定性与性能的关键措施

随着互联网应用规模的不断扩大和用户对系统性能要求的提高&#xff0c;全链路压测成为保障系统稳定性和性能的关键环节。本文将介绍全链路压测的概念、重要性以及实施全链路压测的步骤和最佳实践&#xff0c;以帮助读者更好地理解和应用全链路压测技术。 一、全链路压测的概念与…

客服支持Chatbot提供即时回答,改善用户体验

大家在网上冲浪了那么久&#xff0c;一定对于客户支持Chatbot都有所了解。客户支持Chatbot就像真人客服一样&#xff0c;可以与人进行简单的对话&#xff0c;并针对人们的需求给出相应的回答。虽然有时候得到的答案并不怎么靠谱吧&#xff0c;但是总的还是比较节省人工的&#…

docker概念

docker 容器&#xff1a;就是提供在多台主机上运行的应用程序相同的运行环境。 docker的概念 是开源的容器&#xff0c;是由Go语言开发的&#xff0c;是容器里面运用的工具&#xff0c;他是一个轻量级的虚拟机&#xff0c;可以利用docker在多台主机上创建与运行容器。 docke…

矩阵剪辑系统源码----pc/手机端双开发

剪辑系统&#xff0c;剪辑矩阵系统&#xff0c;剪辑矩阵系统主要是针对抖音、快手、bili平台的一个工具&#xff0c;今天就来给大家交流一下这 个产品的主要功能以及构成。剪辑矩阵系统&#xff0c;矩阵剪辑系统源码-这产品主要功能就是一个视频剪辑功能&#xff0c;这个视频剪…

深入解析:探索Nginx与Feign交锋的背后故事 - 如何优雅解决微服务通信中的`301 Moved Permanently`之谜!

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

2023年中国数据存储市场现状及发展前景预测分析

中商情报网讯&#xff1a;当前&#xff0c;新一代信息技术快速发展推动信息产业发生了重大变革&#xff0c;数据存储行业将很快成为信息领域一个重要的产业分支。生成式人工智能催生算力需求&#xff0c;各种新兴应用场景对数据存储的容量、效率、流动性和安全性等方面提出了更…

快速入门C++

W...Y的主页&#x1f60a; 代码仓库分享&#x1f495; &#x1f354;前言&#xff1a; 我们学习了C语言&#xff0c;有了C语言的底子就更容易学习C&#xff0c;今天让我们认识一下C&#xff0c;并了解分析一下C。 目录 什么是C 为什么会出现C C的发展史 C与C语言的区别 …

漫画:大模型用于腾讯广告,难在哪?

&#xff08;一&#xff09;关键词&#xff1a;三大套路 大模型火了&#xff0c;大模型的套路也火了。 套路一&#xff1a; 但凡有点科技含量的公司&#xff0c; 没个大模型都对不起“市值”和“估值”。 面子谁不要&#xff1f; 那用开源。 套路二&#xff1a; 说早有布局&…

代码随想录Day16 LeetCode T654 最大二叉树 T617 合并二叉树 T700 二叉搜索树中的搜索

本文思路和更详细的解析来自于:代码随想录 (programmercarl.com)​​​​​​ LeetCode T654 最大二叉树 题目链接:654. 最大二叉树 - 力扣&#xff08;LeetCode&#xff09; 题目思路: 这题和昨天的题目很像,我们仍然需要构造一棵二叉树,我们仍然使用递归来完成,以下我们开始…

vue原生实现element上传多张图片浏览删除

vue原生实现element上传多张图片浏览删除 <div class"updata-component" style"width:100%;"><div class"demo-upload-box clearfix"><div class"demo-upload-image-box" v-if"imageUrlArr && imageUrlAr…

使用 Eziriz .NET Reactor 对c#程序加密

我目前测试过好几个c#加密软件。效果很多时候是加密后程序执行错误&#xff0c;或者字段找不到的现象 遇到这个加密软件用了一段时间都很正常&#xff0c;分享一下使用流程 破解版本自行百度。有钱的支持正版&#xff0c;我用的是 Eziriz .NET Reactor 6.8.0 第一步 安装 Ezi…

内存操作函数(memcpy、memmove、memset、memcmp)---- C语言

文章目录 摘要1. memcpy1.1 函数介绍1.2 模拟实现 2. memmove2.1 函数介绍2.2 模拟实现 3. memset3.1 函数介绍 4. memcmp4.1 函数介绍 摘要 本篇文章介绍了C语言中常用的内存操作函数&#xff0c;包括 memcpy、memmove、memset和memcpy。这些函数用于处理内存数据的复制、移动…