Java基础

8大基础类型

类型 存储需求 取值范围 默认值
byte 1字节 -128 ~ 127 0
short 2字节 -32768 ~ 32767 0
int 4字节 -2^31 ~ 2^31 - 1(正好超过20亿) 0
long 8字节 -2^63 ~ 2^63 - 1 0L
float 4字节 大约 ± 3.402 823 47 E + 38 F ( 有效位数为 6 ~ 7 位 ) +0.0F
double 8字节 大约 ± 1.797 693 134 862 315 70 E + 308 ( 有效位数为 15 位 ) +0.0D
char 2字节 使用unicode编码,最大65535 ‘\u0000’
boolean 取决于虚拟机,1字节或4字节。 true或false。 false

boolean占几个字节?

  • boolean在编译后会使用java虚拟机中的int数据类型来代替,占4个字节,而boolean数组会被编译成java虚拟机的byte数组,每个元素占1个字节。

自动装箱和拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来
  • 拆箱:将包装类型转化为基本数据类型
  • (1) 先判断i的大小,如果小于-128,或者大于等于127,就创建一个新的Integer对象,否则取cache中的数据,cache是一个静态Integer数组对象,存放了-128到127之间的数据,类似缓存池的功能。
  • (2) 当一个基础数据类型与封装类进行==、+、-、*、/运算时,会将封装类进行拆箱,对基础数据类型进行运算。

Integer的常量池

  • Integer i = value,如果value是在-128到127之间,不会去堆中创建对象,而是返回IntegerCache中的值。如果值不在上面范围内,则会从堆中创建对象。=走的是valueOf()方法,该方法会调用缓存。
  • Integer i2 = new Integer(xxx); 不管参数的value是多少都会从堆中创建对象。

==、equals和hashcode

  • == 的作用是判断两个对象的地址是否相等。 基本类型比较的是值,引用类型比较的是地址。
  • equals作用也是判断两个对象是否相等。基本类型比较值,引用类型分为两种情况:

    • 如果类没有覆盖equals()方法,则通过equals()比较该类的两个对象时,等价于通过==比较这两个对象。
    • 类覆盖了equals()方法,等价于通过比较两个对象的内容来判断两者是否相等。
  • hashcode:hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回该对象在哈希表中的索引位置。若两个对象具有相同的hascode值,则进一步使用equals方法比较它们是否相等。反之则一定不相等。Equals()方法被覆盖过,则hasCode方法也必须被覆盖。

四种访问修饰符的限制范围

  • public:可以被所有其他类所访问。
  • private:只能被自己访问和修改。
  • protected:自身,子类及同一个包中类可以访问。
  • default(默认):同一包中的类可以访问

面向对象的三大特性

  • (1) 封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。
    (2) 继承是使用已存在的类的定义作为基础建立新类的技术,通过使用继承我们能够非常方便地复用以前的代码。子类不能访问父类的私有属性和方法,只是拥有。
    (3) 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时并不确定,而是在程序运行期间才确定。在java中可以通过继承和接口实现多态。

  • 多态的表现形式:

    • java的方法重载
    • java的方法重写

重载和重写的区别

  • 重载的方法名必须相同,参数列表必须不同。返回值和访问修饰符无影响。
  • 重载发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,方法返回值和访问修饰符可以不同。
  • 重写是子类对父类允许访问的方法的实现过程的重新编写,发生在子类中。方法名和参数列表必须相同,返回值小于等于父类,抛出的异常范围小于等于父类,访问修饰符大于等于父类。

多态的理解

  • (1) 程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译期是不确定的,而在程序运行期间才确定。比如使用父类对象引用子类对象,再调用父类的方法时,会表现出不同结果。
  • (2) 多态存在的三个条件:有继承关系,子类重写父类方法、父类引用指向子类对象。
  • Human man = new Man()该代码中的Human称为变量的静态类型,等号后面的Man称为变量的实际类型。变量本身的静态类型不会被改变,并且最终的静态类型是在编译期是可知的。而实际类型变化的结果在运行期才可确定。
  • 静态分派

    • 所有依赖静态类型来决定方法执行版本的分派动作称为静态分派。静态分派最经典的表现形式是方法重载。
    • 虚拟机在重载时通过参数的静态类型而不是实际类型作为判定依据。由于静态类型在编译期可知,所以在编译阶段,java编译器就根据参数的静态类型决定了会使用哪个重载版本。
  • 动态分派

    • 在运行期间根据实际类型确定方法执行版本的分派过程称为动态分派,表现形式是重写。

函数式接口、匿名内部类、lamda表达式

  1. public class JucTest {
  2. public static void main(String[] args) throws InterruptedException {
  3. /**
  4. * 匿名内部类,直接实现接口
  5. */
  6. Runnable haha = new Runnable() {
  7. @Override
  8. public void run() {
  9. System.out.println("haha");
  10. }
  11. };
  12. new Thread(haha).start();
  13. /**
  14. * lambda表达式,由匿名内部类简化而来的
  15. */
  16. new Thread(()->
  17. System.out.println("haha")).start();

匿名内部类访问的局部变量为什么必须要用final修饰?

  • 匿名内部类之所以可以访问局部变量,是因为在底层将这个局部变量的值传入到了匿名内部类中,并且以匿名内部类的成员变量的形式存在,这个值的传递过程是通过匿名内部类的构造器完成的。
  • 用final修饰是为了保护数据的一致性。对引用变量来说就是引用地址的一致性,对基本类型来说,就是值的一致性。
  • 在JDK8之前,如果我们在匿名内部类中需要访问局部变量,那么这个局部变量必须用final修饰符修饰,在JDK8中如果我们在匿名内部类中需要访问局部变量,那么这个局部变量不需要用final修饰符修饰。底层还是加了final,我们无法改变这个局部变量的引用值,如果改变就会编译报错。

(String) toString() 和 String.valueOf()的区别

  • 第一个是强转,如果遇到不是字符串类型的对象,强转会报错
  • 第二个是依赖于对象的,若对象为空就会报异常
  • 第三个内部做了为空的判断的,最安全。

String,StringBuffer和StringBuilder的区别是什么?

  • string类中使用final关键字修饰字符数组来保存字符串,所以不变。
  • 而后两者继承自AbstractStringBuilder类,在类中使用字符数组保存字符串,所以是可变的。
  • string和stringbuffer是线程安全的,但StringBuilder没有对方法加同步锁,所以非线程安全。
  • 性能:

    • 每次对String类型进行改变时,都会生成一个新的String对象,然后将引用指向新的String对象。
    • StringBuffer每次都会对StringBuffer对象本身进行操作,相同情况下,使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险.

String不可变的优点

  • (1) 便于实现字符串常量池。字符串常量池的实现可以在运行时节约很多的堆空间,不同的字符串变量都指向池中的同一个字符串。
    (2) 避免网络安全问题。数据库的用户名、密码都是以字符形式传入来获得数据库连接,这些信息不能被轻易篡改。
    (3) 保证多线程安全。
    (4) HashCode缓存。由于字符串 hashcode 属性不会变更,保证了唯一性,使得类似 HashMap,HashSet 等容器才能实现相应的缓存功能。由于 String 的不可变,避免重复计算 hashcode,只有使用缓存的 hashcode 即可,这样一来大大提高了在散列集合中使用 String 对象的性能。

new String(xxx) 创建几个对象?

  • String str1 = new String(“ABC”)会创建多少个对象?

    • 一个或两个。如果常量区有ABC的值,则只在堆中创建一个对象。
    • 如果常量池没有,则还会在常量池中创建”ABC”。
    • 怎么得知的呢?通过查看字节码,由ldc指令得知的。
  • String str1 = new String(“A”+”B”) ; 会创建多少个对象?

    • 常量池 三个 “A”,”B”,“AB”

    • 堆 一个 new String("AB")

    • 总共 4个

    • 对应字节码文件

      1. 0 new #13 <java/lang/String>
      2. 3 dup
      3. 4 ldc #18 <AB>
      4. 6 invokespecial #15 <java/lang/String.<init>>
      5. 9 astore_1
      6. 10 return
  • String str2 = new String(“ABC”) + “ABC” ; 会创建多少个对象?

    • 对象1 new StringBuilder()

    • 对象2 new String("ABC")

    • 对象3 常量池中的 ”ABC”

    • 对象4:builder.toString() 方法相当于 new String("ABCABC") (更加深入)

    • 总共4个

    • 对应字节码文件

      1. 0 new #6 <java/lang/StringBuilder>
      2. 3 dup
      3. 4 invokespecial #7 <java/lang/StringBuilder.<init>>
      4. 7 new #13 <java/lang/String>
      5. 10 dup
      6. 11 ldc #14 <ABC>
      7. 13 invokespecial #15 <java/lang/String.<init>>
      8. 16 invokevirtual #8 <java/lang/StringBuilder.append>
      9. 19 ldc #14 <ABC>
      10. 21 invokevirtual #8 <java/lang/StringBuilder.append>
      11. 24 invokevirtual #10 <java/lang/StringBuilder.toString>
      12. 27 astore_1
      13. 28 return
  • String str3 = new String(“A”) +new String(“B”); 会创建多少个对象?

    • 对象1 new StringBuilder()

    • 对象2 new String("A")

    • 对象3 常量池中的“A”

    • 对象4 new String("B")

    • 对象5 常量池中的“B”

    • 对象6 builder.toString() 方法近似于 new String("AB"),强调一下,常量池里面并没有生成AB

    • 总共6个

    • 对应字节码文件:

      1. 0 new #6 <java/lang/StringBuilder>
      2. 3 dup
      3. 4 invokespecial #7 <java/lang/StringBuilder.<init>>
      4. 7 new #13 <java/lang/String>
      5. 10 dup
      6. 11 ldc #16 <A>
      7. 13 invokespecial #15 <java/lang/String.<init>>
      8. 16 invokevirtual #8 <java/lang/StringBuilder.append>
      9. 19 new #13 <java/lang/String>
      10. 22 dup
      11. 23 ldc #17 <B>
      12. 25 invokespecial #15 <java/lang/String.<init>>
      13. 28 invokevirtual #8 <java/lang/StringBuilder.append>
      14. 31 invokevirtual #10 <java/lang/StringBuilder.toString>
      15. 34 astore_1
      16. 35 return

String注意事项

  • 要点:

    • 常量与常量的拼接,结果放在常量池中,原理是编译期优化
    • 常量池中不会存在相同内容的对象。
    • 只要其中一个是变量,结果就放在堆中。变量拼接的原理是StringBuilder
    • 若拼接的结果调用intern()方法,则主动将常量池还没有的字符串对象放入池中,并返回此对象地址。
    • 通过StringBuilderappend()方式添加字符串的效率要远高于使用String的字符串拼接方式。

      • 前者只创建过一个StringBuilder对象, 后者在每次循环中都要创建一个新的StringBuilderString对象。
      • 后者由于内存中创建了较多的StringBuilderString对象,内存占用更大,如果进行GC,需要花费额外的时间。
  • 代码示例: ```java @Test public void test1(){ String s1 = “a”+”b”+”c”; String s2 = “abc”; /**
    1. 执行细节:
    2. 常量池中创建了三个变量 "a" "ab" "abc"
    3. **/
    System.out.println(s1==s2);//true }

@Test public void test2_1(){ String s1 = “a”; String s2 = s1 + “b”; String s3 = “ab”; /** 执行细节: 1 StringBuilder s = new StringBuilder(); 2 s.append(“a”) 3 s.append(“b”)

  1. 4. s.toString(); 类似于 new String("ab")
  2. 补充 jdk5.0之后使用的是StringBuilder,以前使用StringBuffer
  3. **/
  4. System.out.println(s3==s2);

}

@Test public void test2(){ String s1 = “a”; String s2 = “b”; String s3 = “ab”; /** 执行细节: 1 StringBuilder s = new StringBuilder(); 2 s.append(“a”) 3 s.append(“b”)

  1. 4. s.toString(); 类似于 new String("ab")
  2. 补充 jdk5.0之后使用的是StringBuilder,以前使用StringBuffer
  3. **/
  4. String s4 = s1+s2;
  5. System.out.println(s3==s4);

}

@Test public void test3(){ String s1 = null; String s2 = “b”; String s3 = s1+s2; /**

  1. * 执行细节:
  2. * 1 StringBuilder s = new StringBuilder()
  3. * 2 s.append(s1)
  4. * 3 s.append("b")
  5. * 4 s.toString(); 类似于new String("nullb")
  6. */
  7. System.out.println(s3);

}

@Test public void test4(){ final String s1 = “a”; final String s2 = “b”; String s3 = “ab”; String s4 =s1 + s2; //从字符串常量池中取的 System.out.println(s3==s4);//true }

@Test public void test(){ String s1 = “hello”; String s2 = “world”; String s3 = “helloworld”; String s4 = “hello” + “world”; String s5 = s1 + “world”; String s6 = “hello” + s2; String s7 = s1 + s2;

  1. System.out.println(s3==s4);//true
  2. System.out.println(s3==s5);//false
  3. System.out.println(s3==s6);//false
  4. System.out.println(s3==s7);//false
  5. System.out.println(s5==s6);//false
  6. System.out.println(s5==s7);//false
  7. System.out.println(s6==s7);//false
  8. String s8 = s6.intern();
  9. System.out.println(s3==s8);//true

}

  1. -
  2. `StringBuilder`append()方法:
  3. -
  4. ```java
  5. private StringBuilder append(StringBuilder sb) {
  6. if (sb == null)
  7. return append("null");
  8. int len = sb.length();
  9. int newcount = count + len;
  10. if (newcount > value.length)
  11. expandCapacity(newcount);
  12. sb.getChars(0, len, value, count);
  13. count = newcount;
  14. return this;
  15. }

intern()方法

  • jdk6: 执行intern()方法时,若常量池中不存在等值的字符串,JVM就会在池中创建一个等值的字符串,然后返回该字符串的引用。

  • jdk7: 执行intern()方法时,若常量池中已存在该字符串,则直接返回字符串引用,否则复制该字符串的引用到常量池中并返回。

  • 例子:

    1. public static void main(String[] args) {
    2. String s = new String("1"); //创建了两个对象,一个堆中的“1”,一个常量池中的“1”
    3. s.intern(); //没有作用,因为常量池中有"1"了
    4. String s2 = "1";
    5. System.out.println(s==s2);// jdk6 false jdk8 false
    6. String s3 = new String("1")+new String("1");//虽然创建了6个对象,但常量池中没有“11”
    7. s3.intern();//对于1.8来说,直接复制引用到常量池。对于1.6则是创建了一个新对象
    8. String s4 = "11";
    9. System.out.println(s3==s4);// jdk6 false jdk8 true
    10. }
  • 分析
    java基础、集合、多线程 - 图1

final关键字

  • 主要用在三个地方:变量、方法、类
  • 如果是基本数据类型的final变量,则其数值一旦在初始化之后便不能更改。如果是引用类型的变量,则在对其初始化之后便不能让其再指向另外一个对象。
  • 当用final修饰一个类时,表明这个类不能被继承。final类中所有方法都会被隐式地指定为final方法。
  • final方法不能被重写。如果在父类中使用final来修饰方法,那么该方法就被定义为private,即不可重写,方法被私有化了。
  • 好处:

    • final关键字提高了性能,JVM会缓存final变量
    • final变量可以在多线程的环境下保持线程安全
    • 使用final关键字提高了性能,JVM会对方法变量类进行优化。

static关键字

  • 修饰成员变量和成员方法。被static修饰的成员属于类,被类中所有对象共享,建议通过类名调用。
  • 静态代码块。静态代码块在非静态代码块之前执行。(静态代码块—>非静态代码块—>构造方法)
  • 静态内部类。静态内部类和非静态内部类之前存在一个最大区别:非静态内部类在编译完成之后会隐含的保存一个引用,该引用时指向创建它的外部类,但静态内部类没有。没有这个引用意味着:1.它的创建不需要依赖外部类的创建。2.它不能使用任何外部类的非static成员变量和方法。
  • 静态导包:import static这两个关键字连用可以指定导入某个类的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。

接口和抽象类的区别

  • 接口的默认方法是public,所有方法在接口中都不能有实现。(java8后接口的default方法可以有默认实现)而抽象类可以有非抽象的方法。
  • 接口中除了static final变量,不能有其他变量,而抽象类中不一定。
  • 一个类可以有多个接口,但只能继承一个抽象类。接口本身可以用extends关键字扩展多个接口。
  • 接口方法默认修饰符是public,抽象方法可以有public,protected和default这些修饰符。
  • 抽象类有构造方法,接口没有构造方法

成员变量和局部变量的区别

  • 成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数,成员变量可以被访问修饰符所修饰,而局部变量不能被访问修饰符以及static所修饰。
  • 成员变量属于实例或类,存放在堆中,局部变量存放在栈中。
  • 成员变量以类型的默认值赋初值,而局部变量必须赋初值。

java创建对象的方式

  • (1) New
    (2) 反射机制 Constructor的newInstance ()方法
    (3) 调用对象的clone()方法创建对象。
    (4) 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法.

深浅拷贝

  • 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递
  • 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容。

Object对象的方法

    1. public final native Class<?> getClass();//获取Class对象
    2. public native int hashCode(); // 返回对象的哈希代码值。
    3. public boolean equals(Object obj) //判断对象内容是否相等
    4. protected native Object clone() // 创建并返回此对象的副本。
    5. public String toString() // 返回对象的字符串表示形式。
    6. public final native void notify(); // 唤醒正在该对象的监视器上等待的单个线程。
    7. public final native void notifyAll(); // 唤醒正在该对象的监视器上等待的全部线程。
    8. public final native void wait(); // 使当前线程等待,直到另一个线程调用此对象的方法或方法。
    9. protected void finalize(); // 当垃圾回收确定不再有对对象的引用时,由对象上的垃圾回收器调用。

Double和float为什么不能相互转义

  • (1) 因为double和float是浮点数,浮点数在计算机中只是近似表示,对浮点数的运算结果具有不可预知性。
    (2) java中float为四个字节,double为八个字节,float转double时会补位,可能出现数据误差。Double转float可能会丢失精度。
    (3) 解决办法:先将double型和float型转换为字符串型,再转换为精度更高的BigDecimal型,然后进行运算。

序列化和反序列化

  • 序列化:将对象写入到IO流中

  • 反序列化:从IO流中恢复对象

  • (1) 序列化是把java对象转换为字节序列的过程。实现了Serializable或者Externalizable接口的类才能被序列化。ObjectOutputStream的writeObject()方法将传入的obj对象进行序列化,把得到的字节序列写入到目标输出流中进行输出。
    (2) 反序列化是把字节序列恢复为java对象的过程。ObjectInputStream的readObject()方法从输入流中读取字节序列,然后将字节序列反序列化为一个对象并返回。
    (3) 序列化有两种用途,一是把对象的字节序列永久地保存到硬盘上,二是在网络上传送对象的字节序列。

java序列化算法

  • 所有保存到磁盘的对象都有一个序列化编号
  • 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象在此虚拟机从未被序列化过,才会将此对象序列化为字节序列输出。
  • 如果此对象已经序列化过,则直接输出编号即可。
  • 潜在的问题:

    • 由于java序利化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。
    • 使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。

java序列化的三种方式

  • (1) 实现serializable接口(隐式序列化),serializable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,会自动序列化所有非static和transient关键字修饰的成员变量。

    • 序列化步骤:

      • 创建一个ObjectOutputStream输出流
      • 调用ObjectOutputStream对象的writeObject输出可序列化对象
      • 如果一个可序列化的类的成员是引用类型,那么这个引用类型也必须是可序列化的,否则会导致此类不能序列化。
    • 反序列化步骤:

      • 创建一个ObjectInputStream输入流
      • 调用ObjectInputStream对象的readObject得到序列化对象
      • 反序列化不调用构造方法,反序列化的对象是由JVM自己生成的对象,不通过构造方法生成。
  • (2) 实现externalizable接口(显示序列化),在实现该接口时,必须实现writeExternal()和readExternal()方法,手动进行序列化,这个序列化过程是可控的。虽然externalizable接口带来了一定的性能提升,但编程复杂度也提高了,所以一般通过实现serializable接口进行序列化。 | 实现Serializable接口 | 实现Externalizable接口 | | :—- | :—- | | 系统自动存储必要的信息 | 程序员决定存储哪些信息 | | Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 | | 性能略差 | 性能略好 |

Synchronized底层原理

  • (1) synchronized代码块底层使用monitorenter和monitorexit指令,其中monitoreneter指令指向同步代码块的开始位置,monitorexit指明同步代码块的结束位置。
  • (2) Monitor对象在每个java对象的对象头中,synchronized通过获取monitor对象获取锁,当计数器为0可以成功获取,获取后锁计数器+1,执行monitorexit指令后,将锁计数器设为0,表明锁被释放。当获取失败,当前线程就要阻塞等待,直到锁被释放。
  • (3) Synchronized修饰的方法并没有moniterenter和moniterexit指令,而是ACC_SYNCHRONIZED 标识,指明该方法是一个同步方法。

synchronized和ReentrantLock对比

  • 两者都是可重入锁,同一个线程可以再次获取自己的内部锁,将锁计数器值加1,直到锁计数器为0,才能释放锁。
  • synchronized是JVM关键字,而reentrantLock是juc包中的类。
  • synchronized加锁和解锁自动进行,而reentrantlock需要手动进行加锁和解锁。
  • synchronized不可响应中断,一个线程获取不到锁就一直等着。Reentrantlock可以响应中断。
  • Synchronized只能是非公平锁,reentrantlock可以指定是公平锁还是非公平锁。
  • synchronized和wait()/notifyAll() 方法结合实现等待/通知机制。reentrantlock需要借助condition接口,可以与await()/signalAll()方法实现选择性通知。

synchronized 和volatile的区别

  • (1) Volatile仅能用在变量级别,synchronized可以用在变量、方法和类级别上。
    (2) Volatile保证可见性但不能保证原子性。Synchronzied可以实现变量的可见性和原子性。
    (3) Volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。
    (4) Volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。

Integer 和 int的区别

  • (1) Integer是int的包装类,int则是java的一种基本数据类型。
    (2) Integer变量必须实例化后才能使用,而int变量不需要。
    (3) Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 。
    (4) Java会对-128到127之间的Integer对象进行缓存,下次再用就从缓存中取。
    (5) Integer的默认值是null,int的默认值是0。

java.lang包下有什么?

  • (1) Object类
    (2) 数据类型包装类 Integer,Character,Double,Float,Boolean….
    (3) 字符串类String、StringBuilder,StringBuffer
    (4) 线程类Thread,Runnable
    (5) 异常类Throwable,Exception,Error
    (6) Math 数学类
    (7) System,Runtion 系统和运行类
    (8) Class,ClassLoader 操作类

volatile有序性的底层原理

  • (1) 有序性的保证是通过禁止指令重排实现的,底层是加内存屏障。

  • (2) JMM为volatile加内存屏障策略:
    ① 在每个volatile写操作前插入StoreStore屏障,防止写volatile与后面的写操作重排序。在写操作后插入StoreLoad屏障,防止写volatile与后面的读操作重排序。
    ② 在每个volatile读操作后插入LoadLoad屏障,防止读volatile与后面的读操作重排序,在读操作后插入LoadStore屏障,防止读volatile与后面的写操作重排序。
    ③ LoadLoad屏障是在读操作前保证load1要读取的数据读取完毕,StoreStore在写操作前保证store1的写入操作对其他处理器可见。LoadStore在写操作前保证load1要读取的数据读取完毕。StoreLoad在读操作前保证store1的写入对所有操作可见。

volatile可见性底层原理

  • (1) Lock指令,lock指令相当于一个内存屏障。
    ② 将当前处理器缓存行的数据立即写回到系统内存。
    ③ 写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。写回操作时要经过总线传播数据,而每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器要对这个值进行修改的时候,会强制重新从系统内存里把数据读到处理器缓存(也是由volatile先行发生原则保证);

Volatile 指令重排的作用

  • (1) 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保持一致性是不确定的。
    (2) Volatile通过插入内存屏障禁止在内存屏障前后的指令执行重排优化。内存屏障可以保证特定操作的执行顺序,并保证某些变量的内存可见性。
    (3) 比如DCL单例模式中,创建实例对象分了三步,分配内存空间,初始化对象,将对象指向分配的内存地址。后面两步不存在数据依赖关系,可以进行指令重排。若某个线程中先分配内存地址但还没初始化,另外一个线程检测到地址不为空就提前返回,此时它拿到的单例对象是还未初始化的,故报错.

数据库连接池的好处

  • (1) 资源复用。数据库连接得到重用,避免了频繁创建、释放连接引起的巨大性能开销。
  • (2) 更快的系统响应速度。利用数据库连接池中现有的可用连接,避免了数据库连接初始化和释放过程的开销。

java多线程和操作系统的多线程之间的关系

  • (1) 操作系统线程实现有三种方式:使用内核线程实现,使用用户线程实现,混合实现。
    ① 使用内核线程实现优点是切换速度快,开销小,可以跨进程调度其他线程执行。缺点是各种线程操作都需要进行系统调用,而系统调用的代价相对较高。
    ② 用户线程实现和操作系统无关,系统内核不能感知到用户线程的存在,用户线程的建立、同步、销毁和调度都是完全在用户态完成,不需要内核帮助。因此节省了内核态到用户态切换的开销,缺点是会产生阻塞问题,比如一个进程中的某个线程进行系统调用时,os会阻塞整个进程,即使这个进程中其他线程还在工作。
    ③ 混合实现是指即存在用户线程也存在轻量级进程,用户线程完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,且可以支持大规模用户线程并发。而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能以及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这样大大降低整个进程被完全阻塞的风险。

  • (2) Java虚拟机的线程模型普遍被替换为基于操作系统的原生线程模型来实现,也就是1:1的线程模型。以HotSpot为例,它的每个java线程都是直接映射到os原生线程来实现的。线程调度全部交给os来处理。

java和C++之间的区别

  • (1) Java不提供指针来访问内存,更加安全。
    (2) Java的类是单继承的,C++支持多重继承,虽然java的类不可以多继承,但接口可以多继承。
    (3) Java有自动内存管理机制,不需要程序员手动释放无用内存。

jdk1.8新特性

  • lambda表达式
  • 函数式接口
  • 接口中的默认方法和静态方法
  • Hashmap中由数组+链表改成数组+链表/红黑树。

什么时候需要重写equals方法?

  • set元素是引用类型的时候。因为基本类型,Integer,String都已经重写了equals方法和hashcode方法。

java中泛型

  • 泛型的好处

    • ① 实现java的类型安全,增强代码可读性。
      ② 在编译时检查类型安全。
  • 泛型的实现原理

    • ①Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
      ② 消除类型参数声明,即删除<>及其包围的部分。根据类型参数的上下界推断并替换所有的类型参数为原始类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,否则替换为第一个边界的类型变量。如在代码中定义的 List<object>List<String>等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。
  • 泛型擦除

    • 在编译成字节码时首先进行类型检查,接着进行类型擦除。

java异常体系

  • java基础、集合、多线程 - 图2

  • Error :
    是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。

    • stackOverFlowError 如果线程请求的栈深度大于虚拟机允许的深度,将抛出此异常。比如无限递归。
    • OutOfMemoryError 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,就会抛出该异常。或者虚拟机可动态扩展,如果扩展时无法申请到足够的内存,就会抛出该异常。比如无限创建线程,每个线程分配的栈容量越大,可以建立的线程数量越少,建立线程时越容易把剩下的内存耗尽。
  • Exception 包含:RuntimeException 、CheckedException

    • RuntimeException: 运 行 时 异 常。 如 NullPointerException 、 ClassCastException 、ArithmetricException、ArrayIndexOutOfBoundsException等。 RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类,这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

    • CheckedException:受检异常, 如IOExceptionSQLExceptionClassNotFoundException;一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:

    • ①试图在文件尾部读取数据

      • ②试图打开一个错误格式的 URL
    • ③试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在
  • 如何捕获异常?

    • try块,用于捕获异常,后面可以接0个或多个catch块,如果没有catch块,则必须跟一个finally块。
    • catch块,用于处理try块捕获到的异常。
    • finally块,无论是否捕获或处理异常,finally块里的语句都会被执行,当在try块或catch块中遇到return语句时,finally语句块将在方法返回前被执行
  • 如何抛出异常?

    • throw抛出异常,throws是方法可能抛出的异常的声明。

反射原理以及使用场景

Java反射:

是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

反射原理:

反射首先是能够获取到Java中的反射类的字节码,然后将字节码中的方法,变量,构造函数等映射成 相应的 Method、Filed、Constructor 等类

使用场景:

逆向代码 ,例如反编译;

动态生成类框架,如Spring:xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性

反射的API

  • class类,反射的核心类,可以获取类的属性,方法等信息。
  • field类,表示类的成员变量,可以用来获取和设置类的属性值。
  • method类,表示类的方法,可以用来获取类中的方法信息。
  • constructor类,表示类的构造方法

获取class对象的三种方式

  1. Student student = new Student(); *// 这一new 产生一个Student对象,一个Class对象。*
  2. Class studentClass2 = Student.class; // 调用某个类的 class 属性来获取该类对应的 Class 对象
  3. Class studentClass3 = Class.forName("com.reflect.Student") // 使用 Class 类中的 forName() 静态方法 ( 最安全 / 性能最好 )

反射的作用

  • (1) 反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
    (2) 反射机制的主要功能主要有:①得到一个对象所属的类 ②获取一个类的所有成员变量和方法,③在运行时创建对象,调用对象的方法。
    (3) 举例:

    • ① 动态代理
    • ② Spring 通过 XML 配置模式装载 Bean 的过程。

java集合

java基础、集合、多线程 - 图3

解决哈希冲突的方法

  • 开放定址法,线性探测再散列(d = 1,2,3,..m-1 ),平方探测再散列 (java基础、集合、多线程 - 图4)
  • 再哈希法,有多个不同的hash函数,当发生冲突时,使用第二个,第三个…直到没有冲突。
  • 链地址法,每个哈希表结点都有一个next指针,发生哈希冲突时,就通过next指针将结点连接起来。

ArrayList Vector 和Array异同

  • (1) ArrayList和Vector内部都是用了Array来控制集合中的对象。
    (2) Array在创建时必须指定大小,另外两个可以不用。
    (3) Array可以存放Object对象和基本数据类型,但ArrayList和Vector不能存放基本数据类型,只能存放它们的包装类。
    (4) ArrayList和Vector都继承自List接口,而Array不是。
    (5) Vector内部的方法加了Synchronized,它是线程同步的,其他两个不是。
    (6) Array不能动态扩容,而ArrayList和Vector可以,vector长度变为原来的2倍,ArrayList长度变为原来的1.5倍。

ArrayList和LinkedList的区别

  • 两个都不是线程安全的。
  • ArrayList底层使用Object数组,LinkedList底层使用双向链表的数据结构。
  • ArrayList的插入和删除的时间复杂度为O(n),查找为O(1). LinkedList插入和删除的时间复杂度为O(1),查找为O(n)

ArrayList的扩容方式和时机

  • (1) 初始化:如果参数=0,就初始化为一个空数组。如果不等于0,将数组初始化为一个容量为10的数组。
    (2) 扩容时机:当数组的大小大于初始容量时会进行扩容,新容量为旧容量的1.5倍。
    (3) 扩容方式:以新容量建立一个原数组的拷贝,修改原数组,指向新的数组,原数组被GC回收。

ArrayList并发问题

  • 出现问题的地方在于 elementData[size++]=e, size++不是原子性的操作,容易出现数组越界的异常或者产生覆盖的问题。

    • 假设size=10,A和B两个线程去读size都为9,此时都没有越界,当A先设置值,此时size变为10,B也开始设置值,此时size越界了。
    • 假设A,B两个线程都去读size=0,A设置完值后休眠了,还未来得及+1,线程B也去设置同样位置的值,此时B线程的操作结果就把A线程的覆盖了。
  • fial-fast机制

    • arraylist也采用了快速失败机制,通过记录modeCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败。

红黑树

  • 每个节点要么是红的,要么是黑的
  • 根节点和叶子节点是黑色的。
  • 每个红色节点的两个子节点一定是黑色。
  • 任意一个结点到每个叶子结点的路径都包含相同数目的黑色结点。

为什么hashmap底层使用红黑树而不是AVL树?

  • (1) AVL树和红黑树都是最常用的平衡二叉搜索树,他们的查找、删除修改都是O(logn)
    (2) AVL树可以提供更快的查找速度,一般用于查找密集型任务,适用于AVL树。
    (3) 红黑树更适用于插入修改密集型任务。
    (4) AVL树的旋转比红黑树的旋转更加难以平衡和调试。

HashMap红黑树的阈值为什么是8?

java基础、集合、多线程 - 图5

  • 首先和hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。在负载因子0.75的情况下,单个hash槽内元素个数为8的概率小于百万分之一。将7作为分水岭,大于等于8才转化为红黑树,小于等于6才转链表。而且红黑树的结点是链表中结点所占空间的2倍,虽然红黑树的查找复杂度是O(logn),优于链表的O(n),当时链表长度较小时,全部遍历时间复杂度也不会太高。所以要寻找一种时间和空间的平衡。
  • 如果 hashCode的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为8的时候,概率仅为0.00000006,这么小的概率,HashMap的红黑树转换几乎不会发生,因为我们日常使用不会存储那么多的数据,你会存上千万个数据到HashMap中吗?
  • JDK无法阻止用户实现自己的哈希算法,如果用户重写了hashCode,并且算法实现比较差的话,就会使hashmap的链表变得很长。因此这也是hasmap设置链表转红黑树的原因之一,可以有效防止用户自己实现了不好的哈希算法时导致链表过长的情况。

为什么hashmap的容量要是2的整数次幂?

  • Hash值的范围是-21亿到21亿,一个40亿长度的数组,内存是放不下的,所以这个散列值是不能直接拿来用的。用之前还要先做对数组长度的取模运算,得到的余数才是对于数组下标,计算方法是java基础、集合、多线程 - 图6%20%5C%26%20hash#card=math&code=%28n-1%29%20%5C%26%20hash)
  • 取余%操作中如果除数是2的n次幂,等价于与其除数减一的&操作java基础、集合、多线程 - 图7#card=math&code=hash%20%5C%25%20n%3Dhash%5C%26%28n-1%29),故长度必须是2的幂次方。

HashMap为什么不安全?

  • (1) Jdk1.7,在多线程环境下,扩容时会造成环形链或数据丢失。
  • (2) Jdk1.8,多线程环境下,会产生并发修改异常,即数据发生覆盖。

hashmap1.8的优化(了解)

  • 数据+链表改成了数组+链表或红黑树
  • 链表的插入方式从头插法变成了尾插法。
  • 扩充的时候1.7需要对原数组的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或原位置+旧容量大小。
  • 在插入时,1.7需要先判断是否需要扩容,再插入。1.8先进行插入,插入完后再判断是否需要扩容。

HashMap中的Hash算法

  • (1) 使用hash()函数(native方法)计算hash值,然后hash&(n-1)得数组下标。
    (2) Hash值的计算使用了扰动函数。
    1. static final int hash(Object key) {
    2. int h;
    3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    4. }


① h=hashCode() 调用的native方法,返回int型散列值。
② h^(h>>>16) 自己的高半区和低半区做异或,为了混合原始哈希码的高位和地位,以加大低位的随机性。

HashMap扩容机制

  • (1) HashMap中桶数组的长度是2的幂,阈值大小为桶数组长度与负载因子的乘积,当HashMap中的键值对数量超过容量*负载因子(0.75)时进行扩容。
    (2) 扩容:创建一个新的entry空数组,长度是原数组的2倍. 扩容后,要将原来的键值对rehash到新的桶数组中, 1.8的扩容方法如下:
    (3) 遍历旧的桶数组,计算hash&原数组长度的值,将值为0的节点组成一个链表。将值不为0的节点组成另一个链表。然后将分组后的链表映射到新桶中,值为0的节点组成的链表放在原位置,值不为0的节点组成的链表放在原位置+原数组长度的位置。

HashMap扩容会出现什么问题?

  • (1) 1.8时并发修改问题。
  • 1.7会形成环形链或数据丢失。 头插法导致的,线程1刚拿到节点,被阻塞。线程2已经处理好节点关系。线程1恢复执行后,会执行e.next= table[i],将当前节点的next指向头节点,而头节点的next又指向当前节点,故形成了环形链。

map如何扩容

  • 1.7采用头插法进行扩容,容易造成环形链。

    1. for (HashMapEntry<K, V> e : table) {
    2. // 如果这个数组位置上有元素且存在哈希冲突的链表结构则继续遍历链表
    3. while (null != e) {
    4. //取当前数组索引位上单向链表的下一个元素
    5. HashMapEntry<K, V> next = e.next;
    6. //重新依据hash值计算元素在扩容后数组中的索引位置
    7. int i = indexFor(e.hash, newCapacity);
    8. e.next = newTable[i]; // 这一步和下一步就是头插法了,并且这两步出现线程不安全死循环问题
    9. newTable[i] = e;
    10. e = next; // 遍历链表
    11. }
    12. }
  • 具体解释:

  • 1.8版本采用尾插法进行扩容。计算hash&oldCap(旧数组容量),将值为0的结点放在一个链表中,值为1的结点放在另外一个链表中。

  • 新数组的容量是原数组的2倍,故值为0的链表放在原来的位置,值为1的链表放在原来位置+旧数组容量的位置。

map的get方法底层

  • 首先map底层是一个桶数组,数组存放是是node节点,每个node节点有四个属性,hash,key,value,next指针.(计算hash的方法是(h = key.hashCode()) ^ (h >>> 16)
  • get方法的参数是key和key的hash值。首先定位key所在的桶的位置。然后判断第一个节点的hash和key是否与传入的节点相同,若是就直接返回。否则遍历链表或红黑树,比较当前节点的hash和key是否与传入的节点相同,若是则返回,否则遍历到结尾就返回为空。

map的put方法底层

  • put方法的参数是key,value,调用put方法会调用putVal()方法,传入hash值,key和value.
  • 首先定位key所在的桶的位置,若当前位置元素为空,就创建一个节点插入桶数组中。否则就判断节点类型是红黑树还是链表,若是链表就往下查找,若hash和key都相同就覆盖之前的节点。否则就在链表尾插入新结点。若是红黑树就调用红黑树的插入方法。
  • 若桶中节点数量大小超过阈值,则要进行扩容.

List可以边遍历边删除吗?

  • (1) 直接remove()会报并发修改异常。原因是实际执行时使用的Iterator,在调用next()方法时,第一行调用了checkForComodification(),而该方法的核心逻辑就是比较modCountexpectedModCount这2个变量的值。
    (2) 在执行list.remove()时,修改了modCount的值,第二次获取时,两个值就不相等了,所以抛出了并发修改异常。
    (3) 解决方案:
    ① 使用iterator的remove()方法。Jdk1.8后使用Collections.removeIf()方法代替iterator的remove()方法。
    ② 使用for循环遍历,删除后要及时修改下标i的值。
    ③ For循环倒序遍历,不用修改下标。

hasmap和hashtable的区别

  • hashmap是非线程安全的,而hashtable是线程安全的。hashtable内部的方法是经过synchronized修饰的。
  • 因为线程安全问题,hashmap的效率要高一些。
  • hasmap的键可以为null,但hashtable会报空指针异常。
  • 创建时如果不指定初值,那么hashtable默认初始大小为11,之后每次扩充,容量变为原来的2n+1。hashmap默认初始大小为16,之后每次扩充,容量变为原来的2倍。创建时指定了初值,hashmap会将其扩充为2的幂次方大小。
  • 当链表长度大于阈值(默认为8)时,hashmap会将链表转化为红黑树,以减少搜索时间,hashtable没有这样的机制。

concurrentHashMap线程安全的实现方式

  • jdk1.7采用分段锁的形式,首先将数组分为一段一段的存储,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一段数据时,其它线程也可以访问其他段的锁。
  • jdk1.8取消了分段锁,采用CAS和synchronized来保证并发安全。数据结构是数组+链表/红黑树,synchronized只锁定当前链表或红黑树的首节点,只要hash冲突,就不会产生并发。

ThreadLocal底层是怎么做的?

  • (1) Threadlocal是一种变量类型,称为线程局部变量。每个线程访问这种变量的时候会创建该变量的副本。这个变量副本为线程私有。
    (2) 底层是ThreadlocalMap对象,key是线程,泛型为存储的对象,保证了一个线程对应于一个存储对象。
    (3) 这样设计的好处在于保证当前线程结束时,相关对象可以被立即回收。
    (4) 一般用于spring事务管理,保证一个线程一个数据库连接对象。

hashSet如何检查重复

  • 当把对象加入hashset时,hashset会计算对象的hashcode值来判断对象加入的位置,同时与其他对象的hashcode值进行比较,如果没有相同的,证明没有重复。若hash值相同,则会调用equals()方法来检查对象是否真的相同。

TreeSet原理

  • (1) TreeSet支持两种排序方法,自然排序和定制排序。默认采用自然排序。
    (2) 当一个对象加入Tree Set集合中时,TreeSet调用对象的compareTo方法与容器中的其他对象比较大小,然后根据红黑树算法确定对象的存储位置。如果两个对象通过compareto比较相等,treeSet认为它们存储在同一位置。
    (3) 当需要把一个对象放入TreeSet中时,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法有一致结果,其规则是:如果两个对象通过equals方法比较返回true时,这两个对象通过compareTo(Object obj)方法比较应返回0
    (4) 如果需要实现定制排序,则需要在创建TreeSet集合对象时,并提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。

CopyOnWriteArrayList的原理

  • (1) 底层是数组,加锁由Reentrantlock完成。读的时候不加锁,多个线程可以并发读取,写的时候需要加锁,确保同一时间只有一个线程进行写入。写入的过程如下:先用原数组复制出一个新的数组。然后再新数组上进行修改,最后将引用指向新的数组,原数组被GC回收。
    (2) 迭代器遍历的时候,使用的数组是原数组,所以不会发生并发修改异常。
    (3) 缺点:
    ① 内存占用,频繁的增删改操作会导致复制很多数组,比较耗费内存。
    ② 数据一致性:copyOnwriteArrayList只能保持数据最终一致性,不能保证数据的实时一致性。比如线程A在迭代容器中的数据,线程B在A迭代的过程中将容器的部分数据进行修改,但A迭代出来的仍然是原来的数据。

java线程

线程状态

  • (1) 六种,创建,运行,阻塞,等待,计时等待以及终止。
    (2) 创建状态表示新创建了一个线程对象,而此时线程并没有开始执行。
    (3) 运行状态:线程进入就绪后,被cpu调度执行。
    (4) 等待状态:线程调用了wait(),join()等方法,在等待另外一个线程执行notify()或notifyAll()。
    (5) 计时等待状态:线程调用了wait(),join(),sleep(long millis)当超时或者条件满足时,都会切换为就绪状态。
    (6) 阻塞状态,线程阻塞在进入synchronized关键字修饰的方法或代码块。
    (6) 终止状态:当线程执行完毕,则进入该状态。

线程的4种创建方式

  • 继承Thread类

    • 通过继承Thread并且重写其run(),run方法中即线程执行任务。创建后的子类通过调用 start() 方法即可执行线程方法。
    • 缺点:通过继承Thread实现的线程类,多个线程间无法共享线程类的实例变量
      1. public class Test extents Thread {
      2. public void run() {
      3. // 重写Thread的run方法
      4. }
      5. }
  • 实现runnable接口

    • 先定义一个类实现Runnable接口,并重写该接口的 run() 方法,此run方法是线程执行体。
    • 接着创建runnable实现类的对象,作为Thread对象的参数,此Thread对象才是真正的线程对象。
      1. public class Test {
      2. public static void main(String[] args) {
      3. //lambda表达式
      4. new Thread(() -> {
      5. //重写run方法
      6. }).start();
      7. }
      8. }
  • 实现callable接口

    • 从继承Thread类和实现Runnable接口可以看出,上述两种方法都不能有返回值,且不能声明抛出异常。而Callable接口则实现了此两点,Callable接口如同Runnable接口的升级版,其提供的call()方法将作为线程的执行体,同时允许有返回值。
    • 但callable对象不能直接作为Thread对象的参数,因为Callable接口是java5新增的接口,不是runnable接口的子接口。对于这个问题的解决方案就引入Future接口,此接口可以接收call()的返回值。该接口的实现类是FutureTask(), FutureTask对象可以作为Thead对象的参数。
      1. public class Test {
      2. public static void main(String[] args) {
      3. // FutureTask 构造方法包装了Callable
      4. FutureTask<Integer> task = new FutureTask<>(() -> {
      5. //重写run方法
      6. return 0;
      7. });
      8. new Thread(task).start();
      9. }
      10. }
  • 通过线程池创建线程

    1. public class Test {
    2. public static void main(String[] args) {
    3. ExecutorService threadPool = Executors.newFixedThreadPool(1);
    4. threadPool.submit(() -> {
    5. //实现run方法
    6. });
    7. threadPool.shutdown();//关闭线程池
    8. }
    9. }

实现runnable接口相比继承Thead的优势

  • 多个线程间可以共享线程类的实例变量
  • 可以避免由于java的单继承性带来的局限性
  • 增强了程序的健壮性,代码可以被多个线程共享。

Runnable和callable区别

  • (1) Callable规定的方法是call(),runnable规定的方法是run()
  • (2) callable任务执行后可以返回值,而runnable不能返回值。
  • (3) Call方法可以抛出异常,但run()方法不能。

为什么不能直接调用线程的run方法?

  • 通过start方法启动线程的同时也创建了一个线程,并使线程进入就绪状态。start()会执行线程的相应准备工作,然后自动执行run()方法中的内容。而直接执行run()方法,会把run()方法当作main线程下的一个普通方法去执行,而不是多线程。

线程的interrupt方法发生了什么?

  • 设置线程的中断标志为true,但不保证线程真的会中断。
  • Interrupt()方法不会中断一个正在运行的线程。
  • 如果线程在调用wait(),sleep()或join()方法,它会响应中断,收到一个InterruptedException异常
  • Interrupted返回中断标志的状态,线程恢复非中断状态。IsInterrupted判断线程是否已经中断。

停止线程

  • 在Thread类中有两个方法可以打断正在运行的线程,一个是stop()一个是interrupt()方法,其中如果线程被wait,join,sleep三种方法之一阻塞,那么它将接收到一个中断异常,stop()是直接停掉线程,不建议使用。

java线程同步的方法

  • (1) Synchronized方法和代码块
    (2) wait,notify
    (3) 可重入锁 ReentrantLock+condition接口
    (4) 特殊域变量 volatile修饰的变量
    (5) 局部变量 ThreadLocal
    (6) 阻塞队列
    (7) 原子变量类(CAS)

wait(),sleep(),join()和yield()的区别

  • (1) wait()方法是Object类的实例方法,调用wait()方法会让当前线程阻塞,释放锁;
    (2) Sleep()方法是Thread类的静态方法,调用sleep()会让线程进入阻塞状态,但不会释放锁。待阻塞时间超过timeout时,线程会由阻塞状态变为就绪状态。
    (3) Yield()方法是Thread类的静态方法,调用yield方法会使线程退出CPU时间片由运行状态变为就绪状态。让其他同优先级或更高优先级的线程获取cpu时间进入运行状态,yield()方法不会释放锁。
    (4) Join()方法是Thread类的实例方法,线程执行t.join()方法,线程会阻塞等待t线程结束再继续执行。

wait和sleep方法的异同?

  • 相同点:

    • 都可以让线程阻塞
    • 都可以响应interrupt中断,在等待的过程中如果收到中断信号,都可以进行响应,并抛出interruptedException异常。
  • 不同点:

    • wait是Object类的方法,而sleep是Thread类的方法。
    • wait方法必须在synchronized修饰的代码块中使用,而sleep方法并没有这个要求。
    • 在同步方法中执行sleep方法时,并不会释放monitor锁,但执行wait方法时会主动释放monitor锁。
    • sleep不需要被唤醒,但wait需要被notify或notifyAll方法唤醒。

AQS原理

  • AbstractQuenedSynchronizer抽象的队列式同步器,是除了java自带的synchronized关键字之外的锁机制。利用CLH队列来管理同步状态state,CLH队列是一个先进先出的队列。如果state=0,当前线程就可以获得锁,然后将state+1,其他进程进来发现state不为0,AQS就阻塞其他线程,将其封装为一个节点放入CLH队列中。如果同步状态被释放了,公平锁则会从队列头唤醒一个线程去申请同步状态。非公平锁有可能后来的进程先抢到同步状态。

  • 注意用volatile去修饰state.线程通过CAS去改变state,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

  • AQS定义了两种资源共享方式:
    ① 独占,reentrantlock
    ② 共享,Semaphore,CountDownLatch,ReadWriteLock,CyclicBarrier.

CyclicBarrier

用法

  1. public class TestCyclicBarrier {
  2. public static void main(String[] args) {
  3. CyclicBarrier barrier = new CyclicBarrier(7,()->{
  4. System.out.println("召唤神龙成功");
  5. });
  6. for (int i = 0; i < 7; i++) {
  7. final int temp = i;//lambda表达式不能直接操作i
  8. new Thread(()->{
  9. System.out.println(Thread.currentThread().getName()+"集齐"+temp+"颗龙珠");
  10. try {
  11. barrier.await();//已经完成任务的线程达到屏障阻塞自己,等待最后一个线程到达
  12. } catch (Exception e) {
  13. e.printStackTrace();
  14. }
  15. },String.valueOf(i)).start();
  16. }
  17. }
  18. }

底层是怎么实现的

  • (1) CyclicBarrier内部是通过RentrantLock和Condition接口实现的。在内部维护一个计数器变量,还维护了一个叫代的类,当计数器值减为0时,就进入下一代,可以避免线程重复执行,即使线程被唤醒,所处的代不同也是不会继续执行的。
    (2) 当某个线程调用cyclicbarrier的await()方法时,计数器变量值减1,然后调用lock.await()方法让当前线程阻塞。
    (3) 如果中途某个线程出现了异常,就将当前代坏掉的标志设为true,并唤醒所有被拦截的线程,抛出异常。
    (4) 如果没有异常,等到计数器值减为0的时候,就会让当初传入的线程执行任务,执行换代操作,最后唤醒所有被阻塞的线程。

CountDownLatch

用法

  1. public class TestCountDownLatch {
  2. public static void main(String[] args) throws InterruptedException {
  3. //计数器
  4. CountDownLatch latch = new CountDownLatch(6);
  5. for (int i = 0; i < 6; i++) {
  6. new Thread(()->{
  7. System.out.println(Thread.currentThread().getName()+"Go");
  8. latch.countDown(); //计数器减一
  9. },String.valueOf(i)).start();
  10. }
  11. latch.await(); //main线程等待计数器归0(被唤醒),继续向下执行
  12. System.out.println("close door");
  13. }
  14. }

底层是怎么实现的

  • (1) 两个主要方法countDown()和await()方法都是操作AQS的state值来实现的,先给state设置一个初始值,当调用countDown()方法时,将state值-1,每次调用都会判断当前state值是否为0,如果为0就需要唤醒因调用await()方法而被加入到AQS队列中的线程执行操作任务。
    (2) 所以await()方法就是用来将当前线程加入到AQS队列中去,前提是需要判断是否能够获取当前锁,只有在state值为0的情况才能获取到锁,不为0就把当前线程封装成node结点加入到AQS队列中。

Semphore

用法

  1. public class TestSemaphore {
  2. public static void main(String[] args) {
  3. //3个停车位 一般用于限流
  4. Semaphore semaphore = new Semaphore(3);
  5. //6个车去抢3个车位
  6. for (int i = 1; i <= 6; i++) {
  7. new Thread(()->{
  8. try {
  9. semaphore.acquire();//请求车位,如果没有则等待
  10. System.out.println(Thread.currentThread().getName()+"抢到车位");
  11. TimeUnit.SECONDS.sleep(2);//模拟停车时间
  12. System.out.println(Thread.currentThread().getName()+"离开车位");
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }finally {
  16. semaphore.release();//释放车位
  17. }
  18. },String.valueOf(i)).start();
  19. }
  20. }
  21. }

底层是怎么实现的?

  • semphore借助AQS来实现,实例化时传入一个许可证的次数,将该值设置为state.
  • acquire是将state做减法操作,而release操作是将state做加法操作。
  • 线程通过CAS去获取state

semphore和countDownLatch的区别

  • semphore用来限制流量,比如对N个资源的互斥访问。
  • countDownLatch用于启动一系列线程,并等待它们执行完毕。

CAS (compare and set)

  • 比较并交换, 线程的期望值和主内存的真实值一样则修改,否则修改失败。
  • 底层原理:

    • 自旋锁+unsafe类,CAS是一条CPU并发原语
    • CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。
  • 缺点:

    • 循环时间长,开销大
    • 只能保证一个共享变量的原子操作
    • 引发ABA问题

ABA问题

  • (1) CAS算法的一个重要前提是需要取出内存中某时刻的数据并在当下比较并替换,那么在这个时间差内会导致数据的变化。两个线程读取主内存中的A值,其中B线程速度快,将A改为B后又改为A,接着A线程读取的时候,发现主内存中依然是A,以为没有发生变化。
    (2) 原子引用 atomicReference 解决ABA问题, 修改版本号(类似时间戳)

  • 参考代码:

    1. /**
    2. * 原子引用
    3. * 使用版本号来记录每次的更新
    4. * 类似乐观锁的思想
    5. */
    6. public class TestAutomic {
    7. public static void main(String[] args) {
    8. //注意Integer泛型在-128-+127范围内的取值可以直接用==比较
    9. AtomicStampedReference<Integer> reference = new AtomicStampedReference(20,1);
    10. /**
    11. * 捣乱的线程,将20修改为21后,又改回20
    12. */
    13. new Thread(()->{
    14. int stamp = reference.getStamp(); //最初的版本号
    15. try {
    16. TimeUnit.SECONDS.sleep(1);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. System.out.println(reference.compareAndSet(20, 21, reference.getStamp(), reference.getStamp() + 1));
    21. System.out.println(Thread.currentThread().getName()+"=>"+reference.getStamp());
    22. reference.compareAndSet(21,20,reference.getStamp(),reference.getStamp()+1);
    23. System.out.println(Thread.currentThread().getName()+"=>"+reference.getStamp());
    24. },"A").start();
    25. /**
    26. * 希望的线程
    27. */
    28. new Thread(()->{
    29. int stamp = reference.getStamp();//最初的版本号
    30. try {
    31. TimeUnit.SECONDS.sleep(2);
    32. } catch (InterruptedException e) {
    33. e.printStackTrace();
    34. }
    35. System.out.println(reference.compareAndSet(20, 22, stamp, stamp + 1));
    36. System.out.println(Thread.currentThread().getName()+"=>"+reference.getStamp());
    37. },"B").start();
    38. }
    39. }

锁池和等待池

  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中.等待池中的线程不会去竞争该对象的锁.
  • 当有线程调用了对象的notifyAll()方法,会将对象等待池中的所有线程移动到锁池中,等待锁竞争. 当有线程调用了notify()方法,只会有一个线程由等待池进入锁池.

活锁

  • (1) 任务或执行者没有被阻塞,由于有些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。活锁有可能自行解开,但死锁不能。
    (2) 比如线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
    (3) 避免活锁的简单方法是采用先来先服务的策略。当多个事务请求封锁同一数据对象时,封锁子系统按请求封锁的先后次序对事务排队,数据对象上的锁一旦释放就批准申请队列中第一个事务获得锁。
    (4) 锁饥饿:一个或多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。

乐观锁和悲观锁

  • 悲观锁总是假设最坏的情况,每次去拿数据的时候都会认为别人会修改,所以每次在拿数据的时候都会上锁。比如synchronized。
  • 乐观锁总是假设最好的情况,每次拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下有没有其他线程去更新这个数据,一般通过版本号机制和CAS算法实现。比如原子类就是使用了CAS去实现的。

模拟自旋锁

    1. public class SpinLock {
    2. //唯一的位置
    3. private AtomicReference<Thread> owner = new AtomicReference<>();
    4. private int count = 0;
    5. //模拟加锁
    6. public void lock() {
    7. Thread t = Thread.currentThread();
    8. //自旋,如果该位置有线程,则一直自旋
    9. while(!owner.compareAndSet(null,t)){
    10. }
    11. count++;
    12. System.out.println(Thread.currentThread().getName()+"==>get a Lock"+"===>count="+count);
    13. }
    14. //模拟解锁
    15. public void unlock() {
    16. Thread t = Thread.currentThread();
    17. owner.compareAndSet(t,null);
    18. System.out.println(Thread.currentThread().getName()+"==>UnLock");
    19. }
    20. public static void main(String[] args) throws InterruptedException {
    21. SpinLock lockDemo = new SpinLock();
    22. // 5个线程去抢占那个位置
    23. for (int i = 0; i < 5; i++) {
    24. new Thread(()->{
    25. try {
    26. lockDemo.lock();
    27. TimeUnit.SECONDS.sleep(1);//模拟执行时间
    28. } catch (InterruptedException e) {
    29. e.printStackTrace();
    30. } finally {
    31. lockDemo.unlock();
    32. }
    33. },String.valueOf(i)).start();
    34. }
    35. }
    36. }

AutomicInteger

  • 原子类中incrementAndGet()方法分析 ```java /**
    • Atomically increments by one the current value. *
    • @return the updated value */ public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }

//Unsafe类 // var1== obj var2==offset var4==delta //获取内存地址为var1+var2的变量值,并将该变量值加上var4 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //通过对象和偏移量获取变量的值 var5 = this.getIntVolatile(var1, var2); / while中的compareAndSwapInt()方法尝试修改v的值,具体地, 该方法也会通过obj和offset获取变量的值 如果这个值和v不一样, 说明其他线程修改了obj+offset地址处的值, 此时compareAndSwapInt()返回false, 继续循环 如果这个值和v一样, 说明没有其他线程修改obj+offset地址处的值, 此时可以将obj+offset地址处的值改为v+delta, compareAndSwapInt()返回true, 退出循环 Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被其他线程中断 / } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//native方法

  1. return var5;
  2. }
  1. <a name="e19b3f6e"></a>
  2. ### 并发用到的锁
  3. -
  4. (1) 公平锁/非公平锁
  5. -
  6. ① 多个线程按照申请锁的顺序来获取锁。
  7. -
  8. ② 可以抢占,可能后到的线程比之前的线程先获得锁。
  9. -
  10. (2) 可重入锁
  11. - ① 又名递归锁,指同一个线程在外层方法获取锁的时候,进入内层方法也会自动获得锁。好处是避免死锁。
  12. -
  13. (3) 独占锁(写锁)/共享锁(读锁)/读写锁
  14. - 独占锁:该锁一次只能被一个线程锁持有。
  15. -
  16. 共享锁:该锁可以被多个线程所持有。
  17. - 读写锁 ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁。
  18. -
  19. (4) 乐观锁/悲观锁
  20. - ① 悲观锁认为对同一数据的并发操作,一定是会发生修改的。因此对同一个数据的并发操作,悲观锁采取加锁的方式。
  21. - ② 乐观锁认为对于同一数据的并发操作,是不会发生修改的。在更新数据时会采用尝试更新,不断更新的方式更新数据。乐观锁的典型例子是原子类通过CAS自旋实现原子操作的更新。
  22. -
  23. (5) 分段锁
  24. -
  25. ① 分段锁其实是一种锁的设计,并不是具体的一种锁,对于jdk1.7中的ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式实现高效的并发操作。
  26. -
  27. ② ConcurrentHashMap中的分段锁称为Segment,它内部有一个entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock.分段锁的设计目的是细化锁的粒度,当操作不用更新整个数组时,就仅仅针对数组中的一项进行加锁操作.
  28. -
  29. (6) 偏向锁/轻量级锁/重量级锁 ( **synchronized锁升级过程**)
  30. - ① 这三种锁指的是锁的状态,并且针对Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
  31. - ② 偏向锁是指一段同步代码一直被一个线程访问,那么该线程会自动获取锁。降低获取锁的代价。
  32. - ③ 轻量级锁是指当锁是偏向锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的方式获取锁。
  33. - ④ 重量级锁是指当锁为轻量级锁的时候,另外一个线程虽然自旋,但自旋不会一直持续下去,当自旋到一定次数的时候还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
  34. -
  35. (7) 自旋锁(CAS底层原理)
  36. - ① 尝试获取锁的线程不会立即阻塞,而是采用循环的方式区去获取锁,好处是减少线程上下午切换的消耗。缺点是:循环会消耗CPU。
  37. <a name="4b07a26a"></a>
  38. ### 怎么理解线程安全?
  39. - (1) 并发编程需要保证可见性,原子性和有序性。若不能保证,则线程是不安全的。
  40. - (2) 其中可见性和有序性可以通过volatile关键字来实现。
  41. - (3) 而原子性可以通过加锁,如synchronized,Lock等,或者原子类,原子引用实现。
  42. <a name="58bc0acd"></a>
  43. ### 为什么要使用线程池?
  44. - 降低资源消耗,通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。
  45. - 提高响应速度,当任务到达时,任务可以不需要等待线程创建就能立即执行。
  46. - 提高线程的可管理性。使用线程池可以对线程进行统一分配,调优和监控。
  47. <a name="19beffa0"></a>
  48. ### 线程池状态
  49. ![](https://img-blog.csdn.net/20180514165513759?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3NoYWh1aHViYW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70#alt=)
  50. - (1) 线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
  51. - (2) 线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
  52. - (3) 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
  53. - (4) 线程池处在TIDYING状态时,所有任务终止。当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
  54. - (5) 线程池彻底终止,就变成TERMINATED状态。
  55. <a name="e017a42d"></a>
  56. ### 线程池中线程数量如何设定
  57. - 如果是CPU密集型应用,则线程池大小设置为N+1
  58. - 如果是IO密集型应用,则线程池大小设置为2N+1
  59. - 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
  60. <a name="0516478b"></a>
  61. ### 执行execute()和sumbit()方法的区别是什么呢?
  62. -
  63. ① Execute()用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功与否。
  64. <br />② Submit()用于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过这 Future 对象可以判断任务是否执⾏成功,并且可以通过 Future 的 get() ⽅法来获取返回值, get() ⽅法会阻塞当前线程直到任务完成,⽽使⽤ get(long timeout,TimeUnit unit) ⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。
  65. <a name="e7ed088e"></a>
  66. ### 如何创建线程池?
  67. -
  68. (1) 不允许使用excutors去创建,而是通过ThreadPoolExecutor 的⽅式,这样可以规避资源耗尽的风险。
  69. <br />(2) FixedThreadPool 和SingleThreadExecutor 允许请求的队列⻓度为 Integer.MAX_VALUE,可能堆积⼤量的请求,从⽽导致OOM。
  70. <br />(3) CachedThreadPool 和 ScheduledThreadPool 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建⼤量线程,从⽽导致OOM。
  71. <a name="9be97581"></a>
  72. ### ThreadPoolExecutor类分析
  73. -
  74. 七大参数
  75. ```java
  76. public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
  77. int maximumPoolSize,//最大线程池大小
  78. long keepAliveTime,//超时无任务进来会释放多余的进程
  79. TimeUnit unit,//时间单位
  80. BlockingQueue<Runnable> workQueue,//阻塞队列
  81. ThreadFactory threadFactory,//线程工厂
  82. RejectedExecutionHandler handler) {//拒绝策略
  • 四个拒绝策略

    • AbortPolicy() 阻塞队列已经满,还要进入则报异常
    • CallerRunsPolicy() 将任务返回给它的调用者去执行
    • DiscardPolicy()直接丢弃任务
    • DiscardOldestPolicy()尝试和最早的竞争
  • 线程池工作原理分析

    • java基础、集合、多线程 - 图8

线程池如何保证线程的复用

  • (1) 向线程池添加任务时,使用ThreadPoolExcutor对象的execute()方法来完成。该方法根据工作线程数量去执行不同策略,但每种策略都会经过addWorker()方法。
    (2) addWorker()是通过创建Worker来执行任务的,Worker是一个内部类,实现了Runnable接口,线程是在Worker内部被创建的,Worker内的run()方法中只有一个runWorker()方法,通过一个while循环来进行判断,如果当前任务不为空或者从阻塞队列中获取任务不为空,就一直执行。
    (3) 从阻塞队列中获取任务方法中,如果工作线程数大于核心线程数,就通过poll()从队列中取任务,若工作线程数小于核心线程数则通过take()取任务。Take()方法是阻塞的,如果队列中没有任务,就会阻塞当前线程,直到能取出任务为止,并不会被销毁,因此保证了线程池中有n个线程是活的,可以随时处理任务,从而达到重复利用的目的。

ThreadLocal原理以及使用

  • 为每个线程保存一份专属的本地变量。

  • 首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。
    最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。
    我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。

  • 使用场景:

    • 存储用户session
    • 解决线程安全问题。

ThreadLocal内存泄漏

  • 实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
    所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。 假如我们不做任何措施的话,value 永远⽆法被GC 回收,这个时候就可能会产⽣内存泄露。
    ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
    因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法。

BlockingQueue

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列.

  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值Integer>MAX_VALUE)阻塞队列.

  • PriorityBlockingQueue:支持优先级排序(堆)的无界阻塞队列.

  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列.

  • SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列.每个插入操作必须等待另一个线程相应的删除操作,反之亦然。 同步队列没有任何内部容量,甚至没有一个容量。

BlockingQueue的四组API

方式 抛出异常 有返回值 阻塞等待 超时等待
添加 add offer put offer
移除 remove poll take poll
获取队首元素 element peek - -