《Java高并发编程详解:多线程与架构设计》笔记(一)

目录

序言

 

线程的查看

线程生命周期

线程的构造函数

JVM内存结构

守护线程

Thread API

线程的关闭

异常退出

进程假死

线程安全与数据同步

死锁原因

线程间通信

同步阻塞和异步阻塞

单线程间通信

多线程通信

自定义显式锁BooleanLock

ThreadGroup

Hook线程以及捕获线程执行异常

线程池原理以及自定义线程池

总结


序言

当个人的发展遇到了瓶颈,想一想是不是因为自己的基础不够扎实,或者是之前之前看过的书没有理解。静下心来回过头看看书,看会不会有新的理解,毕竟有些东西自己没怎么用很容易忘,我个人也认为基础还是比较重要的东西,看视频会是一个比较容易接受的方式,不过看书才最能巩固基础,毕竟书本上的内容才是最成体系的。

该篇是汪文君2018年第一版的《Java高并发编程详解:多线程与架构设计》一书的第一章至第八章的笔记。

 

线程的查看

1、使用Jconsole或Jstack命令来查看JVM线程。Jconsole是JDK自带的可视化界面,如下图,

《Java高并发编程详解:多线程与架构设计》笔记(一)_第1张图片

Jstack命令查看对应PID(使用命令ps -ef查看进程),如下图我写了一个死锁的demo,使用jstack 进程号可以看到情况。

《Java高并发编程详解:多线程与架构设计》笔记(一)_第2张图片

线程生命周期

线程NEW状态:当用关键字new创建一个Thread对象时,线程为New状态。

线程RUNNABLE状态:当线程调用start方法进入RUNNABLE状态。RUNNABLE状态只能意外终止或者进入RUNNING状态。

线程RUNNING状态:一旦CPU通过轮询或者其他方式从可执行队列中选中了该线程,该线程才真正地执行自己的逻辑代码,进入RUNNING状态。调用yield方法放弃CPU执行权进入RUNNABLE状态。

线程BLOCKED状态:比如从RUNNING状态调用了sleep或者wait方法加入waitSet中;竞争锁资源而加入到阻塞队列;阻塞的IO操作进入阻塞状态。

线程TERMINATED状态:JDK不推荐使用stop方法或者意外死亡(JVM Crash),一般线程正常结束生命周期。

线程的构造函数

线程的构造函数:如果一个线程没有显式的指定ThreadGroup则它和父线程同属一个ThreadGroup。

栈内存通过xss参数设置,线程的构造函数中stacksize越大表明线程内方法递归调用深度就越深,stacksize越小则代表创建的线程数量越多。

JVM内存结构

  1. 程序计数器:存放当前线程接下来要执行的字节码指令、分支、循环、跳转、异常处理等信息。线程私有。
  2. Java虚拟机栈:存放局部变量表、操作栈、动态链接、方法出口等信息。线程私有。
  3. 本地方法栈:存放本地方法(JNI方法)。线程私有。
  4. 堆内存:运行期的对象,分新生代(Eden区:From Survivor区:To Surivor区= 8:1:1)和老年代。线程共享。
  5. 方法区:存储已被JVM加载的类信息、常量、静态变量、即使编译器后的代码等数据。线程共享。
  6. Java8元空间:JDK1.8版本起,元空间取代了持久代内存,可用jstat命令查看JVM的GC内存分布,如下图,持久代被替换为元空间(meta space),元空间同样是堆内存一部分。

《Java高并发编程详解:多线程与架构设计》笔记(一)_第3张图片

 

堆内存不变,栈内存越大,可创建的线程数量越小。这是由于虚拟机栈内存线程私有,每一个线程都会占有指定的内存大小,Java进程的内存大小=堆内存+线程数*栈内存。

JVM可创建多少个线程与堆内存、栈内存有关,线程数量 = \frac{MaxProcessMemory - HeapMemory - ReservedOsMemory}{ThreadStackSize(XSS)},其中MaxProcessMemory是最大地址空间,HeapMemory是JVM堆内存,ReservedOsMemory是系统保留内存(一般136M)。

守护线程

一般用于处理后台工作,如JDK垃圾回收线程。正常情况下,JVM中没有一个非守护线程,则JVM的进程会退出。守护线程具备自动结束声明周期的特性。

设置守护线程只需调用Thread.setDaemon(true)方法即可,它常用作执行一些后台任务,当需要关闭某些线程的时候,或者退出JVM进程的时候,一些线程能够自动关闭,这时采用守护线程。

Thread API

  1. sleep:当前线程进入指定毫秒数休眠,暂停执行,不会放弃monitor锁的所有权。
  2. TimeUnit:sleep方法的封装。
  3. yield:提醒调度器该线程愿意放弃当前CPU资源,如果CPU资源不紧张则会忽略(启发式方式)。调用yield方法会使线程从RUNNING状态切换到RUNNABLE状态。
  4. setPriority:设置线程优先级,同样也是一个hint操作,不会达到预期的效果(除了root用户)。线程优先级默认和父类保持一致,一般都是5。
  5. getId:获取线程唯一ID。
  6. currentThread:返回当前执行线程的引用。
  7. getContextClassLoader:获取线程上下文的类加载器。
  8. interrupt:使当前线程进入阻塞状态。Object类的wait方法,Thread类的sleep方法,Thread类的join方法,InterruptibleChannel的IO操作,Selector的wakeup方法。
  9. isInterrupted:判断当前线程是否被中断。
  10. join:和sleep一样都是可中断的方法。join线程A使线程B进入等待,直到线程A结束生命周期。

 

线程的关闭

JDK有一个过期(Deprecated)方法stop,早已不推荐使用,保留是为了兼容旧服务。stop方法存在的问题是关闭线程时可能不会释放掉monitor的锁,所以强烈不推荐使用。关闭线程有以下几种方法:

1、线程结束生命周期:线程正常运行结束(生命周期结束)。

2、捕获中断信号关闭线程:线程中循环执行某个任务,如心跳检查。通过检查线程interrupt的标识来决定是否退出。

3、使用volatile开关控制:由于线程的interrupt标识很有可能被擦除,或者逻辑单元不会调用任何可中断方法,使用volatile修饰的开发flag关闭线程是一种常用做法。

public class FlagThreadExit {
    static class MyTask extends Thread{
        private volatile boolean closed = false;
        @Override
        public void run(){
            System.out.println("I will start work");
            while(!closed && !isInterrupted()){
                //working
            }
            System.out.println("I will be exiting.");
        }

        public void close(){
            this.closed = true;
            this.interrupt();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyTask t = new MyTask();
        t.start();
        TimeUnit.MINUTES.sleep(1);
        System.out.println("System will be shutdown.");
        t.close();
    }
}

异常退出

在线程执行单元中,不允许抛checked异常(无论Thread.run方法还是Runnable的run方法),如果需要捕获的话将checked异常封装成unchecked异常(RuntimeException)抛出而结束线程生命周期。

进程假死

假死的绝大部分原因是某个线程阻塞了,或者出现死锁的情况。使用jstack、jconsole、jvisualvm工具诊断。

线程安全与数据同步

多个线程同时对同一份资源进行访问(读写操作)时,保证多个线程访问到的数据一致,出现不一致的原因是由于线程的执行是由CPU时间片轮询调度的。

通过synchronized关键字可以防止线程干扰和内存一致性错误,synchronized关键字的具体表现如下:

  • synchronzed关键字提供了锁的机制,能确保共享变量的互斥访问。
  • synchroinzed关键字包含monitor enter和monitor exit两个JVM指令,它能保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit成功之后,共享变量被更新后的值必须刷入主内存。
  • synchronized指令严格遵守happends-before规则,一个monitor exit指令之前必须要有一个monitor enter。

举个简单栗子,创建5个线程,每个线程持有锁1分钟,如下,

package com.hust.zhang.threadSafe;

import java.util.concurrent.TimeUnit;

public class Mutex {
    private final static Object MUTEX = new Object();

    public void accessResource() {
        synchronized (MUTEX) {
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        final Mutex mutex = new Mutex();
        for (int i = 0; i < 5; i++) {
            new Thread(mutex::accessResource).start();
        }
    }
}

Jconsole中可以看到当前持有锁的线程为Thread-0,且线程状态为TIMED_WAITING状态。

《Java高并发编程详解:多线程与架构设计》笔记(一)_第4张图片

 

使用JDK命令javap对编译后的class文件进行反汇编可以看到monitor enter和monitor exit成对出现,且满足happen-before原则。每个对象都有一个监视器锁(monitor),被占用就会处于锁定状态,若已占有该monitor,重新进入(monitor enter),则进入数+1。 

《Java高并发编程详解:多线程与架构设计》笔记(一)_第5张图片

使用synchronized需要注意的几个地方:

  1. monitor关联的对象不能为空。比如上面定义的MUTEX对象为空。
  2. synchronized作用域太大,synchronized一般用于代码块或方法。由于synchronized关键字存在排他性,所有线程必须串行地经过synchronized保护的共享区域,如果作用域越大,其效率就越低。
  3. 不同monitor企图锁相同的方法。
  4. 多个锁的交叉导致死锁。 
  5. 同一个实例对象中的不同方法都加上synchronized关键字时,争抢的时同一个monitor的lock。

 

死锁原因

  1. 交叉锁:线程A持有R1的锁等待R2的锁,线程B持有R2的锁等待R1的锁。
  2. 内存不足:两个线程都在等待彼此能够释放内存资源。
  3. 一问一答式数据交换:客户端和服务器端都在等待双方发送数据。
  4. 数据库锁:无论表锁、行锁,某个线程执行for update语句退出事务,其他线程访问该数据库都会陷入死锁。
  5. 文件锁:某线程获得文件锁意外退出,其他线程进入死锁直到系统释放文件句柄(Handle)资源。
  6. 死循环:死循环造成的死锁一般成为系统假死。

线程间通信

同步阻塞和异步阻塞

同步阻塞消息处理缺点:客户端等待时间过长会陷入阻塞;吞吐量不高;频繁创建开启与销毁;业务高峰系统性能低。

异步非阻塞消息处理:优势明显,但也存在缺陷,如客户端再次调用接口方法仍然需要进行查询(可通过异步回调接口解决)。

单线程间通信

服务器端与客户端通过事件队列进行通信的case比较好的方式就是使用通知机制:创建一个事件队列,有事件则通知工作线程开始工作,没有则工作线程休息并等待通知。下面就是这样的case。

事件队列:

package com.hust.zhang.conn;

import java.util.LinkedList;

import static java.lang.Thread.currentThread;

public class EventQueue {

    private int max;

    public EventQueue(int num) {
        this.max = num;
    }

    public EventQueue() {
        this(DEFAULT_MAX_EVENT);
    }

    //object类是所有类的父类
    static class Event {
    }

    private final LinkedList eventQueue = new LinkedList<>();
    private final static int DEFAULT_MAX_EVENT = 10;

    public void offer(Event event) {
        synchronized (eventQueue) {
            //当共享资源eventQueue队列达到上限,调用eventQueue的wait方法使当前线程进入wait set中并释放monitor的锁
            if (eventQueue.size() >= max) {
                try {
                    console("the queue is full.");
                    /**
                     * wait方法:
                     * 1、可中断,一旦调用wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断。
                     * 2、执行某个对象的wait方法后,加入与之对应的wait set中,每一个对象的monitor都有一个与之关联的wait set。
                     * 3、必须在同步方法中使用wait和notify,因为执行wait和notify前提条件是必须持有同步方法的monitor所有权。否则会出现IllegalMonitorStateException。
                     * */
                    eventQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            console("the event is submitted");
            eventQueue.addLast(event);
            eventQueue.notify();
        }
    }

    public Event take() {
        synchronized (eventQueue) {
            if (eventQueue.isEmpty()) {
                try {
                    console("the queue is empty");
                    //eventQueue是Event类的集合,调用的是父类Object的wait方法
                    eventQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            Event event = eventQueue.removeFirst();
            //notify唤醒在此对象监视器monitor上等待的单个线程
            this.eventQueue.notify();
            console("the event " + event + " is handled.");
            return event;
        }
    }

    private void console(String message) {
        System.out.printf("%s:%s\n", currentThread().getName(), message);
    }

}

模拟服务者和消费者的两个线程:

package com.hust.zhang.conn;

import java.util.concurrent.TimeUnit;

public class EventClient {
    public static void main(String[] args) {
        final EventQueue eventQueue = new EventQueue();
        new Thread(() -> {
            for (; ; ) {
                eventQueue.offer(new EventQueue.Event());
            }
        }, "Producer").start();

        new Thread(() -> {
            for (; ; ) {
                eventQueue.take();
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Consumer").start();
    }
}

多线程通信

上面的case中Producer很快提交了10个Event数据,此时队列已满,然后执行eventQueue的wait方法进入阻塞状态,Consumer线程由于要处理数据,花费1秒处理其中的一条数据,然后通知Producer线程可以继续提交数据了,如此循环。

但是上面的case如果有多个线程同时take或offer上面的程序就会出现数据不一致的问题,当eventQueue元素为空时,两个线程执行take方法分别调用wait方法进入阻塞,另一个offer线程执行addLast方法后唤醒了其中一个阻塞的线程,该线程顺利消费了一个元素之后恰巧再次唤醒了一个take线程,这时导致执行空LinkedList的removeFirst方法。所以再在上面做了一定的优化,判断eventQuque队列满或空变成了轮询队列条件(if -> while),唤醒在此对象监视器monitor等待的单个线程变成唤醒在此对象监视器monitor等待的所有线程(notify -> notifyAll)。这样改进可以防止多个线程同时take或offer造成的线程安全问题。

自定义显式锁BooleanLock

synchronized提供的是一种排他式的数据同步机制,某个线程在获取monitor lock的时候可能会被阻塞,而这种阻塞有两个很明显的缺陷:

  1. 无法控制阻塞时长。
  2. 阻塞不可被中断。

下面是一个缺陷分析的case。

package com.hust.zhang.synchronizedAnalysis;

import java.util.concurrent.TimeUnit;

public class SynchronizedDefect {
    public synchronized void syncMethod() {
        try {
            //阻塞时间长无法控制
            TimeUnit.HOURS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedDefect defect = new SynchronizedDefect();
        Thread t1 = new Thread(defect::syncMethod, "T1");
        //make sure the t1 start
        t1.start();
        TimeUnit.MICROSECONDS.sleep(2);

        //T2因争抢monitor的锁而进入阻塞状态,无法中断
        Thread t2 = new Thread(defect::syncMethod, "T2");
        t2.start();

        //虽然可以设置中断标识,但是无法被中断
        TimeUnit.MICROSECONDS.sleep(2);
        t2.interrupt();
        System.out.println("t2.isInterrupt: " + t2.isInterrupted());    //true
        System.out.println("t1.state: " + t1.getState());               //TIMED_WAITING
        System.out.println("t2.state: " + t2.getState());               //BLOCKED
    }
}

上面的case可以看到线程t2因为争抢monitor的锁而进入阻塞状态,对其调用interrupt方法只会设置中断标识,线程一直处于阻塞状态无法被中断。但如果是休眠中的线程(Thread.sleep),调用interrupt方法会中断该线程并抛出InterruptException异常。

所以这里采用自定义显式锁BooleanLock,demo如下,

锁接口:

package com.hust.zhang.synchronizedAnalysis;

import java.util.List;
import java.util.concurrent.TimeoutException;

public interface Lock {
    //永远阻塞,除非获取到了锁,方法可以被中断
    void lock() throws InterruptedException;

    //增加超时功能
    void lock(long mills) throws InterruptedException, TimeoutException;

    //锁的释放
    void unlock();

    //获取当前哪些线程被阻塞
    List getBlockedThreads();
}

自定义显式锁实现类:

package com.hust.zhang.synchronizedAnalysis;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeoutException;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;

public class BooleanLock implements Lock {
    //当前拥有锁的线程
    private Thread currentThread;
    //boolean开关,true代表该锁被某个线程获得,false代表当前锁没有被哪个线程获得或者已经释放
    private boolean locked = false;
    //存储哪些线程在获取当前线程时进入阻塞状态
    private final List blockedList = new ArrayList<>();

    @Override
    public void lock() throws InterruptedException {
        //同步代码块
        synchronized (this) {
            //当前锁被某线程获得,则该线程加入阻塞队列,并使当前线程wait释放对this monitor的所有权
            while (locked) {
                blockedList.add(currentThread());
                this.wait();
            }
            //如果当前线程没有被其他线程获得,则该线程会从阻塞队列中删除自己(如未进入阻塞队列删除也不会有影响)
            blockedList.remove(currentThread());
            //locked开关设为true
            this.locked = true;
            //记录获取锁的线程
            this.currentThread = currentThread();
        }
    }

    @Override
    public void lock(long mills) throws InterruptedException, TimeoutException {
        //同步代码块
        synchronized (this) {
            //如果mills不合法,则默认调用lock方法,抛出异常也是一个比较好的做法
            if (mills <= 0) {
                this.lock();
            } else {
                long remainingMills = mills;
                long endMills = currentTimeMillis() + remainingMills;
                while (locked) {
                    //如果remainingMills<=0,则表示当前线程被其他线程唤醒或者在指定的wait时间到之后还没有获得锁
                    if (remainingMills <= 0) throw new TimeoutException("can not get the lock during " + mills);
                    if (!blockedList.contains(currentThread)) blockedList.add(currentThread());
                    //等待remainingMills的毫秒数,该值最开始由其他线程传入,但多次wait过程中会重新计算
                    this.wait(remainingMills);
                    //重新计算remainingMills
                    remainingMills = endMills - currentTimeMillis();
                }
                //获得该锁,并且从block队列中删除当前线程,将locked的状态设置为true,并且指定获得锁的线程就是当前线程
                blockedList.remove(currentThread());
                this.locked = true;
                this.currentThread = currentThread();
            }
        }
    }

    @Override
    public void unlock() {
        synchronized (this) {
            //判断当前线程是否为获取锁的那个线程,只有加了锁的线程才有资格进行解锁
            if (currentThread == currentThread()) {
                this.locked = false;
                //Optional类是一个可以为null的容器对象。ifPresent方法可以接受接口段或lambda表达式
                Optional.of(currentThread().getName() + "release the lock.").ifPresent(System.out::println);
                //通知其他在wait set中的线程,大家可以尝试抢锁了
                this.notifyAll();
            }
        }
    }

    @Override
    public List getBlockedThreads() {
        //重构收发Encapsulate Collection(封装集群)将参数中的List返回一个不可修改的List
        return Collections.unmodifiableList(blockedList);
    }
}

测试类:

package com.hust.zhang.synchronizedAnalysis;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static java.lang.Thread.currentThread;
import static java.util.concurrent.ThreadLocalRandom.current;

public class BooleanLockTest {
    private final Lock lock = new BooleanLock();

    public void synMethod() throws InterruptedException {
        lock.lock();
        try {
            int randomInt = current().nextInt(10);
            System.out.println(currentThread() + "get the lock.");
            TimeUnit.SECONDS.sleep(randomInt);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        BooleanLockTest test = new BooleanLockTest();
        IntStream.range(0, 10)
                .mapToObj(i -> new Thread(() -> {
                    try {
                        test.synMethod();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }))
                .forEach(Thread::start);
    }
}

ThreadGroup

默认情况下,新的线程都会被加入到main线程的group中。

        ThreadGroup currentGroup = Thread.currentThread().getThreadGroup();
        ThreadGroup group1 = new ThreadGroup("Group1");
        System.out.println(group1.getParent() == currentGroup);         //true

        ThreadGroup group2 = new ThreadGroup(group1, "Group2");   //true
        System.out.println(group2.getParent() == group1);

ThreadGroup中的enumerate方法会将ThreadGroup中的active线程全部复制到Thread数组中。

package com.hust.zhang.threadGroup;

import java.util.concurrent.TimeUnit;

public class ThreadGroupEnumerateThreads {
    public static void main(String[] args) throws InterruptedException {
        ThreadGroup myGroup = new ThreadGroup("MyGroup");
        Thread thread = new Thread(myGroup, () -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "MyThread");
        thread.start();

        TimeUnit.MICROSECONDS.sleep(2);
        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();

        Thread[] list = new Thread[mainGroup.activeCount()];
        /**
         * enumerate方法获取的线程仅仅是预估值,并不能百分之百的保证当前group的活跃线程数,
         * 比如在调用复制之后,某个线程结束了生命周期或者新的线程加入进来,都会导致数据的不准确。
         */
        int recurseSize = mainGroup.enumerate(list);
        System.out.println("主线程组活跃线程数 = " + recurseSize);   //3

        //递归recurse设置为false,myGroup中的线程不会包含在内
        recurseSize = mainGroup.enumerate(list, false);    //2
        System.out.println(recurseSize);
    }
}

enumerate也可以复制ThreadGroup线程组,如下

package com.hust.zhang.threadGroup;

import java.util.concurrent.TimeUnit;

public class ThreadGroupEnumerateThreadGroup {
    public static void main(String[] args) throws InterruptedException {
        ThreadGroup myGroup1 = new ThreadGroup("MyGroup1");
        ThreadGroup myGroup2 = new ThreadGroup(myGroup1, "MyGroup2");

        TimeUnit.MICROSECONDS.sleep(2);
        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();

        ThreadGroup[] list = new ThreadGroup[mainGroup.activeGroupCount()];

        int recurseSize = mainGroup.enumerate(list);
        System.out.println("主线程组活跃子线程组数 = " + recurseSize);     //2

        recurseSize = mainGroup.enumerate(list, false);
        System.out.println(recurseSize);                                //1

    }
}

注意事项:

  1. 后加入到该线程组ThreadGroup的线程Thread的最大优先级不会高于线程组ThreadGroup的最大优先级。
  2. ThreadGroup的interrupt方法会使该group的所有active线程都被interrupt。
  3. ThreadGroup的destroy方法只是针对一个没有任何active线程的group进行一次destroy标记。
  4. ThreadGroup也可以设置为守护ThreadGroup,设置ThreadGroup为daemon也不会影响线程的daemon属性。设置了daemon属性的线程组在没有任何active线程的时候会自动destroy。
package com.hust.zhang.threadGroup;

import java.util.concurrent.TimeUnit;

public class ThreadGroupBasic {
    public static void main(String[] args) throws InterruptedException {
        ThreadGroup group = new ThreadGroup("group1");
        Thread thread = new Thread(group, () -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "thread");
        thread.setDaemon(true);
        thread.start();

        TimeUnit.MICROSECONDS.sleep(1);

        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
        //活跃线程 = 3: 主线程 + 用户thread + monitor线程
        System.out.println("activeCount = " + mainGroup.activeCount());
        //活跃线程组 = 1: group
        System.out.println("activeGroupCount = " + mainGroup.activeGroupCount());
        //最大优先级:10,线程的最大优先级不能高于所在线程组的最大优先级
        System.out.println("getMaxPriority = " + mainGroup.getMaxPriority());
        //名称:main
        System.out.println("getName = " + mainGroup.getName());
        //java.lang.ThreadGroup[name=system,maxpri=10]
        System.out.println("getParent = " + mainGroup.getParent());
        //list方法会把主线程组中的所有的活跃线程信息全部输出到控制台,也就是System.out
        mainGroup.list();
        System.out.println("------------------------");
        //判断当前group是不是给定group的父group。给定的group是自己本身也为true。
        System.out.println("parentOf = " + mainGroup.parentOf(group));      //true
        System.out.println("parentOf = " + mainGroup.parentOf(mainGroup));  //true
    }
}

Hook线程以及捕获线程执行异常

Hook线程也被成为钩子。Thread类中,处理运行时异常的API总共四个:

  • setUncaughtExceptionHandler方法:为某个特定线程指定UncaughtExceptionHandler。
  • setDefaultUncaughtExceptionHandler方法:设置全局的UncaughtExceptionHandler。
  • getUncaughtExceptionHandler方法:获取特定线程的UncaughtExceptionHandler。
  • getDefaultUncaughtExceptionHandler方法:获取全局的UncaughtExceptionHandler。

UncaughtExceptionHandler是一个FuncationalInterface,只有一个抽象方法,该回调接口会被Thread中的dispatchUncaughtException方法调用。

下面就是一个UncaughtExceptionHandler的栗子,设置的回调接口将获得该异常信息并打印出来

package com.hust.zhang.hook;

import java.util.concurrent.TimeUnit;

public class CaptureThreadException {
    public static void main(String[] args) {
        
        Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
            System.out.println(t.getName() + "occur exception");
            e.printStackTrace();
        });

        final Thread thread = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //这里出现unchecked异常
            System.out.println(1 / 0);
        }, "Test-thread");
        thread.start();
    }
}

Hook线程实战:在开发中为了防止某个程序被重复启动,在进程启动的时候创建一个lock文件,进程收到中断信息的时候会删除这个lock文件。在mysql服务器、zookeeper、kafka等系统中都能看到lock文件的存在。下面模拟一个防止重复启动的程序。

package com.hust.zhang.hook;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class PreventDuplicated {
    private final static String LOCK_PATH = "/Users/kaizhang/workspace/hust-zhang/locks";
    private final static String LOCK_FILE = ".lock";
    private final static String PERMISSIONS = "rw-------";

    public static void main(String[] args) throws IOException {
        //注入hook线程,在程序退出时删除lock文件
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("The program received kill SIGNAL.");
            getLockFile().toFile().delete();
        }));

        //检查是否存在.lock文件
        checkRunning();

        //模拟当前程序运行
        for (; ; ) {
            try {
                TimeUnit.MICROSECONDS.sleep(1);
                System.out.println("program is running.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void checkRunning() throws IOException {
        Path path = getLockFile();
        if (path.toFile().exists()) throw new RuntimeException("The program already running.");

        Set perms = PosixFilePermissions.fromString(PERMISSIONS);
        Files.createFile(path, PosixFilePermissions.asFileAttribute(perms));
    }

    private static Path getLockFile() {
        return Paths.get(LOCK_PATH, LOCK_FILE);
    }
}

启动后,会在目录路径下生成一个.lock文件(checkRunning方法),

《Java高并发编程详解:多线程与架构设计》笔记(一)_第6张图片

kill pid或者kill -9 pid命令后,JVM收到中断信息,并且启动hook线程删除.lock文件,这个大家可以下去自己实操一下。需要注意的是下面几点:

  1. Hook线程只有在收到退出信号的时候会被执行,如果在kill的时候使用了参数-9,那么Hook线程不会得到执行,进程将会立即退出,因此.lock文件将得不到清理。
  2. Hook线程中也可以执行一些资源释放的工作,比如关闭文件句柄、socket链接、数据库connection等。
  3. 尽量不要在Hook线程中执行一些耗时非常长的操作,因为其会导致程序迟迟不能退出。

线程池原理以及自定义线程池

说到线程池之前的一篇文章也写到阿里禁止直接使用JUC(JDK的Java Utilities Concurrent)中的ExecutorService创建线程池。线程池用来异步执行线程任务,主要原理图如下,

然后本章主要是结合前面基础写了一个比较简单的ThreadPool,告诉我们是采用什么样的思路去开发线程池,线程池也存在着很多问题,这里就不贴出来了,直接可以看ThreadPoolExecutor类实现线程池的源码,下面放一个图,可以看到ThreadPoolExecutor是考虑到了线程安全问题的,Worker内部类继承自AQS实现,几个线程池参数都使用了volatile关键字修饰确保线程可见性和禁止指令重排序。

《Java高并发编程详解:多线程与架构设计》笔记(一)_第7张图片

 

 

总结

本书前面八章内容会让大家对多线程有基本的认识,这是后面内容的基础。小伙伴们加油!

你可能感兴趣的:(笔记)