JAVA多线程基础

目录

前言

一、并行和并发

1.1 串行——并行

1.2 顺序——并发

1.3 CPU时间片轮转机制

1.4 CPU个数、CPU核心数、CPU线程数是啥?

二、进程和线程

2.1 进程和线程的概念和区别

2.2 JAVA中创建线程的方式

2.2.1 通过实现 Runnable 接口创建线程

2.2.2 通过继承 Thread 类创建线程

2.2.3 通过 Callable 和 Future 创建线程

2.3 中断线程

2.4 线程的状态

2.4.1 新建状态(new)

2.4.2 就绪状态(runnable)

2.4.3 运行状态(running)

 2.4.4 阻塞状态(blocking)

2.4.5 死亡(death)

2.5 JAVA中的线程的状态

2.6 用户线程和守护线程

2.7 为进程设置异常捕获器

总结


前言

在目前主流的软件中,通常都支持同时在同一时刻运行多个任务,即使有的设备只有单个CPU,也能实现这样的效果,并不受到CPU数量的限制,所以这种效果究竟是如何实现的呢,为了弄清这个问题,我们需要先了解并行并发的概念以及CPU时间片轮转机制。

一、并行和并发

并行和并发是两个相似的概念,仅仅相差一个字,很多人都会将其弄混,我们在理解这两个概念之前,需要知道两个对应关系:1、串行-并行 2、顺序-并发。

1.1 串行——并行

串行:举个例子,如果现在只有一个单核CPU,也就是只有一个任务执行单元,那么从物理层面就只能等一个任务执行完,再执行下一个任务。就好比吃一串糖葫芦,假设每张嘴每次刚好一个容纳一颗糖葫芦,我们现在只有一张嘴,就只能吃完第一颗再吃第二颗,这就叫串行。

并行:并行通常是相对于串行来说的,这里的“行”可以理解为执行,“并行”的中文字面意思就是“一并执行”,并行通常是指在硬件层面上有多个任务处理单元,可以将多个任务一并执行。比如现在我们有多个CPU,就可以给每个CPU都安排一个任务一并执行。用上面糖葫芦的例子来说,也就是说,现在我们有多张嘴在吃一串糖葫芦,我们不必等到第一张嘴吃完一颗再去吃第二颗,而是两张嘴一起吃,比如第一张嘴从头吃,第二张嘴从尾吃,这样就叫并行;

1.2 顺序——并发

谈顺序和并发的概念,通常都是在只有一个物理任务处理单元的前提之下,比如单个单核CPU;

顺序:在单核CPU的情况下,顺序的效果和上边说的串行类似,如果有多个任务,只能一个个的按顺序执行,就好比只有一张嘴,每次只能吃一颗冰糖葫芦,只能等这张嘴吃完一颗再吃下一颗。

并发:现在依然只有单个单核CPU,但是不必等第一个任务完全执行完再去执行下一个任务,而是一起“出发”开始执行,有人可能会问,要怎么一起出发开始执行呢?比如咱们唯一的这个单核CPU可以先花0.1秒执行第一个任务,然后暂停第一个任务,接着花0.1秒执行第二个任务,然后再切回第一个任务执行。我们可以想象,只要切得够快,用户就感觉不到这个切换的过程,就仿佛实现了多个任务并发执行,然而,我们知道,我们从始至终都只有一个单核的CPU在这两个任务之间来回切换,只是通过这种方式让用户感觉到两个任务同时执行了。如果用吃糖葫芦来举例,那就是,只有一张嘴,先把第一个糖葫芦啃一口,再迅速把第二颗糖葫芦啃一口,再立刻啃第一颗。理论上,只要你速度够快,外人看起来就像是两颗糖葫芦同时被你吃掉了。

1.3 CPU时间片轮转机制

其实1.2节中,我们在并发中所描述的单个单核CPU通过快速切换任务来达到同时执行多个任务的这种方式,就叫做CPU时间片轮转机制。我们平时在开发过程中,之所以能够随心所欲开启多个线程,哪怕是在单核CPU上,都是靠操作系统提供的CPU时间片轮转机制。

时间片轮转调度是一种最简单、最公平且使用最广的算法,又称RR调度。该算法中,将一个较小时间单元定义为时间片(时间片的大小通常为 10~100ms),所有需要占用CPU资源的进程都会被添加到一个循环队列中,操作系统会为依次为循环队列中的任进程分配时间片,但最多不超过一个单位的时间片。若该进程需要占用大于一个时间片的时间,则在1个单位的时间片结束后,先暂停该进程,并将该进程添加到队列最后;若该进程只需要不到一个单位时间片之前就完成任务了,则CPU立即释放,调度程序接着处理队列中的下一个进程;在该算法中,没有进程会被连续分配超过一个时间片的 CPU,除非它是唯一可运行的进程。

1.4 CPU个数、CPU核心数、CPU线程数是啥?

CPU个数:CPU个数即CPU芯片个数,通常每台家用计算机上上就只有一个CPU。

CPU核心数:核心数是物理层面的概念,一个核心相当于CPU中的一个独立的处理单元,一个核心可以理解为一个小CPU,我们在计算机的设备管理器中通常能看到很多个CPU,这里显示的每个CPU其实就代表一个核心。

八核CPU有表示该CPU中有八个相对独立的处理单元,若一个核心可以处理一个线程,那八核CPU就可以真正的同时处理八个线程。

CPU线程数:CPU线程数是逻辑上的概念,而且通常只在因特尔的CPU上有这个概念。一般一个CPU核心可以处理一个线程,但是因特尔拥有超线程技术,可以使一个CPU核心拥有同时执行2个线程的能力,也就是说,这个每个核心的被模拟成了一个类似双核心CPU的功能,因此我们经常听到类似于“8核16线程”的这种说法。

二、进程和线程

2.1 进程和线程的概念和区别

进程(process)和线程(thread)是操作系统的基本概念,先来看二者比较官方(高大上)的定义:进程是资源分配的最小单位,线程是CPU调度的最小单位。不过一般人看到这种定义还是会一头雾水,接下来举例说明二者具体是个啥,有时候区别。

我们可以将进程看作一辆运行中给的火车,线程看作车厢,因此一个进程里是可以有多个线程(一辆火车可以有多个车厢的,而且线程必须在进程下行运行(单独的车厢是无法跑起来的)。再者,不同进程之间很难共享数据(一辆火车上的乘客很难换到另外一辆火车),但是同一进程下不同线程间数据很易共享(同一辆火车的不同车厢之间,乘客很容易流通)【本段参考知乎】。

2.2 JAVA中创建线程的方式

本文主要讨论JAVA中的多线程,在JAVA中,创建线程有以下几种方式:

2.2.1 通过实现 Runnable 接口创建线程

这种方式比较简单,先创建一个实现 Runnable 接口的类,再实例化该类,调用run方法即可执行,run方法中写需要在该线程中执行的逻辑。

package com.example.threadtest;
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("run执行了" );
    }
}

package com.example.threadtest;

public class MainClass {
    public static void main(String[] args) {
        new MyRunnable().run();
    }
}

2.2.2 通过继承 Thread 类创建线程

创建一个新的类(该类继承Thread类),重写 run() 方法,然后创建一个该类的实例,这里是调用start()方法让线程执行。

package com.example.threadtest;

public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("run执行了");
    }
}
package com.example.threadtest;

public class MainClass {
    public static void main(String[] args) {
        new MyThread().start();
    }
}

不过,打开Thread类的源码,可以看见Thread其实也是实现了runnable接口

2.2.3 通过 Callable 和 Future 创建线程

我们先创建一个类实现Callable接口,并实现 call() 方法,call()方法中写需要在线程中执行的逻辑,而且通过这种方式创建的线程是可以有返回值的。接着创建 Callable 实现类的实例,并使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

2.3 中断线程

线程自动终止的条件:当run()方法中的语句执行完,并由return返回时,线程将会终止。或者是出现了没有捕获的异常,线程也会终止。

手动终止线程的方法:在JAVA早期版本,可以用stop方法强制终止线程,使用stop终止不会给线程喘息的机会,强行将其终止,这也带来了安全隐患,因此这种方法已经被废弃了。现在,通常使用interrupt()方法来中断线程,这种方法就温柔多了,它仅仅是请求线程尽快中断,线程有机会执行一些后续操作,如果不满足中断条件,线程也可以无视这个请求

判断线程是否终止:有一个静态方法和一个成员方法可以判断线程是否终止

1、public static boolean interrupted()
2、public boolean isInterrupted()

但是, interrupted()方法在测试当前线程是否中断之后,会将当前线程的中断状态重置为false;而isInterrupted()不会重置线程的中断状态;

2.4 线程的状态

为方便理解,通常把线程归纳为以下5种状态,这5种状态分别是:

2.4.1 新建状态(new)

当我们使用new操作符创建一个线程,线程就进入新建状态,这时线程中的代码还没开始执行,只是创建好了。

2.4.2 就绪状态(runnable)

当我们调用start()方法时,线程进入就绪状态,也就是现在已经做好了随时运行的准备,等待被线程调度选中,获取到cpu时间片时就可以运行起来。

2.4.3 运行状态(running)

当线程获得cpu的时间片后,线程进入运行状态,run()方法开始执行。

 2.4.4 阻塞状态(blocking)

线程在运行过程中,可能由于各种原因失去了cpu 使用权,暂时停止运行,也就进入阻塞状态。

2.4.5 死亡(death)

当线程的run()方法执行结束,或者调用stop()等方法后,会导致线程终止,也就进入死亡状态。

2.5 JAVA中的线程的状态

在JAVA中,线程的状态和上文所述的有一定区别,JAVA中获取当前线程状态的方法是getState(),追溯这个方法的源码,可以看到在JAVA中线程有以下6种状态:

1. 新建(NEW):新创建了一个线程对象,但还没有调用start()方法。

2. 运行(RUNNABLE):Java线程中将“就绪”和“运行中”两种状态,统称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

3. 阻塞(BLOCKED):表示线程阻塞于锁。

4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

6. 终止(TERMINATED):表示该线程已经执行完毕。

2.6 用户线程和守护线程

Java中的线程分为User Thread(用户线程)Daemon Thread(守护线程) ,用户线程就是我们用上文所述方式创建出来的普通线程,我们创建的大部分的线程,如果不经过特殊设置,都是用户线程。至于守护线程是啥,其实正如他的名字一样,他是所有用户线程的忠实守护者,说白了也就是为用户线程服务的。为啥说守护线程很忠实呢,因为只要当前尚存在任何一个用户没有结束,守护线程就必须全部工作,只有当最后一个用户线程结束时,守护线程才会结束工作。 GC就是守护线程的经典用例,只要尚有一个用户线程在运行,GC就会一直运行,回收垃圾。

用户线程也可以很轻松的转换为守护线程,只需要调用setDaemon(true)即可,但是必须在start()执行之前设置,因为我们不能随意改变一个运行起来的线程的属性。

public class MainClass {
    public static void main(String[] args) throws Exception {
        MyThread myThread=new MyThread();
        //要在start调用之前设置为守护线程
        myThread.setDaemon(true);
        myThread.start();
    }
}

使用守护线程还有以下几点需要注意:

1、在守护线程中产生的新线程也是守护线程。
2、读写操作或者计算逻辑不要在守护线程中写,因为守护线程在所有用户线程结束时就会结束(守护线程自己不能决定死亡时刻),然而这个时候,守护线程中可能还有一些读写操作等逻辑没有执行完就被终止了,这就会导致异常。 

2.7 为进程设置异常捕获器

我们可以使用setUncaughtExceptionHandler为进程设置一个异常捕获器,设置之后,在run方法中产生的异常会被传递到这里。

我们先人为制造了一个空指针异常,未设置异常捕获器,我们可以看到空指针异常被直接抛出了。

package com.example.threadtest;

public class MyThread extends Thread {
    Integer integer;
    @Override
    public void run() {
        super.run();
        String s = integer.toString();
        System.out.println("run执行完了");
    }
}

接着我们为线程设置了异常捕获器,可以看到空指针异常已经被捕获,并执行了我们写的输出异常

信息语句。

public class MainClass {
    public static void main(String[] args) throws Exception {
        MyThread myThread=new MyThread();
        myThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread thread, Throwable throwable) {
                System.out.println("捕获到异常"+throwable.getMessage());
            }
        });
        myThread.start();
    }
}

总结

本文主要介绍了JAVA多线程的相关概念和基础知识,后面会基础探讨更多关于JAVA多线程编程的知识,如有错误还请各位大神指出,同时也欢迎各位大佬来讨论和交流技术,本人邮箱hbutys@vip.qq.com

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值