并发基础(三):线程

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

请关注公众号:星河之码

线程是一个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:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * 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:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }	

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)
    
  • 线程优先级的等级

    在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());
    }
}

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());
    }
}

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

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

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

  • 实现伪代码
public class CallableTest implements Callable<Integer> {
    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<Integer> future =new FutureTask<Integer>(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());
    }
}

上述实现过程使用到了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());
    }
}

  • 这里我用的线程池是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 结束的时候,所有的线程都会结束

版权声明:本文为Edwin_Hu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>