线程生命周期与创建线程的多种方式
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工具类
可以参考我这篇文章:线程池详解
以上是关于线程生命周期与创建线程的多种方式的主要内容,如果未能解决你的问题,请参考以下文章