线程相关的几个问题

来自小马哥公开课

创建线程

如何创建?

归根到底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());
}
posted @ 2019-04-22 19:19  罪恶斯巴克  阅读(466)  评论(0编辑  收藏  举报