摘要
线程是操作系统调度的最小单元,在多核环境下实现多线程能够显著提升程序性能。本文会先简单的介绍Java线程基础知识,并从启动一个线程到线程间不同的通信方式。
线程
在现代操作系统中运行一个程序时,会为其创建一个进程。在一个进程里可以创建多个线程,这些线程都拥有各自的计数器和局部变量等属性,并且能够访问共享的内存变量,处理器在这些线程上高速切换实现并发。
线程是比进程更轻量级的调度执行单位,各个线程共享着进程资源(内存地址、文件I/O等),也可以进行独立的调度(线程是CPU调度的基本单位)。
实现线程的方式
使用内核线程实现
内核线程直接由操作系统内核支持的线程,由内核完成线程切换,内核通过操纵线程调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上,每个内核线程可以看成是内核的一个分身,这样处理器就可以同时处理多任务。
操作系统会对内核线程进行一个封装,程序一般通过调用其封装后的接口 — 轻量级进程(Light Weight Process,LWP),轻量级进程和内核线程是1:1的对应关系。轻量级进程的局限就是:线程的创建、同步等都需要进行系统调用,系统调用的代价需要从用户态和内核态中来回切换,代价较高;每个轻量级进程都有对应的内核线程支持,轻量级进程需要消耗一定的内核资源,一个系统所支持的轻量级进程的数量也是有限的。
使用用户线程
如果一个线程只要不是内核线程就可以认为是用户线程。轻量级进程的实现始终是建立在内核之上,属于用户线程,但是其许多操作都要进行系统调用,效率会受到限制;
用户线程的建立、同步、销毁等都是在用户态中实现,不需要借助内核,这种操作是快速且低消耗的;
用户线程的优势就在于不需要内核线程的支持,所有对于线程的操作都是要考虑的,比如线程的创建、切换与调度,阻塞如何处理等问题,在设计用户线程时都是需要解决的,现在采用用户线程的程序越来越少;
Java线程
目前JDK版本中,操作系统支持怎样的线程模型就很大程度上决定了Java虚拟机的线程是怎样映射的,线程的实现是基于操作系统原生线程模型来实现。线程模型的差异对于并发规模和操作成本产生影响,但是对于Java程序的编码和运行过程来说,这些差异都是透明的;
线程调度
现在操作系统基本都是采用时分的形式调度和运行线程,操作系统会分出一个个时间片,线程会分配到若干个时间片,当线程的时间片用完之后就会发生线程调度,并且等到下次分配。线程分配到时间片的多少也就决定了线程使用处理器资源的多少。线程调度是指系统为线程分配处理器使用权的过程,主要分为协同式线程调度和抢占式线程调度;
采用协同式线程调度,线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,主动通知并切换到另一个线程上,好处就是实现简单,线程要把自己的事情干完之后才会进行线程切换,切换操作自己可知,不会产生线程同步的问题;弊端就是线程执行时间不可控,如果一个线程有问题且不通知系统进行线程切换,将会导致线程一直阻塞,相当不稳定;
采用抢占式线程调度,每个线程由系统分配执行时间,线程的切换不由线程本身来决定。线程的执行时间是系统可控的,不会出现一个线程导致进程阻塞的问题;
Java线程调度是系统自动完成的,但是如何给线程多分配或者少分配一些处理器资源?
在Java线程中可以通过设置一个整型成员变量priority来控制优先级来实现,一共设置10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),当两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。但是线程的优先级并不是太靠谱,因为Java线程是通过映射到系统原生线程上来实现的,所以线程的调度最终还是取决于操作系统;许多操作系统提供线程优先级但是并不能与Java线程的优先级一一对应;在不同的JVM以及操作系统上,线程规划有差异,有些操作系统甚至会忽略对于线程优先级的设定。
线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。
线程状态
状态名称 | 说明 |
---|---|
NEW | 线程创建后尚未启动 |
RUNNABLE | 运行状态,Java中将就绪和运行状态统称为“运行中” |
BLOCKED | 阻塞状态,线程阻塞于锁 |
WAITING | 等待状态,线程进入等待状态,等待通知或中断 |
TIME_WAITING | 超时等待状态 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程状态转换入如下所示:
Daemon线程
Daemon线程是一种支持型线程,主要用作程序中后台调度以及支持性工作,当一个Java程序中不存在非Daemon线程时,Java虚拟机会退出,可以通过Thread.setDaemon(true)将线程设置为Daemon线程;
在后台默默地完成一些系统性的服务,比如垃圾回收线程等,与之对应的就是用户线程,用户线程就是系统的工作线程,它会先完成这个程序应该要完成的业务操作,如果用户线程全部结束,也意味着这个程序实际上无事可做,设置守护线程必须在start()之前设置;
public final void setDaemon(boolean on){
checkAccess();
if(isAlive()){
throw new IllegalThreadStateException();
}
daemon = on;
}
启动和终止线程
线程的创建
线程的创建共有三种方法:
- 实现Runnable接口;
- 实现Callable接口;
- 继承Thread类
实现Runnable接口
需要实现run方法,通过Thread调用start()方法来启动线程,start()方法会新建一个线程并让这个线程执行run()方法:
public class MyRunnable implements Runnable{
public void run(){
System.out.println("Hello World!");
}
public static void main(String[] args){
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thead.start();
}
}
继承Thread类
需要实现run()方法,因为Thread类也实现了Runnable接口;
当调用start()方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的run()方法
public class MyThread extends Thread{
public static void run(){
Systm.out.println("Hello World!");
}
public static void main(String[] args){
Thread thread = new MyThread();
thread.start();
}
}
实现Callable接口
与Runnable接口相比,Callable可以有返回值,返回值通过FutureTask进行封装;
public class MyCallable implements Callable<Integer> {
public Integer call(){
return 123;
}
public static void main(String[] args) throws ExecutionException,InterruptedException{
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}
实现接口VS继承Thread类
- Java不支持多继承,因此继承了Thread类就无法继承其他类,但是可以实现多个接口;
- 类可能只要求可执行即可,继承整个Thread类开销过大;
启动线程
线程对象在初始化完成之后,调用start()方法就可以启动这个线程,线程start()方法的含义是:当前线程同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。
启动一个线程前最好为该线程设置线程名称,以便在使用jstack分析程序或者进行问题排查时,方便处理bug;
Thread类解析
与线程运行状态有关的方法
1、start()
start()方法用来启动一个线程,当调用该方法时,相应的线程就会进入就绪状态,该线程中的run()方法会在某个时机被调用;
2、run()
run()方法不需要用户来调用,当通过start()方法启动一个线程之后,一旦线程获得了CPU的执行时间,便进入run()方法体中去执行具体的任务;
3.sleep()
在指定的毫秒数内让线程睡眠,并且交出CPU去执行其他的任务,当线程睡眠结束之后,不一定会立即执行线程,因为此时的CPU可能在执行其他的任务,调用sleep()方法相当于让线程进入阻塞状态;
- sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象;
- 使用sleep方法,阻塞的线程被中断时抛出InterruptedException异常
public static native void sleep(long millis) throws InterruptedException;
4、yield()
调用该方法只会让当前线程交出CPU资源,让CPU去执行其他的线程,但是yield不能控制具体的交出CPU的时间;
- yield只会让具有相同优先级的线程具有获取CPU执行时间的机会;
- 调用yield()方法不会让线程进入阻塞状态,而是让线程重回就绪状态,只需要等待重新得到CPU的执行;
- 不会释放锁;
5、join()
在main线程中调用thread.join方法,则main线程会等待thread线程执行完毕或者等待一定的时间;
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);等待时间为0,意味着永远等待,直到线程被唤醒;
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
join方法会调用wait方法让宿主线程进入阻塞状态,并且释放线程占有的锁,并交出CPU执行权限,结合join方法的声明,有以下三条:
- join方法会让线程交出cpu执行权限;
- join方法会让线程释放对一个对象持有的锁;
- 如果调用join方法,必须捕获InterruptedException异常或者将该异常向上层抛出;
6、interrupt()
中断可以理解为线程的一个标识位属性,表示运行中的线程能否被其他线程内进行中断操作。可以把中断表示其他线程对该线程打了一个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作;
interrupt即中断的意思,单独调用interrupt方法可以使得处于阻塞的线程抛出一个异常,可以用来中断一个处于阻塞状态的线程;线程中断只是给线程发送一个通知,告知目标线程有人希望你退出;
线程通过检查自身是否被中断来进行响应,线程可以通过方法isInterrupted()方法来判断是否被中断,也可以调用静态方法Thread.interrupted()方法对当前线程的中断标识位进行复位;
public void Thread.interrupt() //通知目标线程中断,设置中断标志位
public boolean Thread.isInterrupted()//判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态
直接调用interrupt方法不能中断正在运行中的程序;一般会在MyThread类中增加一个volatile属性isStop来标识是否结束while循环,然后在while循环中判断isStop的值
转载:https://blog.csdn.net/ghw15221836342/article/details/103814714