This Monitor和Class Monitor的详细介绍
synchronized同步类的不同实例方法,争抢的是同一个monitor的lock,而与之关联的引用是ThisMonitor的实例引用
package JavaConcurrencyInPractice.book.charpter3;
import java.util.concurrent.TimeUnit;
/**
* @program: JavaLife
* @author: JiaLe Hu
* @create: 2020-12-09 10:38
**/
public class ThisMonitor {
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "enter to method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method2() {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "enter to method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// public synchronized void method2() {
// System.out.println(Thread.currentThread().getName() + "enter to method2");
// try {
// TimeUnit.MINUTES.sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
public static void main(String[] args) {
ThisMonitor thisMonitor = new ThisMonitor();
new Thread(thisMonitor::method1, "T1").start();
new Thread(thisMonitor::method2, "T2").start();
}
}
synchronized同步某个类的不同静态方法争抢的锁也是同一个monitor的lock,该monitor关联的引用是ClassMonitor.class实例
package JavaConcurrencyInPractice.book.charpter3;
import java.util.concurrent.TimeUnit;
/**
* @program: JavaLife
* @author: JiaLe Hu
* @create: 2020-12-09 10:49
**/
public class ClassMonitor {
public static synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "enter to method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// public static synchronized void method2() {
// System.out.println(Thread.currentThread().getName() + "enter to method1");
// try {
// TimeUnit.MINUTES.sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
public static synchronized void method2() {
synchronized (ClassMonitor.class) {
System.out.println(Thread.currentThread().getName() + "enter to method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
重入
内置锁是可以重入的,如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。重入意味着获取锁的操作的粒度是线程。
Wait And notify
- wait 方法是可中断的,当前线程一旦调用了wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断。
- 线程执行了某个对象的wait方法,会加入与之对应的wait set中,每一个对象的monitor都有一个与之关联的wait set
- 必须在同步方法中使用wait和notify,因为执行wait和notify的前提条件是必须持有同步方法的monitor的所有权
- wait会释放掉monitor的锁
synchronized 的缺陷
- 不能控制阻塞的时间
- 同步方法不能被中断,不能像wait和sleep方法一样,能够捕获得到中断信号
package LeetCode;
import java.util.concurrent.TimeUnit;
/**
* @program: JavaLife
* @author: JiaLe Hu
* @create: 2020-12-10 09:32
**/
public class synchronizedDefect {
public synchronized void syncMethod() {
try {
System.out.println(Thread.currentThread().getName() + " get this monitor");
TimeUnit.HOURS.sleep(1);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " is interrupted");
}
}
public static void main(String[] args) throws InterruptedException {
synchronizedDefect synchronizedDefect = new synchronizedDefect();
Thread t1 = new Thread(synchronizedDefect::syncMethod, "t1");
t1.start();
TimeUnit.MILLISECONDS.sleep(2);
Thread t2 = new Thread(synchronizedDefect::syncMethod, "t2");
t2.start();
TimeUnit.MILLISECONDS.sleep(2);
t2.interrupt();
System.out.println(t2.isInterrupted()); // true
System.out.println(t2.getState()); // BLOCKED
}
}
Hook函数
- 该ThreadGroup如果有父ThreadGroup,则直接调用父Group的uncaughtException方法
- 如果设置了全局默认的UncaughtException,则会调用全局的uncaughtException方法
- 若既没有父ThreadGroup,也没有全局默认的UncaughtException,直接将异常的堆栈信息定向到System.err中
Hook注入(Runtime)
package JavaConcurrencyInPractice.book.charpter4;
import java.util.concurrent.TimeUnit;
/**
* @program: JavaLife
* @author: JiaLe Hu
* @create: 2020-12-11 11:05
**/
public class ThreadHook {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
System.out.println("The hook thread 1 is running ");
TimeUnit.SECONDS.sleep(1);
System.out.println("The hook thread 1 will exit");
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
System.out.println("The hook thread 2 is running ");
TimeUnit.SECONDS.sleep(2);
System.out.println("The hook thread 2 will exit");
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
System.out.println("program is exiting");
}
}
Hook线程应用场景以及注意事项
- Hook线程只有在收到推出信号的时候会被执行,如果在kill的时候使用了参数-9,那么Hook线程不会执行,进程将立即退出,因此lock文件不会被删除
- Hook线程中也可以执行一些资源释放的工作,比如关闭文件句柄、socket链接、数据库connection
- 尽量不要在Hook线程中执行一些耗时非常长的操作。因此会导致程序迟迟不能退出。
类加载的过程
- 使用new关键字会导致类的初始化
- 访问类的静态变量
- 访问类的静态方法,会导致类的初始化
- 对某个类进行反射
- 初始化子类会导致父类的初始化(通过子类的静态变量只会导致父类的初始化)
- 启动类
除了上述的6种情况,其余都被称为被动使用,不会导致类的加载和初始化
类的加载阶段
class文件种的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转换为方法区中的运行时的数据结构,并且在堆内存中生成一个该类的java.lang.Class对象,作为访问方法区数据结构的入口。加载过程常伴随着连接阶段交叉工作
类的连接阶段
- 验证
验证文件格式:
元数据的验证:对class的字节流进行语义分析的过程,整个语义分析就是为了确保class字节流符合JVM规范的要求。
字节码验证:主要是验证程序的控制流程,比如循环,分支
符号引用验证(目的是为了保证解析动作的顺利执行):在类的加载过程中,有些阶段是交叉进行的,比如在加载阶段尚未结束之前,连接阶段就可能已经开始了,这样做可以提高类加载的整体效率 - 准备
当一个class的字节流通过了所有的验证过程之后,就开始为该对象静态变量,分配内存并且设置初始值了,类变量的内存会被分配到方法区中,不同于实例变量会被分配到堆内存中 - 解析
解析就是在常量池中寻找类,接口,字段和方法的符号引用,并且将这些符号引用替换成为直接引用的过程。
类初始化的阶段
包含了所有类变量的赋值动作和静态语句块的执行代码
⚠️静态语句块只能对后面的静态变量进行赋值,但是不能对其访问。父类的静态变量总是能够得到优先赋值。
JVM内置三大加载器
根加载器
根加载器又被称为Bootstrap类加载器,该类加载器是最为顶层的加载器,其没有父加载器,它是由C++编写的,主要负责虚拟机核心类库的加载-Xbootclasspath来指定根加载器的路径
扩展类加载器
扩展类加载器的父加载器是根加载器,主要用于加载JAVA_HOME下jre/lb/ext子目录里面的类库。由纯Java语言实现
系统类加载器
负责加载classpath下的类库资源
类加载器命名空间
同一个class实例在同一个类加载器命名空间之下是唯一的。
运行时包
包的作用是为了组织类,防止不同包下同样名称的class引起冲突,还能起到封装的作用,运行时包是由类加载器的命名空间和类的全限定名称共同组成的。JVM规定了不同的运行时包下的类彼此之间是不可以进行访问的
问题 为什么我们在程序中能够使用new Object这些在java.lang下的包呢?
如果某一个类C被类加载器CL加载,那么CL就被称为C的初始类加载器。JVM为每一个类加载器维护了一个列表,该列表中记录了将该类加载器作为初始类加载器的所有class
父系委托机制,类加载器都会尝试将交付给父类加载器去加载,如果父类加载器不能加载,则传递回来加载。
SPI
SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在
java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
SPI具体约定
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader
问题 java.lang.sql中的所有接口都由JDK提供,加载这些接口的类加载器都是根加载器,第三方厂商的类库驱动都是由系统类加载器加载的,是怎么实现使用的呢?
由于JVM类加载器双亲委托机制的限制,启动类加载器不可能加载得到第三方厂商的具体实现,为了解决这个困境,JDK提供了一种——线程上下文类加载器,有了线程上下文加载器,启动类加载器反需要委托子类加载器去加载厂商提供的SPI具体实现。
// 可以通过currentThread去获取线程上下文类加载器
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
volatile
- 保证了不同线程之间对共享变量操作时对可见性,也就是说当一个线程修改volatile修饰的变量,另外一个线程也立即看到最新的值,会修改主内存中的值
- 直接禁止JVM和处理器对volatile关键字修饰的指令重排序
- 但volatile不能保证操作的原子性,只能保证可见性和有序性