java笔记.
JVM
类加载过程
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
指令重排
JVM在创建实例的时候, 是分为如下步骤创建的:
- 在堆内存中, 为新的实例开辟空间
- 初始化构造器, 对实例中的成员进行初始化
- 把这个实例的引用指向1中空间的起始地址
在java中创建一个对象的过程并不是原子性的操作
JVM会分析代码的依赖关系,在不改变程序语义的前提下,调整指令的顺序,以提高代码的执行速度。例如,编译器可能会将一些独立的计算指令提前执行,以减少后续指令的等待时间。 如果重排后变成了132就会导致一些难以捕捉的问题。
如:线程 A 和线程 B 同时调用 getInstance() 方法
- 线程 A 进入 getInstance() 方法,通过了外层的 if (instance == null) 检查,获取到锁并进入同步块。
- 线程 A 执行 instance = new DoubleCheckedLockingSingletonWithoutVolatile(); 时,由于指令重排,先将 instance 引用指向了分配的内存地址,但对象还未完成初始化。
- 此时线程 B 进入 getInstance() 方法,进行外层的 if (instance == null) 检查,发现 instance 不为 null(因为线程 A 已经将引用指向了内存地址),于是直接返回 instance。
- 此时 instance 所指向的对象还未完成初始化,线程 B 在使用这个未完全初始化的对象时,就可能会出现各种不可预期的错误,比如空指针异常或者对象状态不一致等问题。
设计模式.
单例模式
- 饿汉模式,在JVM类加载的时候完成类对象的创建,JVM层面线程安全,但是会造成空间浪费
public class HungrySingleton {
// 实例对象
private static HungrySingleton instance = new HungrySingleton();
// 私有构造方法
private HungrySingleton() {}
/**
* 获取单例对象, 直接返回已创建的实例
* @return instance 本类的实例
*/
public static HungrySingleton getInstance() {
return instance;
}
}
- 懒汉模式,用到时再初始化,但是由于是synchronized锁定,会有性能问题
public class LazySingleton {
// 实例对象
private static LazySingleton instance;
// 私有构造方法
private LazySingleton() {}
/**
* 如果没有就创建有就直接返回
* @return instance 本类的实例
*/
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
- 双检锁,懒汉模式
public class DoubleCheckedLockingSingleton {
// 实例对象,volatile防止指令重排出现一些实例化问题
private static volatile DoubleCheckedLockingSingleton instance;
// 私有构造方法
private DoubleCheckedLockingSingleton() {}
/**
* 双检锁单例对象, 直接返回已创建的实例
* @return instance 本类的实例
*/
public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
}
- 静态内部类,懒汉模式, JVM在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类(SingletonHolder)的属性/方法被调用时才会被加载, 并初始化其静态属性(instance)
public class StaticInnerSingleton {
// 私有构造方法
private StaticInnerSingleton() {}
/**
* 通过静态内部类获取单例对象,没有加锁,线程安全,并发性能高
* @return SingletonHolder.instance 内部类的实例
*/
public static StaticInnerSingleton getInstance() {
return SingletonHolder.instance;
}
// 静态内部类创建单例对象
private static class SingletonHolder {
private static StaticInnerSingleton instance = new StaticInnerSingleton();
}
}
线程.
java编写的程序都是在jvm中运行,当使用java命令启动一个java应用程序后就会启动一个jvm进程。 在同一个jvm进程中,有且只有一个进程,就是它自己。在这个jvm环境中,所有程序代码的运行都是以线程来运行。 多线程可以理解为多任务,一个进程中的多个线程是共享内存块的,当有新的线程产生的时候, 操作系统不分配新的内存,而是让新线程共享原有的进程块的内存。 因此,线程间的通信很容易,速度也很快。 不同的进程因为处于不同的内存块,因此进程之间的通信相对困难。 进程是指一个内存中运行的应用程序, 每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。 在Windows系统中,一个运行的exe就是一个进程。 线程是指进程中的一个执行流程,一个进程可以运行多个线程。 比如java.exe进程可以运行很多线程。 线程总是输入某个进程,进程中的多个线程共享进程的内存。
java中创建线程的方法.
- 继承Thread类创建线程类
public class TestMain extends Thread{
public void run() {
System.out.println(getName());
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new TestMain().start();
}
}
}
提示
Java不支持多继承,继承了thread类限制了拓展,不符合面向接口编程原则。
- 通过Runnable接口创建线程类
public class TestMain implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
TestMain testMain = new TestMain();
new Thread(testMain).start();
}
}
}
提示
适合在需要实现某种任务逻辑而不想与线程的具体实现耦合时使用。
- 通过Callable和Future创建线程
public class TestMain implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName());
return "";
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
TestMain testMain = new TestMain();
FutureTask<String> futureTask = new FutureTask<>(testMain);
new Thread(futureTask).start();
}
}
}
提示
适合需要返回结果或处理异常的复杂任务,特别是在并发计算中使用。
- 使用线程池创建
public class TestMain implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName());
return "";
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 2; i++) {
executorService.submit(new TestMain());
}
}
}
提示
适合在需要处理大量并发任务时使用线程池,如服务器端任务、批处理等。
java线程池.
线程池是一种基于池化思想,用于管理和复用线程资源的机制,它的作用是执行大量的并发任务,同时优化系统的性能和资源利用。 在没有线程池的情况下如果需要执行一个任务,每次都会创建一个新线程,当任务完成后线程被销毁。 这种频繁的创建和销毁线程的操作会消耗大量的资源,尤其在高并发的情况下,系统性能会受到影响, 线程池通过复用已经创建的线程来执行多个任务,避免了频繁的线程创建和销毁,节省了资源。
任务提交到线程池的步骤
- 任务提交:
- 调用 execute(Runnable) 或 submit(Callable) 方法将任务提交到线程池。
- 检查核心线程池:
- 如果当前线程数小于 corePoolSize,线程池会创建一个新的线程来执行任务。
- 如果线程数已经达到 corePoolSize,任务将被放入任务队列中等待执行。
- 任务队列处理:
- 如果任务队列未满,任务会被添加到队列中,等待空闲的线程来执行。
- 如果任务队列已满,且线程池中的线程数量小于 maximumPoolSize,线程池会创建一个新的线程来执行任务。
- 拒绝策略:
- 如果线程池中的线程数量达到 maximumPoolSize,且任务队列也已满,新提交的任务将根据配置的拒绝策略来处理。
- 任务执行:
- 线程池中的线程从队列中获取任务并执行。任务执行完毕后,线程会继续从队列中获取下一个任务。
- 如果一个线程在 keepAliveTime 内没有新的任务可执行,且当前线程数量超过 corePoolSize,该线程会被销毁以释放资源。
- 线程池关闭:
- 调用 shutdown() 方法后,线程池不再接受新任务,但会继续执行队列中的任务。
- 调用 shutdownNow() 方法后,线程池尝试停止所有正在执行的任务,并返回未执行的任务列表。
线程池的核心参数
- 核心线程数(corePoolSize)
- 最大线程数(maximumPoolSize)
- 任务队列(workQueue)
- 线程存活时间(keepAliveTime)
- 时间单位(unit)
- 线程工厂(threadFactory)
- 拒绝策略(handler)
危险
实际开发中应该尽量避免或禁止使用Executors去创建线程。
AQS
信息
AQS是一个用于实现各种锁和同步器的基础组件,提供了一个FIFO队列来管理获取锁的线程。它通过一个整数表示同步状态,并提供了获取和释放资源的方法, 当线程尝试获取锁而失败时,AQS会将其加入到一个等待队列中,并阻塞该线程。当锁被释放时,AQS会唤醒队列中的一个或多个线程。
AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态;还有一个变量记录当前加锁的是哪个线程
以ReentrantLock为例,线程a在调用lock()方法的时候,先判断当前state是否为0,如果是就代表未加锁,然后使用cas操作将state+1,一旦线程a加锁成功了之后,就设置当前加锁线程是自己, 后续如果当前线程再次获取锁就再+1,当当前线程执行完成后释放锁,state-1,直到state=0就将当前加锁线程设置为null
如果线程b尝试获取锁的时候发现已经被获取,就会将自己放入一个FIFO的等待队列,等到线程a释放锁后,就会唤醒队列头的线程,重新尝试加锁
公平锁和非公平锁
- 公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。
公平锁的执行流程: 获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态, 再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。 公平锁的优点是按序平均分配锁资源,不会出现线程饿死的情况,它的缺点是按序唤醒线程的开销大,执行性能不高。
- 非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。
非公平锁执行流程: 当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。 这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。 非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,资源分配随机性强,可能会出现线程饿死的情况。
提示
锁的默认实现都是非公平锁,原因是非公平锁的效率更高
队列模式
- 独占模式 在独占模式下,同一时刻只允许一个线程获取同步状态,其他线程获取失败后会被加入到等待队列中阻塞,直到持有同步状态的线程释放该状态。这就像一把独占的锁,同一时间只能有一个线程持有并使用。如ReentrantLock
入队:当线程尝试获取同步状态失败时,会被封装成一个节点(Node)加入到 AQS 的等待队列尾部 出队:当持有同步状态的线程释放状态后,会唤醒队列中等待的线程,通常是队列头部的下一个节点对应的线程,该线程会尝试重新获取同步状态。
- 共享模式 共享模式允许多个线程同时获取同步状态。当一个线程获取同步状态后,其他线程也可以尝试获取,只要同步状态允许(如剩余的许可数量足够),这些线程可以同时持有同步状态。如Semaphore或CountDownLatch等同步器
入队:与独占模式类似,当线程尝试获取同步状态失败时,会被封装成节点加入到等待队列尾部。 出队:当持有同步状态的线程释放状态后,会唤醒队列中等待的线程,并且可能会有多个线程同时被唤醒并尝试获取同步状态,只要同步状态足够,这些线程都可以成功获取。
线程安全策略
- 使用try-finally块来确保锁在获取后总是被释放,即使在异常情况下。
- 在使用多个锁时,要确保所有线程按照相同的顺序获取锁,避免出现死锁的情况。例如,如果线程 A 先获取锁 L1 再获取锁 L2,那么线程 B 也应该按照相同的顺序获取锁。
- 使用 tryLock(long timeout, TimeUnit unit) 方法尝试获取锁,并设置一个超时时间。如果在规定时间内无法获取锁,可以放弃锁的获取,避免线程长时间阻塞。
- 注意锁的粒度,避免锁范围过大获过小:锁的粒度是指锁所保护的代码范围。如果锁的范围过大,会导致并发性能下降,因为其他线程需要等待更长的时间才能获取锁。如果锁的范围过小,可能无法保证线程安全。需要确保所有对共享资源的访问都在锁的保护范围内。