架构系列——线程实现方式到底是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。



相关文章
|
3月前
|
Java API 调度
Java 线程的生命周期
Java 线程的生命周期
38 0
|
3月前
|
安全 Java 调度
线程的状态和生命周期
线程的状态和生命周期
44 0
|
4月前
|
网络协议 Java 关系型数据库
年薪50W阿里P7架构师必备知识:并发+JVM+多线程+Netty+MySQL
线程基础、线程之间的共享和协作一 线程基础、线程之间的共享和协作二 线程的并发工具类 线程的并发工具类、原子操作CAS 显式锁和AQS一 显式锁和AQS二 并发容器一 并发容器二 并发容器三、线程池一 线程池二、并发安全一
|
6月前
|
程序员 调度
【线程的生命周期】
【线程的生命周期】
|
3月前
|
调度
【面试问题】说说线程的生命周期?
【1月更文挑战第27天】【面试问题】说说线程的生命周期?
|
4天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
32 1
|
1月前
|
存储 开发框架 安全
【C++ 线程】深入理解C++线程管理:从对象生命周期到线程安全
【C++ 线程】深入理解C++线程管理:从对象生命周期到线程安全
88 0
|
1月前
|
存储 安全 IDE
C/C++ 作用域,生命周期,执行线程的概念
C/C++ 作用域,生命周期,执行线程的概念
18 2
|
1月前
|
消息中间件 并行计算 网络协议
探秘高效Linux C/C++项目架构:让进程、线程和通信方式助力你的代码飞跃
探秘高效Linux C/C++项目架构:让进程、线程和通信方式助力你的代码飞跃
34 0
|
6月前
|
监控 Java BI
JUC的线程池架构
如何降低Java线程的创建成本?必须使用到线程池。线程池主要解决了以下两个问题: 1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对以创建的线程进行复用,使得性能明显。 2)线程管理:每个线程线程池会保持一些基本的线程统计信息,例如完成的任务数量,空闲时间等,以便对相乘进行有效的管理,使得能对所接收到的异步任务进行高效调度。
28 0

热门文章

最新文章