美文网首页
线程概念

线程概念

作者: 9283856ddec1 | 来源:发表于2020-02-13 12:10 被阅读0次

线程简介

什么是线程?

线程是比进程更轻量级的调度执行单元,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度。

线程与进程

线程可以看做是cpu调度执行的最小单元,进程可以看做是cpu运行资源的基本单位。

类型 占有内容
线程 (1)程序计数器;(2)寄存器;(3)堆栈;(4)状态;
进程 (1)地址空间; (2)全局变量; (3)文件、账户、定时器:

说明:
程序计数器*:用来记录接着要执行那一条指令;
寄存器:用来保存线程当前的工作变量;
堆栈:用来记录执行历史,其中每一帧保存了一个已调用的但尚未返回的过程;栈帧中村放着相应过程的局部变量、调用之后使用的返回地址。例如,如果过程X调用过程Y,而Y又调用Z,那么当Z执行时,供X、Y和Z使用的栈帧都会存在堆栈中。

线程与进程比较:
类型 | 资源 | 并发 | 效率

  • | -
    线程 | 同一进程下多个线程共享该进程资源 | 多个线程之间并发互不影响 | 创建线程代价小
    进程 | 多个进程不能共享内存等资源 | 多个进程可以并发,互不影响 | 创建进程代价大
线程优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程分配到的时间片多少决定了线程使用处理器资源的多少,而线程优先级决定线程得到处理器资源的多少。

在Java线程中,通过整型成员变量priority控制优先级,优先级的范围从1~10,默认优先级是5。优先级高的线程分配时间片的数量要多于优先级低的线程,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间)的线程则设置较低优先级,确保处理器不会被独占。

注意:线程优先级不能作为程序正确行的依赖,因为操作系统可以完全不会理会Java线程对于优先级的设定。

Android线程优先级是依据Linux设置的,其优先级是数字表示的,范围是-20 ~ 19,其中-20是最高优先级,19是最低优先级。设置优先级方法如下:Process.setThreadPriority(priority)。根据使用场景的不同,Android提供了以下几常见的线程优先级:

优先级 说明
THREAD_PRIORITY_URGENT_AUDIO -19 重要的音频线程
THREAD_PRIORITY_AUDIO -16 用于音乐播放场景
THREAD_PRIORITY_URGENT_DISPLAY -8 重要的显式线程
THREAD_PRIORITY_DISPLAY -4 用于普通显式线程
THREAD_PRIORITY_FOREGROUND -2 用于普通前台程序
THREAD_PRIORITY_MORE_FAVORABLE -1 高于默认优先级
THREAD_PRIORITY_DEFAULT 0 默认优先级
THREAD_PRIORITY_LESS_FAVORABLE 1 低于默认优先级
THREAD_PRIORITY_BACKGROUND 10 用于普通后台程序
THREAD_PRIORITY_LOWEST 19 最低优先级

提示:应当避免混用Thread.setPriority和Process.setThreadPriority,这会使代码一团糟。Linux的优先级是从-20(最高)到19(最低),而线程的优先级是从1(最低)到10(最高)。

线程的状态

Java线程运行的生命周期处于下表所示的6种不同状态,在给定的时刻,线程只能处于其中的一个状态:

状态名称 说明
NEW 初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称作“运行 中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,需等待其他线程做出特定动作(通知或中断)
TIME_WAITING 超时等待状态,表示可以在指定的时间超时后自行返回
TERMINATED 终止状态,表示当前线程已经执行完毕
Java线程状态转换图.jpg

注意:停在synchronized关键字修饰的方法或代码块时的状态是阻塞状态,停在Lock接口的线程状态是等待状态。

线程的常用方法介绍:

函数名 作用
wait 进入等待池中,释放对象的锁,用户可以使用notify、notifyAll或者指定超时时间后来唤醒当前等待池中的线程。
join 等待目标线程执行完成之后再执行此线程
yield 目标线程让出执行权限,由运行状态转为就绪状态,让其它线程得以优先执行,但其它线程是否优先执行是未知的
sleep 使调用线程进入睡眠状态,当在一个Synchronized块中调用sleep()方法时,线程虽然休眠,但是对象的锁并没有被释放。
Daemon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

public class Daemon { 
    public static void main(String[] args) { 
        Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");                
        thread.setDaemon(true); 
        thread.start(); 
    } 
    static class DaemonRunner implements Runnable { 
        @Override public void run() { 
            try {
                SleepUtils.second(10);
             } finally { 
                System.out.println("DaemonThread finally run."); 
             } 
        } 
    } 
}

注意:Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。

线程使用

ThreadLocal

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这 个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个 线程上的一个值。

构造线程

线程对象在构造的时候需要提供线程所需要 的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。线程初始化过程如下所示:

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) { 
    if (name == null) { 
        throw new NullPointerException("name cannot be null"); 
    }
    // 当前线程就是该线程的父线程 
    Thread parent = currentThread(); 
    this.group = g; 
    // 将daemon、priority属性设置为父线程的对应属性 
    this.daemon = parent.isDaemon(); 
    this.priority = parent.getPriority(); 
    this.name = name.toCharArray(); 
    this.target = target; 
    setPriority(priority); 
    // 将父线程的InheritableThreadLocal复制过来 
    if (parent.inheritableThreadLocals != null) 
    this.inheritableThreadLocals=ThreadLocal.createInheritedMap(
                                                    parent.inheritableThreadLocals); 
    // 分配一个线程ID 
    tid = nextThreadID(); 
}
创建线程

线程的创建包括三种方式:

  1. 继承Thread类创建线程类

步骤:
1> 定义一个线程类,需继承Thread类。
2> 重写父类的run( )方法,此方法是线程执行体,供cpu自动调用(cpu会用调度策略去处理就绪状态的线程)。
3> 创建线程类的实例对象,调用start( )方法,这个方法告诉cpu这个线程对象进入就绪状态。

// 1.定义一个线程类,需继承Thread类。
public class MyThread extends Thread {
    // 2.重写run方法
    public void run() {
        // do work..
    }
    
    public static void main(String[] args) throws Exception {
        // 3.创建线程实例,调用start方法,进入就绪状态,交给cpu
        MyThread1 myThread = new MyThread();
        myThread1.start();
    }
}
  1. 实现Runnable接口创建线程类

步骤:
1> 定义类实现Runnable接口。
2> 实现接口的run( )方法,此方法是线程执行体,供cpu自动调用(cpu会用调度策略去处理就绪状态的线程)。
3> 创建线程类的实例对象
4> 包装上面的Runnable实例对象,然后调用start( )方法。

1.定义类实现Runnable接口
public class MyRunnable implements Runnable {
    // 2.实现接口的run( )方法
    @Override
    public void run() {
        // do work!
    }

    public static void main(String[] args) throws Exception {
        // 3.创建Runnable的实例对象
        MyRunnable runnable = new MyRunnable();
        // 4.包装MyRunnable实例对象,然后调用start( )方法
        Thread t = new Thread(runnable);
        t.start();
    }
}
  1. 实现Callable接口创建线程类

步骤:
1> 定义一个类实现Callable接口。
2> 实现Callable接口的call( )方法,此方法是线程执行体。
3> 创建Callable类的实例对象。
4> 创建FutureTask的对象来包装Callable类实例对象
5> 创建Thread的对象来包装Future类的实例对象。

// 1.定义类实现Callable接口
public class MyCallable implements Callable {
    // 2.实现Callable接口的call()方法
    @Override
    public String call() throws Exception {
        // do work!
        return Thread.currentThread().getName();
    }
}

public void doWork() {
    // 3.创建MyCallable类的实例对象
    MyCallable cb = new MyCallable();
    // 4.创建FutureTask的实例对象来包MyCallable类实例对象
    FutureTask futureTask = new FutureTask(cb);
    // 5.创建Thread的实例对象来包装Future类的实例对象
    Thread t = new Thread(futureTask);
    t.start();
}

三种方式的对比:
1)采用继承Thread类这种方式来创建线程,编写简单,可是由于Java不支持多继承,所以不能再继承其他父类。
2)采用实现Runnable接口或Callable接口,可以继承其他类,多个线程可以共享同一个target对象,非常适合多个线程来处理同一资源的情况,可以更好地体现面向对象的特点,不过编写比较复杂。
3)采用实现Callable接口,call( )方法是线程执行体,有返回值,可以抛出异常,其功能比run( )方法更强大。

想问问题

  • Thread和Runnable有什么关系?
public  class Thread implements Runnable{
    // 线程所属的ThreadGroup
    volatile ThreadGroup group;
    // 要执行的目标任务
    Runnable target;

    public Thread(){
        create(null, null, null, 0);    
    }

    public Thread(Runnable runnable){
        create(null, runnable, null, 0);
    }
    
    private void create(ThreadGroup group, Runnable runnable, String threadName, 
                long stackSize){
        Thread currentThread = Thread.currentThread();
        if(group == null){
            group = currentThread.getThreadGroup();
        }
        // 代码省略
        this.group = group;
        this.target = runnable;
        this.group.addThread(this);
    }

    /* 启动一个新的线程,如果target不为空则执行target的run函数,否则执行当前对象 
     * 的run方法
     */
    public synchronized void start(){
        checkNotStarted();
        hasBeenStarted = true;
        // 调用native函数启动新的线程
        nativeCreate(this, stackSize, daemon);
    }
}

说明:
1)当启动一个线程时,如果Thread的target不为空,则会在子线程执行这个target的run函数,否则会执行该线程自身的run函数。
2)最终执行的任务是Runnable,Thread只是对Runnable的包装,通过一些状态对Thread进行管理与调度。

  • 直接run和start有什么区别?
    调用run()方法,而不是start(),这会让Thread对象的run方法在当前线程中被执行调用,而没有产生新的线程。
中断线程

中断可以理解为线程的一个标识属性,它表示一个运行中的线程是否被其他线程进行 了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt() 方法对其进行中断操作。

线程通过方法isInterrupted()来进行判断是否被中断,如果该 线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返 回false。从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位 清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。

过期API:suspend(),resume(),stop()

suspend()、resume()和stop()方法完成了线程的暂停、恢复和终 止工作,而且非常“人性化”。但是这些API是过期的,也就是不建议使用的。不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结 一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会, 因此会导致程序可能工作在不确定状态下。

安全的终止线程

两种方式:

  1. 中断操作:用来取消或者停止任务;
  2. 变量控制:利用boolean变量控制是否需要停止任务与终止该线程;
private static class Runner implements Runnable { 
    private long i; 
    private volatile boolean on = true; 
    @Override public void run() { 
        while (on && !Thread.currentThread().isInterrupted()){ 
            i++; 
        }
        System.out.println("Count i = " + i); 
    }
    public void cancel() { 
        on = false; 
    } 
}

说明:通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地 将线程停止,因此这种终止线程的做法显得更加安全和优雅。

线程底层实现

POSIX线程

为实现可移植的线程程序,IEEE在IEEE标准1003.lc中定义了线程的标准,它定义的线程包叫作pthread。

线程调用 描述
pthread_create 创建一个新线程
pthread_exit 结束调用的线程
pthread_join 等待一个特定的线程退出
pthread_yield 释放CPU来运行另外一个线程
pthread_attr_init 创建并初始化一个线程的属性结构
pthread_attr_destroy 删除一个线程的属性结构,释放占用内存
线程实现途径
1. 用户空间中实现线程
用户级线程包.png

在用户空间管理线程时,每个进程需要有其专用的线程表,用来跟踪该进程中的线程。这些表和内核中的进程表类似,仅仅记录各个线程的属性,该线程表由运行时系统管理。
优点:
1)用户级线程包可以在不支持线程的操作系统上实现;
2)不需要陷入内核,不需要上下文切换,不需要对内存高速缓存进行刷新,线程调度非常快捷;
3)允许每个进程拥有自己定制的调度算法;
4)具有较好的可拓展性,因为在内核空间中内核线程需要一些固定表格空间和堆栈空间;

缺点
1)处理缺页中断存在问题,如果有一个线程引起页面故障,在对所需的指令进行定位和读入时,相关的进程会阻塞,内核由于不知道有线程存在,通常会把整个进程阻塞直到磁盘I/O完成为止。
2)在一个单独的进程内部,没有时钟中断,不能用轮转调度的方法调度线程;
pthread_create | 创建一个新线程

2. 内核空间中实现线程
由内核管理的线程包.png

内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器 (Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程都可以看做是内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫多线程内核(Multi-Threads Kernel)。[2]
在内核中有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新完成线程创建或撤销工作。

所有能够阻塞线程的调用都以系统调用的形式实现,这与运行时系统过程相比,代价是相当可观的。当一个线程阻塞时,内核根据其选择,可以运行同一个进程中的另一个线程(若有一个就绪线程)或者运行另一个进程中的线程。而在用户级线程中,运行时系统始终运行自己进程中的线程,直到内核剥夺它的CPU (或者没有可运行的线程存在了)为止。

由千在内核中创建或撤销线程的代价比较大,某些系统采取 “环保" 的处理方式,回收其线程。当某个线程被撤销时,就把它标志为不可运行的,但是其内核数据结构没有受到影响。稍后,在必须创建一个新线程时,就重新启动某个旧线程,从而节省了一些开销。在用户级线程中线程回收也是可能的,但是由于其线程管理的代价很小,所以没有必要进行这项工作。

内核线程不需要任何新的、 非阻塞系统调用。另外,如果某个进程中的线程引起了页面故障,内核可以很方便地检查该进程是否有任何其他可运行的线程,如果有,在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行。这样做的主要缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止等)比较多,就会带来很大的开销。

3. 混合实现
用户级线程与内核线程多路复用.png

使用内核级线程,然后将用户级线程与某些或者全部内核线程多路复用起来。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程 会被多个用
户级线程多路复用。

4. Java线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式(Cooperative Threads-Scheduling)线程调度和抢占式(Preemptive Threads-Scheduling)线程调度。
协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知线程切换到另一个线程上去。
协同式多线程的最大好处是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。Lua语言中的“协同例程”就是这类实现。它的坏处也很明显:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。

抢占式调度的多线程系统,每个线程将由系统分配执行时间,线程的切换不由线程本身来决定。在这种调度方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题,Java使用的线程调度方式就是抢占式调度。

参考资料:

[1] 现代操作系统,Andrew S. Tanenbaum
[2] 深入理解Java虚拟机,胡志明
[3] Android开发进阶--从小工到专家,何红辉
[4] Java并发编程的艺术,方腾飞,魏鹏,程晓明

相关文章

  • 1 多线程基础和Sync

    目录 进程、线程概念 创建线程的方法 线程常用方法 线程同步:Sync 1 进程线程概念 1.1 进程 进程指的是...

  • Java 多线程之多线程原理

    一、线程的概念、特点及其作用 二、线程的工作原理 三、多线程的概念、特点及其作用 四、多线程中线程安全的概念及其原...

  • iOS多线程.md

    2018-05-22 iOS多线程-概念iOS多线程:『pthread、NSThread』详尽总结 多线程-概念图...

  • 线程概念

    同步: 任务按顺序执行, 有先后顺序, 执行完一个才能执行另一个任务, 任务有一个执行者 异步 有多个执行者同时执...

  • 线程概念

    线程简介 什么是线程? 线程是比进程更轻量级的调度执行单元,线程的引入,可以把一个进程的资源分配和执行调度分开,各...

  • java线程

    java线程的概念 为何要多线程 线程是比线程更小的概念,一个进程里边会有多个线程。一个cup要处理多个事情,只能...

  • synchronized锁

    一、线程安全的概念与synchronized 1、线程安全概念 并发程序开发的一大关注重点就是线程安全,线程安全就...

  • Java一多线程

    目录: 一、进程与线程的概念 二、多线程的概念 三、多线程所存在的问题(线程安全问题、上下文切换) 四、多线程的三...

  • Android 开发艺术探索笔记之十一 -- Android 的

    学习内容 线程基本概念 线程的不同形式AsyncTaskHandlerThreadIntentService 线程...

  • iOS多线程详解:概念篇

    讲多线程这个话题,就免不了先了解多线程相关的技术概念。本文涉及到的技术概念有CPU、进程、线程、同异步、队列等概念...

网友评论

      本文标题:线程概念

      本文链接:https://www.haomeiwen.com/subject/afocfhtx.html