Thread的初始化过程
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name.toCharArray();
//1.设置父线程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
//2.设置线程组
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
//3.设置是否是守护线程
this.daemon = parent.isDaemon();
//4.设置优先级
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 (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 */
//5.设置线程ID
tid = nextThreadID();
}
- 创建线程的时候,获取到的是currentThread(),是创建当前线程的那个线程,比如说一般来说就是那个main线程,main线程在创建ServiceAliveMonitor线程,所以说此时创建线程的过程中,获取到的currentThread()就是main线程
- threadGroup是没有指定的,就会给你分配一个线程组,默认就是父线程组
- 3&4. 默认情况下,如果你没有指定你是否为daemon的话,那么你的daemon的状态是由父线程决定的,就是说如果你的父线程是daemon线程,那么你也是daemon线程;同理,你的优先级如果没有指定的话,那么就跟父线程的优先级保持一致
5.如果你没有指定线程的名称,那么默认就是Thread-0格式的名称。并且后面这个数字是全局递增的,从1开始。
Thread的启动过程
public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ //1.判断threadStatus标志 if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group's list of threads * and the group's unstarted count can be decremented. */ //2.加入线程组 group.add(this); boolean started = false; try { //3.实际的去启动一个线程 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 */ } } }
1.永远都不能对一个线程多次调用和执行start()方法,如果线程已经被启动过后,那么他的threadStatus就一定会变为非0的一个状态,如果threadStatus是非0的状态,说明他之前已经被执行过了,所以这里会有一个判断,如果你对一个线程多次执行start()方法人家会抛出一个异常,IllegalThreadStateException,非法的线程状态的异常。
- 2.将线程加入线程组中,如果指定了线程组那么就是你自己创建的那个ThreadGroup,否则的话就是你的父线程的threadGroup。
- 3.执行这一行private native void start0();,一旦是start0()成功的启动之后,他就会去执行我们覆盖掉的那个run()方法,或者是如果你传入进去的是那个Runnalbe对象,人家就会执行那个Runnable对象的方法
Sleep休眠一会
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers. The thread
* does not lose ownership of any monitors.(跟锁相关)
*
* @param millis
* the length of time to sleep in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;
微服务存活状态监控线程,其实一般会每次检查完一轮之后,就要停顿几秒钟,此时可以用Thread.sleep()这个方法,指定要等待多少毫秒。
JDK 1.5之后就引入了TimeUnit这个类,使用很方便,配置很麻烦
TimeUnit.HOURS.sleep(1)
TimeUnit.MINUTES.sleep(5)
TimeUnit.SECONDS.sleep(30)
TimeUnit.MILLISECONDS.sleep(500)
配置休眠5分钟,还得加一个单位,代码里要判断一下你休眠的时间单位,如果是分钟,那么还得用TimeUnit.MINIUTE来进行休眠,不太方便。
开源项目的话,在线程sleep这块,还是用的最最原始的sleep,因为可以通过毫秒数,动态的传入一个外面配置的一个值。
冷门的yield
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method. It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();
目的:担心说某个线程一直长时间霸占着CPU,导致其他的线程很少得到机会来执行,所以设计了一个yield方法,你调用之后,可以尝试说当前线程先别执行了,让CPU可以去执行其他线程了。
注意:如果你要用这个方法的话,必须在严格的测试环境下,做大量的测试,验证说,你在你需要的场景下,使用了yield方法,真的可以达到你需要的效果。很多人很少可以正确的使用这个yeild方法,这个方法常见于debug和test场景下的程序。
场景:在这样的一些场景下,他可以复现因为锁争用导致的一些bug。他也可以用于设计一些并发控制的工具,比如说在java.util.concurrent.locks包下的一些类
搜遍开源项目几乎都找不到yield这个方法
Join实现服务注册线程的阻塞式运行
什么是Join:
首先main线程里面,如果开启了一个其他线程,这个时候只要你一旦开启了其他线程之后,那么main线程就会跟其他线程开始并发的运行,一会执行main线程的代码,一会儿会执行其他线程的代码
main线程里面开启了一个线程A,main线程如果对A线程调用了join的方法,那么就会导致main线程会阻塞住,直到A线程做完事情后main线程再接着运行。
示例:存在两个线程,一个是注册线程一个心跳线程。有一个规定就是说这个注册线程操作的事情一定要完成后才能去调用心跳线程。加了join就能保证这个注册线程一定是要执行完成的。如果不加join就会出现还没注册成功呢,就开始发心跳了这是不对的。
Interrupt做了什么事情
如果是while循环,可以判断如果没有被中断,那么就正常工作,如果别人中断了这个线程,那么while循环的条件判断里,就会发现说,isInterrupted,被中断了。
Thread thread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
System.out.println("线程1在工作。。。");
}
}
};
thread.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
interrupt打断一个线程,其实是在修改那个线程里的一个interrupt的标志位,打断他以后,interrupt标志位就会变成true,所以在线程内部,可以根据这个标志位,isInterrupted这个标志位来判断,是否要继续运行。
并不是说,直接interrupt一下某个线程,直接就不让他运行了。就是通过修改这个变量,通知别的线程看是否要运行了,预防sleep一直阻塞不停止线程。Thread.sleep()方法需要捕获中断异常,
Interrupt实战
在一个分布式系统里面,一般会有一些核心的工作线程,现在如果这个系统要关闭,一般会设计一个shutdown方法,在这个方法里面,会设置各个工作线程是否需要运行的标志位为false。
因为各个工作线程可能都在不断的while循环运行,但是每次执行完一次之后,都会进入休眠的状态,sleep 30秒。如果系统要尽快停止,那么就应该用interrupt打断各个工作线程的休眠,让他们判断是否运行的标志位为false,sleep就不会一直阻塞在这里就立刻退出。
/**
*服务实例是否运行标志
*/
private Boolean isRunning;
/**
* 停止RegisterClient组件
*/
public void shutdown() {
this.isRunning = false;
System.out.println("设置标记位" + isRunning);
this.heartbeatWorker.interrupt();
}
/**
* 心跳线程
*/
private class HeartbeatWorker extends Thread{
@Override
public void run() {
HeartBeatRequest heartBeatRequest = new HeartBeatRequest();
heartBeatRequest.setServiceInstanceId(serviceInstanceId);
heartBeatRequest.setServiceName(SERVICE_NAME);
HeartBeatResponse heartBeatResponse = null;
while (isRunning) {
System.out.println("当前标记位:"+isRunning);
heartBeatResponse = httpSender.heartBeatRegister(heartBeatRequest);
System.out.println("心跳的结果为:" + heartBeatResponse.getStatus() + ".....");
try {
//休眠30秒执行一次心跳
Thread.sleep(HEARTBEAT_INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
interrupt,isInterrupt,interrupted区别
interrupt方法最简单就是更改里面中断标记为true。
isInterrupt方法看下线程的中断标记是什么。
interrupted方法对当前线程的中断标志位进行复位(变为false)。
一般停止一个线程也可以按如下方法使用。在while循环时条件为!this.isInterrupt(),如果线程是中断的那么isInterrupt返回true,while里面的判断就是fasle,线程就结束了。
但是,一些while中,往往会存在调用sleep方法的情景,这里就需要注意下。因为,调用interrupt后sleep会抛出中断异常。
Java虚拟机对会抛出InterruptedException异常的方法进行了特别处理:Java虚拟机会将该线程的中断标志位清除,然后跑出InterruptedException,这个时候调用isInterrupted方法返回的也是false。这种情况下线程依旧不会停止。那么这个时候,就可以用上面实战的那个方法去做,给一个标记位做循环条件。
/**
* 心跳线程
*/
private class HeartbeatWorker extends Thread{
@Override
public void run() {
while (!this.isInterrupt()) {
......
}
}
}
如何正确终止线程
通常情况下,我们不会手动停止一个线程,而是允许线程运行到结束,然后让它自然停止。但是依然会有许多特殊的情况需要我们提前停止线程,比如:用户突然关闭程序,或程序运行出错重启等。
在这种情况下,即将停止的线程在很多业务场景下仍然很有价值。尤其是我们想写一个健壮性很好,能够安全应对各种场景的程序时,正确停止线程就显得格外重要。但是Java 并没有提供简单易用,能够直接安全停止线程的能力。
为什么不强制停止?而是通知、协作
对于 Java 而言,最正确的停止线程的方式是使用 interrupt。但 interrupt 仅仅起到通知被停止线程的作用。而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。
事实上,Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。比如:线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据不完整,不管是中断命令发起者,还是接收者都不希望数据出现问题。
正确使用Interrupt
public class StopDuringSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (!Thread.currentThread().isInterrupted() && num <= 1000) {
System.out.println(num);
num++;
Thread.sleep(1000000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5);
thread.interrupt();
}
}
这里用interrupt标记判断存在缺陷。因为,有线程发送 interrupt 通知试图中断线程,就会立即抛出异常,并清除中断信号。抛出的异常被 catch 语句块捕捉。但是,捕捉到异常的 catch 没有进行任何处理逻辑,相当于把中断信号给隐藏了,这样做是非常不合理的。
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
最好的办法就是在catch代码块中做再次中断。如果这时手动添加中断信号,中断信号依然可以被捕捉到。这样后续执行的方法依然可以检测到这里发生过中断,可以做出相应的处理,整个线程可以正常退出。
使用volatile修饰的变量当终止条件的问题
错误的停止方法
比如 stop(),suspend() 和 resume(),这些方法已经被 Java 直接标记为 @Deprecated。
是因为 stop() 会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题。
而对于 suspend() 和 resume() 而言,它们的问题在于如果线程调用 suspend(),它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被 resume() 之前,是不会被释放的。
假设线程 A 调用了 suspend() 方法让线程 B 挂起,线程 B 进入休眠,而线程 B 又刚好持有一把锁,此时假设线程 A 想访问线程 B 持有的锁,但由于线程 B 并没有释放锁就进入休眠了,所以对于线程 A 而言,此时拿不到锁,也会陷入阻塞,那么线程 A 和线程 B 就都无法继续向下执行。
正是因为有这样的风险,所以 suspend() 和 resume() 组合使用的方法也被废弃了。
volatile 修饰标记位适用的场景
短时间的阻塞可以单纯使用volatile作为终止条件
public class VolatileCanStop implements Runnable {
private volatile boolean canceled = false;
@Override
public void run() {
int num = 0;
try {
while (!canceled && num <= 1000000) {
if (num % 10 == 0) {
System.out.println(num + "是10的倍数。");
}
num++;
Thread.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
VolatileCanStop r = new VolatileCanStop();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(3000);
r.canceled = true;
}
}
volatile 修饰标记位不适用的场景
当某个线程长时间阻塞,无法唤醒的时候,就必须使用interrupt让线程感受到中断信号才行。