Java并发编程基础总结

进程和线程概念

什么进程

进程是系统运行的基本单位,通俗的理解我们计算机启动的每一个应用程序都是一个进程。如下图所示,在Windows中这一个个exe文件,都是一个进程。而在JVM下,每一个启动的Main方法都可以看作一个进程。

在这里插入图片描述

什么是线程

线程是比进程更小的单位,所以在进行线程切换时的开销会远远小于进程,所以线程也常常被称为轻量级进程。每一个进程中都会有一个或者多个线程,在JVM中每一个Java线程都会共享他们的进程的堆区方法区。但是每一个进程都会有自己的程序计数器虚拟机栈本地方法栈

Java天生就是一个多线程的程序,我们完全可以运行下面这段代码看看一段main方法中会有那些线程在运行

public class MultiThread {
    public static void main(String[] args) {
        // 获取 Java 线程管理 MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程 ID 和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
        }
    }
}

输出结果如下,所以Java程序在main函数运行时,还有其他的线程再跑。

[6] Monitor Ctrl-Break //这个线程是IDEA用来监控Ctrl-Break中断信号的线程
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 方法处理Jvm信号的线程
[3] Finalizer //清除finalize 方法的线程
[2] Reference Handler // 清除引用的线程
[1] main // main入口

从JVM角度理解进程和线程的区别

图解两者区别

如下图所示,可以看出线程是比进程更小的单位,进程是独立的,彼此之间不会干扰,但是线程在同一个进程中共享堆区和方法区,虽然开销较小,但是资源之间管理和分配处理相对于进程之间要更加小心。

在这里插入图片描述

程序计数器、虚拟机栈、本地方法栈为什么线程中是各自独立的

  1. 程序计数器私有的原因:学过计算机组成原理的小伙伴应该都知晓,程序计数器用于记录当前下一条要执行的指令的单元地址,JVM也一样,有了程序计数器才能保证在多线程的情况下,这个线程被挂起再被恢复时,我们可以根据程序计数器找到下一次要执行的指令的位置。
  2. 虚拟机栈私有的原因:每一个Java线程在执行方法时,都会创建一个栈帧用于保存局部变量常量池引用操作数栈等信息,在这个方法调用到完成前,它对应的信息都会基于栈帧保存在虚拟机栈上。
  3. 本地方法栈私有的原因:和虚拟机栈类似,只不过本地方法栈保存的native方法的信息。

所以为了保证局部变量不被别的线程访问到,虚拟机栈和本地方法栈都是私有的,这就是我们解决某些线程安全问题时,常会用到一个叫栈封闭技术

关于栈封闭技术如下所示,将变量放在局部,每个线程都有自己的虚拟机栈,线程安全

public class StackConfinement implements Runnable {

    //全部变量 多线操作会有现场问题
    int globalVariable = 0;

    public void inThread() {
        //栈封闭技术,将变量放在局部,每个线程都有自己的虚拟机栈 线程安全
        int neverGoOut = 0;
        synchronized (this) {
            for (int i = 0; i < 10000; i++) {
                neverGoOut++;
            }
        }

        System.out.println("栈内保护的数字是线程安全的:" + neverGoOut);//栈内保护的数字是线程安全的:10000

    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            globalVariable++;
        }
        inThread();
    }

    public static void main(String[] args) throws InterruptedException {
        StackConfinement r1 = new StackConfinement();
        Thread thread1 = new Thread(r1);
        Thread thread2 = new Thread(r1);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        
        System.out.println(r1.globalVariable); //13257
    }
}

多线程常见面试题

并发和并行的区别是什么?

  1. 并发:并发我们可以理解为,两个线程先后执行,但是从宏观角度来看,他们几乎是并行的。
  2. 并行:并行我们可以理解为两个线程同一时间都在运行。

同步和异步是什么意思?

  1. 同步:同步就是一个调用没有结果前,不会返回,直到有结果的才返回。
  2. 异步:异步即发起一个调用后,不等结果如何直接返回。

为什么需要多线程,多线程解决了什么问题

从宏观角度来看:线程可以理解为轻量级进程,切换开销远远小于进程,所以在多核CPU的计算机下,使用多线程可以更好的利用计算机资源从而提高计算机利用率和效率来应对现如今的高并发网络环境。

从微观场景下来说: 单核场景,在单核CPU情况下,假如一个线程需要进行IO才能执行业务逻辑,若只有单线程,这就意味着IO期间发生阻塞线程却只能干等。假如我们使用多线程的话,在当前线程IO期间,我们可以将其挂起,让出CPU时间片让其他线程工作。

多核场景下,假如我们有一个很复杂的任务需要进程各种IO和业务计算,假如只有一个线程的话,无论我们有多少个CPU核心,因为单线程的缘故他永远只能利用一个CPU核心,假如我们使用多线程,那么这些线程就会映射到不同的CPU核心上,做到最好的利用计算机资源,提高执行效率,执行事件约为单线程执行事件/CPU核心数。

创建线程方式有哪些(重点)

  1. 继承Thread 实现多线程
//继承Thread 然后start
public class Task extends Thread {

    public void run() {
        for (int x = 0; x < 60; x++)
            System.out.println("Task run----" + x);
    }


    public static void main(String[] args) throws InterruptedException {
        Task d = new Task();//创建好一个线程。
        d.start();//开启线程并执行该线程的run方法。
        d.join();
    }
}

  1. Runable接口实现多线程
//实现Runnable 方法
public class Ticket implements Runnable {
    private int tick = 100;


    public void run() {
        synchronized (Ticket.class) {
            while (true) {
                if (tick > 0) {
                    System.out.println(Thread.currentThread().getName() + "....sale : " + tick--);
                }
            }
        }

    }
}


public class Ticket implements Runnable {
    private int tick = 100;


    public void run() {
        synchronized (Ticket.class) {
            while (true) {
                if (tick > 0) {
                    System.out.println(Thread.currentThread().getName() + "....sale : " + tick--);
                }else{
                    break;
                }
            }
        }

    }
}
  1. FutureTask+Callable
FutureTask<String> futureTask=new FutureTask<>(()-> "123");
		new Thread(futureTask).start();
		try {
			System.out.println(futureTask.get());
		} catch (Exception e) {
			e.printStackTrace();
		} 

为什么需要Runnable接口实现多线程

由于Java的类只能单继承,当一个类已有继承类时,某个函数需要扩展为多线程这时候,Runnable接口就是最好的解决方案。

Thread和Runnable使用的区别

  1. 继承Thread:线程代码存放在Thread子类的run方法中,调用start()即可实现调用。
  2. Runnable:线程代码存在接口子类的run方法中,需要实例化一个线程对象Thread并将其作为参数传入,才能调用到run方法。

Thread类中run()和start()的区别

  1. run:仅仅是方法,在线程实例化之后使用run等于一个普通对象的直接调用。
  2. start:开启了线程并执行线程中的run方法,这期间程序才真正执行从用户态到内核态,创建线程的动作。

Java线程有哪几种状态(笔试)

新建(NEW):新创建的了一个线程对象,该对象并没有调用start()
可运行(RUNNABLE):线程对象创建后,并调用了start方法,等待分配CPU时间执行代码逻辑。
阻塞(BLOCKED):阻塞状态,等待锁的释放。当线程在synchronized 中被wait,然后再被唤醒时,若synchronized 有其他线程在执行,那么它就会进入BLOCKED状态。
等待(WAITING):因为某些原因被挂起,等待其他线程通知或者唤醒。
超时等待(TIME_WAITING):等待时间后自行返回,而不像WAITING那样没有通知就一直等待。
终止(TERMINATED):该线程执行完毕,终止状态了。

和操作系统的线程状态的区别

如下图所示,实际上操作系统层面可将RUNNABLE分为Running以及ReadyJava设计者之所以没有区分那么细是因为现代计算机执行效率非常高,这两个状态在宏观角度几乎无法感知。现代操作系统对多线程采用时间分片的抢占式调度算法,使得每个线程得到CPU10-20ms 处于运行状态,然后在让出CPU时间片,在不久后又会被调度执行,所以对于这种微观状态区别,Java设计者认为没有必要为了这么一瞬间进行这么多的状态划分。

在这里插入图片描述

什么是上下文切换

线程在执行过程中都会有自己的运行条件和状态,这些运行条件和状态我们就称之为线程上下文,这些信息例如程序计数器虚拟机栈本地方法栈等信息。当出现以下几种情况的时候就会从占用CPU状态中退出:

  1. 线程主动让出CPU,例如调用wait或者sleep等方法。
  2. 线程的CPU 时间片用完 而退出CPU占用状态 (因为操作系统为了避免某些线程独占CPU导致其他线程饥饿的情况就设定的例如时间分片算法)
  3. 线程调用了阻塞类型的系统中断,例如IO请求等。
  4. 线程被终止或者结束运行。

上述的前三种情况都会发生上下文切换。为了保证线程被切换在恢复时能够继续执行,所以上下文切换都需要保存线程当前执行的信息,并恢复下一个要执行线程的现场。这种操作就会占用CPU和内存资源,频繁的进行上下文切换就会导致整体效率低下。

线程死锁问题

如下图所示,两个线程各自持有一把锁,必须拿到对方手中那把锁才能释放自己的锁,正是这样一种双方僵持的状态就会导致线程死锁问题。

在这里插入图片描述

翻译称代码就如下图所示

public class DeadLockDemo {
    public static final Object lock1 = new Object();

    public static final Object lock2 = new Object();


    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock1){
                System.out.println("线程1获得锁1,准备获取锁2");


                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2){
                    System.out.println("线程1获得锁2");
                }
            }
        }).start();


        new Thread(() -> {
            synchronized (lock2){
                System.out.println("线程2获得锁2,准备获取锁1");

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }


                synchronized (lock1){
                    System.out.println("线程2获得锁1");
                }
            }
        }).start();
    }
}

输出结果

线程1获得锁1,准备获取锁2
线程2获得锁2,准备获取锁1

符合以下4个条件的场景就会发生死锁问题:

  1. 互斥:一个资源任意时间只能被一个线程获取。
  2. 请求与保持条件:一个线程拿到资源后,在获取其他资源而进入阻塞期间,不会释放已有资源。
  3. 不可剥夺条件:该资源被线程使用时,其他线程无法剥夺该线程使用权,除非这个线程主动释放。
  4. 循环等待条件:若干线程获取资源时,取锁的流程构成一个头尾相接的环,如上图。

预防死锁的3种方式

  1. 破坏请求与保持条件:以上面代码为例,我们要求所有线程必须一次性获得两个锁才能进行业务处理。即要求线程一次性获得所有资源才能进行逻辑处理。
  2. 破坏不可剥夺:资源被其他线程获取时,我们可以强行剥夺使用权。
  3. 破坏循环等待:这个就比较巧妙了,例如我们上面lock1 id为1,lock2id为2,我们让每个线程取锁时都按照lock的id顺序取锁,这样就避免构成循环队列。
  4. 操作系统思想(银行家算法):这个就涉及到操作系统知识了,大抵的意思是在取锁之前对资源分配进行评估,如果在给定资源情况下不能完成业务逻辑,那么就避免这个线程取锁,感兴趣的读者可以

sleep和wait方法区别

  1. sleep不会释放锁,只是单纯休眠一会。而wait则会释放锁。
  2. sleep单纯让线程休眠,在给定时间后就会苏醒,而wait若没有设定时间的话,只能通过notify或者notifyAll唤醒。
  3. sleepThread 的方法,而waitObject 的方法
  4. wait常用于线程之间的通信或者交互,而sleep单纯让线程让出执行权。

为什么sleep会定义在Thread

因为sleep要做的仅仅是让线程休眠,所以不涉及任何锁释放等逻辑,放在Thread上最合适。

为什么wait会定义在Object 上

我们都知道使用wait时就会释放锁,并让对象进入WAITING 状态,会涉及到资源释放等问题,所以我们需要将wait放在Object 类上。

可以直接调用 Thread 类的 run 方法吗?

若我们编写run方法,然后调用Threadstart方法,线程就会从用户态转内核态创建线程,并在获取CPU时间片的时候开始运行,然后运行run方法。
若直接调用run方法,那么该方法和普通方法没有任何差别,它仅仅是一个名字为run的普通方法。

假如在进程中, 已经开辟了多个线程, 其中一个线程怎么中断其它线程?

找到线程对应线程组即可定位到线程,然后调用interrupt将其打断即可。但如果想精确定位线程,我们还是建议使用ThreadLocal对线程做个标记。

ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
        if (threadGroup != null) {
            Thread[] threads = new Thread[(int) (threadGroup.activeCount() * 1.2)];
            int count = threadGroup.enumerate(threads, true);
            for (int i = 0; i < count; i++) {
                if (threads[i].getId() == threadId) {
                    return threads[i];
                }
            }
        }

        return null;

IO阻塞的线程会占用CPU资源吗?如何避免线程霸占CPU?

由于讲述问题的篇幅比较大,笔者专门写了一篇文章来讨论这两个问题,感兴趣的朋友可以看看:来聊聊IO阻塞与CPU任务调度

参考文章

Java并发编程:volatile关键字解析:https://www.cnblogs.com/dolphin0520/p/3920373.html

图解 | 你管这破玩意叫线程池?: https://mp.weixin.qq.com/s?__biz=Mzk0MjE3NDE0Ng==&mid=2247491549&idx=1&sn=1d5728754e8c06a621bbdca336d85452&chksm=c2c66570f5b1ec66df623e5300084257bd943b134d34e16abaacdb58834702dbbc4599868b89&scene=178&cur_album_id=1703494881072955395#rd

我是一个线程:https://mp.weixin.qq.com/s/IkNfuE541Mqqbv2iLIhMRQ

Java 并发常见面试题总结(上):https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html#什么是线程和进程

创建线程几种方式_线程创建的四种方式及其区别:https://cloud.tencent.com/developer/article/2135189#:~:text=创建线程的几种方式: 方式1:通过继承Thread类创建线程 步骤:1.定义Thread类的子类,并重写该类的run方法,该方法的方法体就是线程需要执行的任务,因此run,()方法也被称为线程执行体 2.创建Thread子类的实例,也就是创建了线程对象 3.启动线程,即调用线程的start ()方法

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

.Net Reactor 使用心得

主密钥是干嘛的&#xff1f; 1 若要创建有效的许可证文件&#xff0c;必须使用与用于生成受.NET Reactor保护的输出相同的主密钥来创建许可证。 2 主密钥是在创建项目时生成的&#xff01;必须保存该项目才能保留原始密钥。 dll而不是exe 由于使用的是.net6 生成的代码。 …

极智项目 | 实战烟雾火焰检测

欢迎关注我的公众号 [极智视界]&#xff0c;获取我的更多项目分享 大家好&#xff0c;我是极智视界&#xff0c;本文来介绍 实战烟雾火焰检测。 本文介绍的 实战烟雾火焰检测项目&#xff0c;提供完整的可以一键执行的项目工程源码&#xff0c;获取方式有两个&#xff1a; (1…

【离散数学】——期末刷题题库(欧拉图和哈密顿图)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

Springboot整合阿里云短信服务

目录 1.注册登录用户 2.点击AccessKey管理&#xff0c;开通使用子用户AccessKey 2.1点击进入AccessKey管理 2.2点击用户创建用户 2.3选择控制台创建 2.4权限修改 3.短信服务 4.创建Springboot项目使用SDK 4.1创建一个springboot项目 4.2导入阿里云短信Maven依赖 4.3…

唇彩行业分析:我国彩妆细分品类市场占比63%

唇部彩妆是指在唇部起到化妆修饰作用的产品&#xff0c;包括口红/唇膏、唇蜜/唇彩/唇釉、唇笔/唇线笔、唇泥四大类。总体来看&#xff0c;目前我国唇部彩妆细分品类主要集中在唇膏/口红、唇蜜/唇彩/唇釉。唇笔/唇线笔市场接受程度较低&#xff0c;这是由于唇笔/唇线笔的主要成分…

shell脚本定时自动备份mysql数据库和mysql恢复数据

1、设置一些测试的数据 创建一个database&#xff0c;一些tables和一些数据 create database test_bom default charset utf8 collate utf8_general_ci; use test_bom;create table users( id int not null primary key auto_increment, name varchar(64) not null, password…

通俗易懂:插入排序算法全解析(C++)

插入排序算法是一种简单直观的排序算法&#xff0c;它的原理就像我们玩扑克牌时整理手中的牌一样。下面我将用通俗易懂的方式来解释插入排序算法的工作原理。 假设我们手上有一副无序的扑克牌&#xff0c;我们的目标是将它们从小到大排列起来。插入排序算法的思想是&#xff0…

web实习三_JavaScript编程

编写 JavaScript 程序实现 输出“九九乘法表”&#xff08; 左下三角形形式 &#xff09;。 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, …

qiankun中子系统变化透传到主系统调用主系统方法

1、首先在主系统中qiankun启动前把变动的参数初始化 2、初始化之后就可以通过全局状态通信把参数透传为全局 3、在微应用子系统main.js的qiankun的mount中获取到全局设备参数属性并是设置为子系统全局 4、在微应用子系统中需要去调主系统方法时就在那个地方改变透传过来的参数 …

如何性能测试中进行业务验证?

在性能测试过程中&#xff0c;验证HTTP code和响应业务code码是比较基础的&#xff0c;但是在一些业务中&#xff0c;这些参数并不能保证接口正常响应了&#xff0c;很可能返回了错误信息&#xff0c;所以这个时候对接口进行业务验证就尤其重要。下面分享一个对某个资源进行业务…

ros2+在Ubuntu上安装gazebo

Binary Installation on Ubuntu(Ubuntu上binary方式安装gazebo) Harmonic binaries are provided for Ubuntu Jammy (22.04) and Ubuntu 24.04 (when its released). &#xff08;在Ubuntu22.04或者24.04上都是安装Harmonic版本的gazebo&#xff09;The Harmonic binaries are…

issue unit

The Issue Unit issue queue用来hold住&#xff0c;已经dispatched&#xff0c;但是还没有执行的uops&#xff1b; 当一条uop的所有的operands已经ready之后&#xff0c;request请求会被拉起来&#xff1b;然后issue select logic将会从request bit 1的slot中&#xff0c;选择…

指令寻址(顺序寻址和跳跃寻址)

目录 一. 顺序寻址1.1 定长指令字结构1.2 变长指令字结构 二. 跳跃寻址 \quad 指令寻址:如何确定下一条指令的存放地址? \quad 一. 顺序寻址 \quad 1.1 定长指令字结构 \quad 主存按字编址 \quad 按字节编址 1.2 变长指令字结构 \quad 同种颜色代表一条指令 由于无法判断当前…

制衣厂生产ERP系统怎么样?制衣厂生产ERP软件哪个好

有很多的制衣厂在订单处理、物料、仓储、销售、仓储、物料编码、车间成本核算、计件工资核算等方面还存在不少改进空间。 而经过多年的发展&#xff0c;现如今制衣行业的竞争比较激烈&#xff0c;如何提升各业务部门协同效率&#xff0c;减少车间物料损耗&#xff0c;简化生产…

idea的快捷键

1.调整字体的大小 文件夹的循序:setting-Editor-Font 界面: 2.删除当前行 文件夹的循序:setting-Keymap-DeleteLine 界面: 3.导入该行需要的类 文件夹的循序:setting-Editor-General-Auto import 界面: 4.格式化代码 文件夹的循序:setting-keymap-Reformat 界面: 5.快速…

【MySQL】——数据类型及字符集

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

loki 如何格式化日志

部署 grafana-loki 首先介绍一下如何部署 官方文档&#xff1a;部署 grafana-loki 部署命令 设置集群的存储类&#xff0c;如果有默认可以不设置设置命名空间 helm install loki oci://registry-1.docker.io/bitnamicharts/grafana-loki --set global.storageClasslocal -n …

程序员退一步的海阔天空,是考公还是烤冷面?

打败一个志向坚定的程序员只需要一个简单的年龄危机、身体预警.......钱难挣、屎难吃。996的钱更是伤身体&#xff0c;或者是被裁员、劝退的无力。算了~这份工作也不是非要不可&#xff0c;劳资不干了&#xff01;&#xff08;hahahahaha....bushi)人生在世&#xff0c;进可攻、…

Soul 推出“SoulX”AI人工智能模型,已应用于旗下 App“苟蛋”AI聊天机器人

Soul社交平台最近发布了名为”SoulX“的AI人工智能模型&#xff0c;SoulX将作为Soul “AIGC社交”布局的重要基建&#xff0c;具备prompt驱动、条件可控生成、上下文理解、多模态理解等能力&#xff0c;垂直应用于平台上多元社交互动场景&#xff0c;如智能对话机器人、AI辅助聊…

模拟微信、QQ、支付宝那样的随机红包

随机拆分给定金额为给定个数红包&#xff0c;像微信、QQ、支付宝随机红包那种&#xff0c;要求红包总金额绝对与给定金额相等。 (笔记模板由python脚本于2023年12月14日 12:37:58创建&#xff0c;本篇笔记适合熟悉Python随机数模块random的整型随机方法randint&#xff0c;能熟…