线程基础概念

Posted 顧棟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程基础概念相关的知识,希望对你有一定的参考价值。

文章目录

线程基础

引言

进程与线程

进程是可并发执行的程序在某个数据集合上的一次计算活动,也是操作系统进行资源分配和调度的基本单位。

进程是操作系统中除处理器外进行的资源分配和保护的基本单位,它有一个独立的虚拟地址空间,用来容纳进程映像(如与进程关联的程序与数据),并以进程为单位对各种资源实施保护,如受保护地访问处理器、文件、外部设备及其他进程(进程间通信)。

进程的两项功能

1. 进程是资源分配和保护基本单位。

2.进程同时又是一个可独立调度和分派的基本单位。

进程作为一个资源拥有者,在创建、撤消、切换中,系统必须为之付出较大时空开销。所以系统中进程的数量不宜过多,进程切换的频率不宜过高,但这也就限制了并发程度的进一步提高。

为解决此问题,人们想到将进程的上述两个功能分开,即对作为调度和分派的基本单位,不同时作为独立分配资源的单位;对拥有资源的单位,不对之进行频繁切换,线程因而产生。

线程是操作系统进程中能够并发执行的实体,是处理器调度和分派的基本单位。

每个进程内可包含多个可并发执行的线程。线程自己基本不拥有系统资源,只拥有少量必不可少的资源:程序计数器、一组寄存器、栈。同属一个进程的线程共享进程所拥有的主存空间和资源。

目前线程是Java里面进行处理器资源调度的最基本单位。

线程与进程的比较

  1. 调度

    在传统OS中,拥有资源、独立调度和分派的基本单位都是进程,在引入线程的系统中,线程是调度和分派的基本单位,而进程是拥有资源的基本单位。

    在同一个进程内线程切换不会产生进程切换,由一个进程内的线程切换到另一个进程内的线程时,将会引起进程切换。

  2. 并发性

    在引入线程的系统中,进程之间可并发,同一进程内的各线程之间也能并发执行。因而系统具有更好的并发性。

  3. 拥有资源

    无论是传统OS,还是引入线程的OS,进程都是拥有资源的独立单位,线程一般不拥有系统资源,但它可以访问隶属进程的资源。即一个进程的所有资源可供进程内的所有线程共享。

  4. 系统开销

    进程的创建和撤消的开销要远大于线程创建和撤消的开销,进程切换时,当前进程的CPU环境要保存,新进程的CPU环境要设置,线程切换时只须保存和设置少量寄存器,并不涉及存储管理方面的操作,可见,进程切换的开销远大于线程切换的开销。

同时,同一进程内的各线程由于它们拥有相同的地址空间,它们之间的同步和通信的实现也变得比较容易。

线程的实现

线程的实现主要有三种方式

  • **用户级线程(User Level Thread,ULT):(1:N)**对于这种线程的创建、撤消、和切换,由用户程序来实现,内核并不知道用户级线程的存在。
  • **内核级线程(Kernel Level Thread ,KLT):(1:1)**它们是依赖于内核的,即无论是用户进程中的线程,还是系统进程中的线程,它们的创建、撤消、切换都由内核实现。
  • **混合式线程:(N:M)**同时支持ULT和KLT两种线程。

内核线程(KLT)

P:进程

LWP:内核线程的一种高级接口-轻量级进程,也可以理解为线程,每个轻量级进程都由一个内核线程支持。因此只有先支持内核线程才能有轻量级线程。

内核线程就是直接操作内核的线程。由线程调度器来调度这些线程,并将线程的任务映射到各个CPU。

轻量级进程与内核线程的关系是1:1,也成为一对一线程模型。轻量级进程与内核线程的关系如上图所示。

优点:

对多处理器,内核可以同时调度同一进程的多个线程

阻塞是在线程一级完成

缺点:

在同一进程内的线程切换调用内核,系统开销较大

用户线程(ULT)

P:进程

UT:狭义上的用户线程,完全建立在用户空间的线程库上,内部不能感知到用户线程的存在和实现。用户线程的创建、同步、销毁和调度全部在用户态下完成,不需要内核的帮助。

使用用户线程实现的方式称为1:N实现,也成为1:N线程模型。进程与用户线程的关系如上图所示。

优点:

线程切换不调用内核,操作快低消耗

调度是应用程序特定的:可以按需要选择好的算法

ULT可运行在任何操作系统上(只需要线程库),可以在一个不支持线程的OS上实现

缺点:

由于大多数系统调用是阻塞的,因此一个用户级线程的阻塞会引起整个进程的阻塞

内核只将处理器分配给进程,同一进程中的两个线程不能同时运行于两个处理器上等问题复杂,且解决难度高

混合线程

既存在用户级线程,又内核级线程。

用户线程还是完全建立在用户空间中,因此用户线程的创建,切换,析构操作依然不经过内核,是快速度低消耗的;而操作系统支持的轻量级进程则成为了用户线程和内核线程之间的桥梁,这样就可以使用内核的线程调度功能和处理器映射了。在这中实现中,用户线程与轻量级进程的数量比是不确定的,是N:M关系,如上图所示。这是一种多对多线程模型。

Java线程的实现

主流平台的主流的Java虚拟机采用线程模型是1:1的线程模型,基于操作系统的原生线程模型来实现。如HotSpot。

线程的调度

线程调度指的是系统为线程分配处理器使用权的过程,主要分为协同式线程调度和抢占式线程调度。

协同式线程调度

线程的执行时间由线程本身控制,线程把自己的作业完成之后,主动通知系统切换到另一个线程上。这种调度方式通常用于批处理系统中。

优点

  • 实现简单
  • 切换线程操作对线程可知,一般没有同步问题

缺点

  • 线程执行时间不可控,容易阻塞

抢占式线程调度

每个线程将由系统来分配执行时间,线程切换也不由线程本身决定。这种调度方式通常用于分时系统和实时系统中。

优点

  • 线程的执行时间是系统可控的,不会因为一个线程阻塞导致整个进程乃至系统的阻塞问题。

缺点

  • 线程切换不可控,存在同步问题

Java线程的调度

Java使用线程调度方式是抢占式的。

线程优先级

可以通过线程优先级来影响线程调度,是影响并非绝对控制。

不同系统、不同语言之间的线程优先级存在差异。Java一共提供了10个级别线程优先级,window线程优先级一共提供了7个级别线程优先级(THREAD_PRIORITY_IDLE和下表的6种)。

Java与window线程优先级对比图

Java线程优先级window线程优先级
1 (Thread.MIN_PRIORITY)THREAD_PRIORITY_LOWEST
2THREAD_PRIORITY_LOWEST
3THREAD_PRIORITY_BELOW_NORMAL
4THREAD_PRIORITY_BELOW_NORMAL
5 (Thread.NORM_PRIORITY)THREAD_PRIORITY_NORMAL
6THREAD_PRIORITY_ABOVE_NORMAL
7THREAD_PRIORITY_ABOVE_NORMAL
8THREAD_PRIORITY_HIGHEST
9THREAD_PRIORITY_HIGHEST
10 (Thread.MAX_PRIORITY)THREAD_PRIORITY_CRITICAL

Java一共提供了10个级别线程优先级。

JAVA线程的生命周期

java语言一共为线程定义了6中状态,这6种状态构成了java线程的生命周期。一个线程只能有且只有其中的一种状态,并可以通过特定方法在相关状态中进行转换。

  1. 新建(New):线程创建后尚未启动的状态

  2. 可运行(Runnable):线程可能正在执行,也可能在等待CPU执行时间,包括了操作系统线程状态中的Runing和Ready。

  3. 无限期等待(Waiting):不会被分配处理器执行时间,需要被其他线程显示唤醒,以下方法会让线程无限期等待

    • 没有配置Timeout参数的Object::wait()方法 (退出方法 Object.notify() / Object.notifyAll())

    • 没有配置Timeout参数的Thread::join()方法 (退出方法 被调用的线程执行完毕)

    • LockSupport::park()方法

  4. 限期等待(Timed Waiting):不会被分配处理器执行时间,无需其他线程唤醒,在一定时间之后会被系统自动唤醒,以下方法会让线程限期等待

    • 配置Timeout参数的Object::wait()方法 (退出方法 时间结束 / Object.notify() / Object.notifyAll())
    • 配置Timeout参数的Thread::join()方法 (退出方法 时间结束 / 被调用的线程执行完毕)
    • LockSupport::parkNanos()方法
    • LockSupport::parkUntil()方法
    • Thread::sleep()方法 (退出方法 时间结束)
  5. 阻塞(Blocked):线程等待获取一个排他锁,当另一个线程放弃这个锁的时候,会结束这个状态。

    阻塞的状态分三种。

    • 等待阻塞

      正运行的线程调用o.wait方法时,jvm会将线程移去等待队列,此时线程变为阻塞状态。

    • 同步阻塞

      正运行的线程尝试获取其他线程占用的对象同步锁的时候,jvm会将线程放入**锁池(Lock Pool)**此时线程状态变为阻塞。

    • 其他阻塞

    • 当线程执行sleep,join,I/O请求时,jvm把线程转为阻塞,直到sleep结束。join和I/O完毕或者超时。

  6. 结束(Terminated):线程以及结束执行

    • 正常结束

    • Error或者未捕获的Exception

    • 手动结束 调用stop,抛出异常ThreadDeathError,释放线程所持有的锁和资源。这种会造成锁的混乱或者死锁 不建议使用

状态转换

线程基础机制

来自thinking in java 4th edition

基本使用

创建任务

一个完整的任务可以拆分成多个子任务,在不同的线程中执行,来增加任务执行的并行度 ,从而提高执行效率。

实现Runnable接口

在run()方法中编写执行代码。

public class MyJob implements Runnable 
    @Override
    public void run() 
        System.out.println("I`m MyJob");
    

实现Callable接口

可以从任务中返回结果值

public class MyCallable implements Callable<String> 

    @Override
    public String call() throws Exception 
        return "I`m MyCallable";
    

继承Thread类
public class MyThread extends Thread 

    @Override
    public void run() 
        System.out.println("I`m MyThread");
    

执行任务

通过显示的调用start()来执行。

public class Client 

    public static void main(String[] args) throws Exception 
        MyJob instance = new MyJob();
        // 实际开发中 使用线程池管理线程
        Thread threadRunnable = new Thread(instance);
        threadRunnable.start();
        
        MyCallable mc = new MyCallable();
        FutureTask<String> ft = new FutureTask<>(mc);
        Thread threadCallable = new Thread(ft);
        threadCallable.start();
        System.out.println(ft.get());

        MyThread mt = new MyThread();
        mt.start();
    


I`m MyJob
I`m MyCallable
I`m MyThread
使用Exector

从JDK5开始,新增了Executor框架,用来独立执行机制。

    public static void main(String[] args) throws Exception 
        MyJob instance = new MyJob();
        // 线程池的创建最好是手动的,具体可以参考阿里的java开发规约
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) 
            executorService.execute(instance);
        
        executorService.shutdown();
    

线程休眠

Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。

sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

public class SleepRunnable implements Runnable 
    @Override
    public void run() 
        try 
            System.out.println("Start:" + System.currentTimeMillis());
            Thread.sleep(1000L);
            System.out.println("End  :" + System.currentTimeMillis());
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

Start:1648709978088
End  :1648709979089

线程优先级

public class SimplePriorities implements Runnable 
    private int countDown = 5;
    private volatile double d;
    private int priority;

    public SimplePriorities(int priority) 
        this.priority = priority;
    

    @Override
    public String toString() 
        return Thread.currentThread() + ":SimplePriorities" +
                "countDown=" + countDown +
                ", d=" + d +
                ", priority=" + priority +
                '';
    

    @Override
    public void run() 
        Thread.currentThread().setPriority(priority);
        while (true) 
            // 执行100000次浮点数计算,加上volatile,控制编译器不进行优化	
            for (int i = 0; i < 100000; i++) 
                d += (Math.PI + Math.E) / (double) i;
                if (i % 1000 == 0) 
                    Thread.yield();
                
            
            System.out.println(this);
            if (--countDown == 0) 
                return;
            
        
    

    public static void main(String[] args) 
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) 
            executorService.execute(new SimplePriorities(Thread.MIN_PRIORITY));
        
        executorService.execute(new SimplePriorities(Thread.MAX_PRIORITY));

        executorService.shutdown();
    


Thread[线程名,线程优先级,线程组]

Thread[pool-1-thread-6,10,main]:5

多次执行的结果中,优先级高的任务,不一定优先执行,但是大部分会在前执行,所以优先级只能影响优先执行,无法控制优先执行。

线程让步

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。和优先级一样。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

public void run() 
    Thread.yield();

守护线程- daemon

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。main() 属于非守护线程。必须在线程启动之前(start方法之前)调用 setDaemon() 方法,才能将一个线程设置为守护线程。可以通过s.isDaemon()判断线程是否是守护线程。

一个守护线程中新建的线程都是守护线程。

加入线程

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。

join

异常

异常不能逃逸出run()方法,需要在run()中处理掉。

中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否发生过中断操作。可以通过调用线程的interrupt()来对其进行中断操作,线程可以通过isInterrupt() 方法来判断是否被中断过。 I/O 阻塞和 synchronized 锁阻塞是不能中断的。

通过调用 interrupted()对中断标识位进行复位,如果一个线程被中断后,又进行了复位操作,那么再使用 isInterrupt() 判读时,还是会得到false。

InterruptedException

通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。在抛出异常之前 会对线程的中断标识位进行复位。此时isInterrupt()会返回false。

线程之间的协作

java内置了等待/通知机制,相关的方法是任意的JAVA对象都具备,因为方法被定义在Object类上。

方法名称描述
notify()通知一个在对象上等待的线程,使用其wait()方法返回,而返回的前提是该线程获取到了对象的锁
notifyAll()通知所有等待在该对象上的线程
wait()调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁。
wait(long)超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
wait(long,int)对于超时时间更细粒度的控制,可以达到纳秒。

等待/通知机制指一个线程A调用了对象O的wait()方法,之后A进入等待状态;另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象wait()方法返回,进而执行后续的操作。

等待/通知经典范式

等待方原则

  1. 获取对象锁
  2. 如果条件不满足,这调用对象的wait(),将自己挂起,被唤醒后仍然需要检查条件
  3. 条件满足,则执行对应的逻辑。

通知方原则

  1. 获取锁
  2. 改变条件
  3. 通知所有等待对象上的线程。

wait() notify() notifyAll()

只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。

package org.donny.base.test;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @author donny
 * @date 2022/4/1
 */
public class WaitNotify 
    /**
     * 条件项 标识位
     */
    static boolean flag = true;
    /**
     * 锁对象
     */
    static final Object LOCK = new Object();

    public static void main(String[] args) throws Exception 
        Thread w = new Thread(new Wait(), "Wait");
        w.start();
        TimeUnit.SECONDS.sleep(1);
        Thread n = new Thread(new Notify(), "Notify");
        n.start();
    

    static class Wait implements Runnable 
        @Override
        public void run() 
            synchronized (LOCK) 
                while (flag) 
                    try 
                        System.out.println(Thread.currentThread() + " flag is true. wait@ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                        LOCK.wait();
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
                // flag条件被满足 可以继续执行下列代码
                System.out.println(Thread.currentThread() + " flag is false. wait@ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            
        
    

    static class Notify implements Runnable 
        @Override
        public void run() 
            synchronized (LOCK) 
                // 拿到锁之后,然后进行通知
                System.out.println(Thread.currentThread() + " hold lock. notify@ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                // 在通知时 不会释放锁
                LOCK.notifyAll();
                flag = false;
                try 
                    Thread.sleep(5000L);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
            // 执行结束 释放锁,此时Notify和Wait线程会进程锁的竞争

            // 再次加锁
            synchronized (LOCK) 
                System.out.println(Thread.currentThread() + " hold lock again. sleep@ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                try 
                    Thread.sleep(5000L);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        
    

await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

ThreadLocal

线程变量,是一个以ThreadLocal对象作为Key。任务对象作为Value的存储结构。

可以通过set(T)设置一个值,get()获取值。

package org.donny.base.test;

import java.util.concurrent.TimeUnit;

/**
 * @date 2022/4/1 11:13
 */
public class Profiler 
    private static final ThreadLocal<Long> LONG_THREAD_LOCAL = ThreadLocal.withInitial(System::currentTimeMillis);

    public static void begin() 
        LONG_THREAD_LOCAL.set(System.currentTimeMillis());
    

    public static long end() 
        return System.currentTimeMillis() - LONG_THREAD_LOCAL.get();
    

    public static void main(String[] args) throws Exception 
        Profiler.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Cost:" + Profiler.end() + " mills.");
        LONG_THREAD_LOCAL.remove();
    以上是关于线程基础概念的主要内容,如果未能解决你的问题,请参考以下文章

并发系列2-- 线程基础

并发系列2-- 线程基础

多线程编程之基础概念

多线程基础概念与认识

Python 中的协程 基础概念

网络 基础 6