• 并发基础(三):线程


    尺有所短,寸有所长;不忘初心,方得始终。

    请关注公众号:星河之码

    线程是一个Java开发者必备的基础知识,整个并发编程离不来线程,那么线程有些基本概念呢?本文通过以下七点对线程的基本概念做一个简单的认识。

    一、线程与进程的区别和关系

    1.1 进程

    • 进程是指在系统中正在运行的一个应用程序

    • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存

    1.2 线程

    • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行

    • 进程要想执行任务,必须得有线程,进程至少要有一条线程

    • 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程

    1.3 进程与线程的区别

    • 地址空间

      同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间

    • 【资源拥有】

      同一进程内的线程共享本进程的资源(如内存、I/O、cpu等),但是进程之间的资源是独立的

    • 【执行过程】

      每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。

      线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

    • 【崩溃影响】

      一个进程崩溃后,在保护模式下不会对其他进程产生影响,而一个线程崩溃整个进程都死掉。因此多进程要比多线程健壮。

    • 【资源切换】

      进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。而对于同时进行又要共享某些变量的并发操作,只能用线程不能用进程

    1.4 总结

    • 进程是资源分配的最小单位,线程是CPU处理器调度的最小单位
    • 进程有独立的地址空间,且进程之间互不影响,线程没有独立的地址空间,属于同一进程的多个线程共享同一块地址空间
    • 进程切换的开销比线程切换大

    二、线程的特点

    • 原子性
    • 可见性
    • 有序性

    针对这三个特性的的描述在《并发基础(一):并发理论》中有解释,这里不在赘述。

    三、线程的状态

    话不多说先上源码,在Java的Thread类中有一个内部枚举类State,State的枚举就是表示的线程的六种状态

    public enum State {
            /**
             * Thread state for a thread which has not yet started.
             */
            NEW,
    
            /**
             * Thread state for a runnable thread.  A thread in the runnable
             * state is executing in the Java virtual machine but it may
             * be waiting for other resources from the operating system
             * such as processor.
             */
            RUNNABLE,
    
            /**
             * Thread state for a thread blocked waiting for a monitor lock.
             * A thread in the blocked state is waiting for a monitor lock
             * to enter a synchronized block/method or
             * reenter a synchronized block/method after calling
             * {@link Object#wait() Object.wait}.
             */
            BLOCKED,
    
            /**
             * Thread state for a waiting thread.
             * A thread is in the waiting state due to calling one of the
             * following methods:
             * 
      *
    • {@link Object#wait() Object.wait} with no timeout
    • *
    • {@link #join() Thread.join} with no timeout
    • *
    • {@link LockSupport#park() LockSupport.park}
    • *
    * *

    A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called Object.wait() * on an object is waiting for another thread to call * Object.notify() or Object.notifyAll() on * that object. A thread that has called Thread.join() * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: *

      *
    • {@link #sleep Thread.sleep}
    • *
    • {@link Object#wait(long) Object.wait} with timeout
    • *
    • {@link #join(long) Thread.join} with timeout
    • *
    • {@link LockSupport#parkNanos LockSupport.parkNanos}
    • *
    • {@link LockSupport#parkUntil LockSupport.parkUntil}
    • *
    */
    TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    3.1 New 新建状态

    线程刚刚创建,还未启动时的状态,此时【没有调用start()方法】。

    3.2 Runnable 运行状态

    操作系统中的【就绪】和【运行】两种状态,在Java中统称为RUNNABLE

    3.2.1 就绪状态(READY)

    线程对象调用了start()方法之后,线程处于就绪状态,就绪表示着该线程可以执行,但具体啥时候执行将取决于JVM里线程调度器的调度。

    其他状态 —>就绪状态

    • 线程调用start(),新建状态转化为就绪状态。
    • 线程sleep(long)时间到),等待状态转化为就绪状态。
    • 阻塞式IO操作结果返回),线程变为就绪状态。
    • 其他线程调用join()方法),结束之后转化为就绪状态。
    • 线程对象拿到对象锁之后),进入就绪状态。
    3.2.2 运行状态(RUNNING)

    JVM调度器调用就绪状态的线程时,该线程就获得了CPU,开始真正执行run()方法的线程执行体,该线程转换为运行状态

    对于单处理器,同一个时刻只能有一个线程处于运行状态。对于抢占式策略的系统来说,系统会给每个线程一小段时间(CPU时间片)处理各自的任务。时间用完之后,系统负责夺回线程占用的资源。下一段时间里,系统会根据一定规则,再次进行调度。

    运行状态 —> 就绪状态

    当线程未执行完就失去了CPU处理器资源(CPU时间片到了,资源被其他线程抢占),CPU时间片到了之后会线程会调用yield()静态方法,向调度器提出释放CPU时间片的请求,不会释放锁。线程进入就绪状态

    所有线程再次竞争CPU资源,此时这个线程完全有可能再次获得CPU资源,再次运行。

    • yield方法

      yield()由线程自己调用,其作用官方描述如下:

      A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint.

      提示调度程序,当前线程愿意放弃当前对处理器的使用。此时当前线程将会被置为就绪状态,和其他线程一样等待调度,这时候根据不同优先级决定的概率,当前线程完全有可能再次抢到处理器资源。

    • sleep和yield的不同之处

      • sleep(long)方法会使线程转入超时等待状态,时间到了之后才会转入就绪状态。

      • yield()方法不会将线程转入等待,而是强制线程进入就绪状态

      • 使用sleep(long)方法需要处理异常,而yield()不用。

    3.3 Blocked 阻塞状态

    线程被阻塞等待监视器锁定的状态。线程进入阻塞状态一般有三种方式:

    • 线程休眠

      调用**【sleep(),sleep(long millis),sleep(long millis, int nanos)】**等方法的时候,线程会进入休眠状态,当前线程被阻塞。

    • 线程阻塞

      代码中出现耗时比较长的逻辑,比如:慢查询,读取文件、接受用户输入都会导致其他线程阻塞

    • 线程死锁

      两个线程都在等待对方先执行完,而导致程序死锁在那里。

    线程取得锁,就会从阻塞状态转变为就绪状态。阻塞状态类型也有三种:

    • 等待阻塞

      通过调用线程的wait()方法,让线程等待某工作的完成。

    • 同步阻塞

      线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

    • 其他阻塞

      通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。

      当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    3.4 Waiting 等待状态

    线程无限期等待另一个线程执行特定操作(通知或中断)的状态

    运行状态->等待状态

    • 当前线程运行过程中,其他线程调用join方法,当前线程将会进入等待状态。

    • 当前线程对象调用wait()方法

    • 调用LockSupport.park():出于线程调度的目的禁用当前线程

    等待状态->就绪状态

    • 等待的线程被其他线程对象唤醒,notify()和notifyAll()。

    • LockSupport.unpark(Thread),解除线程等待状态

      LockSupport.park()方法对应

    3.5 Time_Waiting 超时等待状态

    线程正在等待另一个线程执行【特定时间】的操作的状态。区别于WAITING,它可以在指定的时间自行返回。

    • 运行状态->超时等待状态

      • 调用静态方法Thread.sleep(long)
      • 线程对象调用wait(long)方法
      • 其他线程调用指定时间的join(long)。
      • LockSupport.parkNanos()。
      • LockSupport.parkUntil()。
    • 超时等待状态->就绪状态

      • 超时时间到了自动进入就绪状态
      • 等待的线程被其他线程对象唤醒,即其他线程调用notify()和notifyAll()。
      • LockSupport.unpark(Thread)。

    3.6 Terminated 终止状态

    线程执行完了或者因异常退出了run()方法,该线程结束生命周期。有两个原因会导致线程死亡:

    • run()和call()线程执行体中顺利执行完毕,线程正常终止

    • 线程抛出一个没有捕获的Exception或Error。

    主线成和子线程互不影响,子线程并不会因为主线程结束就结束。

    可以使用使用isAlive方法方法确定当前线程是否存活(可运行状态,阻塞状态),如果是如果是可运行或被阻塞状态,该方法返回true,如果当前线程是new状态且不是可运行的, 或者线程死亡了,则返回false

    3.7 总结

    线程的在同一时刻只会处于一种状态,这些状态【属于是虚拟机状态】,不反映任何操作系统线程的状态

    一个线程从创建到终止都是在这六种状态中流转,流转示意图如下:

    四、线程优先级

    • 线程的优先级是什么

      在操作系统中,线程可以划分优先级,线程优先级越高,获得CPU时间片的概率就越大,但线程优先级的高低与线程的执行顺序并没有必然联系,优先级低的线程也有可能比优先级高的线程先执行。

    • 设置线程优先级

      在Java的Thread类中提供了一个setPriority(int newPriority)方法来设置线程的优先级,一般默认为5

      Thread.currentThread().setPriority(int newPriority)
      
      • 1
    • 线程优先级的等级

      在Java的Thread源码中,提供了 3 个常量值可以用来定义优先级

      从Thread中的setPriority方法可知:线程的优先级分为 1~10 一共 10 个等级,如果优先级小于 1 或大于 10则会抛出 java.lang.IllegalArgumentException 异常

    • 线程优先级的继承

      在 Java 中,线程的优先级具有继承性,如果主线程启动了子线程,则子线程的优先级与主线程的优先级是一样的。例如

      • 调整主线程优先级之前

      • 调整主线程优先级之后

    • 总结

      • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行

      • 优先级低只是表示获取调度的概率低,并不一定会在后面执行,主要cpu的调度

      • 在Java中,main线程的优先级默认为5,因此在Java应用中由main线程衍生的线程优先级都默认为5

    五、线程的实现

    线程的实现方式耳熟能详,在Java中有四种方式可以创建一个线程,分别是:

    • 继承Thread类,重写run方法
    • 实现Runnable接口,实现run方法
    • 实现Callable接口,实现call方法
    • 通过线程池的创建线程

    5.1 继承Thread类,重写run方法

    public class ThreadTest extends Thread{
    
        @Override
        public void run() {
            System.out.println("子线程执行 : "+ Thread.currentThread().getName());
        }
        public static void main(String[] args) {
            System.out.println("main线程开始执行 : "+ Thread.currentThread().getName());
            ThreadTest t1=new ThreadTest();
            ThreadTest t2=new ThreadTest();
            t1.start();
            t2.start();
            System.out.println("main线程结束执行 : "+ Thread.currentThread().getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    5.2 实现Runnable接口,实现run方法

    实现Runnable接口之后,由于Runnable是接口,没有启动线程的start的方法,因此我们需要用Thread类进行封装

    public class RunnableTest implements Runnable{
        @Override
        public void run() {
            System.out.println("Runnable测试----->>>>>子线程执行 : "+ Thread.currentThread().getName());
        }
        public static void main(String[] args) {
            System.out.println("Runnable测试----->>>>>main线程开始执行 : "+ Thread.currentThread().getName());
            RunnableTest runnableTest1=new RunnableTest();
            Thread t1=new Thread(runnableTest1);
            Thread t2=new Thread(runnableTest1);
            t1.start();
            t2.start();
            System.out.println("Runnable测试----->>>>>main线程结束执行 : "+ Thread.currentThread().getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    5.3 实现Callable接口,实现call方法

    在Java中,Callable接口中有声明了一个方法call方法,

    从源码可知,该方法的无参且返回类型是Callable接口的类泛型

    • 实现伪代码
    public class CallableTest implements Callable {
        private Integer anInt;
        public CallableTest(int anInt) {
            this.anInt = anInt;
        }
        
        @Override
        public Integer call() {
            System.out.println("Callable 测试----->>>>>子线程执行 : "+ Thread.currentThread().getName());
            return anInt + 1;
        }
        
        public static void main(String[] args) {
            System.out.println("Callable 测试----->>>>>main线程开始执行 : "+ Thread.currentThread().getName());
            Callable callable = new CallableTest(2);
            FutureTask future =new FutureTask(callable);
            Thread t =new Thread(future);
            t.start();
            Integer integer = null;//获取到线程执行体的返回值
            try {
                integer = future.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("Callable测试----->>>>> 执行结果 : " + integer);
            System.out.println("Callable 测试----->>>>>main线程结束执行 : "+ Thread.currentThread().getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    上述实现过程使用到了FutureTask与Thread两个类,通过FutureTask获取返回值,通过Thread执行线程

    • Runnable与Callable的区别
      • Runnable没有返回值,Callable可以有返回值
      • Callable接口实现类中的run方法允许异常向上抛出,可以在内部try catch,但是Runnable接口实现类中run方法的异常必须在内部处理,不能抛出

    5.4 通过线程池的创建线程

    jdk1.5之后就有了线程池的概念,利用ExecutorService一次性创建很多个线程,需要的时候直接充线程池中获取线程

    public class ExecutorServiceTest implements Runnable{
            public static void main(String[] args) {
            System.out.println("ExecutorService 测试----->>>>>main线程开始执行 : "+ Thread.currentThread().getName());
            ExecutorService executorService= Executors.newFixedThreadPool(10);
            for (int i = 0; i < 10; i++) {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("ExecutorService测试----->>>>>子线程执行 : "+ Thread.currentThread().getName());
                    }
                });
            }
            System.out.println("ExecutorService 测试----->>>>>main线程结束执行 : "+ Thread.currentThread().getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    • 这里我用的线程池是newFixedThreadPool,线程池有很多种,这里不做展开讲。
    • 线程池是一个池子,池里面可以通过Runnable、Callable、Thread中任意一种方式创建的线程。这里我用的是Runnable的方式

    六、线程调度

    在线程池中,多个处于就绪状态的线程在等待CPU,JAVA虚拟机会负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权

    在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。多线程的并发运行本质上各个线程轮流获得CPU的使用权,分别执行各自的任务。

    线程调度分时调度和抢占式调度有两种。

    • 分时调度

      所有线程轮流拥有cpu的使用权,平均分配每个线程占用cpu的时间 (前面说的CPU时间片)。

    • 抢占式调度

      抢占式优先让优先级高的线程使用cpu,优先级相同,则会随机选择一个。Java为抢占式调度

      优先级越高,抢夺cpu的几率就越大,从而优先级高的占用cpu的时间会更长。

    七、守护线程和用户线程

    在 Java 中有两种线程:守护线程(Daemon Thread)和用户线程(User Thread)

    • 守护线程

      是一种特殊的线程,在后台默默地完成一些系统性的服务

      比如垃圾回收线程、JIT 线程都是守护线程

    • 用户线程

      可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作

      如 Thread 创建的线程在默认情况下都属于用户线程

      Java守护线程一般可开发一些为其它用户线程服务的功能。比如说心跳检测,事件监听等。Java 中最有名的守护进程当属 GC 垃圾回收

    • 设置线程成为用户线程与守护线程

      • 通过 Thread.setDaemon(false) 设置为用户线程,默认
      • 通过 Thread.setDaemon(true) 设置为守护线程
    • 用户线程与守护线程的区别

      • 主线程结束后,用户线程会继续运行的,此时 JVM 是存活的。
      • 如果没有用户线程,只有守护线程,当 JVM 结束的时候,所有的线程都会结束
  • 相关阅读:
    计算器到计算机的发展历史
    Anaconda常用操作(亲测有效果)
    [排序]leetcode1636:按照频率将数组升序排序(easy)
    java spring security oauth2 动态 修改当前登录用户的基础信息以及权限2.0(无需重新登录)
    华为机试 - 简易内存池
    SpringBoot详解(二)
    02- pytorch 实现 RNN
    #边学边记 必修5 高项:对人管理 第1章 项目人力资源管理 之 项目团队组建
    【以图会意】文件系统从外存到内存到用户空间
    Dubbo——Dubbo协议整合Jackson序列化解决方案
  • 原文地址:https://blog.csdn.net/Edwin_Hu/article/details/126044988