前言:要秋招了,复习一下应对秋招,纠结该先看啥,最后决定先学习《Java高并发编程详解》,此博客为看书所写的笔记,因为是笔记,所以会只记比较重要的东西,不适合初学者。
参考:https://blog.csdn.net/dufufd/article/details/80537638
目录
第一章 ThreadAPI的详细介绍
1.1 sleep和yield
sleep是一个静态方法,有两个重载方法,一个需要传入毫秒数,另一个需要传入毫秒与纳秒数,休眠时不会放弃monitor锁的所有权。
JDK1.5之后TimeUnit代替了Threa.sleep,使用TimeUnit更为方便,如休眠3小时24分
TimeUnit.HOURS.sleep(3)
TimeUnit.MINUTES.sleep(24)
yield属于一种启发式的方法,其会提醒调度器愿意放弃当前的CPU资源,如果CPU资源不紧张,则会忽略这种提醒,调用yield会使线程从RUNNING状态切换到RUNNABLE状态,一般这个方法不太常用。
在JDK1.5之前yield方法实际上是调用了sleep(0),但后面就不是了。
sleep和yield有着本质的区别,sleep会导致线程暂停指定的时间,没有CPU时间片的消耗,会在指定时间释放CPU资源,yield如果CPU调度器没有忽略这个提示,会导致上下文的切换。
1.2 现成的优先级
thread.setPriority()
如果CPU比较忙,设置优先级会使线程获得更多的时间片,不忙的话则几乎无影响,不要再程序设计中使用线程优先级绑定某些特定的业务或者让业务严重依赖于线程优先级。
线程的优先级不能大于1也不能小于10,如果线程的优先级大于线程所在group的优先级,那么线程的优先级将会失效,取而代之的是group的优先级。
线程默认的优先级和所在的线程组优先级一致,main线程的优先级是5。
1.3 获取线程ID与当前线程
getID()可以获取线程ID,线程的ID在整个JVM进程中都会是唯一的,并且是从0开始递增。
Thread.currentThread()可以获取当前线程
1.4 interrupt
使用wait、sleep,join使得线程进入阻塞状态时,另一个线程调用阻塞线程的interrupt方法,可以打断阻塞,一旦线程在阻塞的情况下被打断,都会抛出一个InterruptedException的异常。
interrupt方法到底做了什么事呢?在线程内部有interrupt flag标识:
如果一个线程被interrupt,那么它的flag将被设置。
如果线程正在执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清除。
isInterrupted方法用来判断当前线程是否被中断,该方法仅是堆interrupt标识的一个判断,当线程没有被interrupt时,调用isInterrupted方法显示是false,如果线程没执行可中断方法被阻塞时调用isInterrupted后显示是true,如果线程执行可中断方法被阻塞时调用isInterrupted后显示是false,因为会擦除flag。
interrupted也用于判断当前线程是否被中断,不同于isInterrupted的是,它会直接擦除线程的interrupt标识,需要注意的是,如果线程被打断了,第一次调用interrupted会返回true,并且立即擦除interrupt标识,以后调用永远都会返回false。
如果一个线程设置了interrupt标识,当执行到可中断方法时,可中断方法会立刻中断并抛出异常。
1.5 join
B线程join线程A,会使B线程进入等待,直到A线程结束生命周期,或者到达给定时间。
应用场景:当我们像三个不同的网站请求航班信息,当请求信息结束后,将获得信息进行整理然后返回给用户。
一个一个请求网站太麻烦,如果并发请求可能不知道什么时候能请求玩,这时我们可以使用join,等待三个线程请求完后再执行主线程进行整理。
1.6 关闭一个线程
有个stop方法,但是已被Deprecated了,因为该方法在关闭时可能不会释放掉monitor锁,所以线程关闭有如下3个方法:
1.等待线程自然结束
2.捕获中断信号关闭线程。此方式派生成本较高,当线程循环执行某任务时,比如心跳检查,不断地接收网络消息报文等,系统决定退出,可以借助中断线程的方式使其退出。
通过线程中用while(!isInterrupted){} 或者 try {} catch (InterruptedException e){ }来判断是否退出,然后调用thread.interrupt来退出线程。
3.使用volatile开关控制。由于线程的interrupt标识可能被擦除,所以使用volatile修饰开关的flag关闭线程也是一种常用的方法。
while(!closed&&isInterrupted()){ //正在运行}
public void close(){
this.closed = true;
this.interrupt();
}
1.7 线程假死
进程假死就是进程虽然存在,但没有日志输出,程序不进行任何的作业,看起来像死了一样,但实际上它是没有死的,绝大部分的原因就是某个线程阻塞了,线程出现了死锁的情况。
第二章 线程安全与数据同步
2.1 共享数据不一致的问题
多个线程操作一个共享数据时可能会出现数据不一致的问题(具体为啥自行百度)。
2.2 synchronized
可以采用synchronized关键字解决。
synchronized提供了一种锁的机制,确保共享变量的互斥访问,从而防止数据不一致问题出现。
synchronized关键字包括monitor eneter和monitor exit两个JVM指令,它能保证在任何时候线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存。
synchronized的指令严格遵守java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter。
2.3 synchronized的用法
synchronized可以对代码块或者方法进行修饰,而不能对class以及变量进行修饰。
同步方法:
public synchronized void sync(){
}
public synchronized static void staticSync(){
}
同步代码块:
private final Object MUTEX = new Object()
public void sync()
{
synchronized(MUTEX)
{
}
}
2.4 深入分析synchronized关键字
synchronized关键字提供了一种互斥机制,同一时刻,只能有一个线程访问同步资源,准确的说是某线程获取了与mutex关联的锁。
将加了synchronized的字节码进行反编译,发现加了synchronized的代码先会获取mutex引用,然后执行monitor enter指令,然后执行synchronized里的逻辑后,再执行monitor exit退出。
monitor enter:
每个对象都与monitor相关联,当线程尝试获取与对象关联的monitor时会发生几件事情:
如果monitor计数器为0,说明该monitor的lock还没被获得,某个线程获得后立刻对该计数器加1,从此该线程就是这个monitor的拥有者了;如果一个已拥有monitor的线程重入,则会导致monitor计数器再次累加;如果monitor已被其它线程拥有,其它线程在尝试获取monitor所有权时,会陷入阻塞直到monitor计数器变为0,才再次尝试。
注:关于具体monitor的实现原理,后面章节会进行讲解
monitor exit:
想要释放某个对象关联的monitor的所有权的前提是,曾经获得所有权,释放过程较简单,即将monitor计数器减一,与此同时,被该monitor block的线程将再次尝试获取对monitor的所有权。
2.5 使用synchronized需要注意的问题
1.与monitor关联的对象不能为空
2.synchronized作用域太大
3.不同的monitor企图锁相同的方法,即不同的对象monitor锁一个方法,这样没用,需要一个对象的monitor锁一个方法。
synchronized修饰某个方法时其实是用的this的monitor
synchronized修饰静态方法时用的是类.class的实例引用作为monitor,如:
public class classMonitor{
public static void method1(){
synchronized(classMonitor.class) { //...}
}
public synchronized static void method2(){ //... }
}
//以上两个synchronized需要获取的是同一个对象的monitor锁,这个对象是classMonitor的实例引用
2.5.1 类.class的实例简介
比如为什么classMonitor.class是一个对象的引用,这个对象是什么时候建立的?有什么作用?
参考:
每一个类都有一个Class对象,每当编译一个新类就产生一个Class对象,基本类型 (boolean, byte, char, short, int, long, float, and double)有Class对象,数组有Class对象,就连关键字void也有Class对象(void.class)。Class对象对应着java.lang.Class类,如果说类是对象抽象和集合的话,那么Class类就是对类的抽象和集合。
Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。一个类被加载到内存并供我们使用需要经历如下三个阶段:
-
加载,这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(Class字节码),将这个字节流所代表的静态存储结构转化为方法去的运行时数据接口,根据字节码在java堆中生成一个代表这个类的java.lang.Class对象。
-
链接。在链接阶段将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求,为静态域分配存储空间并设置类变量的初始值(默认的零值),并且如果必需的话,将常量池中的符号引用转化为直接引用。
-
初始化。到了此阶段,才真正开始执行类中定义的java程序代码。用于执行该类的静态初始器和静态初始块,如果该类有父类的话,则优先对其父类进行初始化。
所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。
在类加载阶段,类加载器首先检查这个类的Class对象是否已经被加载。如果尚未加载,默认的类加载器就会根据类的全限定名查找.class文件。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良java代码。一旦某个类的Class对象被载入内存,我们就可以它来创建这个类的所有对象。
答:每一个类都有一个Class对象;Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象;一旦某个类的Class对象被载入内存,我们就可以它来创建这个类的所有对象。
2.6 程序死锁的原因以及如何诊断
可能导致死锁的情况:
1.交叉锁可导致程序出现死锁
A持有R1锁等待R2锁,B持有R2锁等待R1锁
2.内存不足
两个线程T1,T2都需要30MB内存,T1获取了10M,T2获取了10M,但剩下的内存只有10M了。
3.一问一答式的数据交换
服务端开启端口等待客户端请求,客服端发送请求等待服务端返回数据,服务端错过了请求,然后变成了客户端等待服务端返回数据,服务端等待客户端请求数据。
4.数据库锁
如某个线程执行for update语句退出了事务,其它线程访问数据库都将陷入死锁。
5.文件锁
同理,某线程获得文件锁后意外退出,其它读取文件的线程也将进入死锁直到线程释放文件句柄资源。
6.死循环引起的死锁
处理不当进入死循环,查看你线程堆栈不会发现任何死锁的迹象,但是程序不工作,CPU占有率居高不下,一般称之为假死,是一种最致命也是最难排查的死锁现象。
死锁诊断:
交叉锁引起的死锁:可以打开jstack工具或者jconsole工具,Jstack-1PID会直接发现死锁信息。
死循环引起的死锁:也可以使用jstack、jconsole、jvisualvm、jProfiler进行诊断,但是不会有明显的提示,因为线程并未BLOCKED,而是始终处于RUNNABLE状态,CPU使用率居高不下,甚至不能够正常执行命令。使用jprofile可以发现某个方法线程运行状态,如果某普通方法运行时间过长如100ms,可能是死循环。