线程生命周期与创建线程的多种方式

Posted 小样5411

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程生命周期与创建线程的多种方式相关的知识,希望对你有一定的参考价值。

目录

前言

最近读完了《深入理解Java虚拟机》大部分理论章节,感觉对JVM内部执行豁然开朗,并且发现并发编程和虚拟机工作也密不可分,强推先读一读JVM,或者读我归纳的几篇JVM文章,现在再系统读一读多线程、并发这块的书籍,以前也学过多线程,不过没有系统看书,图书馆选了一本看目录还不错的《Java高并发编程详解:多线程与架构设计》汪文君 著。网上都推荐《实战Java高并发程序设计》葛一鸣著,我也找到了对应pdf版本,先看第一本,如果觉得不全,再看第二本,配合一起看,不过大多内容都大同小异,不多说,一起啃书吧!!!


后续补充:还是看《实战Java高并发程序设计》,第一本后面讲的确实不太好

一、线程的生命周期(重点)

记住五大基本状态转换过程:
NEW(创建状态)
RUNNABLE(可运行状态/就绪状态)
RUNNING(运行状态)
BLOCKED(阻塞状态)
TERMINATED(终止状态)

面试:一个线程创建到消亡的过程(考你对线程生命周期的理解)
首先,一个线程对象被new后,就创建出一个线程Thread对象,注意此时线程并没有运行或者启动,只是一个Thread对象而已,处于创建状态(NEW),通过调用start方法,线程对象才变为就绪状态(RUNNABLE)的就绪线程,注意start()只是让线程就绪,并没有执行,它在等待CPU调度,没有CPU调度它是不会运行的,等到CPU轮转调度时,线程分到CPU的时间片,然后就会真正运行,也就是运行态(RUNNING),要知道CPU是按照时间片轮转的方式调用线程的,这就涉及一些操作系统的知识,每个线程会分到CPU一定长度的时间片,时间片用完,就要让出CPU给其他线程执行,又切换变为RUNNABLE状态,等到再轮转到之前没执行完的线程,然后线程才会继续执行。运行态的线程还可以调用yield()方法让出CPU资源,和其他线程一样再等待CPU调度,存在让出后马上就又得到CPU调度情况。在RUNNING态的线程可以直接进入TERMINATED状态,生命周期就结束了,比如调用stop()方法,但官方不推荐使用。另外,运行态调用sleep()方法、wait()方法或join()方法可以使得线程进入阻塞状态,只不过称呼不一样,如果按五大基本状态划分,则阻塞状态又可细分为三种情况:

1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态,等待时间结束,或者用notify()和notifyAll()可以让线程回到就绪状态

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

3.其他阻塞: 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时,join()等待线程终止或者超时.,或者I/O处理完毕时,线程重新转入就绪状态。

下面图捋一下整个创建到消亡过程,说明五个状态之间的转换(创建、就绪、运行、阻塞、终止)。

注:其中暂停线程的suspend()和恢复线程的resume()已经弃用,所以不讲。yield()方法是放弃当前CPU资源,但放弃后,一定几率马上又获得CPU时间片,可以理解为线程A让出CPU,然后和其他线程一起等CPU调度,属于让出后跟大家同一起跑线公平竞争一样。

:有些可能会说6种状态,注意这里说基本状态,6种状态你可以认为是一种扩展,将阻塞状态中的等待导致阻塞部分再细分出来,细分成等待状态,不过综合查阅资料,这个5大基本状态承认度、规范度还是最高的。

二、实现多线程的多种方式

下面根据一个案例来讲这两种实现,并进行对比,当然创建多线程不止这两种,还可以用Callable或者线程池,但下面两种是最常见的,其中用Runnable接口最常用。

案例:银行排队叫号窗口4个,用户会被叫去每个窗口办理业务,假设最多一天受理50笔业务,我们来写这个程序

2.1 继承Thread类并重写run方法

public class Main 
    public static void main(String[] args) 
        //线程1(窗口1)
        new ServiceWindow("一号窗口").start();
        //线程2(窗口2)
        new ServiceWindow("二号窗口").start();
        //线程3(窗口3)
        new ServiceWindow("三号窗口").start();
        //线程4(窗口4)
        new ServiceWindow("四号窗口").start();
    

//继承Thread
public class ServiceWindow extends Thread

    private String name;//柜台名称

    private int count = 1;//叫号

    private static final int MAX = 50;//最多50笔业务

    ServiceWindow()

    

    ServiceWindow(String name)
        this.name = name;
    

    //重写run方法,底层启动线程是调用底层的run方法执行的
    @Override
    public void run()
        while (count < MAX)
            System.out.println(name + " -> 当前叫号:" + (count++));
        
    

效果

为什么会出现这种重复叫同一个号的情况?因为我们创建了四个叫号线程,Thread实现方式,线程之间资源不共享,这就出现多线程并发访问的不安全问题,有可能存在覆盖情况,并且count++自增也不是原子操作,然后导致输出的值重复,出现“脏读”,可以看下我的这篇关于JVM内存区域的文章,原子操作可见该文章

改进1:加static修饰

count加了staic修饰,那么就属于静态变量,静态变量和常量是会加载到方法区的,而方法区是线程共享的,所以count就被四个线程共享,就不会重复叫号,如下,没有重复的

简单补充上面JVM相关知识,JVM内存区域(运行时数据区)分5大块,堆、方法区、虚拟机栈、本地方法栈、程序计数器,其中方法区和堆是线程共享,虚拟机栈、本地方法栈、程序计数器是线程私有的。

但是static虽然能解决,做到了共享资源,不过static修饰的变量生命周期很长,如果有很多这种变量都用static修饰,那么方法区本来就不大,很容易满。所以我们需要再改进

改进2:用Runnable接口实现共享资源

2.2 实现Runnable接口并重写run方法

public class Main 
    public static void main(String[] args) 

        ServiceWindowRunnable task = new ServiceWindowRunnable();

        new Thread(task,"一号窗口").start();//这里给线程实现接口和取名字,Thread.currentThread().getName()可以获取名字

        new Thread(task,"二号窗口").start();

        new Thread(task,"三号窗口").start();

        new Thread(task,"四号窗口").start();
    

public class ServiceWindowRunnable implements Runnable

    private int count = 1;//叫号,不用static修饰

    private static final int MAX = 50;//最多50笔业务

    @Override
    public void run() 
        while (count < MAX)
            System.out.println(Thread.currentThread().getName() + " -> 当前叫号:" + (count++));
        
    


四个线程共享Runnable接口的资源,这样就既能不重复叫,也不会像static修饰一样生命周期太长而让方法区满, 实现Runnable接口的ServiceWindowRunnable类会创建在堆内存,堆内存是最大的,并且有垃圾回收机制,所以不用担心满的问题。故以后需要共享都用Runnable实现,这也是相比用继承Thread,Runnable最常用的原因,能用Runnable就不用Thread

现在实现线程用lambda表达式,简单、快捷,给出一个简单案例给大家参考理解

public class Main
    public static void main(String[] args) 
        //重写run方法,lanbda表达式写法()->...
        //@Overide public void run()...英文字全省略,只留()->,一个表示带的参数,一个表示方法体
        new Thread(()->
            for (int i = 0 ; i < 10 ; i++)
                System.out.print(i+" ");
            
        ,"一号窗口").start();
    


补充:另外两种线程创建方式

1、FutureTask + Callable

public class test1 
    public static void main(String[] args) throws ExecutionException, InterruptedException 
        FutureTask futureTask = new FutureTask(new Callable() 
            @Override
            public String call() throws Exception 
                TimeUnit.SECONDS.sleep(1);
                System.out.println("call方法执行了");
                return "call方法返回值";
            
        );
        futureTask.run();
        System.out.println("获取返回值: " + futureTask.get());//get方法用于获取任务完成的返回值

        FutureTask futureTask1 = new FutureTask(new Callable() 
            @Override
            public String call() throws Exception 
                TimeUnit.SECONDS.sleep(1);
                System.out.println("call方法执行了1");
                return "call方法返回值1";
            
        );
        futureTask1.run();
        System.out.println("获取返回值1: " + futureTask1.get());
    



这种方式相比Runnable来说,有两个不同:1、有返回值 2、会抛出异常

同时,futureTask先执行,那么其他的就会阻塞,如此处futureTask1就会阻塞,不管futureTask 中设置sleep多久,futureTask1都要等它执行完才会执行,大家可以自己运行看看。另外get方法可以获取到返回值

2、线程池中的Executors工具类
可以参考我这篇文章:线程池详解

以上是关于线程生命周期与创建线程的多种方式的主要内容,如果未能解决你的问题,请参考以下文章

iOS小技能:导航控制器(控制器view的多种创建方式控制器的生命周期)

Java多线程 - 定时器-并发与并行-线程生命周期

Java线程的生命周期与状态流转

Java线程生命周期

10 张图聊聊线程的生命周期和常用 APIs

创建线程的多种方式