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

作者专注于Java、架构、Linux、小程序、爬虫、自动化等技术。 工作期间含泪整理出一些资料,微信搜索【程序员高手之路】,回复 【java】【黑客】【爬虫】【小程序】【面试】等关键字免费获取资料。技术交流、项目合作可私聊:shuhao-99999。

目录

一、线程概念

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

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

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

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

4. 线程池实现

5. 区别

6. 其他写法

三、线程生命周期

1. 新建状态(new)

2. 就绪状态(runnable)

3. 运行状态(running)

4. 阻塞状态(blocked)

4.1 调用sleep()

4.2 调用wait()

4.3 调用yield()

4.4 另一个线程调用join()

4.5 调用suspend()

5. 死亡状态(dead)


一、线程概念

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() + "	" + 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() + "	" + 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() + "	" + Thread.currentThread().getId() + "	" + new Date() + " 	starting...");

        int sum = 0;
        for (int i = 0; i <= 100000; i++) {
            sum += i;
        }
        Thread.sleep(5000);

        System.out.println(Thread.currentThread().getName() + "	" + Thread.currentThread().getId() + "	" + new Date() + " 	over...");
        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() + "	" + Thread.currentThread().getId());
            }
        }).start();

        // 使用Lamda表达式形式
        new Thread(() ->System.out.println(Thread.currentThread().getName() + "	" + Thread.currentThread().getId())).start();

        // 尾部代码块, 是对匿名内部类形式的语法糖
        new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "	" + Thread.currentThread().getId());
            }
        }.start();

        // Runnable是函数式接口,所以可以使用Lamda表达式形式
        Runnable runnable = () -> {System.out.println(Thread.currentThread().getName() + "	" + 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。

原文地址:https://www.cnblogs.com/shuhao66666/p/12925384.html