java线程生命周期
- 关于线程生命周期的不同状态,java5以后,线程状态已内部枚举类的方式明确定义在java.lang.Thread.State
- 1.新建(new),表示该线程刚被创建还未真正启动,可认为其是java内部状态
- 2.就绪(RUNNABLE),表示该线程已经在JVM中执行,由于执行需要资源,他可能处于正运行,也可能是等待系统分配给它CPU片段,在就绪队列里面
- 3.在其他一些分析中,会有额外区分一种状态RUNNING,但从JAVA API(内部线程状态枚举类)来说,并不能变现处理
- 4.阻塞(BLOCKED),这个状态和同步相关,表示线程在等待获取Monitor lock来锁住资源供自己使用。如线程通过synchronized区获取某个锁,但是其他线程已经独占,那么该线程就处于阻塞。
- 5.等待(Waiting),表示正在等待期限线程采取某些操作。场景如生产者消费者模式,发现任务条件不满足,就让消费者线程等待(Wait),然后生产者线程准备数据,输出结果,然后通过notify等动作,通知消费者可以继续工作。Thread.join()也会令线程进入WAITING.
- 6.计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,如wait或join等方法的指定超时版本。如下
public final native void wait(long timeout) throws InterruptedException; - 7.终止(TERMINATED),不管意外退出还是正常执行结束,线程已经完成。也叫死亡。
若线程被调用两次start()方法
- java线程不允许启动两次,第二次比如抛出运行时IllegalThreadStateException,多次调用会被认为编译错误
- 第二次调用start()方法时,线程可能处于终止或其他(非new)状态。不论如何,都不可以再次被启动
知识扩展
线程
- 从OS角度,线程是系统调度最小单元,一个进程包含多个线程
- 作为任务真正运作者,有自己的栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是回合进程内其他线程共享FileDescriptor、虚拟地址空间等
- 具体实现,分为内核线程、用户线程,java线程实现与JVM相关。如最熟悉的Sun/Oracle JDK,其线程经历演进,java1.2后,jdk抛弃所谓Green Thread(用户调度的线程),现在的模型是一对一映射到OS内核线程
Thread源码
- 基本操作逻辑以JNI(java native interface)调用的本地代码
Runnable task = () -> {System.out.println("Hello World!");}; Thread myThread = new Thread(task); myThread.start(); myThread.join();
- 上述实现利弊
优点:得益于java精细粒度的线程和相关并发操作,胜任构建高扩展大型应用。 缺点:其复杂性也提高了并发编程的门槛。
- 线程基本操作
1.创建、启动线程,执行join方法等待结束,Runnable不会受java不支持类多继承的限制,重用代码实现,提高代码复用。 Runnable task = () -> {System.out.println("Hello World!");}; Thread myThread = new Thread(task); myThread.start(); myThread.join(); 2.将上述逻辑start()和join()改成如下结构,提交task线程任务,线程创建、管理交由线程池,也能利用Future等机制更好处理执行结果。 Future future = Executors.newFixedThreadPool(1).submit(task).get();
影响线程状态因素
- 线程自身方法,除start(),还有多个join方法,等待线程结束;yield是告知调度器,主动让出CPU;另外,就是一些标记过时的resume、stop、suspend等,为了destroy、stop会被移除
- 基于Object提供的基础方法(wait/notify/notifyAll).如果我们持有某个对象的Monitor锁,调用wait会让当前线程处于WAITTING状态,直到其他线程notify或者notifyAll,本质上是提供Monitor的获取和释放的能力,是基本线程间通信方式
- 并发类库的工具,如CountDownLatch.await()会让当前线程进入等待状态,指定latch被基数为0,这可以看做线程间通信的Signal
- 线程状态转换与方法直接的对应图
API使用
- 守护线程(Daemon Thread),有时应用需要一个长期驻留的服务程序,不希望其影响应用退出,就可以将其设置为守护线程,如果jvm发现只有守护线程存在,将结束进程。
注意:必须在线程启动前设置 Thread daemonThread = new Thread(); daemonThread.setDaemon(true); daemonThread.start();
- Spurious wakeup ,多核CPU系统中,线程等待存在一种肯,即在无任何线程广播或发出信号情况下,线程被唤醒,如处理不当就会出现诡异并发问题,所以在在等待条件过程中,建议采用如下模式来写
// 推荐 while ( isCondition()) { waitForAConfition(...); } // 不推荐,可能引入bug if ( isCondition()) { waitForAConfition(...); }
- Thread.opSpinWait,java9 中引入特性,用于自旋锁,自旋锁(spin-wait,busy-waitng,未获得锁,一直处于空转),也可以认为岂不是一种锁,而是一个针对短期等待的性能优化技术。“onSpinWait()”无任何行为保证,而是对jvm的暗示,jvm可能会利用CPU的pause指令进一步提高性能,性能敏感应用可以关注。
- 慎用ThreadLLocal(本地存储),java提高保存线程私有的机制,在整个线程生命周期有效,方便地在一个线程关联的不同业务模块之间传递信息,如事务id,Cookie等上下文相关信息。其实现结构如下,数据存储于ThreadLocalMap,内部为弱引用
1.当key为null时,该条目为”废弃条目”,相关”value”的回收,往往依赖于几个关键点,即set,remove,rehash static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // … } 2.set示例,具体逻辑在cleanSomeSlots 和 expungeStaleEntry 之中 private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i];; …) { //… if (k == null) { // 替换废弃条目 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; // 扫描并清理发现的废弃条目,并检查容量是否超限 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();// 清理废弃条目,如果仍然超限,则扩容(加倍) }
- 结合java引用类型,发现一个特别地方,通常弱引用都会和引用队列配合清理机制,但是ThreadLocal(本地存储),是个例外。这意味废弃项目回收依赖于显式地触发,否则就要等线程结束,进而回收相应的ThreadLocalMap.这也是很多OOM来源。因此谨慎使用ThreadLocal,在使用后一定要自己负责remove,并且不要和线程池配合,因为worker线程往往不会退出,仍然会造成OOM.