【记录】Java面试备战

一、Java基础

1、面向对象三大特性

封装:将类的某些信息用方法封起来,这样对外是不可见得,只能通过类的方法调用。

继承:继承是类与类之间的关系,子类继承父类可以使用父类除了private修饰的所有方法和属性,还可以重写父类的方法。提升了代码的复用性。

多态:一个对象的多种形态。比如动物可以是狗,也可以是猫。多态涉及到一个动态绑定。动态绑定是指程序在运行时虚拟机根据对象的具体类型与方法进行绑定。与之相对应的静态绑定,在程序运行前就能确定类与方法绑定关系。有final,static,private修饰的方法和构造方法都是静态绑定,其余是动态绑定。

2、泛型,类型擦除

泛型:即“参数化类型”。可用在类,接口和方法中。

为什么要用泛型:比如我们创建一个list,list默认支持的可传类型是object,我们可以向list添加任何类型。当同时添加不同类型的数据时,我们在对取到的数据进行强转时可能会出现无法强制转换,报“java.lang.ClassCastException"异常。

类型擦除:泛型信息只存在于代码编译阶段,编译后jvm虚拟机会将与泛型相关的信息擦除掉,这就叫类型擦除。

3、反射,原理,优缺点

反射的使用:
(1)获取类所有的构造方法

Test test = new Test();
Class c4 = test.getClass();
Constructor[] constructors ;
constructors = c4.getDeclaredConstructors();

(2)调用指定构造方法

Class c = Class.forName("Test3");//获取该类的class对象
Constructor constructors = c.getDeclaredConstructor(Integer.TYPE,String.class);//获取指定构造方法的Constructor对象
constructors.setAccessible(true);//允许调用私有的构造方法
Object test = constructors.newInstance(1,"2");//构造方法调用

(3)调用类的普通方法

Method m = c.getDeclaredMethod("fun");//获取指定方法的method对象
m.invoke(test);//调用方法

(4)访问类的私有字段并修改值

Field field = c.getDeclaredField("name");//获取字段的Field对象
field.setAccessible(true);
field.set(test,"asd");

反射的原理:
Java在编译之后会生成一个class文件,反射通过字节码文件找到其类中的方法和属性等。

优点:
(1)对于任意一个类,都能够知道这个类的所有属性和方法;
(2)对于任意一个对象,都能够调用它的任意一个属性和方法;
缺点:
(1)反射的性能较低;
(2)打破了类的封装性,通过反射可以获取该类所有私有属性和方法

4、static,final关键字

static:
(1)可以修饰方法,属性,语句块
(2)修饰方法时,该方法不能被重写,但可以被继承。并可以通过类直接访问该方法
(3)修饰属性时,jvm会将其分配到内存堆上,并只分配一次。所有对象共用这一个静态成员变量,当一个对象对其修改时,其他对象对该变量的值也会修改
(4)修饰语句块时,虚拟机会优先加载该语句块,主要用于初始化
(5)静态方法只能访问静态成员变量,非静态方法可以访问所有

final:
(1)可以修饰方法,变量,类
(2)修饰方法时,表示该方法不能被重写
(3)修饰类时,表示该类不能被继承
(4)修饰变量时,该变量在声明时除非在构造方法中初始化了,否则必须初始化。只能被初始化一次。引用性变量不能再指向其他对象,但可以修改该对象的内容

5、String , StringBuffer , StringBuilder 底层区别

(1)String是不可变字符串。其底层是通过final修饰的char数组存储字符串的。
(2)StringBuilder和StringBuffer是不可变字符串。而StringBuffer是线程安全的,其底层大多数方法都有synchorized修饰。

6、BIO、NIO、AIO

(1)BIO:同步并阻塞,数据的读写被阻塞在一个线程内,必须等待其完成才会处理下一个请求。当并发量过大时,即便使用线程池处理服务器暂时无法处理的连接也依然会导致服务器过载、限流等问题。适用于低并发,架构固定的场景
(2)NIO:同步非阻塞,客户端的连接请求会被注册到多路复用器Selector上,当多路复用器轮询到有IO请求时才会启动一个线程处理。适用于并发多连接短的场景。
(3)AIO:异步非阻塞,客户端的请求先由操作系统处理后再通知服务器启动线程处理,适用于并发多连接长的场景。

7、⾃动拆箱和⾃动装箱

自动将基本数据类型转为对应包装类型叫自动装箱,反之是自动拆箱。

Integer a = 10;//自动装箱
int b = a;//自动拆箱

二、Java集合框架

1、List

List存储的元素是有序可重复的。

1.1、Arraylist 与 LinkedList 区别

(1)ArrayList存储形式是object数组,LinkedList存储形式是双向链表。
(2)插入和删除是否受元素位置影响:①ArrayList受位置影响,在指定位置i插入元素时,时间复杂度为O(n-i),因为第i个和第n-i个元素都要往后移一位;②LinkedList不受位置影响,时间复杂度为O(n),每次指定位置插入时,循环遍历找到并移动到该位置
(3)ArrayList支持高效随机访问,而LinkedList不支持。ArrayList可以通过下标索引获取。
(4)占用空间:ArrayList占用内存除了存储的数组外,会预留一定容量的空间;而LinkedList的每一个元素除了包含数据本身外还包含直前驱和直后驱。

1.2、ArrayList 与 Vector 区别呢?为什么要⽤Arraylist取代
Vector呢?

区别:ArrayList不支持线程安全,而Vector底层方法有synchorized修饰,支持线程安全,但性能上ArrayList比Vector要好。

不需要支持线程安全时推荐使用Arraylist。

1.3、CopyOnWriteArrayList
CopyOnWriteArrayList是一个支持线程安全的ArrayList。使用fail-safe(安全失败)方式遍历。读操作无锁,写操作有锁。由于写操作会大面积的拷贝数组,消耗性能,所以CopyOnWriteArrayList适用于读多写少需要支持线程安全的场景。

2、Map

Map以键值对的形式存储元素。“key”是无序不可重复的,“value”是无序可重复的。

2.1、HashMap 的底层实现

HashMap 的底层是一个数组和链表相结合的链表散列。HashMap通过key的hashcode值经过扰动函数(HashMap的hash()方法),再计算(n-1)&hash来判断当前元素的位置,如果当前位置存在元素的话,就比较该元素和要存入元素的hash值和key值是否相同,如果相同,直接覆盖,如果不同,就利用拉链法解决冲突。所谓拉链法,即数组的每一个就是一个链表,当发生冲突时,只需将其加入到链表中即可。(jdk1.8后利用红黑树解决冲突)

2.2、HashMap 和 Hashtable 的区别

(1)HashMap是非线程安全的,HashTable是线程安全的(如需要线程安全,建议使用ConcurrentHashMap);
(2)HashMap的性能比HashTable好;
(3)HashMap支持null的key和value,只允许一个null的kei,但可以有多个null的value。而HahTable不支持。
(4)扩容机制:①对于不指定容量,HashMap的默认容量是16,每次扩容到原来的两倍,而HashTable的默认容量是11,每次扩容到原来的2n+1;②对于指定容量,HashMap不会直接使用给定的容量,而是2的幂次方,而HashTable则直接使用给定容量;
(5)底层数据结构:HashMap在链表长度大于阈值8时,会将链表转化为红黑树,以减少搜索时间,如果当前数组长度不足64会先进行数组扩容。而HashTable没有这样的机制。

2.3、LinkedHashMap底层原理

LinkedHashMap继承自HashMap,其底层也是数组加链表或红黑树的结构,不过LinkedHashMap多了一条双向列表,用于记录键值对的插入顺序,实现访问顺序的逻辑。

3、Set

Set存储的元素是无序不可重复的。

(1)HashSet:基于HashMap实现的,底层采用HashMap保存元素。
(2)LinkedHashSet:HashSet的子类,基于LinkedHashMap实现的。
(3)TreeSet:红黑树,自平衡的二叉树.

4、Queue

Queue(队列)是一种与Stack(栈)相对的数据结构,其特点是先进先出。

4.1、什么是PriorityQueue

PriorityQueue是一个优先级队列,它是基于优先堆得无界队列。其底层是通过二叉小顶堆(完全二叉树)实现。在优先队列中,数据会自然排序或按照特定规则排序(实例PriorityQueue时可传入比较器)。PriorityQueue存储的是对象时必须传入比较器Comparetor,否则会跑出异常。

4.2、PriorityQueue的主要方法

(1)add(E e),offer(E e):都是向队列添加元素的作用。但添加失败时的返回不一样,前者是抛异常,后者是返回false;
(2)element(),peek():都是获取队首的元素但不删除。但获取失败时 的返回不一一样,前者抛异常,后者是返回null;
(3)remove(),pool():都是获取队首的元素并删除。但获取失败时 的返回不一一样,前者抛异常,后者是返回null;

5、fast-fail,fast-safe机制

fast-fail(快速失败):直接在容器上进行遍历,一旦发现集合被修改,就会立刻抛出ConcurrentModificationException异常。一般出现在多线程环境下。ArrayList和HashMap有这样的机制。
fast-safe(安全失败):不会直接遍历容器本身,而是基于容器的一个克隆遍历,因此,在遍历时即便容器被修改了,也不会影响遍历。常见的使用fail-safe方式遍历的容器有ConcerentHashMap和CopyOnWriteArrayList等。

三、Java虚拟机

1、类加载机制、双亲委派模式、3种类加载器

1.1、什么是类的加载?

当程序需要支持使用某个类时,如果该类还未加载到内存中,则jvm会通过加载、链接、初始化三个过程来完成对类的初始化,这个过程叫类的加载。

1.1.1、加载

加载是指将类读入到内存中,并为之创建一个Java.lang.Class对象。这个过程需要依靠jvm提供的类加载器。当然,开发中也可以继承ClassLoader实现自己的类加载器。根据类加载器的不同,可以从不同来源获取类的二进制数据,主要来源有:
(1)从本地系统文件中加载class文件;
(2)通过jar包加载class文件;
(3)通过网络加载class文件;
(4)通过编译一个Java文件,实现加载;

1.1.2、链接

链接是指将类加载后生成的二进制数据合并到jre中,一般包含验证、准备、解析三个过程:
(1)验证:主要是验证被加载类的结构是否完整,以及是否符合虚拟机的要求,防止对虚拟机产生危害。主要包含文件验证、元数据验证、字节码验证、符号引用验证四种验证。
(2)准备:为静态变量分配内存,并设置默认初始值。
(3)解析:将二进制的数据的符号引用转化为直接引用。

1.1.3、初始化

给类的静态变量赋以正确的默认值。此过程与链接中的准备阶段是有区别的。例如:private static int a=10,首先类加载到内存中,然后进行验证,通过后为a分配内存,并赋给a一个为0的初始值,到初始化阶段才赋值a=10。

1.2、类加载机制

1.2.1、类加载机制主要有哪几种?

(1)全盘负责:当类加载器加载一个类时,会加载该类及其所依赖和引用的其他Class,除非显示有其他类加载器载入。
(2)双亲委派:首先尝试使用父类加载器加载该类,如加载不成功再从自己类的路径中加载该类。
(3)缓存机制:当一个类被加载后会被存入缓存区。之后加载时会先从缓存区中搜索该Class。

1.2.2、双亲委派原理?

当一个类加载器收到加载请求时,会首先委托给父类加载器,如果其父类也存在父类加载器,会继续向上委托,依次递归,最终到达顶层类。如果父类加载器无法完成加载,该类才会尝试自己加载。

1.2.3、双亲委派的优势?

主要可以防止类重新加载。比如一个类加载器将加载任务委托给父类加载器,然后父类加载器发现该类已被加载过,就会直接返回该类的class对象。这样也恰好防止了Java核心API被随意篡改。

1.3、类加载器

(1)根类加载器:主要负责加载<JAVA_HOME>/lib目录下并被虚拟机识别的类。例如java.util,java.lang等包下的类;
(2)扩展类加载器:主要负责加载<JAVA_HOME>/lib/ext下的类。例如swing系列,内置js,xml解析器;
(3)系统类加载器:主要加载用户路径上所指定的类。主要包括我们自己所写的类和第三方jar包里面的类;

2、运⾏时内存分区(Java虚拟机栈,本地⽅法栈,堆,⽅法区(永久代,元空间))

运行时内存分区主要有堆,方法区,本地方法栈,虚拟机栈,程序计数器。其中堆,方法区为线程共享,其余为线程私有。

2.1、程序计数器

相当于一种线程执行字节码的信号指示器,它指向下一条要执行的指令代码。也就是说线程可以通过程序计数器获取下一个需要执行的字节码指令。另外,每个线程都有自己的程序计数器,这样可以保证线程被切换后能恢复到支持的执行位置。

2.2、虚拟机栈和本地方法栈

两者的作用非常相似。都是为虚拟机调用方法服务。前者主要作用在java方法,而后者是Native方法。栈中存储的是栈帧,每一个方法在执行时都会创建一个栈帧,主要存储操作数栈,局部变量表和常量池引用等信息。两者都是私有的,可以避免局部变量不被其他线程访问到。

2.2.1、局部变量表

栈帧中用于存储数据的表。其存储的数据包含基本数据类型的局部变量,以及对象的引用(不存储对象的内容)。变量槽是局部变量表的最小的单位,一个变量槽可以存储32位的数据,long,double这种需要用到两个连续变量槽。jvm通过索引定位的方式使用局部变量表。对于静态方法和普通方法,索引为0的变量槽存储的数据不一样,普通方法存储的是调用该方法的对象的引用。

2.2.2、操作数栈

操作数栈具有栈的特性(后进先出),它可以存储所有Java数据类型。方法调用开始前,它是空的,通过字节码指令进行压栈和出栈操作。操作数方法栈通常用来计算或传递参数。因而可以说操作数栈就是一个栈帧用于计算的临时存储区。

2.3、堆

堆是Java虚拟机中最大的一块存储区域,主要存储被new出来的对象和数组。所有对象实例和数组都会被分配内存。垃圾收集器就是通过GC算法在堆上收集对象的占用内存空间(并非对象本身)。

2.3.1、年轻代和老年代

Java堆主要分为年轻代和老年代两块区域。年轻代主要存储新生对象,即我们新new出来的对象,当年轻代内存占满后会触发Minor GC,清理内存空间。老年代主要存储长期存活的对象和大对象。经过多轮Minor GC清理后仍然存活的年轻代对象会被移动到老年代对象。当老年代内存占满后,会触发Full GC,清理老年代内存。Full GC会清理所有年轻代和老年代的对象。

2.4、方法区

方法区主要存储静态变量,常量,类信息以及运行时的常量池。JDK1.8后由元空间取代了方法区,元空间并不在虚拟机中,而在内存中。

2.5、常量池

常量池主要存储final修饰的常量,基本数据类型的值,字符串,类,字段,方法和接口名。常量池避免了频繁的创建和销毁对象所带来的系统性能消耗。

2.5.1、Integer常量池

Integer a = 111;
Integer b = 111;
Integer c = new Integer(111);
Integer d = new Integer(111);
Integer e = 133;
Integer f = 133;
System.out.println(a==b);//true
System.out.println(c==d);//false
System.out.println(e==f);//false

对于a= =b为什么是true,而e= =f为什么是false,这其中涉及一个装箱过程,首先会将int类型的“111”和“133”调用valueOf()方法进行装箱,该方法会判断该数是否在-127到128之间,如果在之间返回常量池的内容,否则直接创建一个新的Integer对象。“133”不在Integer常量池存储的范围,所以创建了新对象导致e==f为false。

2.5.2、String常量池

String str1 = "asd";
String str2 = "asd";
String str3 = new String("asd");
String str4 = new String("asd");
String str5 = "a";
final String str6 = "a";
String str7 = "ab";
String str8 = str5+"b";
String str9 = str6+"b";
System.out.println(str1==str2);//true
System.out.println(str3==str4);//false
System.out.println(str7==str8);//false
System.out.println(str7==str9);//true

对于str1==“asd”,首先会去常量池中寻找有没有“asd”,如果有,则将str1指向常量池中的asd,如果没有,则将asd存储到常量池中,然后str1指向常量池中的asd。所以str1= =str2为true。对于str3和str4,由于在堆上创建了不同的对象,所以str3==str4为false。str8和str9都含有“+”运算,且都是字符串引用加字符串常量,普通字符串引用在编译器无法确定其值,只能在运行时动态分配并将新的地址给str8,所以str7= =str8为false。而str6由于有final修饰,是一个常量,在编译期就能确定,所以str7= =str9为true。

3、JMM:Java内存模型

Java内存模型是基于底层处理器内存模型实现的,只是多了一项Happens-Before规则,它保证了线程间的可见性和有序性。该规则分如下几种:
(1)单线程规则:一个线程中的每个动作都Happens-Before该线程后续的每个动作;
(2)监听器锁定规则:一个监听器的解锁动作都Happens-Before对该监听器的锁定动作;
(3)volatile变量规则:对volatile字段的写入动作都Happens-Before对volatile的读取动作;
(4)线程start()规则:线程start()方法的执行都Happens-Before一个启动线程内的任意动作;
(5)线程join()规则:一个线程的任意动作都Happens-Before其他线程在该线程的join()返回之前;
(5)传递性:A动作Happens-Before B动作,B动作Happens-Before C动作;那么A动作Happens-Before C动作;

volatile,final,synchronized:这三个关键字旨在满足开发者的并发要求:
(1)volatile:保证被修饰变量的可见性,防止jvm指令重排;
(2)final:通过禁止在构造方法初始化和给变量赋初始值,保证可见性;
(3)synchronized:保证可见性和有序性,通过管道保证原子性;

4、引⽤计数、可达性分析

这两种算法主要用来判断对象是否存活。

4.1、引用计数算法

该算法就是给每个对象都添加一个计数器,当该对象被引用时,计数器加1,当引用失效时,计数器减1.任何时候一个对象计数为0表示该对象不能再被使用了。此算法的优点是简单易执行,但容易出现对象间的相互循环引用。比如objA.instance = objB,objB.instance = objA;此时objA和objB就出现了相互引用,计数器再不可能为0,这两个对象也就永远不能被垃圾回收器回收。

4.2、可达性分析算法

该算法主要思路是将一系列对象看做命名为GC Roots的节点,然后从这些节点向下搜索,经过的路径叫引用链。当一个对象到GC Roots没有任何引用链时,这个对象就不会再被使用,被垃圾收集器回收。

4.3、哪些对象可以作为GC Roots?

(1)虚拟机栈中的引用的对象;
(2)方法区中类的静态引用的对象;
(3)方法区中常量引用的对象;
(4)本地方法栈中Native方法引用的对象;

5、垃圾回收算法:标记-清除,标记-整理,复制

5.1、标记-清除算法

该算法的主要思路是先标记出经过可达性分析算法分析的垃圾对象,然后将标记的对象直接清除。这种算法的优点是操作简单,无需移动对象,但很容易产生无法存储大对象的内存残片。

5.2、复制算法

该算法的主要思路是将堆内存分为两块,每次只使用其中一块,当这一块内存用完后,就将存活的对象复制到另一块内存,最后将这块内存直接清除掉。优点是操作简单,不会产生内存残片。缺点是内存被缩小一半,算法的效率和存活的对象数目有关,存活数目越多,效率越低。

5.3、标记-整理算法

该算法结合了前两个算法的优点,即先将垃圾对象进行标记,然后将存活对象移动到一端,再清除掉端界意外的内存。

5.4、分代收集算法

现在垃圾收集器都采用此算法。此算法并没有什么新思想,只是把内存分为了新生代和老年代,然后根据各自特点采用更加合适的收集算法。新生代的特点是对象的使用周期大都很短,因此存活率低,会产生大量的无用对象,所以采用复制算法更合适。而老年代的对象大都存活很长,只会有少量对象是无用对象,因而采用标记-清除算法或者标记-整理算法更合适。

6、强、软、弱、虚引⽤

(1)强引用:我们平时使用的对象普遍都是强引用。此类对象被垃圾收集器认为是必不可少的,即便内存溢出,也不会去回收此类对象,除非强引用失效。
(2)软引用:表示一些有用但并不是必不可少的对象,当内存不足的时候,会被回收掉。java.lang.ref.SoftReference类表示与软引用相关联的对象。
(3)弱引用:表示非必须对象,当jvm进行垃圾回收时,会直接回收该类对象。java.lang.ref.WeakReference类表示与弱引用相关联的对象。
(4)虚引用:如果一个对象与虚引用关联,表示该对象随时会被垃圾收集器回收。

7、内存溢出、内存泄漏排查

https://blog.csdn.net/fishinhouse/article/details/80781673

四、Java并发

1、三种线程初始化⽅法( Thread 、 Callable , Runnable )区别

1.1、继承Thread,重写run()方法:

public class ThreadTest extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread t1 = new ThreadTest();
        t1.start();
    }
}

1.2、实现Runnable接口,重写run()方法

public class ThreadTest implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        Thread t1 = new Thread(threadTest);
        t1.start();
    }
}

1.3、实现Callable接口,重写call()方法:

import java.util.concurrent.*;

public class ThreadTest implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return 1000;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        ThreadTest threadTest = new ThreadTest();
        Future<Integer> future = executorService.submit(threadTest);
        try {
            Integer integer = future.get();
            future.cancel(false);
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

1.4、区别:

(1)类只支持单继承。接口可以多实现。所以继承Thread类创建线程不如实现接口那么灵活;
(2)Callable相对于Runnable,重写的是call(),而不是run();并且可以有返回值,可以抛出异常;

2、线程池

2.1、为什么要使用线程池

(1)降低资源消耗:通过重复利用已创建的线程,来降低频繁创建和销毁线程带来的资源消耗;
(2)提高相应速度:请求的任务可以不需要等待线程创建完成,立即执行;
(3)提高对线程的管理性:使用线程池可以实现对线程的统一分配,调优和监控;

2.2、为什么不推荐使用Executors创建线程池

(1)FixedThreadPool和SinglleThreadExecutor:可能会导致任务大量堆积,出现内存溢出;
(2)CachedThreadPool和ScheduledThreadPool:可能会创建大量的线程,出现内存溢出;

2.3、ThreadPoolExecutor构造方法

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }

corePoolSize:线程池最少维护线程数(核心线程);
maximumPoolSize:线程池最大维护线程数;
keepAliveTime:线程允许的最大空闲时间;
unit:空闲时间单位;
workQueue:缓冲队列;
handler:线程池的拒绝策略;

2.4、线程池实现原理:
(1)任务通过线程池的execute()或submit()进入线程池;
(2)当线程池中线程数小于corePoolSize时,即使线程池中有空闲线程,也会去创建线程来处理任务;
(3)当线程池中线程数等于corePoolSize,且无空闲线程时,任务会被发送到workQueue缓冲队列;
(4)当线程池中线程数等于corePoolSize,且无空闲线程,缓冲队列已满,则创建线程处理任务;
(5)当线程池中线程数等于maximumPoolSize,且无空闲线程,缓冲队列已满,则调用线程池的拒绝策略handler处理任务;

2.5、线程池的拒绝策略

(1)ThreadPoolExecutor.AbortPolicy():抛出java.util.concurrent. RejectedExecutionException异常;
(2)ThreadPoolExecutor.CallerRunsPolicy(): 重试添加当前的任务,他会自动重复调用execute()方法;
(3)ThreadPoolExecutor.DiscardOldestPolicy(): 抛弃旧的任务;
(4)ThreadPoolExecutor.DiscardPolicy(): 抛弃当前的任务;

2.6、execute()和submit()的区别

(1)execute():用于提交没有返回值的任务,所以不能判断任务是否被线程执行完成;
(2)submit():用于提交有返回值的任务,方法会返回一个Future对象,利用该对象的get()可以获得返回的值。get()会阻塞当前线程直到任务完成;⽽使⽤ get(long timeout,TimeUnit unit) ⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。

2.7、线程池常见四种类型

(1)FixedThreadPool:创建一个有固定数量线程的线程池。当一个新任务提交时,有空闲线程则直接执行,没有则将任务发送到缓冲队列,待有空闲线程再继续执行;
(2)SIngleThreadExecutor:创建只有一个线程的线程池。当一个新任务提交时,该线程空闲则直接执行,没有则将任务发送到缓冲队列,待线程空闲再继续执行;
(3)CachedThreadPool:创建一个线程数量随实际情况发生变化的线程池。当任务提交时,若有空闲线程可复用,则直接执行;没有则创建新线程区执行任务;
(4)ScheduledThreadPool:创建一个线程数量随实际情况发生变化的线程池。一般用于定时任务。

3、乐观锁和悲观锁

乐观锁,每次线程执行任务时不会加锁,遇到冲突时,会重复执行。主要实现方式是CAS,适用于读多写少的情况。优势是减少系统资源消耗,提高系统吞吐量;缺点是会产生ABA问题;
悲观锁,每次线程执行任务时会加锁,其他线程会被挂起,直到任务完成。synchronized就是一种悲观锁,适用于写多读少的情况。优势是可以解决高并发带来的数据不一致的问题,缺点是加锁需要消耗系统资源。

3.1、什么是CAS,什么是ABA问题?

CAS:对于内存中的某个值V,提供一个旧值A和一个新值B,如果A与V相等,就把B值写入V;

ABA问题:一个线程把V修改成了A,此时另一个线程又把A修改成B再修改成A,但这个线程通过CAS并不能发现V值是被修改过。

3.2、对synchronized的了解

synchronized主要解决多线程之间访问资源的同步性问题,可以保证被该关键字修饰的方法,代码块任何时间只有一个线程可以访问。构造方法不能被synchronized修饰,因为构造方法本身就是线程安全的。早期的synchronized是一个重量级锁,因为对线程的挂起和唤醒操作需要依靠操作系统来完成,很耗费资源和时间。在JDK1.6后,对synchronized有了很大优化,如⾃旋锁、适应性⾃旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。因此现在很多API的底层都是用到了synchronized。

3.3、synchronized的使用场景

(1)修饰普通方法:对当前对象实例加锁。要调用该方法就必须获得当前对象实例的锁;
(2)修饰静态方法:对当前类加锁,作用于当前类的所有对象实例,要调用该方法必须获得当前类的class对象的锁。值得注意的是,因为静态成员属于类,当一个线程调用对象的普通synchronize的方法时,另一个线程调用静态的synchronized方法是允许;
(3)修饰代码块:指定加锁对象,既可以是某类的class对象,也可以是一个对象的实例;

3.4、手写一个单例模式?

public class SingleTest {

    private static volatile SingleTest singleTest;

    private SingleTest(){

    }

    public static SingleTest getInstance(){
        if(singleTest==null){
            synchronized (SingleTest.class){
                if(singleTest==null){
                    singleTest = new SingleTest();
                }
            }
        }
        return singleTest;
    }
}

对于singleTest = new SingleTest(),实际上分三步进行:
(1)为singleTest 分配内存空间;
(2)初始化singleTest ;
(3)将singleTest 指向分配的内存地址;
由于jvm存在指令重排的特性,因此可能会获得一个没有初始化的实例。使用volatile 可以解决指令重排的问题。

4、ReentrantLock

ReentrantLock的功能和synchronized类似,都能实现独占锁的功能。但ReentrantLock相比synchronized而言功能更加丰富,更加灵活。它的加锁和释放锁需手动进行,不易操作。ReentrantLock可以实现公平锁机制,即等待时间最长的线程拥有优先获得锁的权利。

4.1、ReentrantLock实现

简易实现:

public class ReentrantLockTest {

    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        Runnable runnable1 = ReentrantLockTest::test;
        Runnable runnable2 = ReentrantLockTest::test;
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                2,4,3,TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),new ThreadPoolExecutor.DiscardPolicy()
        );
        poolExecutor.execute(runnable1);
        poolExecutor.execute(runnable2);
        poolExecutor.shutdown();
    }

    private static void test(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"获得了锁");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName()+"释放了锁");
            lock.unlock();
        }
    }
}

公平锁:

public class ReentrantLockTest {

    private static final Lock lock = new ReentrantLock(true);

    public static void main(String[] args) {
        Runnable runnable = ReentrantLockTest::test;
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                2,4,3,TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1),new ThreadPoolExecutor.DiscardPolicy()
        );
        for(int i=0;i<5;i++){
            poolExecutor.execute(runnable);
        }
        poolExecutor.shutdown();
    }

    private static void test(){
        for(int i=0;i<2;i++){
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"获得了锁");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

在ReentrantLock的构造方法传入true表示支持公平锁。这里我们向线程池中一次性提交了5个任务,线程池使用了四个线程执行任务,第三个被加入的任务暂时存入了缓冲队列。运行之后如下图:
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值