Skip to content

java笔记.

JVM

类加载过程

加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

指令重排

JVM在创建实例的时候, 是分为如下步骤创建的:

  1. 在堆内存中, 为新的实例开辟空间
  2. 初始化构造器, 对实例中的成员进行初始化
  3. 把这个实例的引用指向1中空间的起始地址

在java中创建一个对象的过程并不是原子性的操作

JVM会分析代码的依赖关系,在不改变程序语义的前提下,调整指令的顺序,以提高代码的执行速度。例如,编译器可能会将一些独立的计算指令提前执行,以减少后续指令的等待时间。 如果重排后变成了132就会导致一些难以捕捉的问题。

如:线程 A 和线程 B 同时调用 getInstance() 方法

  1. 线程 A 进入 getInstance() 方法,通过了外层的 if (instance == null) 检查,获取到锁并进入同步块。
  2. 线程 A 执行 instance = new DoubleCheckedLockingSingletonWithoutVolatile(); 时,由于指令重排,先将 instance 引用指向了分配的内存地址,但对象还未完成初始化。
  3. 此时线程 B 进入 getInstance() 方法,进行外层的 if (instance == null) 检查,发现 instance 不为 null(因为线程 A 已经将引用指向了内存地址),于是直接返回 instance。
  4. 此时 instance 所指向的对象还未完成初始化,线程 B 在使用这个未完全初始化的对象时,就可能会出现各种不可预期的错误,比如空指针异常或者对象状态不一致等问题。

设计模式.

单例模式

  1. 饿汉模式,在JVM类加载的时候完成类对象的创建,JVM层面线程安全,但是会造成空间浪费
java
public class HungrySingleton {
    // 实例对象
    private static HungrySingleton instance = new HungrySingleton();

    // 私有构造方法
    private HungrySingleton() {}

    /**
     * 获取单例对象, 直接返回已创建的实例
     * @return instance 本类的实例
     */
    public static HungrySingleton getInstance() {
        return instance;
    }
}
  1. 懒汉模式,用到时再初始化,但是由于是synchronized锁定,会有性能问题
java
public class LazySingleton {

    // 实例对象
    private static LazySingleton instance;

    // 私有构造方法
    private LazySingleton() {}

    /**
     * 如果没有就创建有就直接返回
     * @return instance 本类的实例
     */
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
  1. 双检锁,懒汉模式
java
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;
    }
}
  1. 静态内部类,懒汉模式, JVM在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类(SingletonHolder)的属性/方法被调用时才会被加载, 并初始化其静态属性(instance)
java
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进程可以运行很多线程。 线程总是输入某个进程,进程中的多个线程共享进程的内存。

thread-01

java中创建线程的方法.

  1. 继承Thread类创建线程类
java
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类限制了拓展,不符合面向接口编程原则。

  1. 通过Runnable接口创建线程类
java
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();
        }
    }
}

提示

适合在需要实现某种任务逻辑而不想与线程的具体实现耦合时使用。

  1. 通过Callable和Future创建线程
java
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();
        }
    }
}

提示

适合需要返回结果或处理异常的复杂任务,特别是在并发计算中使用。

  1. 使用线程池创建
java
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线程池.

线程池是一种基于池化思想,用于管理和复用线程资源的机制,它的作用是执行大量的并发任务,同时优化系统的性能和资源利用。 在没有线程池的情况下如果需要执行一个任务,每次都会创建一个新线程,当任务完成后线程被销毁。 这种频繁的创建和销毁线程的操作会消耗大量的资源,尤其在高并发的情况下,系统性能会受到影响, 线程池通过复用已经创建的线程来执行多个任务,避免了频繁的线程创建和销毁,节省了资源。

任务提交到线程池的步骤

thread-02

  1. 任务提交:
    • 调用 execute(Runnable) 或 submit(Callable) 方法将任务提交到线程池。
  2. 检查核心线程池:
    • 如果当前线程数小于 corePoolSize,线程池会创建一个新的线程来执行任务。
    • 如果线程数已经达到 corePoolSize,任务将被放入任务队列中等待执行。
  3. 任务队列处理:
    • 如果任务队列未满,任务会被添加到队列中,等待空闲的线程来执行。
    • 如果任务队列已满,且线程池中的线程数量小于 maximumPoolSize,线程池会创建一个新的线程来执行任务。
  4. 拒绝策略:
    • 如果线程池中的线程数量达到 maximumPoolSize,且任务队列也已满,新提交的任务将根据配置的拒绝策略来处理。
  5. 任务执行:
    • 线程池中的线程从队列中获取任务并执行。任务执行完毕后,线程会继续从队列中获取下一个任务。
    • 如果一个线程在 keepAliveTime 内没有新的任务可执行,且当前线程数量超过 corePoolSize,该线程会被销毁以释放资源。
  6. 线程池关闭:
    • 调用 shutdown() 方法后,线程池不再接受新任务,但会继续执行队列中的任务。
    • 调用 shutdownNow() 方法后,线程池尝试停止所有正在执行的任务,并返回未执行的任务列表。

线程池的核心参数

  1. 核心线程数(corePoolSize)
  2. 最大线程数(maximumPoolSize)
  3. 任务队列(workQueue)
  4. 线程存活时间(keepAliveTime)
  5. 时间单位(unit)
  6. 线程工厂(threadFactory)
  7. 拒绝策略(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 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。 这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。 非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,资源分配随机性强,可能会出现线程饿死的情况。

提示

锁的默认实现都是非公平锁,原因是非公平锁的效率更高

队列模式

  1. 独占模式 在独占模式下,同一时刻只允许一个线程获取同步状态,其他线程获取失败后会被加入到等待队列中阻塞,直到持有同步状态的线程释放该状态。这就像一把独占的锁,同一时间只能有一个线程持有并使用。如ReentrantLock

入队:当线程尝试获取同步状态失败时,会被封装成一个节点(Node)加入到 AQS 的等待队列尾部 出队:当持有同步状态的线程释放状态后,会唤醒队列中等待的线程,通常是队列头部的下一个节点对应的线程,该线程会尝试重新获取同步状态。

  1. 共享模式 共享模式允许多个线程同时获取同步状态。当一个线程获取同步状态后,其他线程也可以尝试获取,只要同步状态允许(如剩余的许可数量足够),这些线程可以同时持有同步状态。如Semaphore或CountDownLatch等同步器

入队:与独占模式类似,当线程尝试获取同步状态失败时,会被封装成节点加入到等待队列尾部。 出队:当持有同步状态的线程释放状态后,会唤醒队列中等待的线程,并且可能会有多个线程同时被唤醒并尝试获取同步状态,只要同步状态足够,这些线程都可以成功获取。

线程安全策略

  1. 使用try-finally块来确保锁在获取后总是被释放,即使在异常情况下。
  2. 在使用多个锁时,要确保所有线程按照相同的顺序获取锁,避免出现死锁的情况。例如,如果线程 A 先获取锁 L1 再获取锁 L2,那么线程 B 也应该按照相同的顺序获取锁。
  3. 使用 tryLock(long timeout, TimeUnit unit) 方法尝试获取锁,并设置一个超时时间。如果在规定时间内无法获取锁,可以放弃锁的获取,避免线程长时间阻塞。
  4. 注意锁的粒度,避免锁范围过大获过小:锁的粒度是指锁所保护的代码范围。如果锁的范围过大,会导致并发性能下降,因为其他线程需要等待更长的时间才能获取锁。如果锁的范围过小,可能无法保证线程安全。需要确保所有对共享资源的访问都在锁的保护范围内。