前面文章:
按照专辑顺序阅读,姿势更佳哦~
Runnable接口
我们先来学习Runnable接口,因为Thread类也是实现了Runnable接口的
看上图的源码,汇总一下几个特点:
-
使用方式是实现Runnable接口,然后实现**run()**方法,方法中可以写我们的线程业务逻辑
-
解决了Java中单继承的Thread类的缺点
-
实现Runnable接口降低了线程对象和线程任务的耦合性,增强程序的可扩展性
Runnable是一个接口,里面只有run方法,没有start()启动线程的方法,所以我们在使用中还需要依托其它类来启动线程,如下所示
我们看用Runnable接口实现线程的方式:
Thread类,有一个构造方法,参数是Runnable对象,也就是说可以通过Thread类来启动Runnable实现多线程。
所以,实现Runnable接口后,需要使用Thread类来启动,当然也可以用我们继承的Thread类的定义类来实现
Thread类剖析
先看下定义:
public class Thread implements Runnable {}
发现Thread类竟然实现了Runnable接口,它的实现线程的方式则是直接new Thread()即可
构造函数:
我们发现在Thread的构造函数中,都是调用的**init()**方法,接下来我们来重点看下init()方法是什么样子的
再继续进,look
《java编程思想》有云:把ThreadGroup当做是一次不成功的尝试即可,不用理会
**守护线程setDaemon()
**
-
守护线程是为其它线程服务的,比如垃圾回收线程
-
用户线程执行完,虚拟机退出,守护线程也会被停止
-
守护线程是一个服务线程,没有服务对象即没必要运行,和用户线程同生共死
-
使用的时候**setDaemon(true)**来设置守护线程,守护线程不要访问共享资源,因为它可能在任何时候会挂掉,守护线程中产生的新线程也是守护线程
接下来我们看下源码:
线程优先级问题:
-
线程优先级代表获取到CPU时间片的机率高,但这不是一个确定因素
-
Java提供的优先级默认是5,最低是1,最高是10
-
线程优先级和操作系统有关,Windows和Linux有差别,Linux可能直接忽略掉线程优先级
run()和start()方法:
-
run()是线程的任务执行过程,start()是启动线程的函数
-
直接调用run()方法只是当做一个类方法执行,不会去启动一个新线程
/* What will be run. */private Runnable target;@Overridepublic void run() { if (target != null) { target.run(); }}
如果是构造Thread对象的时候,传入了该对象预期执行的任务——Runnable对象时,执行该任务,否则,什么都不做,当然,可以通过集成Thread类,重写run(),来修改其行为
接着我们看start()方法--启动线程方法
线程生命周期:
在上一篇我们说过,线程在运行周期有6种不同状态:
-
New:初始状态,线程刚刚被创建还未调用start()方法
-
RUNNABLE:运行,线程就绪和运行中都称为运行状态,即调用start之后还未获得CPU执行权和正在执行
-
BLOCKED:阻塞状态,线程阻塞于锁,即等待进入Synchronized块或者方法,还未获取到锁的状态
-
WAITING:等待状态,像wait()、join()等方法使线程进入等待状态,CPU不会分配执行时间,需要显示的被其它线程唤醒
-
TIMED_WAITING**:超时等待**,和等待类似,只不过不会无期限的等待其他线程唤醒,而是达到一定时间自动唤醒
-
TERMINATED:终止状态,线程执行完毕,不可复生,在终止的线程上再次调用start()方法,会抛出java.lang.IllegalThreadStateException异常
也说了和线程生命周期和状态相关的方法
-
sleep()或者sleep(time)之后进入的是等待或者超时等待状态,等到达时间进入的是运行中的就绪状态,而非运行状态,此时等待获取CPU的执行权
-
yield()方法让出CPU的执行权可能进入等待,但是同样有机会再次抢夺到CPU的执行权
-
join()方法调用,会等待该线程执行完毕之后才执行别的线程
-
我们使用interrupt()方法来设置标志位,请求终止线程,interrupt不会真正停止一个线程,它只是发了一个信号,设置了一个标志位,告诉线程,你应该结束了
-
过期的suspend()、resume()、stop()方法
ThreadLocal线程本地变量:
-
ThreadLocal是定义在Thread类内部的,如果定义了一个ThreadLocal,每个线程往这个对象中读写是线程隔离的,互相之间不会影响,提供了一种将可变数据通过每个线程有自己独立的副本从而实现线程封闭的机制
-
ThreadLocal可以理解为线程本地变量,即每个定义的ThreadLocal对象,每个线程对这个ThreadLocal中的读写是线程隔离的,互相之间不会影响
-
ThreadLocal内部有个static类型的ThreadLocalMap类,Key是ThreadLocal,Value是存储的值
举个例子:现有ThreadLocal类的对象local,在每个线程的内部都有属于自己的ThreadLocalMap,可以简单的将它认为Key是ThreadLocal,Value为代码中放的值(实际上Key并不是ThreadLocal本身,而是它的一个弱引用),每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
**源码如下:
**
存储结构:
底层存储结构是ThreadLocalMap,有自己的Key和Value,我们可以将ThreadLocal简单的视为Key,放入的值则是Value,实际上ThreadLocal中存放的是弱引用
Entry是ThreadLocalMap里定义的节点,它继承了WeakReference类,定义了一个类型为Object的value,用于存放我们放的值
为什么要用弱引用?
因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。
Java中的引用分为四种:强引用、软引用、弱引用、虚引用;弱引用用于描述非必需对象的,强度处于第三个等级,对象只能生存到下一次垃圾回收之前,在垃圾回收器工作时无论是否内存足够,都会回收掉被弱引用关联的对象;想了解更多的可以看ConcurrentHashMap、IdentityHashMap以及弱键WeakHashMap【源码剖析】,里面有对四种引用的详细描述
弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
**内存泄漏问题
**
关于ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题,其实就是要看对内存泄漏的准确定义是什么。认为ThreadLocal不会引起内存泄漏的说法是因为ThreadLocal.ThreadLocalMap源码实现中自带一套自我清理的机制。
下面重点说下认为内存泄漏是什么样子的:
每一个Thread对应一个ThreadLocalMap映射表,ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收,value 是真正需要存储的 Object。
ThreadLocalMap使用ThreadLocal的弱引用作为Key,如果一个ThreadLocal没有外部强引用时,势必会被回收,这样便会出现Key为null的Entry,那么我们无法访问到这些Key为null的Entry的value值了
若线程再迟迟不结束,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
**如何防止内存泄漏?
**
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就需要清理。
课外知识:InheritableThreadLocal,ThreadLocal本身是线程隔离的,InheritableThreadLocal提供了一种父子线程之间的数据共享机制
听我唠叨一句
你知道的越多,你不知道的也越多。
建议:在适当的时机,去参加几场面试,这样能让你更清楚的认识到自己