架构系列——线程实现方式到底是4种还是2种?(附带线程生命周期)

简介: 架构系列——线程实现方式到底是4种还是2种?(附带线程生命周期)

一、线程概念

CPU调度分配的基本单位,它被包含在进程之中,是进程中的实际运作单位。

与进程的关系:线程是进程的最小单位,一个进程可以有1个线程,也可以有多个线程(多线程)

参考:架构系列——进程与线程的关系探索

二、实现线程的方式以及区别

1. 继承Thread类,重写run方法

public class Test {
    public static void main(String[] args) {
        new MyThread().start();
    }
}
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
    }
}

2. 实现Runnable接口,重写run方法

用Thread类来包装实现类的实例, 然后调用Thread类的start()方法来启动线程:

public class Test {
    public static void main(String[] args) {
       // 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
    }
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
    }
}

3. 实现Callable接口,重写call方法

public class Test {
    public static void main(String[] args) throws Exception {
       // 将Callable包装成FutureTask,FutureTask也是一种Runnable
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        // get方法会阻塞调用的线程
        Integer sum = futureTask.get();
        System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
    }
}
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");
        int sum = 0;
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
        return sum;
    }
}

4. 线程池实现

java提供了很多线程池,最核心的是ThreadPoolExecutor

java.util.concurrent.Executor 负责线程的使用和调度的根接口
        |--ExecutorService 子接口: 线程池的主要接口
                |--ThreadPoolExecutor 线程池的实现类
                |--ScheduledExceutorService 子接口: 负责线程的调度
                    |--ScheduledThreadPoolExecutor : 继承ThreadPoolExecutor,实现了ScheduledExecutorService
ExecutorService newFixedThreadPool() : 创建固定大小的线程池
ExecutorService newCachedThreadPool() : 缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。
ExecutorService newSingleThreadExecutor() : 创建单个线程池。 线程池中只有一个线程
ScheduledExecutorService newScheduledThreadPool() : 创建固定大小的线程,可以延迟或定时的执行任务

下面用 newFixedThreadPool创建一个固定大小的线程池来举例说明:

public class Test {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        while(true) {
            // 提交多个线程任务,并执行
            threadPool.execute(new Runnable() { 
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " is running ..");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

5. 区别

Thread: 继承方式,不建议使用,因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活;


Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制;


Callable: Thread和Runnable都是重写run()方法并且没有返回值,Callable是重写call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行;

线程池:线程和数据库连接这些资源都是非常宝贵的资源,每次创建和销毁是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。当然了,线程池也不需要我们来实现,jdk的官方也给我们提供了API


当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,而是通过Thread类来启动线程。

Thread类实现了Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以前三种实现方式本质上都是Runnable实现,而线程池方式则是使用线程池来执行前三种的某一种线程

6. 其他写法

public class Test {
    public static void main(String[] args) {
        // 匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
            }
        }).start();
        // 使用Lamda表达式形式
        new Thread(() ->System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId())).start();
        // 尾部代码块, 是对匿名内部类形式的语法糖
        new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
            }
        }.start();
        // Runnable是函数式接口,所以可以使用Lamda表达式形式
        Runnable runnable = () -> {System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());};
        new Thread(runnable).start();
    }
}

三、线程生命周期

1. 新建状态(new)

用上面的几种方法创建一个线程

2. 就绪状态(runnable)

当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。


处于就绪状态的线程并不一定立即运行run()方法,在单CPU的计算机系统中,如果同时有10000个线程调用了start()方法,从微观上来看,必然只有一个线程运行run()方法,而其他9999个线程则处于就绪状态。

3. 运行状态(running)

当线程获得CPU执行权以后,它才进入运行状态,真正开始执行run()方法。

4. 阻塞状态(blocked)

阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。

可以通过以下几个方式阻塞一个线程:

4.1 调用sleep()

当一个线程执行代码的时候调用了sleep()方法后,线程处于阻塞状态(睡眠),需要设置一个睡眠时间,此时有其他线程需要执行时就会造成线程阻塞,而且sleep方法被调用之后,线程不会释放锁对象,也就是说锁还在该线程手里,等睡眠时间一过,该线程就会继续运行,进入运行状态;


4.2 调用wait()

当一个线程正在运行时,调用了wait()方法,此时该线程将锁释放出去进入阻塞状态(等待),另一个线程获取到锁。


与睡眠状态不一样的是,进入等待状态的线程不需要设置睡眠时间,但是必须执行notify()方法或者notifyall()方法才能唤醒这个线程,自己是不会主动醒来的,等被唤醒之后,该线程进入就绪状态;


4.3 调用yield()

当一个线程正在运行时,调用了yield()方法之后,该线程会将执行权礼让给同等级的线程或者比它高一级的线程优先执行,此时该线程有可能只执行了一部分而此时把执行权礼让给了其他线程,这个时候也会进入阻塞状态(礼让);


4.4 另一个线程调用join()

当一个线程正在运行时,另一个线程在这个线程里调用了一个join()方法,此时该线程会进入阻塞状态(插队),另一个线程会运行,直到运行结束后,原线程才会进入就绪状态;比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。


4.5 调用suspend()

与wait()、notify()类似,suspend() 是让线程进入阻塞状态(挂起),它的解药就是resume(),没有resume()它自己是不会恢复的,由于这种比较容易出现死锁现象,所以jdk1.5之后就已经被废除了,这对就是相爱相杀的一对。


5. 死亡状态(dead)

有两个原因会导致线程死亡:


①run方法正常退出而自然死亡;


②一个未捕获的异常终止了run方法而使线程猝死;


为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法,如果是可运行或被阻塞,这个方法返回true;如果线程仍旧是new状态且不是可运行的,或者线程死亡了,则返回false。



相关文章
|
5月前
|
Java
【编程侦探社】追踪 Java 线程:一场关于生命周期的侦探故事!
【6月更文挑战第19天】在Java世界中,线程如同神秘角色,编程侦探揭示其生命周期:从新生(`new Thread()`)到就绪(`start()`),面临并发挑战如资源共享冲突。通过`synchronized`实现同步,处理阻塞状态(如等待锁`synchronized (lock) {...}`),最终至死亡,侦探深入理解并解决了多线程谜题,成为编程侦探社的传奇案例。
34 1
|
18天前
|
Java API 调度
Java 线程的生命周期
在JDK 1.5之前,线程的生命周期包括五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。JDK 1.5及之后增加了三种阻塞状态,共六种状态:新建、可运行、终止、锁阻塞、计时等待和无限等待。这些状态描述了线程在操作系统和JVM中的不同阶段。
Java 线程的生命周期
|
22天前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
35 1
[Java]线程生命周期与线程通信
|
4月前
|
调度 数据库 uml
高级系统架构设计师问题之线程状态变化如何解决
高级系统架构设计师问题之线程状态变化如何解决
|
1月前
|
Java 调度
Java一个线程的生命周期详解
Java中,一个线程的生命周期分为五个阶段:NEW(新建),RUNNABLE(可运行),BLOCKED(阻塞),WAITING(等待),TERMINATED(终止)。线程创建后处于新建状态,调用start方法进入可运行状态,执行中可能因等待资源进入阻塞或等待状态,正常完成或异常终止后进入终止状态。各状态间可相互转换,构成线程的生命周期。
|
2月前
|
存储 缓存 Java
JAVA并发编程系列(11)线程池底层原理架构剖析
本文详细解析了Java线程池的核心参数及其意义,包括核心线程数量(corePoolSize)、最大线程数量(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务存储队列(workQueue)、线程工厂(threadFactory)及拒绝策略(handler)。此外,还介绍了四种常见的线程池:可缓存线程池(newCachedThreadPool)、定时调度线程池(newScheduledThreadPool)、单线程池(newSingleThreadExecutor)及固定长度线程池(newFixedThreadPool)。
|
3月前
|
Java 调度
【多线程面试题 五】、 介绍一下线程的生命周期
线程的生命周期包括新建、就绪、运行、阻塞和死亡状态,线程状态会根据线程的执行情况在这些状态之间转换。
【多线程面试题 五】、 介绍一下线程的生命周期
|
3月前
|
安全 Java 调度
线程的状态和生命周期
在多线程编程中,线程的状态和生命周期是两个非常重要的概念。了解线程的状态和生命周期可以帮助我们更好地理解和编写多线程程序。
57 4
|
3月前
|
Java 测试技术 Android开发
Android项目架构设计问题之构造一个Android中的线程池如何解决
Android项目架构设计问题之构造一个Android中的线程池如何解决
29 0
|
4月前
|
存储 缓存 NoSQL
架构设计篇问题之在数据割接过程中,多线程处理会导致数据错乱和重复问题如何解决
架构设计篇问题之在数据割接过程中,多线程处理会导致数据错乱和重复问题如何解决