Thread篇
前言
今天约了招银的面试在10号早上,所以感觉需要加快一点进度了,接下来花两天时间搞定线程和线程池的内容,然后花一两天时间搞定锁这一块的源码,最后再把Mysql和Redis、JVM的相关内容复习一些,进行一个查漏补缺。
Thread源码
Thread的一般有两种实现方式(翻译自Thread类的注释):第一种,继承Thread类,第二种继承Runnable接口。
当然事实上在1.5中增加callable接口用于弥补Thread和runnable接口不能获取执行结果的问题。
接口
我们点开Thread类,可以发现实际上Thread类也实现了Runnable接口,那么这个Runnable接口里面有什么呢?
public class Thread implements Runnable
其实里面什么都没有就只有一个run的抽象方法,@FunctionalInterface是函数式接口,java1.8引入,说白了就是只包含了一个抽象方法的接口。这个注解的存在意味着我们可以使用lambda表达式来创建一个线程。
根据代码上面的注释,我们可以了解到当一个对象实现了接口Runnable的时候是用来创建一个线程的。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
成员变量
Thread的成员变量非常多,而且有一些百度了一下也没找到是干嘛的,所以这边还是挑一些重要的讲一下吧,总的来说比较重要的就是ThreadLocalMap、target、priority和status。
//线程名字
private volatile String name;
//优先级
private int priority;
//没注释,没百度到,也没找到引用,不知道干嘛的
private Thread threadQ;
//JVM中的JavaThread指针
private long eetop;
/* 是否单步执行此线程 */
private boolean single_step;
/* 是否是守护线程 */
private boolean daemon = false;
/* 虚拟机状态 */
private boolean stillborn = false;
/* 传进来的对象 */
private Runnable target;
/* 线程的组 */
private ThreadGroup group;
/* 线程的上下文加载器,可以用来加载一些资源 */
private ClassLoader contextClassLoader;
/* The inherited AccessControlContext of this thread */
private AccessControlContext inheritedAccessControlContext;
/* 给匿名线程命名的整数,匿名线程会被命名为Thread-1这类名字,这个int会自增 */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
/*ThreadLocal的Map用于保存变量副本,后面会提到 */
ThreadLocal.ThreadLocalMap threadLocals = null;
/* InheritableThreadLocal用于子线程能够拿到父线程往ThreadLocal里设置的值 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
/*给线程分配的栈大小*/
private long stackSize;
/*
* 本地线程终止之后的专用状态
*/
private long nativeParkEventPointer;
/*
* 线程ID
*/
private long tid;
/* 用于生成线程ID */
private static long threadSeqNumber;
/* 线程状态 */
private volatile int threadStatus = 0;
线程状态和优先级
先说线程的优先级吧,线程的优先级并非采用的是线程状态的枚举类,而是在成员变量中设置了默认值,下限值和上限值,可以通过调用Thread.setPriority()
方法来设定优先级,数值在1~10之间,如果不在这区间内会直接抛出异常,默认是5。
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
在java中的线程状态一共有6种,不同于操作系统中定义,java中把运行状态和就绪状态合称为可运行状态。
public enum State {
/**
* 初始状态,还没有start的线程
*/
NEW,
/**
* 可运行状态,出于运行或者就绪状态的线程
*/
RUNNABLE,
/**
* 线程在等待monitor锁(synchronized关键字)
*/
BLOCKED,
/**
* 无时间限制的等待,通过调用wait、join等方法进入,主要是等待其他线程进行一些
* 操作,不同于等待资源的阻塞状态。
*/
WAITING,
/**
* 有时间限制的等待状态,通过sleep,wait(time),join(time)等方法进入
*/
TIMED_WAITING,
/**
* 终止状态,表示线程已经完成执行。
*/
TERMINATED;
}
构造函数
Thread的构造函数一共有9个之多,但是都是调用的init
方法,这里就只列出一个构造方法。在这个参数最多的构造方法中,可以指定线程的分组、任务、名字和栈的大小。
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
init(group, target, name, stackSize);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
//获得当前线程
Thread parent = currentThread();
//获得系统的安全管理器
SecurityManager security = System.getSecurityManager();
//设定分组
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
//检查是否允许调用线程修改线程组参数
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
start()和run()
线程的start方法和run方法是两个概念,start是指这个线程可以进入运行状态,调用start之后线程会开始执行这个方法(不一定是立刻执行,要等待cpu调度),进入了runnable状态。
如果调用run的话相当于直接调用了这个target的方法,这个过程中并没有启动这个线程,还是在原来的线程上执行。
public synchronized void start() {
//判断一下线程的状态,如果不是new状态则说明已经start了,抛出异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
//向线程组中添加该线程
group.add(this);
boolean started = false;
try {
//start0是一个本地方法,用于执行线程
start0();
started = true;
} finally {
try {
//如果失败则抛出异常
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
@Override
public void run() {
if (target != null) {
target.run();
}
}
exit()
exit()方法其实没有什么好说的,当线程完成run()方法之后就会执行这个方法,用于清空所有的内容,方便GC。
/**
* This method is called by the system to give a Thread
* a chance to clean up before it actually exits.
*/
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
yield()、wait()、sleep()和join()
wait和sleep方法都是本地方法,前者是在object类中的方法,让出cpu的同时释放资源,而后者则是在Thread类中的本地方法,让出cpu的同时不会释放资源。
public static native void sleep(long millis) throws InterruptedException;
//在java.lang.Object中
public final native void wait(long timeout) throws InterruptedException;
//也是一个本地方法,表示让其他线程先执行,不确保能够释放cpu
public static native void yield();
虽然join方法重载了多个但是最后都会调用到这个join方法上,可以看到最后其实调用的还是wait方法。
//使用synchronized进行修饰的方法锁定了调用这个方法的对象
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
interrupt()
调用interrupt()
相当于告诉线程需要进入终止状态。
在interrupt()
之前采用的是stop()
方法,这个方法比较暴力,直接停止线程,容易造成一些错误。
而interrupt()
则是仅仅修改了线程的标志位,希望程序员自己根据这个标志位选择何时终止线程。如果对阻塞的线程调用interrupt()
方法则会抛出异常。
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
//判断一下是否是阻塞的线程
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
ThreadLocal
ThreadLocal是指线程保存的变量的副本,每个线程可以单独的操作这些变量而互不影响。
构造方法
ThreadLocal的构造方法比较特殊,里面什么都没有。主要还是因为ThreaLocal虽然看上去是一个存储着不同线程对象的类,实际上真正的对象都存储在线程自身的内部ThreadLocalMap中,get和set方法也是先获取线程的中map。
所以ThreadLocal实质上只是提供了map类、get、set方法,自身其实不保存任何内容。
public ThreadLocal() {
}
ThreadLocalMap类
ThreadLocalMap是一个内部类,和之前的HashMap有比较高的相似度。同样的是采用Entry的键值对,都是采用数组实现,如果数组中的元素超过了负载上限则需要进行扩容。
但是这个内部类并没有实现Map接口,在哈希冲突的时候采用方法也有一些差别。
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;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
}
set方法
ThreadLocal的set方法实际上是调用的map的set方法,所以我们直接来看map的set方法。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
//计算哈希
int i = key.threadLocalHashCode & (len-1);
//当发现哈希地址冲突的时候采用的是线性探测法
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
//如果k==null说明这个key已经被回收了
//所以进行替换,顺便清楚其他的弱引用
replaceStaleEntry(key, value, i);
return;
}
}
//清除之后判断一下长度,如果达到负载上限则需要扩容。
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法
get方法调用的是map中的getEntry方法,相比较set方法,get方法会稍微简单一些。
简单来说就是计算一下key的哈希,然后看一下对应位置的对象是否是想要的对象。
如果不是说明可能发生了冲突,向数组的下一个地址移动,直到数组元素为空或者找到对象。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//当前元素就是想要的对象,或者为空,直接返回
if (e != null && e.get() == key)
return e;
else
//向后查找
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
//清除无效的entry
expungeStaleEntry(i);
else
//不断的向后找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
remove()
remove()的原理其实和get差不多。
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
后记
今天主要总结了一下关于Thread类的源码,通过这次总结还是有了不少收获,从面经资料上看的只是原理,但是具体的实现方法还是有很多不同的讲究。
明天需要花点力气总结下线程池的内容了,相比较线程的内容,线程池在面试中问到的概率应该会更加高一些,可以扣的细节也更加多。