线程 学习教程(二): 线程相关的几个问题
创建线程
如何创建?
归根到底new Thread()
并调用start()
是java创建并运行线程的唯一方式. Runnable
,Callable
以及继承 Thread
, 还有lamda表达式等类似代码其实都是线程的使用方式.
相比较而言, 创建进程的Java方式是使用Runtime#exec(String command)
方法实现.
如何销毁?
Java正在执行的线程只有在执行完毕, 或者抛出异常后才能停止. 也就是基本可以认为: 线程一旦开始执行就无法从外部控制其销毁.
Thread#stop()
方法用来停止线程, 但此方法过于暴力, 可能会导致资源不会正确释放, 会导致死锁, 此方法已经标记为过期.
while(true)
循环类线程可以使用interrupt()
方法通过抛出InterruptedException
来结束线程, 但要注意只有线程阻塞情况下才会抛出此异常, 因此, 对于非阻塞状态的线程, 是使用isInterrupted()
方法来判断中断标志来结束控制循环.
while(!isInterrupted()){ try{ Thread.sleep(5000); }catch(InterruptedException e){ e.pringStackTrace(); break; } }
如何停止?
使用状态标志变量停止方式如下:
class TaskThread implements Runnable{ private volatile boolean stoped = fasle; // 标识字段, 注意线程安全问题, 使用volatile保证可见性 @Override public void run(){ if(!stopped){ // todo } } public void setStoped(boolean stop){ stoped=stop; } }
控制线程的执行顺序
使用join()
join()
方法会阻塞调用它的线程. 方法的关键代码如下:
public final synchronized void join(long millis){ while (isAlive()) { wait(millis); } }
可见其最终是使用Object#wait(long millis)
来实现, 也就是说, 我们自己也可以使用Object#wait()
来控制线程的执行顺序.
另外的方法
- 使用单线程池的方式
ExecutorService singlePool = Executors.newSingleThreadExecutor()
singlePool.submit(t1);
singlePool.submit(t2);
singlePool.shutdown();
- 使用
CountdownLatch
- 使用循环检测线程
t1.start() while(t1.isAlive()){ //Thread.sleep(5); } t2.start();
- 使用
Thread.sleep(long millis)
线程异常
Java只能捕获unchecked异常. 线程异常时线程即终止.
如何捕获?
可以使用下面方式捕获线程抛出的异常, 这样做的话线程的异常堆栈将不会输出到System.error标准输出流.
对于堆栈过多的场景来自定义错误handler处理, 可以防止错误堆栈导致内存耗尽. Spring boot 中的SpringApplication有相关的接口.
Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh);
线程池中的异常处理
除了上面的方法, 还可以使用如下方法捕获异常.
ThreadPoolExecutor#afterExecute(Runnable r, Throwable t)
线程状态
NEW 新建线程 new
RUNABLE 可运行, start()
BLOCKED 阻塞, 获取锁
WAITING 等待
TIME_WAITING 超时等待
TERMINATED 终结
如何获取 JVM 中所有线程状态
- 使用外部工具:
jstack pid
可以显示出所有线程. - 代码使用JMX:
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long[] ThreadIds = threadMXBean.getAllThreadIds(); for(long threadId : ThreadIds){ ThreadInfo threadInfo =ThreadMXBean.getThreadInfo(threadId); }
线程同步
synchronized 在方法和代码块上的区别,可以查看字节码文件. 关注monitorenter和monitorexit指令, 方法的话关注其flag的标志位.
synchronized 和 ReentrantLock 的区别
二者均可重入. ReentrantLock 多了三个功能, 可中断, 公平锁, 绑定多个Condition. ReentrantLock 性能可能更高一点, 也有更多的控制, 比如尝试获得锁, 释放等, 同时也可以绑定多个条件.
偏向锁在 ReentrantLock 中的体现
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 当前的state if (c == 0) { //首次获取到锁 if (compareAndSetState(0, acquires)) { //CAS并设置状态 setExclusiveOwnerThread(current); //线程暂存 return true; } } else if (current == getExclusiveOwnerThread()) { //判定是否存储当前线程, 是的话就是重入 int nextc = c + acquires; //状态新增 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); //状态回写 return true; } return false; }
线程通讯
wait() 和 notity() notifyAll() 在 Object 中定义的原因?
一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。锁是线程争用的对象, 与线程不应该过度连接. Object上也有一些头信息, 为锁的管理提供方便.
wait() 和 notity() 为什么必须在 synchronized 中执行?
很简单, 多线程环境下的锁才有意义, synchronized 表明有资源争用, 即锁的争夺, 这是锁存在的前提.
线程退出
主线程退出时deamon子线程何时退出
关于这个问题, 实际上是一个depands-on的答案, 有时候子线程会继续执行一段时间, 有时候则在主线程退出后立即退出, 一般认为主线程退出后台线程也会退出, 但并不能认为子线程一定不会执行
Shutdown Hook 方法的使用
方法签名: Runtime#addShutdownHook(Thread thread)
这个钩子方法可以在线程退出时回调完成一些工作, 比如资源回收,还有一些对象的销毁工作.
如何确保主线程退出前所有线程执行完毕
ThreadGroup ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); int count = threadGroup.activeCount(); Thread[] threads = new Thread[count]; threadGroup.enumerate(threads,true); //复制到这个数组 for (Thread t : threads) { System.out.println(t.getState()); }