深入了解多线程原理

news/2024/11/28 6:28:43/

 

目录

     背景知识:

什么是进程?

什么是线程?

线程与进程的区别:

Thread类及常用方法:

循环打印的例子:

start() 和 run() 的区别:

通过监视窗口查看线程:

创建线程:

1.继承 Thread 类, 重写 run() 方法:

2.实现 Runnable 接口:

3.使用匿名内部类,继承 Thread 类:

4.使用匿名内部类,实现 Runnable:

5.使用 Lambda 表达式:

Thread类常见构造方法:

Thread 的几个属性:

getId():

getName():

getSate():

isAlive():

中断一个线程

等待一个线程

获取当前线程引用

休眠当前线程

线程的状态

 1.NEW:

2.RUNNABLE:

3.TERMINATED:

4.阻塞状态:

5.线程状态转换流程图:

实例验证多线程效率问题:



 

 

     背景知识:

什么是进程?

        进程是计算机操作系统对一个正在运行的程序的一种抽象,是操作系统内部进行资源分配和调度的基本单位。

        在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;

        在当代面向线程设计的计算机结构中,进程是线程的容器。

        程序是指令、数据及其组织形式的描述,进程是程序的实体。

        上面这句话换言之就是,计算机内部要管理任何现实事物,都需要将其抽象成一组有关联的数据,在java中就使用类/对象类描述这种特征。

class PCB {//进程的唯一标识:pid;//进程关联的程序信息,如哪个程序,加载到内存的区域;//使用的资源;//进度调度信息
}

如上面的例子:这样的一个 PCB 对象,就代表了一个运行着的程序,即“进程”; 操作系统再通过各种数据结构将每个 PCB 对象组织起来,方便对这些进程进行管理。

什么是线程?

       使用线程的主要目的是为了解决“并发编程”问题。每一个线程就是一个“执行流”,每个线程按照一定的顺序来执行代码,多个线程可以同时执行多份代码;

        这里举个例子就非常好理解了:我们在餐厅买饭时,档口的工作有很多种,有负责后勤备材料的、有负责烹饪的还有负责打包收费的,如果这些工作都让张三一个人来完成,那么对于他而言工作量自然很大,并且工作效率也有一定影响,那么此时张三就可以再叫来李四和王五来帮忙,这样他们分别负责意见事务,如此一来,三个人各自有不同的任务,但又密不可分,好比一个任务交给了三个执行流来执行,我们就把这种分配叫做多线程模式。同时,洗菜烹饪收费这三件事是有先后顺序的,对应到线程里也就是每个执行流要排队执行,至于为什么要排队执行,这个问题涉及到优化线程随机调度,后面将会仔细讲解。

        既然刚开始提到“多线程为了解决‘并发编程’问题”,现在我们展开分析其中道理:

        计算机的核心计算逻辑是由 CPU 来控制的,随着技术进展,CPU 的核心也在快速升级,对于并发问题,结合多进程,多核CPU可以起到一定的效果,但是并非 CPU 核心多了程序就一定能排的快了,核心再多不能让部分核心很忙,部分又在闲置,所以这还需要结合具体的程序代码,合理地将多核心运用起来才行。

        再谈到多进程吧,其实多进程模式已经可以解决并发问题,并且可以利用起来CPU多核资源了,但是多进程的缺点太明显,那就是“太重”——消耗资源多、速度较慢。为了中和这种缺陷,就产生了“多线程模式”。

        线程也可以叫做“轻量级进程”,不仅可以解决并发编程问题,还能使创建、销毁、调度等速度得到显著提升管。

线程与进程的区别:

  • 包含关系:
            进程包含线程,一个进程可以包含多个线程(不能没有),是一对多的关系,但一个线程不能同时存在于多个进程中;
  • 公用关系:
             同一个进程中的多个线程之间,公用了进程的同一份资源(即内存文件描述符表);
    内存:指在 线程1 中 new 的对象在于其处于同一进程的其他线程也能使用;
    文件描述符表:指在 线程1 中打开的文件在于其处于同一进程的其他线程也能直接使用,因为操作系统内核是利用文件描述符表来访问文件的,打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件;
  • 随机调度:
             一个核心上执行一个线程,如果你的一个进程中有两个线程,那么操作系统在调度的时候,那个线程在哪个核心上执行都是有可能的;
  • PCB的组织:
             线程也是通过PCB来描述的,一个线程可能对应一个PCB或者多个,PCB中得到的“状态、上下文、优先级、记账信息等”都是每个线程各自的记录,但是在同一个进程中的PCB之间,“pid、内存指针、文件描述符表”是一样的。
  • 基本单位:
             不要拘泥于教材,站在“轻量级进程”角度而言,操作系统和CPU在实际调度的时候,是以线程为基本单位进行调度的,每个线程也都有自己的执行逻辑;实际中只要涉及到“调度”就基本上和进程没关系了,可以理解为,进程专门负责资源分配,线程主要来接管和调度相关的一切内容;

 下面这个生动的例子可以帮助我们更清晰的区别多进程与多线程:

由此图不难提出疑问:既然线程解决了两面性的问题,那线程一定就能更快嘛?

就上面吃蛋糕而言,许多人在一起吃,相当于多个线程来处理同一个任务,就会出现三种情况:

1.桌子大小有限,不够所有人坐(对应CPU核心有限,不能将所有线程调度起来);

2.人太多,有的人正在吃,有的人吃完了又要去拿,推推搡搡导致正在吃的人不能专心吃(对应有的线程正在处理中,其他线程又要对其产生各种干扰);

3.线程太多,在线程调度上开销较高

4.两个人看上同一块蛋糕(对应线程安全问题);

5.张三把李四的蛋糕抢走了,李四直接生气干扰大家都吃不了了(对应一个线程抛异常,如果处理不好,很可能导致整个进程的崩溃);

看来多线程模式也存在诸多问题,那么下面就要对所有问题一一展开讲解。


Thread类及常用方法:

        每个执行流都需要一个对象来描述,而Thread对象就是用来描述执行流的,此时JVM再将这些Thread对象组织起来,作一系列的调度与管理。

        Thread类是在java.lang下的,所以用的时候不需要导包。

循环打印的例子:

        先写一个交替打印的例子来直观感受一下吧:

        首先定义一个类 MyThread 继承 Thread 类,内部方法打印“hello thrad”,接着 main 方法内部实例出一个 t 对象,这个对象就是用来描述线程的,即称为 t 线程,调用 t.start() 方法启动 t 线程,同时 main 方法内部也要打印“hello main”,如此一来,main 方法和MyThread类内部的 run 方法都要打印不同的内容,那么就会因为线程的随机调度和抢占式执行导致“hello Thread” 和 “hello main”形成交替打印。至于先打印哪个,顺序是不一定的,取决于操作系统内部具体调度策略。

class MyThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("hello thread");}}
}
public class ThreadDemo01 {public static void main(String[] args) {Thread t = new MyThread();t.start();while (true) {System.out.println("hello main");}}
}

 深入理解:

        从微观角度来看:main() 方法是一个主线程,start() 方法在这里创建了一个新的线程,新的线程负责执行 t.run() 方法,也就是调度操作系统的API,通过操作系统内部创建新的PCB,并且把要执行的指令交给这个PCB,当PCB被调度到CPU上执行时,也就可以执行到线程中 run() 方法了;

        从宏观角度来看:如果直接在main()方法内部写一个打印,想要实现交替不是非常容易实现,那么此时程序的进程中就只有一个线程main;如果主线程调用 t,start() ,创建出一个新的线程,新的线程再调用 t.run(),如果run()方法执行完毕,这个新的线程也就自然销毁了。

start() 和 run() 的区别:

        执行 start() 就是真正创建一个线程;run() 只是描述了线程要干的活,如果没有创建新的线程,而是直接在 main() 内部调用 run(),那就是 main() 把所有的活都干了。

通过监视窗口查看线程:

在 JDK 中自带了一个可以监视线程的窗口:jconsole

可以直接搜索,也可以在自己电脑JDK的文件夹内找: 

 选择连接刚才运行的程序,接着同意不安全连接

学会了使用 jconsole 的使用,就能在后续调试过程中方便找出 bug 所在地。


创建线程:

创建线程有很多种写法:

1.继承 Thread 类, 重写 run() 方法:

class MyThreads extends Thread {@Overridepublic void run() {while (true) {System.out.println("my thread");}}
}

2.实现 Runnable 接口:

        Runnable 的作用是描述一个要执行的任务,借助了“解耦合”的思想,将 线程 和 线程要干的活 之间分开,这样写的好处在于,如果某一天不需要使用多线程,而是用多进程,或者说线程池、协程时,此时代码改动就会小一些,错误的概率的可以稍作降低。

class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("hello myRunnable");}
}
public class ThreadDemo03 {public static void main(String[] args) {//用 runnable 来描述一个任务Runnable runnable = new MyRunnable();//将任务交给 t 线程来执行Thread t = new Thread(runnable);t.start();}
}

3.使用匿名内部类,继承 Thread 类:

public class ThreadDemo04 {public static void main(String[] args) {Thread t = new MyThread() {@Overridepublic void run() {System.out.println("匿名内部类");}};t.start();}
}

4.使用匿名内部类,实现 Runnable:

public class ThreadDemo05 {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("run");}});t.start();}
}

5.使用 Lambda 表达式:

         把任务用 lambda 表达式来描述,直接把 Lambda 传给 Thread 构造方法;

public class ThreadDemo06 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("run");});t.start();}
}


Thread类常见构造方法:

Thread()创建线程对象
Thread(Runnable target)使用Runnable描述的“任务”创建对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target,String name)使用Runnable描述的“任务”创建对象,并命名
Thread(ThreadGroup group,Runnable target)分组管理线程

其实给线程起名字,调试时便于观察;

举个例子:

public class ThreadDemo07 {public static void main(String[] args) {Thread t = new Thread(() -> {while (true) {System.out.println("run");}}, "myThread");t.start();}
}


Thread 的几个属性:

属性获取方法解释
IDgetId()ID是线程的唯一标识,不同的线程不会重复
名称getName()构造方法里的名字
状态getState()当前线程所处的状态,下面将仔细介绍
优先级getPriority()优先级高的线程理论上来说更容易被调度到,这个可以获取也可以设置,但一般设置了没有用
是否后台线程isDeamon()后台线程(也叫守护线程)JVM会在一个进程的所有非后台线程结束后,才会结束运行。前台线程不结束,进程就走不完;后台线程没做完,进程是可以结束的,也可以使用setDeamon手动设置为后台线程
是否存活isAlive()简单的理解为 run 方法是否运行结束了。start之前为false;start之后为true
是否被中断isInterrupt()下面将大篇幅讲解

getId():

public class ThreadDemo07 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("run");}, "myThread");t.start();System.out.println(t.getId());;}
}

getName():

public class ThreadDemo07 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("run");}, "myThread");t.start();System.out.println(t.getName());;}
}

getSate():

public class ThreadDemo07 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {System.out.println("run");}, "myThread");System.out.println(t.getState());t.start();System.out.println(t.getState());Thread.sleep(5000);System.out.println(t.getState());}
}

线程的这三种状态将在下面的内容中大篇幅介绍.

isAlive():

public class ThreadDemo07 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {try {Thread.sleep(1000); //保证线程至少执行 5s - 便于观察} catch (InterruptedException e) {e.printStackTrace();}System.out.println("running");}}, "myThread");//①观察 start 之前是否存活System.out.println(t.isAlive());t.start();//②观察 start 之后线程执行过程中是否存活System.out.println(t.isAlive());//③至少等待 8s,观察线程 t线程执行完后是否存活Thread.sleep(8000);System.out.println(t.isAlive());}
}

此处再强调一遍:只有 start 之后才创建真正的线程,start 之后才真正开始做任务,调用 start,就会让内核创建一个 PCB,此时这个 PCB 才表示一个真正的线程。isAlive是在判断当前系统里这个线程是不是真的存在了;

  • 调用 start 之前,isAlive为 false;
  • 调用 start 之后,isAlive为 true;

        如果内核里的线程将 run 里面的活干完了,此时线程就会销毁,PCB也随之消失,但是Thread 创建出来的这个 t 对象还不一定被释放,此时的 isAlive 也是false;

  • 如果 t 的 run 还没启动,isAlive 就是 false;
  • 如果 t 的 run 启动中,isAlive 就是 true;
  • 如果 t 的 run 执行完毕,isAlive 就是 false;

上面这一部分,也就可以称之为“启动一个线程”;


中断一个线程

首先列出中断线程相关的方法:

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

代码示例:

public class ThreadDemo09 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("running");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();Thread.sleep(3000);t.interrupt();}
}

执行结果: 

 从结果来看,我们会疑惑:既然我都让线程终止了,为什么它报异常后又执行下去了呢?

        我们一步步来看:

        其实,该程序在执行时,sleep遇到异常后虽然配合进程终止了,但随后又清除了标志位,将原来的 false 改为了 true。(同样,wait / join 在线程阻塞挂起时也会有这样的效果);

        因此,我们这里就要强调:中断线程的含义,并非真正地让进程立即停止,而只是一个“通知停止”的效果,至于线程是否真正停止,何时停止,取决于线程内代码的具体实现;

        为了解决这一问题,将上述代码修改一下再来看:

①第一种: 将catch的语句修改成结束语句即可

或者:

②第二种:让它遇到异常后稍微等待一会儿再结束

         还有其他各种处理方法,具体为什么要这么设计?其实就是为了唤醒之后,线程到底是否要终止?何时终止?选择权交给了程序员自己。

        以上紫色字体内容甚为重要!!!


等待一个线程

        相关方法:

方法说明
public void join()等待某个线程结束,“死等”
public void join(long millis)等待某个线程结束,最多等millis毫秒
public void join(long millis,int nanos)精度更高

 代码实例:

public class ThreadDemo10 {public static void main(String[] args) {Thread t = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("running");}try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}});t.start();System.out.println("join之前");try {t.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("join之后");}
}

        原本执行完 start 后,t 线程和 main 线程分头行动,并发执行,但是 t.join() 就起到了等待作用,使 main 线程等待 t 线程执行结束再继续执行自己,在这段代码中也就体现在打印了三次“running”之后,t 线程阻塞三秒,之后 main 继续执行。


获取当前线程引用

        这个方法在中断线程中使用过.这是一个静态方法

方法说明
public static Thread currentThread()返回当前线程对象的引用


休眠当前线程

相关方法:

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis 毫秒
public static void sleep(long millis,int nanos) throws InteruptedException更高精度的休眠

        休眠线程本质上就是让这个线程不参与调度了,可以理解为,有两个队列,一个是执行队列,另一个是阻塞队列(后面介绍),被sleep的线程就会从执行队列进入到阻塞队列中(这个状态也叫作“hang”)。

        一旦线程进入阻塞状态,对应PCB也就进入阻塞队列了,此时就暂时无法参与调度,比如调用 sleep(1000),对应的线程PCB就要在阻塞队列中等待 1000ms ,试想:当这个 PCB 回到了就绪队列,会立即被调度吗?其实虽然是 sleep(1000),但考虑到还有地调度的开销,实际时间就要大于 1000ms.


线程的状态

        通过下面这段代码我们可以观察到线程的所有状态:

public class ThreadDemo11 {public static void main(String[] args) {for (Thread.State state : Thread.State.values()) {System.out.println(state);}}
}

进入Thread.State内部就可以看到六种状态:

 1.NEW:

创建了 Thread 对象,但是还没有调用 start ,内核还没有创建对应PCB;

2.RUNNABLE:

可运行的,它有两种情况:正在CPU上执行的,或者在就绪队列中,随时可以去CPU上执行的;

3.TERMINATED:

表示内核中的PCB已经执行完毕,但是 Thead 对象依然在;

4.阻塞状态:

WAITIING、TIMED_WAITING、BLOCKED都是线程PCB在阻塞队列中,下面会有介绍;

5.线程状态转换流程图:

 通过运行代码来观察:

public class ThreadDemo12 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {});System.out.println("start之前:" + t.getState());t.start();System.out.println("执行中:" + t.getState());t.join();System.out.println("执行后:" + t.getState());}
}


实例验证多线程效率问题:

CPU密集问题:假设有两个变量,需要把这两个变量都自增1000亿次。

两种处理方法:

第一种,串行执行,使用一个线程,先对a自增,再对b自增:

public class ThreadDemo13 {public static void main(String[] args) {serial();}public static void serial() {//获取时间long beg = System.currentTimeMillis();long a = 0;for (long i = 0; i < 100_0000_0000L; i++) {a++;}long b = 0;for (long i = 0; i < 100_0000_0000L; i++) {b++;}long end = System.currentTimeMillis();System.out.println("执行时间:" + (end - beg) + " ms");}
}

 第二种,并发执行,两个线程同时分别对a和b自增:

public class ThreadDemo14 {public static void main(String[] args) {concurrency();}public static void concurrency() {Thread t1 = new Thread(() -> {long a = 0;for (long i = 0; i < 100_0000_0000L; i++) {a++;}});Thread t2 = new Thread(() -> {long b = 0;for (long i = 0; i < 100_0000_0000L; i++) {b++;}});long beg = System.currentTimeMillis();t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}long end = System.currentTimeMillis();System.out.println("并发执行时间:" + (end - beg) + " ms");}}

        可见,多线程在这种 CPU 密集型的任务中,有非常大的作用,可以充分利用 CPU 的多核资源,从而加快程序的运行效率;

        除此之外还需要注意,并非使用多线程就一定能提高效率:一方面要看是否多核、另一方面还要满足核心空闲才行。


        这一篇幅重点介绍了多线程的基础知识,后续文章讲逐一展开多线程实际终点问题“线程安全”“Synchronized锁的使用”“单例模式”“锁策略”“接口、原子类”等面试常考知识点



http://www.ppmy.cn/news/22217.html

相关文章

分布式系统分布式锁

分布式锁 锁是对同一时间只能有一个线程访问某个资源的限制,synchronized关键字虽然可以处理多线程问题,保证同一时间只有一个线程访问到某个资源,做不到粗细度控制,只适用于单机系统,对分布式集群系统就满足不了。Java虽然提供了Lock接口,可以做到粗细度控制,却依然只…

从零开始配置vim(32)——最后再说两句

很抱歉我决定结束这个系列的内容了。原本我打算介绍markdown、orgmode相关的配置&#xff0c;甚至还打算介绍如何在vim 中使用 emacs 的 org-agenda 来进行日常的任务管理。但是出于一些原因我打算放弃了。 首先如果将markdown 理解为另一种类似于HTML 的标记语言的话&#xf…

【Linux】进程的概念

目录1.什么是进程2.描述进程 - PCB3.进程的具体操作3.1进程的属性与文件属性的关系3.2查看进程准备工作使用指令查找对应进程&#xff1a;在文件中查看进程3.3关闭进程ctrlckill3.4进程的一些特性3.5通过系统调用获取进程标识符3.6通过系统调用创建子进程1.什么是进程 背景&am…

总结继承和多态的一些问题

在学习了继承和多态后&#xff0c;本人有以下容易造成混乱的点以及问题&#xff1a; 1.区分虚表和虚基表 虚表即虚函数表&#xff0c;存储的是虚函数的地址。另外&#xff1a;虚表是在编译阶段就生成的&#xff0c;一般存在于常量区&#xff08;代码段&#xff09;。 虚基表…

C++ 面试题-设计模式类问题(万余字总结)

C 面试题-设计模式类问题1 、说说什么是单例设计模式&#xff0c;如何实现2、 简述一下单例设计模式的懒汉式和饿汉式&#xff0c;如何保证线程安全3、 请说说工厂设计模式&#xff0c;如何实现&#xff0c;以及它的优点4 、请说说装饰器计模式&#xff0c;以及它的优缺点5 、请…

Spring Aop 底层责任链思路实现

动手AOP责任链实现 简单了解 AOP Spring 的两个重要特性之一 AOP 面向切面编程 它的作用简单来说就是解耦 可以通过代理类的方式实现在不改变类本身的代码的情况下做到功能上的增强 , 了解它能做些什么我们就可以去思考怎么去实现它 , 这里涉及到责任链模式 (后续在细说) 。 …

软件测试面试之道(持续更新)

1. 软件的生命周期&#xff1f; 软件生命周期是软件从提出&#xff0c;实现&#xff0c;使用&#xff0c;到停止使用的过程。是从可行性研究到需求分析、软件设计、编码、测试、软件发布维护的过程。 2. 常见的测试用例设计方法&#xff1f; &#xff08;1&#xff09;等价类…

基于linux5.15.5的IMX 参考手册 --- 5

基于linux5.15.5的IMX 参考手册 — 5 3.7 SDMA (Smart Direct Memory Access)接口 3.7.1概述 Smart Direct Memory Access (SDMA) API驱动程序控制SDMA硬件。它为其他驱动程序提供了一个API&#xff0c;用于在MCU内存空间和外围设备之间传输数据。支持以下特性: •将通道脚本从…