Java学习笔记

文章目录

Java基础

1、反射

1.1 概述

反射就是把Java的各种成分映射成相应的Java类。

反射是java语言的一个特性,它允程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作。例如它允许一个java的类获取他所有的成员变量和方法并且显示出来。Java 的这一能力在实际应用中也许用得不是很多,但是在其它的程序设计语言中根本就不存在这一特性。例如,Pascal、C 或者 C++ 中就没有办法在程序中获得函数定义相关的信息。

Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的。

2、注解

3、异常

3.1 概述

异常

3.2 异常处理

3.2.1 捕获异常
3.2.2 抛出异常

3.3 NullPointerException

3.4 断言

4、泛型

4.1 概述

泛型就是编写模板代码来适应任意类型;

泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查;

注意泛型的继承关系:可以把ArrayList<Integer>向上转型为List<Integer>T不能变!),但不能把ArrayList<Integer>向上转型为ArrayList<Number>T不能变成父类)。

4.2 泛型使用

使用泛型时,把泛型参数<T>替换为需要的class类型,例如:ArrayList<String>ArrayList<Number>等;

可以省略编译器能自动推断出的类型,例如:List<String> list = new ArrayList<>();

不指定泛型参数类型时,编译器会给出警告,且只能将<T>视为Object类型;

可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。

4.3 泛型编写

静态方法

静态方法不能引用泛型类型<T>,必须定义其他类型(例如<K>)来实现静态泛型方法。例如:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // 对静态方法使用<T>:
    public static Pair<T> create(T first, T last) {
        return new Pair<T>(first, last);
    }
}

上述代码会导致编译错误,因为我们无法在静态方法create()的方法参数和返回类型上使用泛型类型T

对于静态方法,我们可以单独改写为“泛型”方法,只需要使用另一个类型即可。对于上面的create()静态方法,我们应该把它改为另一种泛型类型,例如,<K>

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // 静态泛型方法应该使用其他类型区分:
    public static <K> Pair<K> create(K first, K last) {
        return new Pair<K>(first, last);
    }
}

这样才能清楚地将静态方法的泛型类型和实例类型的泛型类型区分开。

多个泛型类型

泛型还可以定义多种类型。例如,我们希望Pair不总是存储两个类型一样的对象,就可以使用类型<T, K>

public class Pair<T, K> {
    private T first;
    private K last;
    public Pair(T first, K last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() { ... }
    public K getLast() { ... }
}

在使用的时候,指出两种类型即可:

Pair<String, Integer> p = new Pair<>("test", 123);

Java标准库的Map<K, V>就是使用两种泛型类型的例子。它对Key使用一种类型,对Value使用另一种类型。

4.4 擦拭法

泛型是一种类似”模板代码“的技术,不同语言的泛型实现方式不一定相同。Java语言的泛型实现方式是擦拭法(Type Erasure)。所谓擦拭法是指,虚拟机对泛型其实一无所知,所有的工作都是编译器做的。

例如,我们编写了一个泛型类Pair<T>,这是编译器看到的代码:

public class Pair<T> {
    private T first;
    private T last;
    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }
    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
}

而虚拟机根本不知道泛型。这是虚拟机执行的代码:

public class Pair {
    private Object first;
    private Object last;
    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }
    public Object getFirst() {
        return first;
    }
    public Object getLast() {
        return last;
    }
}

因此,Java使用擦拭法实现泛型,导致了:

  1. 编译器把类型<T>视为Object
  2. 编译器根据<T>实现安全的强制转型。

使用泛型的时候,我们编写的代码也是编译器看到的代码:

Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

而虚拟机执行的代码并没有泛型:

Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

所以,Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。

Java泛型的局限:

  1. <T>不能是基本类型,例如int,因为实际类型是ObjectObject类型无法持有基本类型:

    Pair<int> p = new Pair<>(1, 2); // compile error!
    
  2. 无法取得带泛型的Class。观察以下代码:

    public class Main {    public static void main(String[] args) {    	Pair<String> p1 = new Pair<>("Hello", "world");        Pair<Integer> p2 = new Pair<>(123, 456);        Class c1 = p1.getClass();        Class c2 = p2.getClass();        System.out.println(c1==c2); // true        System.out.println(c1==Pair.class); // true    }}class Pair<T> {    private T first;    private T last;    public Pair(T first, T last) {        this.first = first;        this.last = last;    }    public T getFirst() {        return first;    }    public T getLast() {        return last;    }}
    

    因为TObject,我们对Pair<String>Pair<Integer>类型获取Class时,获取到的是同一个Class,也就是Pair类的Class

    换言之,所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair<Object>

  3. 无法判断带泛型的类型:

    Pair<Integer> p = new Pair<>(123, 456);// Compile error:if (p instanceof Pair<String>) {}
    

    原因和前面一样,并不存在Pair<String>.class,而是只有唯一的Pair.class

  4. 不能实例化T类型:

    public class Pair<T> {    private T first;    private T last;    public Pair() {        // Compile error:        first = new T();        last = new T();    }}
    

    要实例化T类型,我们必须借助额外的Class<T>参数:

    public class Pair<T> {    private T first;    private T last;    public Pair(Class<T> clazz) {        first = clazz.newInstance();        last = clazz.newInstance();    }}
    

    上述代码借助Class<T>参数并通过反射来实例化T类型,使用的时候,也必须传入Class<T>。例如:

    Pair<String> pair = new Pair<>(String.class);
    

4.5 泛型继承

泛型方法要防止重复定义方法,例如:public boolean equals(T obj)

4.6 extends通配符

使用类似<? extends Number>通配符作为方法参数时表示:

  • 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();
  • 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);

即一句话总结:使用extends通配符表示可以读,不能写。

使用类似<T extends Number>定义泛型类时表示:

  • 泛型类型限定为Number以及Number的子类。

4.7 super通配符

使用类似<? super Integer>通配符作为方法参数时表示:

  • 方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);
  • 方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();

即使用super通配符表示只能写不能读。

使用extendssuper通配符要遵循PECS原则。

无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。

4、I/O流

5、日期

6、代码规范


Java集合


并发编程

1、并发

1.1 概述

1.1.1 多线程概念

Java的多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。而使用多线程的好处是可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。至于多线程的坏处么,主要有三点。第一点是线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;第二点是多线程需要协调和管理,所以需要 CPU 时间跟踪线程;最后是线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。

进程是系统进行资源分配和调度的独立单位,每一个进程都有它自己的内存空间和系统资源。进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销。为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理,所以有了线程,线程取代了进程了调度的基本功能。简单来说,进程作为资源分配的基本单位,线程作为资源调度的基本单位。

1.1.2 并行和并发

并发(concurrency),就是多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。

并行(parallelism),就是单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。

1.1.3 用户级线程和内核级线程

在操作系统的设计中,为了防止用户操作敏感指令而对OS带来安全隐患,我们把OS分成了用户空间(user space)和内核空间(kernel space)。

通过用户空间的库类实现的线程,就是用户级线程(user-level threads,ULT)。这种线程不依赖于操作系统核心,进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。

由OS内核空间直接掌控的线程,称为内核级线程(kernel-level threads,KLT)。其依赖于操作系统核心,由内核的内部需求进行创建和撤销。

内核线程的线程表(thread table)位于内核中,包括了线程控制块(TCB),一旦线程阻塞,内核会从当前或者其他进程(process)中重新选择一个线程保证程序的执行。对于用户级线程来说,其线程的切换发生在用户空间,这样的线程切换至少比陷入内核要快一个数量级。但是该种线程有个严重的缺点:如果一个线程开始运行,那么该进程中其他线程就不能运行,除非第一个线程自动放弃CPU。因为在一个单独的进程内部,没有时钟中断,所以不能用轮转调度(轮流)的方式调度线程。同一进程中的用户级线程,在不考虑调起多个内核级线程的基础上,是没有办法利用多核CPU的,其实质是并发而非并行。

对于内核级线程来说,其线程在内核中创建和撤销线程的开销比较大,需要考虑上下文切换的开销。但是内核级线程是可以利用多核CPU的,即可以并行。

Java里的多线程,既不是用户级线程,也不是内核级线程。因为,Java是跨操作平台的语言,是使用JVM去运行编译文件的。不同的JVM对线程的实现不同,相同的JVM对不同操作平台的线程实现方式也有区别。

1.1.4 多线程模型

多对1模型:在多对一模型中,多个ULT映射到1个KLT上去,此时ULT的进程表处于进程之中。

1对1模型:在一对一模型中,1个ULT对应1个KLT。自己不在进程中创建线程表来管理,几行代码之后直接通过系统调用调起KLT就能实现。

多对多模型:在多对多模型中,N个ULT对应小于等于N个的KLT。这种模型结合了1对1和多对1的优点,用户创建线程没有限制,阻塞内核系统的命令不会阻塞整个进程。

最后,就拿最热门的HotSpot VM来说吧,他在Solaris上就有两种线程实现方式,可以让用户选择一对一或多对多这两种模型;而在Windows和Linux下,使用的都是一对一的多线程模型,Java的线程通过一一映射到Light Weight Process(轻量级进程,LWP)从而实现了和KLT的一一对应。

1.1.5 Java线程的运行过程:
  1. 在Java中,使用java.lang.Thread的构造方法来构建一个java.lang.Thread对象,此时只是对这个对象的部分字段(例如线程名,优先级等)进行初始化;
  2. 调用java.lang.Thread对象的start()方法,开始此线程。此时,在start()方法内部,调用start0() 本地方法来开始此线程;
  3. start0()在VM中对应的是JVM_StartThread,也就是,在VM中,实际运行的是JVM_StartThread方法(宏),在这个方法中,创建了一个JavaThread对象;
  4. 在JavaThread对象的创建过程中,会根据运行平台创建一个对应的OSThread对象,且JavaThread保持这个OSThread对象的引用;
  5. 在OSThread对象的创建过程中,创建一个平台相关的底层级线程,如果这个底层级线程失败,那么就抛出异常;
  6. 在正常情况下,这个底层级的线程开始运行,并执行java.lang.Thread对象的run方法;
  7. 当java.lang.Thread生成的Object的run()方法执行完毕返回后,或者抛出异常终止后,终止native thread;
  8. 最后就是释放相关的资源(包括内存、锁等)。

上下文切换:

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。

时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。

当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括而言就是,当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自身状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的,每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

减少上下文切换的方式:

  1. 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
  2. CAS算法:利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换。
  3. 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。
  4. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

协程:

协程(Coroutine)是一个程序组件,它既不是线程也不是进程。其执行过程更类似于一个方法,或者说不带返回值的函数调用。

1.1.6 线程安全

Java的线程安全在三个方面体现:

  1. 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic和synchronized这两个关键字来确保原子性;

  2. 可见性:一个线程对主内存的修改可以及时地被其他线程看到,在Java中使用了synchronized和volatile这两个关键字确保可见性;

  3. 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序(在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序),该观察结果一般杂乱无序,在Java中使用了happens-before原则来确保有序性。

代码重排序需要满足以下两个条件:

  1. 在单线程环境下不能改变程序运行的结果;
  2. 存在数据依赖关系的不允许重排序。

重排序不会对单线程有影响,只会破坏多线程的执行语义(保障这一结果是因为在编译器,runtime 和处理器都必须遵守as-if-serial语义规则)。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。

Happens-Before原则

HB规则是Java内存模型(JMM)向程序员提供的跨线程内存可见性保证。具体的定义为:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法。

具体的规则有8条:

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生其finalize()方法的开始。

2、线程池

2.1 概述

线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。

池化思想在计算机的应用也比较广泛,比如以下这些:

  1. 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
  2. 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
  3. 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。

线程池的优势主要体现在以下 4 点:

  1. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  4. 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

2.2 创建方式

线程池的创建方法总共有 7 种,但总体来说可分为 2 类: 一类是通过 ThreadPoolExecutor 创建的线程池, 另一个类是通过 Executors 创建的线程池。

线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):

  1. Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
  2. Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
  3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
  4. Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
  5. Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
  6. Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定,JDK 1.8 添加)。
  7. ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置。

单线程池的意义从以上代码可以看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 创建的都是单线程池,那么单线程池的意义是什么呢?答:虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。

2.2.1 FixedThreadPool

创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。

使用示例:

public static void fixedThreadPool() {    // 创建 2 个数据级的线程池    ExecutorService threadPool = Executors.newFixedThreadPool(2);    // 创建任务    Runnable runnable = new Runnable() {        @Override        public void run() {            System.out.println("任务被执行,线程:" + Thread.currentThread().getName());        }    };    // 线程池执行任务(一次添加 4 个任务)    // 执行任务的方法有两种:submit 和 execute    threadPool.submit(runnable);  // 执行方式 1:submit    threadPool.execute(runnable); // 执行方式 2:execute    threadPool.execute(runnable);    threadPool.execute(runnable);}
2.2.2 CachedThreadPool

创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。

使用示例:

public static void cachedThreadPool() {    // 创建线程池    ExecutorService threadPool = Executors.newCachedThreadPool();    // 执行任务    for (int i = 0; i < 10; i++) {        threadPool.execute(() -> {            System.out.println("任务被执行,线程:" + Thread.currentThread().getName());            try {                TimeUnit.SECONDS.sleep(1);            } catch (InterruptedException e) {            }        });    }}
2.2.3 SingleThreadExecutor

创建单个线程数的线程池,它可以保证先进先出的执行顺序。

使用示例:

public static void singleThreadExecutor() {    // 创建线程池    ExecutorService threadPool = Executors.newSingleThreadExecutor();    // 执行任务    for (int i = 0; i < 10; i++) {        final int index = i;        threadPool.execute(() -> {            System.out.println(index + ":任务被执行");            try {                TimeUnit.SECONDS.sleep(1);            } catch (InterruptedException e) {            }        });    }}
2.2.4 ScheduledThreadPool

创建一个可以执行延迟任务的线程池。

使用示例:

public static void scheduledThreadPool() {    // 创建线程池    ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);    // 添加定时执行任务(1s 后执行)    System.out.println("添加任务,时间:" + new Date());    threadPool.schedule(() -> {        System.out.println("任务被执行,时间:" + new Date());        try {            TimeUnit.SECONDS.sleep(1);        } catch (InterruptedException e) {        }    }, 1, TimeUnit.SECONDS);}
2.2.5 SingleThreadScheduledExecutor

创建一个单线程的可以执行延迟任务的线程池。

使用示例:

public static void SingleThreadScheduledExecutor() {    // 创建线程池    ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();    // 添加定时执行任务(2s 后执行)    System.out.println("添加任务,时间:" + new Date());    threadPool.schedule(() -> {        System.out.println("任务被执行,时间:" + new Date());        try {            TimeUnit.SECONDS.sleep(1);        } catch (InterruptedException e) {        }    }, 2, TimeUnit.SECONDS);}
2.2.6 newWorkStealingPool

创建一个抢占式执行的线程池(任务执行顺序不确定),注意此方法只有在 JDK 1.8+ 版本中才能使用。

使用示例:

public static void workStealingPool() {    // 创建线程池    ExecutorService threadPool = Executors.newWorkStealingPool();    // 执行任务    for (int i = 0; i < 10; i++) {        final int index = i;        threadPool.execute(() -> {            System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());        });    }    // 确保任务执行完成    while (!threadPool.isTerminated()) {    }}
2.2.7 ThreadPoolExecutor

最原始的创建线程池的方式,它包含了 7 个参数可供设置。

使用示例:

public static void myThreadPoolExecutor() {    // 创建线程池    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));    // 执行任务    for (int i = 0; i < 10; i++) {        final int index = i;        threadPool.execute(() -> {            System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        });    }}

ThreadPoolExecutor 参数介绍

ThreadPoolExecutor 最多可以设置 7 个参数,代码如下所示:

public ThreadPoolExecutor(int corePoolSize,                           int maximumPoolSize,                           long keepAliveTime,                           TimeUnit unit,                           BlockingQueue<Runnable> workQueue,                           ThreadFactory threadFactory,                           RejectedExecutionHandler handler) {     // 省略... }

参数的含义如下:

  1. corePoolSize:核心线程数,线程池中始终存活的线程数。

  2. maximumPoolSize:最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。

  3. keepAliveTime:最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。

  4. unit:时间单位,与存活时间(KeepAliveTime)配合使用,合在一起用于设定线程的存活时间,有以下 7 种可选。

    • TimeUnit.DAYS(天)
    • TimeUnit.HOURS(小时)
    • TimeUnit.MINUTES(分)
    • TimeUnit.SECONDS(秒)
    • TimeUnit.MILLISECONDS(毫秒)
    • TimeUnit.MICROSECONDS(微秒)
    • TimeUnit.NANOSECONDS(纳秒)
  5. workQueue:阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型。

    • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
    • SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
    • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
    • DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
    • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
    • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

    较常用的是 LinkedBlockingQueueSynchronous,线程池的排队策略与 BlockingQueue 有关。

  6. threadFactory:线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。

  7. handler:拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选。

    • AbortPolicy:拒绝并抛出异常。
    • CallerRunsPolicy:使用当前调用的线程来执行此任务。
    • DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
    • DiscardPolicy:忽略并抛弃当前任务。

    默认策略为 AbortPolicy

线程池的执行流程

ThreadPoolExecutor 关键节点的执行流程如下:

  1. 当线程数小于核心线程数时,创建线程。
  2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
  3. 当线程数大于等于核心线程数,且任务队列已满:若线程数小于最大线程数,创建线程;若线程数等于最大线程数,抛出异常,拒绝任务。

线程拒绝策略

我们来演示一下 ThreadPoolExecutor 的拒绝策略的触发,我们使用 DiscardPolicy 的拒绝策略,它会忽略并抛弃当前任务的策略,实现代码如下:

public static void main(String[] args) {    // 任务的具体方法    Runnable runnable = new Runnable() {        @Override        public void run() {            System.out.println("当前任务被执行,执行时间:" + new Date() +                               " 执行线程:" + Thread.currentThread().getName());            try {                // 等待 1s                TimeUnit.SECONDS.sleep(1);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    };    // 创建线程,线程的任务队列的长度为 1    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,                                                           100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),                                                           new ThreadPoolExecutor.DiscardPolicy());    // 添加并执行 4 个任务    threadPool.execute(runnable);    threadPool.execute(runnable);    threadPool.execute(runnable);    threadPool.execute(runnable);}

自定义拒绝策略

除了 Java 自身提供的 4 种拒绝策略之外,也可以自定义拒绝策略,示例代码如下:

public static void main(String[] args) {    // 任务的具体方法    Runnable runnable = new Runnable() {        @Override        public void run() {            System.out.println("当前任务被执行,执行时间:" + new Date() +                               " 执行线程:" + Thread.currentThread().getName());            try {                // 等待 1s                TimeUnit.SECONDS.sleep(1);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    };    // 创建线程,线程的任务队列的长度为 1    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,                                                           100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),                                                           new RejectedExecutionHandler() {                                                               @Override                                                               public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {                                                                   // 执行自定义拒绝策略的相关操作                                                                   System.out.println("我是自定义拒绝策略~");                                                               }                                                           });    // 添加并执行 4 个任务    threadPool.execute(runnable);    threadPool.execute(runnable);    threadPool.execute(runnable);    threadPool.execute(runnable);}

2.3 选用标准

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,因为这样的处理方式,能够更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回的线程池对象的弊端如下:

  1. FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

  2. CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。


3、JUC

3.1 概述

4、锁机制

4.1 锁分类

4.1.1 自旋锁
4.1.2 偏向锁
4.1.3 适应性自旋锁
4.1.4 锁粗化
4.1.5 锁消除
4.1.6 轻量级锁
4.1.7 重量级锁

5、事务

5.1 概述


JVM

1、内存区域

2、垃圾回收机制

3、JVM调优


日志管理

1、日志级别

常见的日志级别有5种,分别是error、warn、info、debug、trace。

  1. error:错误日志,指比较严重的错误,对正常业务有影响,需要运维配置监控的;
  2. warn:警告日志,一般的错误,对业务影响不大,但是需要开发关注
  3. info:信息日志,记录排查问题的关键信息,如调用时间、出参入参等;
  4. debug:用于开发DEBUG的,关键逻辑里面的运行时数据;
  5. trace:最详细的信息,一般这些信息只记录到日志文件中。

2、日志格式

理想的日志格式,应当包括这些最基本的信息:如当前时间戳(一般毫秒精确度)、日志级别,线程名字等。

3、日志框架

3.1 logback和slf4j

3.1.1 pom依赖

在pom文件中添加logback的maven依赖:

<!-- 用来设置版本号 --><properties>	<slf4j.version>1.7.25</slf4j.version>	<logback.version>1.2.3</logback.version></properties> <!-- 用到的jar包 --><dependencies>	<!-- logback start -->	<dependency>		<groupId>org.slf4j</groupId>		<artifactId>slf4j-api</artifactId>		<version>${slf4j.version}</version>	</dependency>	<dependency>		<groupId>ch.qos.logback</groupId>		<artifactId>logback-core</artifactId>		<version>${logback.version}</version>	</dependency>	<dependency>		<groupId>ch.qos.logback</groupId>		<artifactId>logback-classic</artifactId>		<version>${logback.version}</version>	</dependency>	<!-- logback end --></dependencies>
3.1.2 配置文件

在当前项目的src/main/resources文件夹下,新增文件logback.xml,配置样例如下:

<?xml version="1.0" encoding="UTF-8"?><configuration>    <!--文件资源的引用-->    <property resource="app.properties"/>    <!-- 默认输出文件 -->    <appender name="DEFAULT-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <fileNamePattern>${log.home}/default/common-default.log.%d{yyyyMMdd}</fileNamePattern>            <maxHistory>30</maxHistory>        </rollingPolicy>        <encoder charset="UTF-8">            <pattern>%d [%t] %-5p %c{2} [%X{traceRootId}] - [%m]%n</pattern>        </encoder>    </appender>    <!-- 默认错误文件 -->    <appender name="ERROR-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <fileNamePattern>${log.home}/error/common-error.log.%d{yyyyMMdd}</fileNamePattern>        </rollingPolicy>        <encoder charset="UTF-8">            <pattern>%d [%t] %-5p %c{2} [%X{traceRootId}] - [%m]%n</pattern>        </encoder>        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">            <level>ERROR</level>        </filter>    </appender>    <!-- 性能日志文件 -->    <appender name="PERF-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <fileNamePattern>${log.home}/perf/common-perf.log.%d{yyyyMMdd}</fileNamePattern>            <maxHistory>30</maxHistory>        </rollingPolicy>        <encoder charset="UTF-8">            <pattern>%d [%t] %-5p %c{2} [%X{traceRootId}] - [%m]%n</pattern>        </encoder>    </appender>    <!-- 默认dao日志文件 -->    <appender name="DAO-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <fileNamePattern>${log.home}/dao/common-dao.log.%d{yyyyMMdd}</fileNamePattern>            <maxHistory>30</maxHistory>        </rollingPolicy>        <encoder charset="UTF-8">            <pattern>%d [%t] %-5p %c{2} [%X{traceRootId}] - [%m]%n</pattern>        </encoder>    </appender>    <!-- service日志文件 -->    <appender name="SERVICE-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <fileNamePattern>${log.home}/service/common-service.log.%d{yyyyMMdd}</fileNamePattern>            <maxHistory>30</maxHistory>        </rollingPolicy>        <encoder charset="UTF-8">            <pattern>%d [%t] %-5p %c{2} [%X{traceRootId}] - [%m]%n</pattern>        </encoder>    </appender>    <!-- 业务日志文件 -->    <appender name="BUSINESS-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <fileNamePattern>${log.home}/business/common-business.log.%d{yyyyMMdd}</fileNamePattern>            <maxHistory>30</maxHistory>        </rollingPolicy>        <encoder charset="UTF-8">            <pattern>%d [%t] %-5p %c{2} [%X{traceRootId}] - [%m]%n</pattern>        </encoder>    </appender>    <!-- WEB日志文件 -->    <appender name="WEB-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <fileNamePattern>${log.home}/web/common-web.log.%d{yyyyMMdd}</fileNamePattern>            <maxHistory>30</maxHistory>        </rollingPolicy>        <encoder charset="UTF-8">            <pattern>%d [%t] %-5p %c{2} [%X{traceRootId}] - [%m]%n</pattern>        </encoder>    </appender>    <!-- 报警日志 -->    <appender name="ALARM-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <fileNamePattern>${log.home}/alarm/common-alarm.log.%d{yyyyMMdd}</fileNamePattern>            <maxHistory>30</maxHistory>        </rollingPolicy>        <encoder charset="UTF-8">            <pattern>%d [%t] %-5p %c{2} [%X{traceRootId}] - [%m]%n</pattern>        </encoder>    </appender>    <!--默认追踪日志-一般model或工具类日志用此  -->    <appender name="TRACE-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">            <fileNamePattern>${log.home}/trace/tracing.log.%d{yyyy-MM-dd-HH}.gz</fileNamePattern>            <maxHistory>72</maxHistory>        </rollingPolicy>        <encoder charset="UTF-8">            <pattern>%d - [%m]%n</pattern>        </encoder>    </appender>    <!-- ===================================================================== -->    <!-- Loggers                                                               -->    <!-- ===================================================================== -->    <logger name="com.myself.ssmTest.controller" additivity="false">        <level value="${log.root.level}"/>        <appender-ref ref="WEB-APPENDER"/>        <appender-ref ref="ERROR-APPENDER"/>    </logger>        <logger name="com.myself.ssmTest.service" additivity="false">        <level value="${log.root.level}"/>        <appender-ref ref="SERVICE-APPENDER"/>        <appender-ref ref="ERROR-APPENDER"/>    </logger>     <logger name="com.myself.ssmTest.dao" additivity="false">        <level value="${log.root.level}"/>        <appender-ref ref="DAO-APPENDER"/>        <appender-ref ref="ERROR-APPENDER"/>    </logger>     <logger name="com.myself.ssmTest.model" additivity="false">        <level value="INFO"/>        <appender-ref ref="TRACE-APPENDER"/>        <appender-ref ref="ERROR-APPENDER"/>    </logger>     <logger name="businessLogger" additivity="false">        <level value="${log.root.level}"/>        <appender-ref ref="BUSINESS-APPENDER"/>        <appender-ref ref="ERROR-APPENDER"/>    </logger>    <logger name="com.tuan.core.common.aop.pref.PerformanceMonitorInterceptor" additivity="false">    	<level value="INFO"/>    	<appender-ref ref="PERF-APPENDER"/>    </logger>       <logger name="java.sql" additivity="false">        <level value="${log.root.level}"/>        <appender-ref ref="DAO-APPENDER"/>        <appender-ref ref="ERROR-APPENDER"/>    </logger>        <logger name="alarmLogger" additivity="false">        <level value="${log.root.level}"/>        <appender-ref ref="ALARM-APPENDER"/>        <appender-ref ref="ERROR-APPENDER"/>    </logger>    <!-- 屏蔽logger-start -->    <logger name="org.springframework" level="${log.root.level}"/>    <logger name="org.apache" level="WARN"/>    <logger name="org.mybatis.spring" level="${log.root.level}"/>    <!-- 屏蔽jdk日志 -->    <logger name="java" level="WARN"/>    <logger name="com.mchange" additivity="false">        <level value="WARN"/>        <appender-ref ref="DAO-APPENDER"/>    </logger>    <!-- 屏蔽logger-end-->    <root level="${log.root.level}">        <appender-ref ref="DEFAULT-APPENDER"></appender-ref>        <appender-ref ref="ERROR-APPENDER"></appender-ref>    </root></configuration> 

如果是web项目,请在src\main\webapp\WEB-INF\web.xml文件的最上方配置logback监听器,如下:

<!--logback-start  --><context-param>  <param-name>logbackConfigLocation</param-name>  <param-value>/WEB-INF/classes/logback.xml</param-value></context-param><listener>  <listener-class>ch.qos.logback.ext.spring.web.LogbackConfigListener</listener-class></listener><!--logback-end  -->
3.2.3 Java代码中使用
package com.test.justTest; import org.slf4j.Logger;import org.slf4j.LoggerFactory; public class LogbackTest {    /**     * 日志:因logback.xml未配置此包下的日志,<br/>     * 所以会输入到默认日志文件:${log.home}/default/common-default.log.%d{yyyyMMdd}     */    private static final Logger commonLogger = LoggerFactory.getLogger(LogbackTest.class);     /**     * 业务日志:打印至${log.home}/business/common-business.log.%d{yyyyMMdd}<br/>     * 查看app.properties得知${log.home}=/data/application/logs/myProject     */    public static final Logger businessLogger = LoggerFactory.getLogger("businessLogger");     /**     * 告警日志:打印至${log.home}/alarm/common-alarm.log.%d{yyyyMMdd}<br/>     * 查看app.properties得知${log.home}=/data/application/logs/myProject     */    public static final Logger alarmLogger = LoggerFactory.getLogger("alarmLogger");     public static void main(String[] args) {        System.out.printf("------LogbackTest-打印日志开始------");        printTestLog(commonLogger);        printTestLog(businessLogger);        printTestLog(alarmLogger);        System.out.printf("------LogbackTest-打印日志结束------");        /**         * 执行完上述代码请至相应文件夹下查看相应文件中的内容是否有打印         */    }     /**     * 打印日志     * @param logger 日志打印者     */    public static void printTestLog(Logger logger){        //debug级别的日志        logger.debug("this is a debug log ,logger={}",logger.getName());         //info级别的日志        logger.info("this is a info log,logger={}",logger.getName());         //warn级别的日志        logger.warn("this is a warn log,logger={}",logger.getName());          //error级别的日志        logger.error("this is a error log,logger={}",logger.getName(),new RuntimeException());     }}

3.2 log4j

3.2.1 pom依赖

在pom文件中添加log4j的maven依赖:

<dependency>    <groupId>log4j</groupId>    <artifactId>log4j</artifactId>    <version>1.2.17</version></dependency>
3.2.2 配置文件

在当前项目的src/main/resources文件夹下,新增文件log4j.properties,配置样例如下:

### set log levels ###log4j.rootLogger = DEBUG,Console,File###  输出到控制台  ###log4j.appender.Console=org.apache.log4j.ConsoleAppenderlog4j.appender.Console.Target=System.outlog4j.appender.Console.layout=org.apache.log4j.PatternLayoutlog4j.appender.Console.layout.ConversionPattern= %d{ABSOLUTE} %5p %c{1}:%L - %m%n### 输出到日志文件 ###log4j.appender.File=org.apache.log4j.RollingFileAppender log4j.appender.File.File=${project}/WEB-INF/logs/app.loglog4j.appender.File.DatePattern=_yyyyMMdd'.log'log4j.appender.File.MaxFileSize=10MBlog4j.appender.File.Threshold=ALLlog4j.appender.File.layout=org.apache.log4j.PatternLayoutlog4j.appender.File.layout.ConversionPattern=[%p][%d{yyyy-MM-dd HH\:mm\:ss,SSS}][%c]%m%n
3.2.3 Java代码中使用
package com.xwx.text;import org.apache.log4j.Logger;import org.junit.Test;public class Test {		private static Logger log = Logger.getLogger(Test.class.getClass());	@Test	public void testLog(){		log.debug("debug");		log.error("error");	}	}

3.3 log4j2

3.3.1 pom依赖

在pom文件中添加log4j2的maven依赖:

<!-- log4j 新版本依赖--><dependency>        <groupId>org.apache.logging.log4j</groupId>        <artifactId>log4j-api</artifactId>        <version>2.10.0</version>    </dependency>    <dependency>        <groupId>org.apache.logging.log4j</groupId>        <artifactId>log4j-core</artifactId>        <version>2.10.0</version>    </dependency>    <dependency>      <groupId>org.apache.logging.log4j</groupId>      <artifactId>log4j-web</artifactId>      <version>2.9.1</version>    </dependency>
3.3.2 配置文件

在src/main/resources目录下增加log4j2的配置文件,默认log4j2.xml

<?xml version="1.0" encoding="utf-8" ?><Configuration status="off" monitorInterval="1800">	<properties>		<property name="LOG_HOME">F:\logs</property>		<property name="ERROR_LOG_FILE_NAME">error</property>	</properties>	<Appenders>		<Console name="Console" target="SYSTEM_OUT">			<PatternLayout pattern="%d %-5p (%F:%L) - %m%n" />		</Console>		<!-- <RollingRandomAccessFile name="ErrorLog"			fileName="${LOG_HOME}/${ERROR_LOG_FILE_NAME}.log" filePattern="${LOG_HOME}/${ERROR_LOG_FILE_NAME}.log.%d{yyyy-MM-dd}.gz">			<PatternLayout pattern="%d %-5p (%F:%L) - %m%n" />			<Policies>				<TimeBasedTriggeringPolicy />				<SizeBasedTriggeringPolicy size="100MB" />			</Policies>			<DefaultRolloverStrategy max="20" />		</RollingRandomAccessFile> -->	</Appenders>	<Loggers>		<logger name="log4j.logger.com.ibatis" level="trace"			additivity="false">			<appender-ref ref="Console" />		</logger>		<logger name="log4j.logger.com.ibatis.common.jdbc.SimpleDataSource"			level="debug" additivity="false">			<appender-ref ref="Console" />		</logger>		<logger name="log4j.logger.com.ibatis.common.jdbc.ScriptRunner"			level="debug" additivity="false">			<appender-ref ref="Console" />		</logger>		<logger			name="log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate"			level="debug" additivity="false">			<appender-ref ref="Console" />		</logger>		<logger name="log4j.logger.org.mybatis" level="debug"			additivity="false">			<appender-ref ref="Console" />		</logger>		<logger name="log4j.logger.java.sql" level="debug" additivity="false">			<appender-ref ref="Console" />		</logger>		<logger name="log4j.logger.java.sql.Connection" level="debug"			additivity="false">			<appender-ref ref="Console" />		</logger>		<logger name="log4j.logger.java.sql.Statement" level="debug"			additivity="false">			<appender-ref ref="Console" />		</logger>		<logger name="log4j.logger.java.sql.PreparedStatement" level="debug"			additivity="false">			<appender-ref ref="Console" />		</logger>		<logger name="log4j.logger.java.sql.ResultSet" level="debug"			additivity="false">			<appender-ref ref="Console" />		</logger>		<root level="debug" includeLocation="true">			<appender-ref ref="Console" />		</root>	</Loggers></Configuration>

4、日志打印原则

4.1 不要使用e.printStackTrace()

原因如下:

  1. e.printStackTrace()打印出的堆栈日志与业务代码日志是交错混合在一起的,通常排查异常日志不太方便;
  2. e.printStackTrace()语句产生的字符串记录的是堆栈信息,如果信息太长太多,会消耗字符串常量池所在的内存块;
  3. e.getMessage()不会记录详细的堆栈异常信息,只会记录错误基本描述信息,不利于排查问题。

4.2 异常日志不要只打印一半,而是输出全部错误信息

处理示例:

try {    //业务代码处理} catch (Exception e) {    // 错误    LOG.error('你的程序有异常啦', e);} 

4.3 禁止在线上环境开启debug

因为一般系统的debug日志会很多,并且各种框架中也大量使用 debug的日志,线上开启debug不久可能会打满磁盘,影响业务系统的正常运行。

4.4 不要记录了异常,又抛出异常

反例如下:

log.error("IO exception", e);throw new MyException(e);

原因如下:

  1. 因为这样实现,通常会把栈信息打印两次。在捕获MyException异常处,还会再打印一次日志信息;
  2. 这样的日志记录,或者包装后再抛出去,不要同时使用,因为这样的日志看起来会让人很迷惑。

4.5 避免重复打印日志

避免重复打印日志,因为会浪费磁盘空间。

反例如下:

if(user.isVip()){  log.info("该用户是会员,Id:{}",user,getUserId());  //冗余,可以跟前面的日志合并一起  log.info("开始处理会员逻辑,id:{}",user,getUserId());  //会员逻辑}else{  //非会员逻辑}

4.6 日志文件分离

日志文件分离操作,有利于数据统计和问题排查:

  1. 把不同类型的日志分离出去,比如access.log,或者error级别error.log等都可以单独打印到一个文件里面;
  2. 也可以根据不同的业务模块,打印到不同的日志文件里。

4.7 核心功能模块,建议打印完整的日志


设计模式

1、创建型

1.1 单例模式

1.1.1 意图

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

1.1.2 主要解决

一个全局使用的类频繁地创建与销毁。

1.1.3 何时使用

当您想控制实例数目,节省系统资源的时候。

如何解决

判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码

构造函数是私有的。

几种实现方式

单例模式的 UML 图

1、懒汉式,线程不安全
public class Singleton {      private static Singleton instance;      private Singleton (){}        public static Singleton getInstance() {      if (instance == null) {          instance = new Singleton();      }      return instance;      }  }
2、懒汉式,线程安全
public class Singleton {      private static Singleton instance;      private Singleton (){}      public static synchronized Singleton getInstance() {      if (instance == null) {          instance = new Singleton();      }      return instance;      }  }
3、饿汉式,线程安全
public class Singleton {      private static Singleton instance = new Singleton();      private Singleton (){}      public static Singleton getInstance() {      return instance;      }  }
4、双重校验锁,线程安全
public class Singleton {      private volatile static Singleton singleton;      private Singleton (){}      public static Singleton getSingleton() {      if (singleton == null) {          synchronized (Singleton.class) {          if (singleton == null) {              singleton = new Singleton();          }          }      }      return singleton;      }  }
5、静态内部类,线程安全
public class Singleton {      private static class SingletonHolder {      private static final Singleton INSTANCE = new Singleton();      }      private Singleton (){}      public static final Singleton getInstance() {      return SingletonHolder.INSTANCE;      }  }
6、枚举,线程安全
public enum Singleton {      INSTANCE;      public void whateverMethod() {      }  }

1.2 建造者模式(Builder Pattern)

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。

意图

将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。

主要解决

主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定

何时使用

一些基本部件不会变,而其组合经常变化的时候。

如何解决

将变与不变分离开。

关键代码

建造者:创建和提供实例

导演:管理建造出来的实例的依赖关系。

实现方式

假设一个快餐店的商业案例,其中,一个典型的套餐可以是一个汉堡(Burger)和一杯冷饮(Cold drink)。汉堡(Burger)可以是素食汉堡(Veg Burger)或鸡肉汉堡(Chicken Burger),它们是包在纸盒中。冷饮(Cold drink)可以是可口可乐(coke)或百事可乐(pepsi),它们是装在瓶子中。

先创建一个表示食物条目(比如汉堡和冷饮)的 Item 接口和实现 Item 接口的实体类,以及一个表示食物包装的 Packing 接口和实现 Packing 接口的实体类,汉堡是包在纸盒中,冷饮是装在瓶子中。

然后创建一个 Meal 类,带有 ItemArrayList 和一个通过结合 Item 来创建不同类型的 Meal 对象的 MealBuilderBuilderPatternDemo 类使用 MealBuilder 来创建一个 Meal

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZvuZfSHA-1640940359197)(https://gitee.com/wuyang556/pic-bed/raw/master//bookwarm/avatar/20210315-builder-pattern.svg)]

步骤1:创建一个表示食物条目和食物包装的接口
public interface Item {   public String name();   public Packing packing();   public float price();    }
public interface Packing {   public String pack();}
步骤2:创建实现 Packing 接口的实体类
public class Wrapper implements Packing {    @Override   public String pack() {      return "Wrapper";   }}
public class Bottle implements Packing {    @Override   public String pack() {      return "Bottle";   }}
步骤3:创建实现 Item 接口的抽象类,该类提供了默认的功能。
public abstract class Burger implements Item {    @Override   public Packing packing() {      return new Wrapper();   }    @Override   public abstract float price();}
public abstract class ColdDrink implements Item {     @Override    public Packing packing() {       return new Bottle();    }     @Override    public abstract float price();}
步骤4:创建扩展了 Burger 和 ColdDrink 的实体类
public class VegBurger extends Burger {    @Override   public float price() {      return 25.0f;   }    @Override   public String name() {      return "Veg Burger";   }}
public class ChickenBurger extends Burger {    @Override   public float price() {      return 50.5f;   }    @Override   public String name() {      return "Chicken Burger";   }}
public class Coke extends ColdDrink {    @Override   public float price() {      return 30.0f;   }    @Override   public String name() {      return "Coke";   }}
public class Pepsi extends ColdDrink {    @Override   public float price() {      return 35.0f;   }    @Override   public String name() {      return "Pepsi";   }}
步骤5:创建一个 Meal 类,带有上面定义的 Item 对象。
import java.util.ArrayList;import java.util.List; public class Meal {   private List<Item> items = new ArrayList<Item>();        public void addItem(Item item){      items.add(item);   }    public float getCost(){      float cost = 0.0f;      for (Item item : items) {         cost += item.price();      }              return cost;   }    public void showItems(){      for (Item item : items) {         System.out.print("Item : "+item.name());         System.out.print(", Packing : "+item.packing().pack());         System.out.println(", Price : "+item.price());      }           }    }
步骤6:创建一个 MealBuilder 类,实际的 builder 类负责创建 Meal 对象
public class MealBuilder {    public Meal prepareVegMeal (){      Meal meal = new Meal();      meal.addItem(new VegBurger());      meal.addItem(new Coke());      return meal;   }       public Meal prepareNonVegMeal (){      Meal meal = new Meal();      meal.addItem(new ChickenBurger());      meal.addItem(new Pepsi());      return meal;   }}
步骤7:BuiderPatternDemo 使用 MealBuilder 来演示建造者模式(Builder Pattern)
public class BuilderPatternDemo {   public static void main(String[] args) {      MealBuilder mealBuilder = new MealBuilder();       Meal vegMeal = mealBuilder.prepareVegMeal();      System.out.println("Veg Meal");      vegMeal.showItems();      System.out.println("Total Cost: " +vegMeal.getCost());       Meal nonVegMeal = mealBuilder.prepareNonVegMeal();      System.out.println("\n\nNon-Veg Meal");      nonVegMeal.showItems();      System.out.println("Total Cost: " +nonVegMeal.getCost());   }}

原型模式(Prototype Pattern)

原型模式被用于创建重复的对象,同时又能保证性能,它提供了一种创建对象的最佳方式。这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。

意图

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

主要解决

在运行期建立和删除原型。

何时使用
  1. 当一个系统应该独立于它的产品创建,构成和表示时。
  2. 当要实例化的类是在运行时刻指定时,例如,通过动态装载。
  3. 为了避免创建一个与产品类层次平行的工厂类层次时。
  4. 当一个类的实例只能有几个不同状态组合中的一种时,建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
如何解决

利用已有的一个原型对象,快速地生成和原型对象一样的实例。

关键代码
  1. 实现克隆操作,在 JAVA 继承 Cloneable,重写 clone()。
  2. 原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。
实现方式

我们将创建一个抽象类 Shape 和扩展了 Shape 类的实体类。下一步是定义类 ShapeCache,该类把 shape 对象存储在一个 Hashtable 中,并在请求的时候返回它们的克隆。

原型模式的 UML 图

步骤1:创建一个实现了 Cloneable 接口的抽象类
public abstract class Shape implements Cloneable {      private String id;   protected String type;      abstract void draw();      public String getType(){      return type;   }      public String getId() {      return id;   }      public void setId(String id) {      this.id = id;   }      public Object clone() {      Object clone = null;      try {         clone = super.clone();      } catch (CloneNotSupportedException e) {         e.printStackTrace();      }      return clone;   }}
步骤2:创建扩展了上面抽象类的实体类。
public class Rectangle extends Shape {    public Rectangle(){     type = "Rectangle";   }    @Override   public void draw() {      System.out.println("Inside Rectangle::draw() method.");   }}
public class Square extends Shape {    public Square(){     type = "Square";   }    @Override   public void draw() {      System.out.println("Inside Square::draw() method.");   }}
public class Circle extends Shape {    public Circle(){     type = "Circle";   }    @Override   public void draw() {      System.out.println("Inside Circle::draw() method.");   }}
步骤3:创建一个类,从数据库获取实体类,并把它们存储在一个 Hashtable 中。
import java.util.Hashtable; public class ShapeCache {       private static Hashtable<String, Shape> shapeMap       = new Hashtable<String, Shape>();    public static Shape getShape(String shapeId) {      Shape cachedShape = shapeMap.get(shapeId);      return (Shape) cachedShape.clone();   }    // 对每种形状都运行数据库查询,并创建该形状   // shapeMap.put(shapeKey, shape);   // 例如,我们要添加三种形状   public static void loadCache() {      Circle circle = new Circle();      circle.setId("1");      shapeMap.put(circle.getId(),circle);       Square square = new Square();      square.setId("2");      shapeMap.put(square.getId(),square);       Rectangle rectangle = new Rectangle();      rectangle.setId("3");      shapeMap.put(rectangle.getId(),rectangle);   }}
步骤4:PrototypePatternDemo 使用 ShapeCache 类来获取存储在 Hashtable 中的形状的克隆。
public class PrototypePatternDemo {   public static void main(String[] args) {      ShapeCache.loadCache();       Shape clonedShape = (Shape) ShapeCache.getShape("1");      System.out.println("Shape : " + clonedShape.getType());               Shape clonedShape2 = (Shape) ShapeCache.getShape("2");      System.out.println("Shape : " + clonedShape2.getType());               Shape clonedShape3 = (Shape) ShapeCache.getShape("3");      System.out.println("Shape : " + clonedShape3.getType());           }}

1.3 工厂模式

意图

定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

主要解决

主要解决接口选择的问题。

何时使用

我们明确地计划不同条件下创建不同实例时。

如何解决

让其子类实现工厂接口,返回的也是一个抽象的产品。

关键代码

创建过程在其子类执行。

实现方式

工厂模式的 UML 图

步骤1:创建一个接口
public interface Shape {   void draw();}
步骤2:创建接口的实现类
public class Rectangle implements Shape {    @Override   public void draw() {      System.out.println("Inside Rectangle::draw() method.");   }}
public class Square implements Shape {    @Override   public void draw() {      System.out.println("Inside Square::draw() method.");   }}
public class Circle implements Shape {    @Override   public void draw() {      System.out.println("Inside Circle::draw() method.");   }}
步骤3:创建一个工厂,生成基于给定信息的实体类的对象
public class ShapeFactory {       //使用 getShape 方法获取形状类型的对象   public Shape getShape(String shapeType){      if(shapeType == null){         return null;      }              if(shapeType.equalsIgnoreCase("CIRCLE")){         return new Circle();      } else if(shapeType.equalsIgnoreCase("RECTANGLE")){         return new Rectangle();      } else if(shapeType.equalsIgnoreCase("SQUARE")){         return new Square();      }      return null;   }}
步骤4:使用该工厂,通过传递类型信息来获取实体类的对象
public class FactoryPatternDemo {    public static void main(String[] args) {      ShapeFactory shapeFactory = new ShapeFactory();       //获取 Circle 的对象,并调用它的 draw 方法      Shape shape1 = shapeFactory.getShape("CIRCLE");       //调用 Circle 的 draw 方法      shape1.draw();       //获取 Rectangle 的对象,并调用它的 draw 方法      Shape shape2 = shapeFactory.getShape("RECTANGLE");       //调用 Rectangle 的 draw 方法      shape2.draw();       //获取 Square 的对象,并调用它的 draw 方法      Shape shape3 = shapeFactory.getShape("SQUARE");       //调用 Square 的 draw 方法      shape3.draw();   }}

1.4 抽象工厂模式

意图

提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

主要解决

主要解决接口选择的问题。

何时使用

系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。

如何解决

在一个产品族里面,定义多个产品。

关键代码

在一个工厂里聚合多个同类产品。

实现方式

抽象工厂模式的 UML 图

步骤1:为形状创建一个接口
public interface Shape {   void draw();}
步骤2:创建形状接口的实体类。
public class Rectangle implements Shape {    @Override   public void draw() {      System.out.println("Inside Rectangle::draw() method.");   }}
public class Square implements Shape {    @Override   public void draw() {      System.out.println("Inside Square::draw() method.");   }}
public class Circle implements Shape {    @Override   public void draw() {      System.out.println("Inside Circle::draw() method.");   }}
步骤3:为颜色创建一个接口
public interface Color {   void fill();}
步骤4:创建实现颜色接口的实体类
public class Red implements Color {    @Override   public void fill() {      System.out.println("Inside Red::fill() method.");   }}
public class Green implements Color {    @Override   public void fill() {      System.out.println("Inside Green::fill() method.");   }}
public class Blue implements Color {    @Override   public void fill() {      System.out.println("Inside Blue::fill() method.");   }}
步骤5:为颜色和形状对象创建抽象类来获取工厂
public abstract class AbstractFactory {   public abstract Color getColor(String color);   public abstract Shape getShape(String shape) ;}
步骤6:创建扩展了 AbstractFactory 的工厂类,基于给定的信息生成实体类的对象。
public class ShapeFactory extends AbstractFactory {       @Override   public Shape getShape(String shapeType){      if(shapeType == null){         return null;      }              if(shapeType.equalsIgnoreCase("CIRCLE")){         return new Circle();      } else if(shapeType.equalsIgnoreCase("RECTANGLE")){         return new Rectangle();      } else if(shapeType.equalsIgnoreCase("SQUARE")){         return new Square();      }      return null;   }      @Override   public Color getColor(String color) {      return null;   }}
public class ColorFactory extends AbstractFactory {       @Override   public Shape getShape(String shapeType){      return null;   }      @Override   public Color getColor(String color) {      if(color == null){         return null;      }              if(color.equalsIgnoreCase("RED")){         return new Red();      } else if(color.equalsIgnoreCase("GREEN")){         return new Green();      } else if(color.equalsIgnoreCase("BLUE")){         return new Blue();      }      return null;   }}
步骤7:创建一个工厂创造器/生成器类,通过传递形状或颜色信息来获取工厂
public class FactoryProducer {   public static AbstractFactory getFactory(String choice){      if(choice.equalsIgnoreCase("SHAPE")){         return new ShapeFactory();      } else if(choice.equalsIgnoreCase("COLOR")){         return new ColorFactory();      }      return null;   }}
步骤8:使用 FactoryProducer 来获取 AbstractFactory,通过传递类型信息来获取实体类的对象
public class AbstractFactoryPatternDemo {   public static void main(String[] args) {       //获取形状工厂      AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");       //获取形状为 Circle 的对象      Shape shape1 = shapeFactory.getShape("CIRCLE");       //调用 Circle 的 draw 方法      shape1.draw();       //获取形状为 Rectangle 的对象      Shape shape2 = shapeFactory.getShape("RECTANGLE");       //调用 Rectangle 的 draw 方法      shape2.draw();            //获取形状为 Square 的对象      Shape shape3 = shapeFactory.getShape("SQUARE");       //调用 Square 的 draw 方法      shape3.draw();       //获取颜色工厂      AbstractFactory colorFactory = FactoryProducer.getFactory("COLOR");       //获取颜色为 Red 的对象      Color color1 = colorFactory.getColor("RED");       //调用 Red 的 fill 方法      color1.fill();       //获取颜色为 Green 的对象      Color color2 = colorFactory.getColor("Green");       //调用 Green 的 fill 方法      color2.fill();       //获取颜色为 Blue 的对象      Color color3 = colorFactory.getColor("BLUE");       //调用 Blue 的 fill 方法      color3.fill();   }}

2、结构型

2.1 代理模式

2.2 装饰器模式

3、行为型

3.1 观察者模式(Observer Pattern)

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。

意图

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

主要解决

一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

何时使用

一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

如何解决

使用面向对象技术,可以将这种依赖关系弱化。

关键代码

在抽象类里有一个 ArrayList 存放观察者们。

实现方式

观察者模式使用三个类 Subject、Observer 和 Client。Subject 对象带有绑定观察者到 Client 对象和从 Client 对象解绑观察者的方法。

观察者模式的 UML 图

步骤1:创建Subject类
import java.util.ArrayList;import java.util.List; public class Subject {      private List<Observer> observers       = new ArrayList<Observer>();   private int state;    public int getState() {      return state;   }    public void setState(int state) {      this.state = state;      notifyAllObservers();   }    public void attach(Observer observer){      observers.add(observer);         }    public void notifyAllObservers(){      for (Observer observer : observers) {         observer.update();      }   }  }
步骤2:创建Observer类
public abstract class Observer {   protected Subject subject;   public abstract void update();}
步骤3:创建实体观察者类
public class BinaryObserver extends Observer{    public BinaryObserver(Subject subject){      this.subject = subject;      this.subject.attach(this);   }    @Override   public void update() {      System.out.println( "Binary String: "       + Integer.toBinaryString( subject.getState() ) );    }}
public class OctalObserver extends Observer{    public OctalObserver(Subject subject){      this.subject = subject;      this.subject.attach(this);   }    @Override   public void update() {     System.out.println( "Octal String: "      + Integer.toOctalString( subject.getState() ) );    }}
public class HexaObserver extends Observer{    public HexaObserver(Subject subject){      this.subject = subject;      this.subject.attach(this);   }    @Override   public void update() {      System.out.println( "Hex String: "       + Integer.toHexString( subject.getState() ).toUpperCase() );    }}
步骤4:使用 Subject 和实体观察者对象
public class ObserverPatternDemo {   public static void main(String[] args) {      Subject subject = new Subject();       new HexaObserver(subject);      new OctalObserver(subject);      new BinaryObserver(subject);       System.out.println("First state change: 15");         subject.setState(15);      System.out.println("Second state change: 10");        subject.setState(10);   }}

3.2 模板方法模式

3.3 责任链模式

数据结构与算法

1、数据结构

2、算法

参考

1、Java基础

1.1 反射

  1. 面试官:Java 反射是什么?我回答不上来! (qq.com)

1.2 注解

  1. Java反射注解妙用(获取所有接口说明) (qq.com)
  2. 想自己写框架?不会写Java注解可不行 (qq.com)

1.3 异常

  1. 处理Java异常的10个最佳实践 (qq.com)
  2. Java 处理 Exception 的 9 个最佳实践! (qq.com)

1.4 I/O流

  1. 一文彻底理解 I/O 多路复用 (qq.com)
  2. 7000字带你死磕Java I/O流知识 (qq.com)

1.5 日期

  1. 死磕18个Java8日期处理,工作必用!收藏起来~ (qq.com)
  2. Java 8 异步 API、循环、日期,用好提高生产力! (qq.com)

1.6 代码规范

  1. 为什么阿里巴巴强制要求使用包装类型定义属性? (qq.com)

  2. 来了!你们要的万字 Java 知识地图 (qq.com)

  3. 常见For 循环优化方式,网友:有点骚! (qq.com)

  4. 如何实现Java类隔离加载? (qq.com)

  5. 小小的 float,藏着大大的学问 (qq.com)

  6. 扒了 Java 的皮,看见它的心 (qq.com)

  7. 千万不要这样写代码!9种常见的OOM场景演示! (qq.com)

  8. 【干货】连肝7个晚上,总结了关于Java基础的16个问题! (qq.com)

  9. 「万字图文」史上最姨母级Java继承详解 (qq.com)

  10. 图文并茂:HashMap 经典详解! (qq.com)

  11. 今天终于搞懂了:为什么 Java 的 main 方法必须是 public static void? (qq.com)

  12. 从String中移除空白字符的多种方式!?差别竟然这么大! (qq.com)

  13. 终于明白 Java 为什么要加 final 关键字了! (qq.com)

  14. 同事争论Java是不是值传递,总监甩出了几张图,深藏功与名! (qq.com)

  15. 一个工作三年的同事,居然还搞不清深拷贝、浅拷贝… (qq.com)

  16. 原来 Lamda 表达式是这样写的 (qq.com)

  17. 面试官:JavaBean为什么要重写hashCode()方法和equals方法 (qq.com)

  18. 4.6 W 字总结!Java 11—Java 17特性详解 (qq.com)

2、Java集合

  1. 面试官邪魅一笑: 你说说 Java8 的 ConcurrentHashMap ? (qq.com)
  2. 踩坑了,JDK8中HashMap依然会死循环! (qq.com)
  3. 干货 | 45张图庖丁解牛18种Queue,你知道几种? (qq.com)
  4. 你确定 LinkedList 在新增/删除元素时,效率比 ArrayList 高? (qq.com)
  5. 面试难题:List 如何一边遍历,一边删除? (qq.com)
  6. 有关 HashMap 面试会问的一切 (qq.com)
  7. 「我是大厂面试官」 Java 集合,你肯定也会被问到这些 (qq.com)
  8. Java 18 种队列图解,还有比这更好的安排吗? (qq.com)

2、并发编程

2.1 并发

  1. 多线程那些事,硬核有趣 (qq.com)
  2. 求求大厂给个Offer:多线程基础面试题 (qq.com)
  3. 有的线程它死了,于是它变成一道面试题 (qq.com)
  4. 2020年Java多线程与并发系列22道高频面试题解析 (qq.com)
  5. 10 张图聊聊线程的生命周期和常用 APIs (qq.com)
  6. 吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快? (qq.com)
  7. 打开线程/进程/协程的大门 (qq.com)
  8. 讲真,你是没懂 volatile 的设计原理,所以不会用 (qq.com)
  9. Java 线程池会自动关闭吗?

2.1 线程池

  1. 线程池的7种创建方式,强烈推荐你用它… (qq.com)
  2. 线程池最佳线程数量到底要如何配置? (qq.com)
  3. 2 w字长文带你深入理解线程池 (qq.com)
  4. 面试官问:高并发下,你都怎么选择最优的线程数? (qq.com)
  5. Java线程池的实现原理,你清楚么? (qq.com)
  6. 阿里巴巴建议的线程池创建方式,你用上了吗? (qq.com)
  7. 为什么阿里巴巴要禁用 Executors 创建线程池? (qq.com)
  8. 线程与锁 (qq.com)

2.2 JUC

  1. 1.5w字,30图带你彻底掌握 AQS! (qq.com)
  2. 面试官:CountDownLatch 与 CyclicBarrier 的使用场景?有什么区别? (qq.com)
  3. 终于有人把 CountDownLatch,CyclicBarrier,Semaphore 说明白了! (qq.com)
  4. 10分钟搞定 Java 并发队列好吗?好的! (qq.com)
  5. JUC 常用 4 大并发工具类 (qq.com)

2.3 锁机制

  1. 24张图带你彻底理解Java中的21种锁 (qq.com)
  2. 8000字 | 32 张图 | 一文搞懂事务+隔离级别+阻塞+死锁 (qq.com)

2.4 事务

  1. 30张图解:高并发服务模型哪家强? (qq.com)

  2. 科普:CPU缓存一致性协议 (qq.com)

  3. CPU缓存L1/L2/L3工作原理 (qq.com)

  4. 面试阿里被问:“你的项目是如何处理重复请求/并发请求的?” (qq.com)

  5. 模拟一次超过 5 万的并发用户,你会吗? (qq.com)

  6. 一网打进,高并发中的 限流、熔断、降级、预热、背压 (qq.com)

3、JVM

3.1 内存区域模型

  1. JVM:我就想知道我是怎么没的 (qq.com)
  2. JVM GC耗时频频升高,这次排查完想说:还有谁? (qq.com)
  3. 炸了!一口气问了我18个JVM问题! (qq.com)
  4. 教你如何通过分析GC日志来进行JVM调优 (qq.com)
  5. 生产环境JVM内存溢出案例分析! (qq.com)
  6. JAVA 线上故障排查完整套路,从 CPU、磁盘、内存、网络、GC 一条龙! (qq.com)
  7. 秒懂JVM的三大参数类型,就靠这十个小实验了 (qq.com)
  8. 面试官:你对JVM垃圾收集器了解吗?13连问你是否抗的住? (qq.com)
  9. 13 张图解 Java 中的内存模型 (qq.com)
  10. 77道JVM系列面试题总结(2万字解析) (qq.com)
  11. 阿里面试: 说说强引用、软引用、弱引用、虚引用吧 (qq.com)
  12. 手把手教你,线上服务频繁Full GC问题如何排查 (qq.com)
  13. 动图图解GC算法 - 让垃圾回收动起来! (qq.com)
  14. 面试官:谈谈你对G1垃圾收集器有哪些了解? (qq.com)
  15. JVM垃圾回收的 “三色标记算法” 实现,内容太干! (qq.com)
  16. JMH + Arthas,性能监控的神器 (qq.com)
  17. JMH - Java 微基准测试工具 (qq.com)

4、日志管理

  1. 工作总结!日志打印的15个建议 (qq.com)
  2. 最牛逼的 Java 日志框架,性能无敌,横扫所有对手(qq.com)
  3. Maven项目引入log4j的详细配置_小熊的专栏-CSDN博客_log4j pom
  4. 为什么推荐你使用logback取代slf4j

5、设计模式

  1. 如何理解这6种常见设计模式? (qq.com)
  2. 25000 字详解 23 种设计模式(多图 + 代码) (qq.com)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值