Java高并发编程详解 读书笔记

第一章:快速认识线程

线程的生命周期

线程的生命周期大体可以分为5个主要的阶段:

  • NEW

  • RUNNABLE

  • RUNNING

  • BLOCKED

  • TERMINATED

Runnable 接口

重写Thread类的run方法和实现Runnable接口的run方法的区别:

Thread类的run方法是不能够共享的,也就是说A线程不能把B线程的run方法当作自己的执行单元,而使用Runnable接口则很容易实现这一点。

第二章:深入理解Thread构造函数

命名线程

好的编程实践建议:构造一个新的线程时,最好赋予线程一个名字,否在线程在jvm内部以自增的形式给线程命名为Thread-#,对线程命名用到的构造函数为:

Thread(Runnable target, String name)

Thread被命名之后,还有一次修改的机会,一旦启动则不能被修改。这个修改的时机发生在NEW这个阶段,此时threadStatus这个内部属性为0,通过判断这个属性的状态,来限定修改时机。

线程父子关系

所有线程都是创建该线程的子线程,公共父线程为main函数所在的线程;可以显示的创建线程组ThreadGroup,否则默认加入到main线程所在的组;在默认设置中,子线程会和父线程同属于一个Group,拥有同样的优先级和daemon;

Thread与JVM虚拟机栈

线程的创建数量是随着虚拟机栈内存的增多而减少的,是一种反比关系;粗略的可以认为一个Java进程的内存大小为:堆内存+线程数量*栈内存

较为精准的线程数量公式:

线程数量=(最大地址空间)(MaxProcessMemory)-JVM堆内存-ReservedOsMemory/ThreadStackSize(XSS)

守护进程

守护线程经常用作与执行一些后台任务,在正常情况下,若JVM中没有一个非守护线程,则JVM的进程会退出;相关的API:

void setDaemon(boolean);
boolean isDaemon();

第三章:Thread API的详细介绍

API 功能 使用
sleep 使当前线程进入休眠,暂停执行 Thread.sleep()
TimeUnit.sleep() //推荐使用
yield 使当前线程从RUNNING状态切换到RUNNABLE状态,当CPU资源不紧张时,会被忽略;
set/getPriority 设置/获取当前线程的优先级 \(1 \le 优先级 \le0\)
getId 获取当前线程的Id
currentThread 获取当前线程对象
set/getContextClassLoader 设置线程上下文类加载器

线程interrupt

  • interrupt():是thread对象的一个成员方法,如果当前线程进入阻塞状态,而调用当前线程的interrupt方法,就可以打断阻塞。一旦线程在阻塞的情况下被打断,都会抛出一个称为InterruptedException的异常,这个异常就像一个signal一样通知当前线程被打断了。

    原理:一个线程内部存在着名为interrupt flag的标识,如果一个线程被interrupt,那么它的flag将被设置;如果一个线程已经是死亡状态,那么尝试对其的interrupt会直接被忽略;

  • isInterrupted():是thread的一个成员方法,只用来判断当前线程是否被中断,并不会影响标识发生任何变化;

  • interrupted():是Thread的一个静态方法,除了用来判断当前线程是否被打断,还会直接擦除线程的interrupt标识,第二次调用后永远都是false;

线程join

join某个线程A,会使得当前线程B进入等待,直到线程A结束生命周期,或者达到给定的时间,那么在此期间B线程是处于BLOCKED的,而不是A线程。join方法会使当前的线程永远的等待下去,直到期间被另外的线程中断,或者join的线程执行结束。

理解:在需要执行多线程任务时,通过将这些任务join起来,来阻塞发起任务的主线程,等待这些多进程任务全部执行完毕后,再让主线程来收集多线程执行的所有结果。

第四章:线程安全与数据同步

线程的执行与CPU时间片的调度有关,不同线程对同一资源的读写因时间片的分配和调度不同可能会产生不同的结果,出现不一致问题。

synchronized关键字

  • 提供了一种锁的机制,确保共享变量的互斥访问
  • 使用monitor enter/exit两个JVM指令,保证变量与主内存中的值同步,确保不同线程获取一致的结果,且enter必须发生在exit之前
  • 只能用于代码块或方法,不能时class以及变量进行修饰;在编程实践中,同步代码块获得的锁一般为this(代码块在成员方法)或###.class(代码块在静态方法)
  • synchronized的作用域不应太大,尽可能地只作用于共享资源的读写部分
  • 使用synchronized关键字同步类的不同实例方法,争抢的是同一个monitor的lock

程序死锁的原因以及如何诊断

死锁的原因:

  1. 交叉锁:线程A持有R1的锁等待R2的锁,线程B持有R2的锁等待R1的锁
  2. 内存不足:资源不足而互相等待对方先释放
  3. 一问一答式的数据交换:请求确认死锁
  4. 数据库锁
  5. 死循环引起的死锁

第五章:线程间通信

服务端有若干个线程会从队列中获取对应的Event进行异步处理,那么这些线程如何从队列中获取数据呢?一般来说,服务端获知队列中是否有数据可以采取:

  • 轮询
  • 通知机制:如果队列中有Event,则通知工作的线程开始工作;没有Event,则工作线程休息并等待通知;

wait和notify方法

wait和notify不是Thread特有的方法,而是Object中的方法;

  • wait方法会导致当前线程进入阻塞,直到其他线程调用了Object的notify或者notifyAll方法才能将其唤醒,或者阻塞时间达到了timeout时间自动唤醒;底层原理是当前对象放弃对该monitor的所有权,并进入与该对象关联的wait set中,也就是说执行wait方法时必须拥有该对象的monitor,即wait方法必须在同步方法中使用。

    注意:wait方法是可中断方法,其他线程可以使用interrupt 方法将其打断并唤醒;与wait类似的sleep方法,也能使线程进入阻塞状态,但是不会释放monitor的锁。

  • notify方法可以唤醒单个正在执行该对象wait方法的线程;如果没有被阻塞,则忽略;被唤醒的线程需要重新获取该对象所关联的monitor lock才能继续执行;必须在同步方法中使用。

同步代码的monitor必须与执行wait notify方法的对象一致;

单线程间通信

synchronized(lock_obj){
    if(not satisfy){
        lock_obj.wait(); //阻塞
    }
    do_something;
    lock_obj.notify(); //唤醒wait_set中的某一个线程
}

多线程间通信

synchronized(lock_obj){
    if(not satisfy){
        lock_obj.wait(); //阻塞
    }
    do_something;
    lock_obj.notifyAll(); //唤醒wait_set中的全部线程
}

notifyAll方法与notify方法类似,都可以唤醒由于调用了wait方法而阻塞的线程,但是notify只能唤醒其中的一个线程,而notifyAll方法则可以同时唤醒全部的阻塞线程,同样被阻塞的线程仍要继续争抢monitor的锁。

第六章:ThreadGroup详细讲解

第七章:Hooc线程以及捕获线程执行异常

获取线程运行时异常

  • API

    public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh); //为某个线程指定eh
    public static void setUncaughtExceptionHandler(UncaughtExceptionHandler eh); //设置全局默认eh
    public UncaughtExceptionHandler getUncaughtExceptionHandler(); 
    public static UncaughtExceptionHandler getUncaughtExceptionHandler(); 
    
  • UncaughtExceptionHandler介绍

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

    private void dispatchUncaughtException(Throwable e){
    	getUncaughtExceptionHandler().uncaughtException(this, e);
    }
    

    当线程在运行过程中出现异常时,JVM会调用dispatchUncaughtException方法,该方法会将对应的线程实例以及异常信息传递给回调接口。

    若没有为当前线程指定UncaughtExceptionHandler,则遇到异常时,执行流程为:

注入钩子线程

在JVM进程退出的时候,Hook线程会启动执行,通过Runtime可以为JVM注入多个Hook线程,例如:

Runtime.getRuntime().addShutdownHook(new Thread(){
	@Override
	public void run(){
		try{
			System.out.println("this hook is running.");
			TimeUnit.SECONDS.sleep(1);
		}catch(InterruptedException e){
			e.printStackTrace();
		}
		System.out.println("this hook is stopping.");
	}
})

Hook线程只有在收到退出信号时会被执行,如果在kill的时候使用参数-9,那么Hook线程不会得到执行,进程将会立即退出。Hook线程可以用来执行一些释放资源的工作。




posted @ 2021-03-27 21:31  &Yhao  阅读(206)  评论(0编辑  收藏  举报