java基础面经--上

1.请你谈谈你对volatile的理解?

volatile是JVM提供的轻量级的同步机制

1.保证可见性

2.不保证原子性

3.禁止指令重排

<1> volatile保证可见性:(JMM内存模型)

说一说java内存模型?

Java语言为了保证并发编程中可以满足原子性,可见性及有序性,于是推出了一个概念就是JMM内存模型。JMM内存模型,目的是为了在多线程条件下,使用共享内存进行数据通信时,通过对多线程程序读操作,写操作行为规范约束,来尽量避免多次内存数据读取不一致,编译器对代码指令重排序、处理器对代码乱序执行带来的问题。

  • JMM 内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。
  • JMM 内存模型将内存主要划分为主内存和工作内存两种。规定 所有的变量都存储在主内存中,每条线程都拥有自己的工作内存,线程的工作内存中保存了该线程所需要用到的变量在主内存中的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读、写主内存。
  • 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要线程自己的工作内存和主存之间进行数据交互。
     

JMM 内存模型工作内存、主内存和 JVM 内存有什么关系?

JMM 内存模型中,工作内存和主内存其实跟JVM内存的划分是在不同层次上进行的,是自己的一套抽象概念,大概可以理解为,主内存对应的是 Java 堆中的对象实例部分,而工作内存对应的则是栈中的部分区域。

JMM(Java内存模型java memory model)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范。

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁和解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建是JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有的线程都可以访问。线程对变量的操作(读取赋值等)都必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,不同的线程之间无法访问对方发的工作内存。

java基础面经--上_第1张图片

而这就可能存在 当一个线程AAA修改了共享变量X的值但还未写回主内存时,另一个线程BBB又对主内存中同一个变量X进行操作,但此时AAA线程工作内存中的变量X对线程BBB来讲并不课件,这种工作内存与主内存同步延迟现象就造成了可见性问题。

<2> volatile不保证原子性:

原子性:不可分割,完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者分割,需要整体完成,要么同时成功要么同时失败。

class MyData2 {
    /**
     * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
     */
    volatile int number = 0;


    public void addPlusPlus() {
        number ++;
    }
}

public class VolatileAtomicityDemo {

	public static void main(String[] args) {
        MyData2 myData = new MyData2();

        // 创建10个线程,线程里面进行1000次循环
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                // 里面
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }

        // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
        // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
        while(Thread.activeCount() > 2) {
            // yield表示不执行
            Thread.yield();
        }

        // 查看最终的值
        // 假设volatile保证原子性,那么输出的值应该为:  20 * 1000 = 20000
        System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);

	}

}

最后的结果总是小于20000。

number++在多线程下是非线程安全的。

我们可以将代码编译成字节码,可看出number++被编译成3条指令。

java基础面经--上_第2张图片

假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。
问题解决:

  • 可以加synchronized解决,但它是重量级同步机制,性能上有所顾虑。
  • 如何不加synchronized解决number++在多线程下是非线程安全的问题? 使用AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;

class MyData2 {
    /**
     * volatile 修饰的关键字,是为了增加 主线程和线程之间的可见性,只要有一个线程修改了内存中的值,其它线程也能马上感知
     */
	volatile int number = 0;
	AtomicInteger number2 = new AtomicInteger();

    public void addPlusPlus() {
        number ++;
    }
    
    public void addPlusPlus2() {
    	number2.getAndIncrement();
    }
}

public class VolatileAtomicityDemo {

	public static void main(String[] args) {
        MyData2 myData = new MyData2();

        // 创建10个线程,线程里面进行1000次循环
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                // 里面
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                    myData.addPlusPlus2();
                }
            }, String.valueOf(i)).start();
        }

        // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
        // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
        while(Thread.activeCount() > 2) {
            // yield表示不执行
            Thread.yield();
        }

        // 查看最终的值
        // 假设volatile保证原子性,那么输出的值应该为:  20 * 1000 = 20000
        System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
        System.out.println(Thread.currentThread().getName() + "\t finally number2 value: " + myData.number2);
	}
}

输出结果为:

main	 finally number value: 18766
main	 finally number2 value: 20000

 <3>volatile禁止指令重排

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:

单线程环境里确保程序最终执行结果和代码顺序执行的结果一致。

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线性中使用的变量能否保障一致性是无法确定的,结果无法预测。

public class ReSortSeqDemo{
	int a = 0;
	boolean flag = false;
    
	public void method01(){
		a = 1;//语句1
		flag = true;//语句2
	}
    
    public void method02(){
        if(flag){
            a = a + 5; //语句3
        }
        System.out.println("retValue: " + a);//可能是6或1或5或0
    }
    
}

多线程环境中线程交替执行method01()method02(),由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。 

volatile实现禁止指令重拍优化,从而避免多线程环境下程序出现乱序执行的现象。

先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  1. 保证特定操作的执行顺序,
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。
java基础面经--上_第3张图片

对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

java基础面经--上_第4张图片

2.CAS你知道吗?

CAS的全称为Compare-And-Swap,比较并交换,是一条CPU并发原语。

CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

public class CASDemo{
    public static void main(string[] args){
        AtomicInteger atomicInteger = new AtomicInteger(5);// mian do thing. . . . ..
        System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
        System.out.println(atomicInteger.compareAndset(5, 1024)+"\t current data: "+atomicInteger.get());
    }
}

输出结果为

true    2019
false   2019

 CAS底层原理?谈谈你对UnSafe的理解?

atomiclnteger.getAndIncrement();源码

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    
    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * Creates a new AtomicInteger with initial value {@code 0}.
     */
    public AtomicInteger() {
    }
    
    ...
            
    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    
    ...
}
    

1. UnSafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地(native)方法来访问,而基于UnSafe类可以直接操作特定内存的数据,UnSafe类存在于sun.misc包中,其内部方法操作可以像c的指针一样直接操作内存,因为java中CAS操作的执行依赖于UnSafe类的方法。

2.变量valueOffset 表示该变量值在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的。

3.变量value用volatile修饰,保证了多线程之间的内存可见性。

CAS是什么?

CAS的全称为Compare-And-Swap,比较并交换,是一条CPU并发原语。
他的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)

java基础面经--上_第5张图片

上面类似自旋锁

UnSafe.getAndAddInt()源码解释:

  • var1 AtomicInteger对象本身。
  • var2 该对象值得引用地址。
  • var4 需要变动的数量。(步长)
  • var5是用过var1,var2找出的主内存中真实的值。
  • 用该对象当前的值与var5比较:        如果相同,更新var5+var4并且返回true, 如果不同,继续取值然后再比较,直到更新完成。

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :

  1. Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  3. 线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功

CAS缺点:

1.循环时间长开销很大

// ursafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4){
	int var5;
	do {
		var5 = this.getIntVolatile(var1, var2);
	}while(!this.compareAndSwapInt(varl, var2, var5,var5 + var4));
    return var5;
}

可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

2.只能保证一个共享变量的原子操作

当对一个共享变量执行操作是,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

3.CAS引来ABA问题

3.原子类AtomicInteger的ABA问题谈谈?原子类更新引用知道吗?

CAS会导致“ABA问题”(狸猫换太子).

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作变成了B,然后线程two又将V位置的数据变成了A,这时候线程one进行CAS操作的时候发现内存中仍然是A,然后线程one操作成功

尽管线程one的CAS操作成功,但不代表这个过程是没有问题的。

AtomicReference原子引用(自定义的类,原理和AtomicInteger差不多)

import java.util.concurrent.atomic.AtomicReference;

class User{
	
	String userName;
	
	int age;
	
    public User(String userName, int age) {
		this.userName = userName;
		this.age = age;
	}

	@Override
	public String toString() {
		return String.format("User [userName=%s, age=%s]", userName, age);
	}
    
}

public class AtomicReferenceDemo {
    public static void main(String[] args){
        User z3 = new User( "z3",22);
        User li4 = new User("li4" ,25);
		AtomicReference atomicReference = new AtomicReference<>();
        atomicReference.set(z3);
		System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString());
    }
}

输出结果

true	User [userName=li4, age=25]
false	User [userName=li4, age=25]

ABA的危害:

 下面是一段伪代码,将就着看一下。场景是用链表来实现一个栈,初始化向栈中压入B、A两个元素,栈顶head指向A元素。

在某个时刻,线程1试图将栈顶换成B,但它获取栈顶的oldValue(为A)后,被线程2中断了。线程2依次将A、B弹出,然后压入C、D、A。然后换线程1继续运行,线程1执行compareAndSet发现head指向的元素确实与oldValue一致,都是A,所以就将head指向B了。但是,注意我标黄的那行代码,线程2在弹出B的时候,将B的next置为null了,因此在线程1将head指向B后,栈中只剩了一个孤零零的元素B。但按预期来说,栈中应该放的是B → A → D → C。

Node head;
head = B;
A.next = head;
head = A;


Thread thread1 = new Thread(
    ->{
          oldValue = head;
          sleep(3秒);
          compareAndSet(oldValue, B);

    }
);

Thread thread2 = new Thread(
    ->{
        // 弹出A
          newHead = head.next;
          head.next = null; //即A.next = null;
          head = newHead;
         // 弹出B
          newHead = head.next;
          head.next = null; // 即B.next = null; //标黄的那行
          head = newHead; // 此时head为null
          
          // 压入C
          head = C;
          // 压入D
          D.next = head;
          head = D;
          // 压入A
          A.next = D;
          head = A;
          

    }
);

thread1.start();
thread2.start();

 AtomicStampedReference版本号原子引用:

 原子引用 + 新增一种机制,那就是修改版本号(类似时间戳),它用来解决ABA问题。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {
	/**
	 * 普通的原子引用包装类
	 */
	static AtomicReference atomicReference = new AtomicReference<>(100);

	// 传递两个值,一个是初始值,一个是初始版本号
	static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1);

	public static void main(String[] args) {

		System.out.println("============以下是ABA问题的产生==========");

		new Thread(() -> {
			// 把100 改成 101 然后在改成100,也就是ABA
			atomicReference.compareAndSet(100, 101);
			atomicReference.compareAndSet(101, 100);
		}, "t1").start();

		new Thread(() -> {
			try {
				// 睡眠一秒,保证t1线程,完成了ABA操作
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// 把100 改成 101 然后在改成100,也就是ABA
			System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());

		}, "t2").start();

		/
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (Exception e) {
			e.printStackTrace();
		}
		/

		
		System.out.println("============以下是ABA问题的解决==========");

		new Thread(() -> {

			// 获取版本号
			int stamp = atomicStampedReference.getStamp();
			System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);

			// 暂停t3一秒钟
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			// 传入4个值,期望值,更新值,期望版本号,更新版本号
			atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(),
					atomicStampedReference.getStamp() + 1);

			System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());

			atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),
					atomicStampedReference.getStamp() + 1);

			System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());

		}, "t3").start();

		new Thread(() -> {

			// 获取版本号
			int stamp = atomicStampedReference.getStamp();
			System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);

			// 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);

			System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:"
					+ atomicStampedReference.getStamp());

			System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());

		}, "t4").start();

	}
}

输出结果

============以下是ABA问题的产生==========
true    2019
============以下是ABA问题的解决==========
t3     第一次版本号1
t4     第一次版本号1
t3     第二次版本号2
t3     第三次版本号3
t4     修改成功否:false     当前最新实际版本号:3
t4     当前实际最新值100

什么是happen-before?

happen-before出现的原因:


为了明确定义多线程场景下重排序的问题,Java引入了JMM(Java Memory Model),也就是Java内存模型。如果有了重排序就会出现原子性,可见性,有序性的问题,但是性能会提升。所以Java内存模型不是真实存在的,而是一套规范,可以方便的使开发者在运行效率和程序开发的方便性之间找到一个平衡点。

一方面要让CPU和编译器可以灵活的进行重排序,另一方面也要告诉开发者,在什么情况下什么样的重排序不需要感知,需要感知什么样的重排序并作出处理。

为了描述这个规范,JMM引入了happen-before,使用happen-before描述两个操作之间的内存可见性。

简单来说,happen-before的意思就是,如果 操作A happen-before 操作B,那么操作A的执行结果必须对操作B可见。

happen-before的七条原则:

  1. 单线程规则:同一个线程中的每个操作都happens-before于出现在其后的任何一个操作。
  2. 对一个监视器的解锁操作happens-before于每一个后续对同一个监视器的加锁操作。
  3. 对volatile字段的写入操作happens-before于每一个后续的对同一个volatile字段的操作。
  4. Thread.start()的调用操作会happens-before于启动线程里面的操作。
  5. 一个线程中的所有操作都happens-before于其他线程成功返回在该线程上的join()调用后的所有操作。
  6. 一个对象构造函数的结束操作happens-before与该对象的finalizer的开始操作。
  7. 传递性规则:如果A操作happens-before于B操作,而B操作happens-before与C操作,那么A动作happens-before于C操作。

4.我们知道ArrayList是线程不安全的,请编码给出一个不安全的案例并给出解决方案。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.Vector;

public class ArrayListNotSafeDemo {
	public static void main(String[] args) {
        List list = new ArrayList<>();
        //List list = new Vector<>();
        //List list = Collections.synchronizedList(new ArrayList<>());

        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
	}
}

上述程序会抛出java.util.ConcurrentModificationException 异常(并发修改异常)

解决方法:

(1)Vector

  (2) Collections.synchronizedList(new ArrayList<>());  (包书皮)

(3)JUC下有一个类 CopyOnWriteArrayList 写时复制

CopyOnWriteArrayList 源码:

public class CopyOnWriteArrayList
    implements List, RandomAccess, Cloneable, java.io.Serializable {

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
    
    final Object[] getArray() {
        return array;
    }

    final void setArray(Object[] a) {
        array = a;
    }
    
    ...
    
	public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    
    ...
    
    public String toString() {
        return Arrays.toString(getArray());
    }
    
    ...
}

CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newELements,然后新的容器Object[ ] newELements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray (newELements)。

这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁(区别于Vector和Collections.synchronizedList()),因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
 

5.集合类不安全之Set

hashset 是非线程安全的,hashset内部是包装了一个hashmap的。

解决方法:

  1. Collections.synchronizedSet(new HashSet<>())(类似于包书皮)
  2. CopyOnWriteArraySet<>()(推荐)

6.HashSet的底层是HashMap,但map需要key,value两个值,为什么set只需放一个值?面试官的套路,你确定是hashmap吗?hashSet为什么能去重? 

分析源码:这是set的add方法,这里把值放到了map的key上面,而value是一个常量值PRESENT

hashset之所以可以去重就是因为利用了hsahMap的key不能重复的原理

public HashSet() {
        map = new HashMap<>();
}
 
//add 本质其实就是一个map的key,map的key是无法重复的,所以使用的就是map存储
//hashSet就是使用了hashmap key不能重复的原理
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}
//PRESENT是什么? 是一个常量  不会改变的常量  无用的占位
private static final Object PRESENT = new Object();

7.集合类不安全之Map

解决方法:

  1. HashTable
  2. Collections.synchronizedMap(new HashMap<>()) (类似包书皮)
  3. ConcurrentHashMap<>()(推荐)

8.ConcurrentHashMap<>()

HashMap: https://blog.csdn.net/Mcdull__/article/details/118493781

TreeMap:https://blog.csdn.net/Mcdull__/article/details/118915576

ConcurrentHashMap:https://blog.csdn.net/Mcdull__/article/details/118550908

9.作用域,值传递和引用传递

值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。

引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。
 

下面代码输出的结果是:?

class Person {
    private Integer id;
    private String personName;

    public Person(String personName) {
        this.personName = personName;
    }

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getPersonName() {
		return personName;
	}

	public void setPersonName(String personName) {
		this.personName = personName;
	}
}

public class TransferValueDemo {
    public void changeValue1(int age) {
        age = 30;
    }

    public void changeValue2(Person person) {
        person.setPersonName("XXXX");
    }
    public void changeValue3(String str) {
        str = "XXX";
    }

    public static void main(String[] args) {
        TransferValueDemo test = new TransferValueDemo();

        // 定义基本数据类型
        int age = 20;
        test.changeValue1(age);
        System.out.println("age ----" + age);

        // 实例化person类
        Person person = new Person("abc");
        test.changeValue2(person);
        System.out.println("personName-----" + person.getPersonName());

        // String
        String str = "abc";
        test.changeValue3(str);
        System.out.println("string-----" + str);

    }
}
age ----20
personName-----XXXX
string-----abc

解析:

(1)age = 20是main方法里的,要打印的是main方法中的age ,changeValue1改变的是main的复印件,而main里面的原件没有改变,故输出的是20.  值传递

java基础面经--上_第6张图片

(2)要打印的是main方法中的person,引用传递,传递内存地址,changeValue2和main两个引用指向同一个内存地址abc,修改之后,变成xxx,故输出的是xxx

java基础面经--上_第7张图片                 java基础面经--上_第8张图片

(3)这是面试官故意挖的坑,要打印的是main方法的str,由于String方法的特殊性,String str = "abc";会在字符串常量池中先找,没有就新建.changeValue3方法开始会和main方法指向同一个地abc,当执行 changeValue3 中的 str = "xxx"时,由于string的特殊性,会先在常量池中找有没有xxx,没有就新建,随后changeValue3方法就指向xxx了,而main依然指向abc。

java基础面经--上_第9张图片  java基础面经--上_第10张图片

10.java的锁

公平锁、非公平锁、可重入锁(递归锁)、自旋锁(要会手写自旋锁),独占锁(写锁,互斥锁、排他锁、X锁)、共享锁(读锁、S锁),乐观锁、悲观锁。

(1)公平和非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁(类似加塞)。在高并发情况下,有可能造成优先级反转或者饥饿现象。

区别:

公平锁很公平,按照FIFO的规则从队列中取到自己,非公平锁比较粗鲁,上来就尝试直接占有锁,如果尝试失败,就再采用类似公平锁那种方式。非公平锁的优点在于吞吐量比公平锁大。

题外话:

ReentrantLock 通过传递true/false来指定该锁是否是公平锁,默认是非公平锁。

Synchronized 是一种非公平锁。 

这也是lock和synchronized之间的一点区别

(2)可重入锁(递归锁):

Synchronized和ReentrantLock是典型的可重入锁。

作用: 最大的作用是避免死锁

意思就是 家里的防盗门是一把锁,家里的厕所是另一把锁,只要能进家门(拿到防盗门这一把锁),就能进厕所了(不需要厕所的锁了)。自己可以获取自己的内部锁。

官方定义:

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

Synchronized可入锁演示程序

class Phone {

    public synchronized void sendSMS() throws Exception{
        System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");

        // 在同步方法中,调用另外一个同步方法
        sendEmail();
    }


    public synchronized void sendEmail() throws Exception{
        System.out.println(Thread.currentThread().getName() + "\t invoked sendEmail()");
    }
}

public class SynchronizedReentrantLockDemo {

	public static void main(String[] args) {
        Phone phone = new Phone();

        // 两个线程操作资源类
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t1").start();

        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "t2").start();
	}

}

输出结果:

t1	 invoked sendSMS()
t1	 invoked sendEmail()
t2	 invoked sendSMS()
t2	 invoked sendEmail()

ReentrantLock可重入锁演示程序

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Phone2 implements Runnable{

    Lock lock = new ReentrantLock();

    /**
     * set进去的时候,就加锁,调用set方法的时候,能否访问另外一个加锁的set方法
     */
    public void getLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t get Lock");
            setLock();
        } finally {
            lock.unlock();
        }
    }

    public void setLock() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t set Lock");
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
        getLock();
    }
}

public class ReentrantLockDemo {


    public static void main(String[] args) {
        Phone2 phone = new Phone2();

        /**
         * 因为Phone实现了Runnable接口
         */
        Thread t3 = new Thread(phone, "t3");
        Thread t4 = new Thread(phone, "t4");
        t3.start();
        t4.start();
    }
}

输出结果

t3	 get Lock
t3	 set Lock
t4	 get Lock	
t4	 set Lock

注意,上面可以同时加多把锁  ,但是一定要和unlock()匹配,有几个lock就得有几个unlock,否则会卡死。

lock.lock();

lock.lock();

lock.unlock();

lock.unlock();

(3)自旋锁(spinlock)

自旋的反义词叫做阻塞

定义:自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁

优点:减少线程上下文切换的消耗

缺点:循环会消耗CPU

提到了互斥同步对性能最大的影响阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。

《深入理解JVM.2nd》Page 398

代码验证:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    // 现在的泛型装的是Thread,原子引用线程
    AtomicReference  atomicReference = new AtomicReference<>();

    public void myLock() {
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in ");

        // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
        while(!atomicReference.compareAndSet(null, thread)) {
			//摸鱼
        }
    }

    public void myUnLock() {
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();

        // 自己用完了后,把atomicReference变成null
        atomicReference.compareAndSet(thread, null);

        System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
    }
    
	public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        // 启动t1线程,开始操作
        new Thread(() -> {

            // 开始占有锁
            spinLockDemo.myLock();

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t1").start();


        // 让main线程暂停1秒,使得t1线程,先执行
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 1秒后,启动t2线程,开始占用这个锁
        new Thread(() -> {

            // 开始占有锁
            spinLockDemo.myLock();
            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t2").start();
	}
}

输出结果:

t1	 come in 
t2	 come in 
t1	 invoked myUnlock()
t2	 invoked myUnlock()

(4)读写锁(ReadWriteLock)

独占锁(写锁)

指该锁一次只能被一个线程锁持有。ReentrantLock  Synchronized

共享锁(读锁)

指该锁可以被多个线程锁持有。ReentrantReadWriteLock的读锁是共享锁,其写锁是独占锁。

读可以多个人来读,写只能一个人来写,即读写,写读,写写的过程是互斥的,读读是可以共存的。

读写锁的场景:签名时,有人看的同时有人可以写   

代码验证:

实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

class MyCache {

    private volatile Map map = new HashMap<>();

    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
        try {
            // 模拟网络拥堵,延迟0.3秒
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "\t 写入完成");
    }

    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
        try {
            // 模拟网络拥堵,延迟0.3秒
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object value = map.get(key);
        System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
    }
}

public class ReadWriteWithoutLockDemo {

	public static void main(String[] args) {
        MyCache myCache = new MyCache();
        // 线程操作资源类,5个线程写
        for (int i = 0; i < 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt +  "");
            }, String.valueOf(i)).start();
        }
        
        // 线程操作资源类, 5个线程读
        for (int i = 0; i < 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }

	}

}

输出结果:

0	 正在写入:0
1	 正在写入:1
3	 正在写入:3
2	 正在写入:2
4	 正在写入:4
0	 正在读取:
1	 正在读取:
2	 正在读取:
4	 正在读取:
3	 正在读取:
1	 写入完成
4	 写入完成
0	 写入完成
2	 写入完成
3	 写入完成
3	 读取完成:3
0	 读取完成:0
2	 读取完成:2
1	 读取完成:null
4	 读取完成:null

写操作  原则:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断

看到有些线程读取到null,而且写操作不满足原子+独占原则,可用ReentrantReadWriteLock解决

package com.lun.concurrency;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MyCache2 {

    private volatile Map map = new HashMap<>();

    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {

        // 创建一个写锁
        rwLock.writeLock().lock();

        try {

            System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);

            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            map.put(key, value);

            System.out.println(Thread.currentThread().getName() + "\t 写入完成");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 写锁 释放
            rwLock.writeLock().unlock();
        }
    }

    public void get(String key) {

        // 读锁
        rwLock.readLock().lock();
        try {

            System.out.println(Thread.currentThread().getName() + "\t 正在读取:");

            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Object value = map.get(key);

            System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 读锁释放
            rwLock.readLock().unlock();
        }
    }

    public void clean() {
        map.clear();
    }


}

public class ReadWriteWithLockDemo {
    public static void main(String[] args) {

        MyCache2 myCache = new MyCache2();

        // 线程操作资源类,5个线程写
        for (int i = 1; i <= 5; i++) {
            // lambda表达式内部必须是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt +  "");
            }, String.valueOf(i)).start();
        }

        // 线程操作资源类, 5个线程读
        for (int i = 1; i <= 5; i++) {
            // lambda表达式内部必须是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }
}

输出结果:

1	 正在写入:1
1	 写入完成
2	 正在写入:2
2	 写入完成
3	 正在写入:3
3	 写入完成
5	 正在写入:5
5	 写入完成
4	 正在写入:4
4	 写入完成
2	 正在读取:
3	 正在读取:
1	 正在读取:
5	 正在读取:
4	 正在读取:
3	 读取完成:3
2	 读取完成:2
1	 读取完成:1
5	 读取完成:5
4	 读取完成:4

(5)乐观锁、悲观锁

java的乐观锁机制:

乐观锁体现的是悲观锁的反面,他是一种积极的思想,总是认为数据时不会被修改的,所以是不会对数据上锁的。但是乐观锁在更新的时候会去判断数据是否被修改过。乐观锁的实现方案一般有两种(版本号机制和CAS)。乐观锁适用于读多写少的场景,这样可以提高系统的并发量。

乐观锁大多是基于数据版本记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个version字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本和数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

java的悲观锁机制:

假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作,在查询完数据的时候就把事务锁起来,直到提交事务,实现方式:使用数据库中的锁机制。一般多写的场景下用悲观锁就比较合适。

锁优化?自适应自旋锁?锁消除?偏向锁?轻量级锁?重量级锁?

1.什么是锁优化?

  • 减少锁的时间:不需要同步执行的代码,能不放在同步块里面执行就不要放在同步块内,可以让锁尽快释放。
  • 减少锁的粒度:他的思想是将物理上的一个锁,拆成逻辑上的多个锁,提高并行度,从而降低锁竞争。它的思想也是用空间来换时间。
  • 锁粗化:大部分情况下我们是要让锁的粒度最小化,锁粗化则是要增大锁的粒度。假设有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环都进出一次临界区,效率是非常差的。
  • 使用读写锁:ReentrantReadWriteLock是一个读写锁,读操作加读锁,可并发读,写操作加写锁,只能单线程写。
  • 使用CAS:如果需要同步的操作执行非常快,并且线程竞争并不激烈,这时候使用volatiled+cas效率会更高,因为加锁会导致线程的上下文切换。

2.自适应自旋锁:

我们都知道如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销;自旋锁默认的自旋次数是10

对自旋锁方式进行优化,使它的自旋次数不再固定,自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

3.锁消除:锁消除是Java虚拟机在JIT(即时)编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

4.偏向锁:

所谓的偏向,就是偏心锁会偏向于当前已经占有锁的线程;也就是说,这个线程已经占有这个锁,当他再次试图去获取这个锁的时候,他会以最快的方式去拿到这个锁,而不需要再进行一些monitor操作,因此在这方面是会对性能有所提升的,因为在大部分情况下是没有竞争的,所以锁此时是没用的,所以使用偏向锁是可以提高性能的。

5.重量级锁:

重量级锁的加锁,解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下。

说说锁升级?

一开始是无锁的状态,一上来会先去判断一下有没有锁,有锁的话最开始的时候锁是支持偏向锁的。偏向锁当前获取到锁资源的这个线程,我会优先让他再去获取这个锁,如果它没获取到这个锁,就升级为一个轻量级的,一个cas锁,即乐观锁,乐观锁的时候它是一个比较和交换的过程,如果没有设置成功的话,它会进行一个自旋,然后自旋到一定次数之后才会升级成一个synchronized的这样一个重量级的锁,这样的话他就保证了性能的问题。你想想如果一开始就是synchronized这样一个重量级的锁,那性能就比较差了。

java基础面经--上_第11张图片

锁状态一共有四种:无锁,偏向锁,轻量级锁,重量级锁。锁升级的过程是单向的,不能退化,只能是从偏向锁到轻量级锁再到重量级锁的过程,记录这几种锁状态的标记是在对象头的Mark Word中。

偏向锁

一开始的时候是无锁状态。然后此时第一个线程进来了,在对象头的Mark Word中看到此时是无锁状态,就把此时的锁升级为偏向锁,并将自己的线程id用CAS的方式赋值到Mark Word中。然后就进入到了该线程的同步块中。

轻量级锁

如果此时有第二个线程进来,它会去查看当前偏向锁指向的线程id是否是自己,结果发现不是,但是此时还是会CAS去尝试修改线程id指向自己,去赌一下第一个线程此时已经用完了释放了。如果释放了,它会将锁改为无锁状态,将线程id置空。然后第二个线程拿到这个资源,将线程id赋值给自己,锁升级为偏向锁。如果第一个线程此时没释放,则JVM会在第一个线程到达安全点的时候撤销当前的偏向锁。下一步当前线程栈中会分配锁记录,并拷贝Mark Word到锁记录中。然后两个线程用CAS的方式去修改Mark Word中的指针指向自己,假如说第一个线程修改成功了,然后将锁升级为轻量级锁,去执行同步语句块中的内容。

重量级锁

修改失败的第二个线程会进入自旋状态,自旋结束后会继续去尝试CAS修改指针指向自己。如果自旋失败超过一定次数的时候(这个次数会动态进行调整),会请求JVM将此时的锁状态升级为重量级锁,这是依赖于底层操作系统的调度库来实现的。接着将Mark Word指向重量级锁Monitor的指针,然后挂起当前第二个线程(被放在Monitor的_EntryList中)。等一个线程执行完毕后,会查看当前Mark Word中的指针是否仍然指向自己,如果是自己的话就释放锁,否则不是自己的话,说明此时已经升级成了重量级锁,除了释放锁之后,还会唤醒阻塞的线程,进行新一轮的锁竞争。在此之后,该锁就一直会是重量级锁存在了

为什么要引入偏向锁?

因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

为什么要引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋着等待锁释放。

轻量级锁什么时候升级为重量级锁

自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

11、CountDownLatch/CycliBarrier/Semaphore(信号量)使用过吗?

CountDownLatch:

JUC下面的包,功能类似于火箭发射倒计时。

主要有两个方法:await(),countDown()

当一个或多个线程调用await()时,调用线程会被阻塞。其他线程调用countDown()会将计数器减一,直到计数器的值变为0时,因调用await()方法被阻塞的线程会被唤醒,执行

eg:

假设一个自习室里有7个人,其中有一个是班长,班长的主要职责就是在其它6个同学走了后,关灯,锁教室门,然后走人,因此班长是需要最后一个走的,那么有什么方法能够控制班长这个线程是最后一个执行,而其它线程是随机执行的

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {

        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }

        countDownLatch.await();

        System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
    }
}

输出结果:

0	 上完自习,离开教室
6	 上完自习,离开教室
4	 上完自习,离开教室
5	 上完自习,离开教室
3	 上完自习,离开教室
1	 上完自习,离开教室
2	 上完自习,离开教室
main	 班长最后关门

枚举 + CountDownLatch

枚举相当于数据库中的表

java基础面经--上_第12张图片

例2:程序演示秦国统一六国

import java.util.Objects;

public enum CountryEnum {
	ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"), FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");

	private Integer retcode;
	private String retMessage;

	CountryEnum(Integer retcode, String retMessage) {
		this.retcode = retcode;
		this.retMessage = retMessage;
	}

	public static CountryEnum forEach_countryEnum(int index) {
		
		CountryEnum[] myArray = CountryEnum.values();
		
		for(CountryEnum ce : myArray) {
			if(Objects.equals(index, ce.getRetcode())) {
				return ce;
			}
		}
		
		return null;
	}

	public Integer getRetcode() {
		return retcode;
	}

	public void setRetcode(Integer retcode) {
		this.retcode = retcode;
	}

	public String getRetMessage() {
		return retMessage;
	}

	public void setRetMessage(String retMessage) {
		this.retMessage = retMessage;
	}

}
import java.util.concurrent.CountDownLatch;

public class UnifySixCountriesDemo {

	public static void main(String[] args) throws InterruptedException {
        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "国被灭了!");
                countDownLatch.countDown();
            }, CountryEnum.forEach_countryEnum(i).getRetMessage()).start();
        }

        countDownLatch.await();

        System.out.println(Thread.currentThread().getName() + " 秦国统一中原。");
	}
}

输出结果:

齐国被灭了!
燕国被灭了!
楚国被灭了!
魏国被灭了!
韩国被灭了!
赵国被灭了!
main 秦国统一中原。

CycliBarrier:与CountDownLatch相反

JUC下面的包,功能类似于召集七颗龙珠才能召唤神龙!

让一组线程到达一个屏障(也叫做同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过await()方法。


程序演示集齐7个龙珠,召唤神龙:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class SummonTheDragonDemo {
    public static void main(String[] args) {
        /**
         * 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
         */
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙");
        });

        for (int i = 1; i <= 7; i++) {
            final Integer tempInt = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");

                try {
                    // 先到的被阻塞,等全部线程完成后,才能执行方法
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

输出结果:

2	 收集到 第2颗龙珠
6	 收集到 第6颗龙珠
1	 收集到 第1颗龙珠
7	 收集到 第7颗龙珠
5	 收集到 第5颗龙珠
4	 收集到 第4颗龙珠
3	 收集到 第3颗龙珠
召唤神龙

Semaphore:信号量

多个线程枪多份资源   JUC下面的包,功能类似于争车位:20个车抢30个车位

正常的锁(lock或synchronized)在任何时刻都只允许一个任务访问一个资源,而Semaphore允许n个任务同时访问n个资源

信号量主要用于两个目的

  1. 用于多个共享资源的互斥使用
  2. 并发线程数的控制

主要有两个方法:acquire()和release()

模拟一个抢车位的场景,假设一共有6个车,3个停车位

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {
    public static void main(String[] args) {

        /**
         * 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
         */
        Semaphore semaphore = new Semaphore(3, false);

        // 模拟6部车
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                try {
                    // 代表一辆车,已经占用了该车位
                    semaphore.acquire(); // 抢占

                    System.out.println(Thread.currentThread().getName() + "\t 抢到车位");

                    // 每个车停3秒
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "\t 离开车位");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放停车位
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

输出结果:

1	 抢到车位
2	 抢到车位
0	 抢到车位
0	 离开车位
2	 离开车位
1	 离开车位
5	 抢到车位
4	 抢到车位
3	 抢到车位
5	 离开车位
4	 离开车位
3	 离开车位

12、阻塞队列知道吗?

定义:

java基础面经--上_第13张图片

为什么用BlockingQueue?有什么好处?

好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这些BlockingQueue都一手包办了。

在cuncurrent包发布以前,在多线程环境下,程序员必须自己控制这些细节,尤其还要兼顾效率和线程安全,而这会带给我们的程序不小的复杂度。

架构介绍:

java基础面经--上_第14张图片

种类分析:

java基础面经--上_第15张图片

BlockingQueue的核心方法

竖着看(一组一组的)

方法类型 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用

java基础面经--上_第16张图片

面试题:在Queue中poll()和remove()有什么区别?

poll()和remove()都将移除并且返回队头,但是在poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。 

SynchronousQueue:

SynchronousQueue没有容量。

与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。

每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class SynchronousQueueDemo {
	public static void main(String[] args) {
		BlockingQueue blockingQueue = new SynchronousQueue<>();

		new Thread(() -> {
		    try {       
		        System.out.println(Thread.currentThread().getName() + "\t put A ");
		        blockingQueue.put("A");
		       
		        System.out.println(Thread.currentThread().getName() + "\t put B ");
		        blockingQueue.put("B");        
		        
		        System.out.println(Thread.currentThread().getName() + "\t put C ");
		        blockingQueue.put("C");        
		        
		    } catch (InterruptedException e) {
		        e.printStackTrace();
		    }
		}, "t1").start();
		
		new Thread(() -> {
			try {
				
				try {
					TimeUnit.SECONDS.sleep(5);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				blockingQueue.take();
				System.out.println(Thread.currentThread().getName() + "\t take A ");
				
				try {
					TimeUnit.SECONDS.sleep(5);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				blockingQueue.take();
				System.out.println(Thread.currentThread().getName() + "\t take B ");
				
				try {
					TimeUnit.SECONDS.sleep(5);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				blockingQueue.take();
				System.out.println(Thread.currentThread().getName() + "\t take C ");
				
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}, "t2").start();
	}
	
}

阻塞队列的应用:

  • 1.生产者消费者(传统版,阻塞队列版)
  • 2.线程池
  • 3.消息中间件

实现一个简单的生产者消费者模式(传统版)

java基础面经--上_第17张图片

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ShareData {

    private int number = 0;

    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    public void increment() throws Exception{
        // 同步代码块,加锁
        lock.lock();
        try {
            // 判断
            while(number != 0) {
                // 等待不能生产
                condition.await();
            }

            // 干活
            number++;

            System.out.println(Thread.currentThread().getName() + "\t " + number);

            // 通知 唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement() throws Exception{
        // 同步代码块,加锁
        lock.lock();
        try {
            // 判断
            while(number == 0) {
                // 等待不能消费
                condition.await();
            }

            // 干活
            number--;

            System.out.println(Thread.currentThread().getName() + "\t " + number);

            // 通知 唤醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class TraditionalProducerConsumerDemo {
    
	public static void main(String[] args) {

        ShareData shareData = new ShareData();

        // t1线程,生产
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();

        // t2线程,消费
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
        
    }
}

输出结果:

t1	 1
t2	 0
t1	 1
t2	 0
t1	 1
t2	 0
t1	 1
t2	 0
t1	 1
t2	 0
t1	 1
t2	 0
t1	 1
t2	 0
t1	 1
t2	 0
t1	 1
t2	 0
t1	 1
t2	 0

注意,increment()和decrement()内的

// 判断
while(number != 0) {
    // 等待不能生产
    condition.await();
}

不能用

// 判断
if(number != 0) {
    // 等待不能生产
    condition.await();
}

否则会出现虚假唤醒,出现异常状况。(出现加到2或者减到-1的情况,需要用while进行再一次判断。)

实现生产者消费者阻塞队列版

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class MyResource {
    // 默认开启,进行生产消费
    // 这里用到了volatile是为了保持数据的可见性,也就是当TLAG修改时,要马上通知其它线程进行修改
    private volatile boolean FLAG = true;

    // 使用原子包装类,而不用number++ 保证原子性
    private AtomicInteger atomicInteger = new AtomicInteger();

    // 这里不能为了满足条件,而实例化一个具体的SynchronousBlockingQueue
    BlockingQueue blockingQueue = null;

    // 而应该采用依赖注入里面的,构造注入方法传入
    public MyResource(BlockingQueue blockingQueue) {
        this.blockingQueue = blockingQueue;
        // 查询出传入的class是什么
        System.out.println(blockingQueue.getClass().getName());
    }


    public void myProducer() throws Exception{
        String data = null;
        boolean retValue;
        // 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
        // 当FLAG为true的时候,开始生产
        while(FLAG) {
            data = atomicInteger.incrementAndGet() + "";

            // 2秒存入1个data
            retValue = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
            if(retValue) {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data  + "成功" );
            } else {
                System.out.println(Thread.currentThread().getName() + "\t 插入队列:" + data  + "失败" );
            }

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(Thread.currentThread().getName() + "\t 停止生产,表示FLAG=false,生产介绍");
    }


    public void myConsumer() throws Exception{
        String retValue;
        // 多线程环境的判断,一定要使用while进行,防止出现虚假唤醒
        // 当FLAG为true的时候,开始生产
        while(FLAG) {
            // 2秒取出1个data
            retValue = blockingQueue.poll(2L, TimeUnit.SECONDS);
            if(retValue != null && retValue != "") {
                System.out.println(Thread.currentThread().getName() + "\t 消费队列:" + retValue  + "成功" );
            } else {
                FLAG = false;
                System.out.println(Thread.currentThread().getName() + "\t 消费失败,队列中已为空,退出" );

                // 退出消费队列
                return;
            }
        }
    }

    /**
     * 停止生产的判断
     */
    public void stop() {
        this.FLAG = false;
    }

}
public class ProducerConsumerWithBlockingQueueDemo {
    public static void main(String[] args) {
        // 传入具体的实现类, ArrayBlockingQueue
        MyResource myResource = new MyResource(new ArrayBlockingQueue(10));

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 生产线程启动\n\n");

            try {
                myResource.myProducer();
                System.out.println("\n");

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "producer").start();


        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 消费线程启动");

            try {
                myResource.myConsumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "consumer").start();

        // 5秒后,停止生产和消费
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        System.out.println("\n\n5秒中后,生产和消费线程停止,线程结束");
        myResource.stop();
    }
}

输出结果:

java.util.concurrent.ArrayBlockingQueue
producer	 生产线程启动


consumer	 消费线程启动
producer	 插入队列:1成功
consumer	 消费队列:1成功
producer	 插入队列:2成功
consumer	 消费队列:2成功
producer	 插入队列:3成功
consumer	 消费队列:3成功
producer	 插入队列:4成功
consumer	 消费队列:4成功
producer	 插入队列:5成功
consumer	 消费队列:5成功


5秒中后,生产和消费线程停止,线程结束
producer	 停止生产,表示FLAG=false,生产介绍


consumer	 消费失败,队列中已为空,退出

13、Synchronized和Lock有什么区别?用新的lock有什么好处?


1.原始构成

  • synchronized属于JVM层面,属于java的关键字,monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也属于monitor对象,只能在同步块或者方法中才能调用wait/notify等方法)
  • lock是具体类(java。util.concurrent.locks.Lock)是api层面的锁。

2.使用方法

  • synchronized不需要手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用。
  • lock需要用户手动释放锁,若没有主动释放锁,就有可能出现死锁现象,需要lock()和unlock()配合try catch语句来完成

3.等待是否中断

  1. synchronized不可中断,除非跑抛出异常或者正常运行完成。
  2. lock可以中断,可以设置超时方法
  •    设置超时方法,trylock(long timeout, TimeUnit unit)
  •    lockInterrupible() 放代码块中,调用interrupt() 方法可以中断

4.加锁是否公平

  • synchronized非公平锁
  • ReentrantLock默认非公平锁,但是构造函数可以传递boolean值,true为公平锁,false为非公平锁

5.锁绑定多个条件Condition

  • synchronized 没有,要么随机唤醒一个线程,要么全部唤醒
  • RenntrantLock用来实现分组唤醒需要唤醒的线程,可以精确唤醒(比如下面的例子 按次序唤醒)

用新的lock的好处:第5点

实现场景

多线程之间按顺序调用,实现 A-> B -> C 三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
紧接着
AA打印5次,BB打印10次,CC打印15次

来10轮

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ShareResource {
    // A 1   B 2   c 3
    private int number = 1;
    // 创建一个重入锁
    private Lock lock = new ReentrantLock();

    // 这三个相当于备用钥匙
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void print5() {
        lock.lock();
        try {
            // 判断
            while(number != 1) {
                // 不等于1,需要等待
                condition1.await();
            }

            // 干活
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
            }

            // 唤醒 (干完活后,需要通知B线程执行)
            number = 2;
            // 通知2号去干活了
            condition2.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print10() {
        lock.lock();
        try {
            // 判断
            while(number != 2) {
                // 不等于1,需要等待
                condition2.await();
            }

            // 干活
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
            }

            // 唤醒 (干完活后,需要通知C线程执行)
            number = 3;
            // 通知2号去干活了
            condition3.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void print15() {
        lock.lock();
        try {
            // 判断
            while(number != 3) {
                // 不等于1,需要等待
                condition3.await();
            }

            // 干活
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t " + number + "\t" + i);
            }

            // 唤醒 (干完活后,需要通知C线程执行)
            number = 1;
            // 通知1号去干活了
            condition1.signal();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class SynchronizedAndReentrantLockDemo {
    public static void main(String[] args) {

        ShareResource shareResource = new ShareResource();
        int num = 10;

        new Thread(() -> {
            for (int i = 0; i < num; i++) {
                    shareResource.print5();
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < num; i++) {
                shareResource.print10();
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < num; i++) {
                shareResource.print15();
            }
        }, "C").start();
    }
}

输出结果:

...
A	 1	0
A	 1	1
A	 1	2
A	 1	3
A	 1	4
B	 2	0
B	 2	1
B	 2	2
B	 2	3
B	 2	4
B	 2	5
B	 2	6
B	 2	7
B	 2	8
B	 2	9
C	 3	0
C	 3	1
C	 3	2
C	 3	3
C	 3	4
C	 3	5
C	 3	6
C	 3	7
C	 3	8
C	 3	9
C	 3	10
C	 3	11
C	 3	12
C	 3	13
C	 3	14
A	 1	0
A	 1	1
A	 1	2
A	 1	3
A	 1	4
B	 2	0
B	 2	1
B	 2	2
B	 2	3
B	 2	4
B	 2	5
B	 2	6
B	 2	7
B	 2	8
B	 2	9
C	 3	0
C	 3	1
C	 3	2
C	 3	3
C	 3	4
C	 3	5
C	 3	6
C	 3	7
C	 3	8
C	 3	9
C	 3	10
C	 3	11
C	 3	12
C	 3	13
C	 3	14

14、线程启动的四种方式:

  • 1.继承Thread类;
  • 2.实现Runnable接口
  • 3.实现Callable接口
  • 4.使用Executors工具类创建线程池

方法一:继承 Thread 类

步骤

  1. 定义一个Thread类的子类,重写run方法,将相关逻辑实现,run()方法就是线程要执行的业务逻辑方法
  2. 创建自定义的线程子类对象
  3. 调用子类实例的start()方法来启动线程
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
    }

}
public class TheadTest {

    public static void main(String[] args) {
        MyThread myThread = new MyThread(); 	
        myThread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
    }

}

运行结果

main main()方法执行结束
Thread-0 run()方法正在执行...

方法二:实现Runnable接口

步骤

  1. 定义Runnable接口实现类MyRunnable,并重写run()方法
  2. 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
  3. 调用线程对象的start()方法
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }

}
public class RunnableTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}

执行结果

main main()方法执行完成
Thread-0 run()方法执行中...

方法三:实现Callable接口 (有返回值)

步骤

  1. 创建实现Callable接口的类myCallable    (重写的是call()方法,不是run())
  2. 以myCallable为参数创建FutureTask对象
  3. 将FutureTask作为参数创建Thread对象
  4. 调用线程对象的start()方法

为什么使用FutureTask?(适配器模式)(spring bean 是单例模式,beanfactory是简单工厂模式,factorybean是抽象工厂模式)

FutureTask 实现了RunnableFuture接口,而RunnableFuture又继承了Runnable 和Future接口,

而有FutureTask(Callable callable),即FutureTask可以直接将实现了Runnable和Callable接口的对象封装成FutureTask对象。从而调用FutureTask对象的方法。

java基础面经--上_第18张图片

代码:

public class MyCallable implements Callable {

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
        return 1;
    }

}
public class CallableTest {

    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Thread.sleep(1000);
            System.out.println("返回结果 " + futureTask.get());//建议放在最后
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}

执行结果:

Thread-0 call()方法执行中...
返回结果 1
main main()方法执行完成

注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。所以建议放在最后。

说一下 runnable 和 callable 有什么区别?

相同点

  1. 都是接口
  2. 都可以编写多线程程序
  3. 都采用Thread.start()启动线程

主要区别

  1. Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
  2. Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理(相当于不抛异常);Callable 接口 call 方法允许抛出异常,可以获取异常信息
  3. 接口的方法不一样,一个是run(),一个是call()

方法四:使用 Executors 工具类创建线程池(下面详细会讲)

Executors提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。

主要有newFixedThreadPool(int)(一池固定个处理线程)newCachedThreadPool()(一池N个处理线程)newSingleThreadExecutor()(一池一个处理线程)newScheduledThreadPool()(带时间调度的线程池),后续详细介绍这四种线程池

15、线程池用过吗?生产上你如何设置合理的参数?ThreadPoolExecutor谈谈你的理解?

为什么用线程池?他的优势是什么?

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

主要特点:线程复用;控制最大并发数;管理线程

优势:

  1. 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  2. 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的架构:

java基础面经--上_第19张图片

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。其中Executors是Executor的资源类

了解

  • Executors.newScheduledThreadPool()
  • Executors.newWorkStealingPool(int) - Java8新增,使用目前机器上可用的处理器作为它的并行级别

重点

  • Executors.newSingleThreadExecutor() (一池一个处理线程)
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue()));
}

主要特点如下:

  1. 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
  2. newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue。

  • Executors.newFixedThreadPool(int) (一池固定个处理线程)
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue());
}

主要特点如下:

  1. 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  2. newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue。

  • Executors.newCachedThreadPool() (一池N个处理线程)
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue());
}

主要特点如下:

  1. 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  2. newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
     

代码验证:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {

    	// 一池5个处理线程(用池化技术,一定要记得关闭)
//    	ExecutorService threadPool = Executors.newFixedThreadPool(5);

    	// 创建一个只有一个线程的线程池
//    	ExecutorService threadPool = Executors.newSingleThreadExecutor();

    	// 创建一个拥有N个线程的线程池,根据调度创建合适的线程
    	ExecutorService threadPool = Executors.newCachedThreadPool();

        // 模拟10个用户来办理业务,每个用户就是一个来自外部请求线程
        try {

            // 循环十次,模拟业务办理,让5个线程处理这10个请求
            for (int i = 0; i < 10; i++) {
                final int tempInt = i;
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

输出结果:

pool-1-thread-1	 给用户:0 办理业务
pool-1-thread-6	 给用户:5 办理业务
pool-1-thread-5	 给用户:4 办理业务
pool-1-thread-2	 给用户:1 办理业务
pool-1-thread-4	 给用户:3 办理业务
pool-1-thread-3	 给用户:2 办理业务
pool-1-thread-10	 给用户:9 办理业务
pool-1-thread-9	 给用户:8 办理业务
pool-1-thread-8	 给用户:7 办理业务
pool-1-thread-7	 给用户:6 办理业务

线程池的七个重要参数?

源码:

public class ThreadPoolExecutor extends AbstractExecutorService {
    
    ...
    
	public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
    
    ...
    
}

1、corePoolSize:线程池中的常驻核心线程数(今日当值窗口)

  • 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。
  • 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。

2、maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1

3、keepAliveTime:多余的空闲线程的存活时间,当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下coolPoolSize个线程为止

4、unit:keepAliveTime的单位

5、workQueue:任务队列,被提交但是尚未被执行的任务(阻塞队列,候客区)

6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可。

7、handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程时(maximumPoolSize)时,如何来拒绝(最大线程也满了,阻塞队列也满了,就去拒绝来访)

例:

java基础面经--上_第20张图片

说说线程池的底层工作原理?

(就是上面说的七个参数)

java基础面经--上_第21张图片

java基础面经--上_第22张图片

重要重要重要

1.在创建了线程池后,等待提交过来的任务请求。

2.当调用execute()方法添加一个请求任务时,线程池会做出如下判断:

(1)如果正在正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务。

(2)如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列

(3)如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务。

(4)如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

3.当一个线程完成任务时,他会从队列中取出下一个任务来执行。

4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:

(1)如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉

(2)所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小

线程池的拒绝策略你谈谈(四大策略)

  1. AbortPolicy(默认):直接抛出 RejectedExecutionException异常阻止系统正常运知。
  2. CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
  4. DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
     

下面有具体代码验证

你在工作中单一的/固定数的/可变的 三种创建线程池的方法,你用哪个多?超级大坑

正确答案:一个都不用,我们生产上只能使用自定义的

Executor中JDK已经给你提供了,为什么不用?

阿里巴巴java开发手册明确提出:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理范法规让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

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

(1) FixedThreadPool和SingleThreadPool:

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

(2)CachedThreadPool和ScheduledThreadPool:

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

你在工作中是如何使用线程池的,是否自定义过线程池使用?

手写线程池和拒绝策略代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolExecutorDemo {

	public static void doSomething(ExecutorService executorService, int numOfRequest) {
	    
        try {

            System.out.println(((ThreadPoolExecutor)executorService).getRejectedExecutionHandler().getClass() + ":");
            TimeUnit.SECONDS.sleep(1);

            for (int i = 0; i < numOfRequest; i++) {
                final int tempInt = i;
                executorService.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 给用户:" + tempInt + " 办理业务");
                });
            }
            
            TimeUnit.SECONDS.sleep(1);
            System.out.println("\n\n");
            
        } catch (Exception e) {
        	System.err.println(e);
        } finally {
            executorService.shutdown();
        }
	}
	
	public static ExecutorService newMyThreadPoolExecutor(int corePoolSize,
           int maximumPoolSize, int blockingQueueSize, RejectedExecutionHandler handler){
		return new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                1,//keepAliveTime
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(blockingQueueSize),
                Executors.defaultThreadFactory(),
                handler);
	}
	
	
	public static void main(String[] args) {
		doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.AbortPolicy()), 10);
		doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.CallerRunsPolicy()), 20);
		doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.DiscardOldestPolicy()), 10);
		doSomething(newMyThreadPoolExecutor(2, 5, 3, new ThreadPoolExecutor.DiscardPolicy()), 10);
	}

}

输出结果:

class java.util.concurrent.ThreadPoolExecutor$AbortPolicy:
pool-1-thread-1	 给用户:0 办理业务
pool-1-thread-3	 给用户:5 办理业务java.util.concurrent.RejectedExecutionException: Task com.lun.concurrency.MyThreadPoolExecutorDemo$$Lambda$1/303563356@eed1f14 rejected from java.util.concurrent.ThreadPoolExecutor@7229724f[Running, pool size = 5, active threads = 0, queued tasks = 0, completed tasks = 8]

pool-1-thread-2	 给用户:1 办理业务
pool-1-thread-5	 给用户:7 办理业务
pool-1-thread-3	 给用户:3 办理业务
pool-1-thread-4	 给用户:6 办理业务
pool-1-thread-1	 给用户:2 办理业务
pool-1-thread-2	 给用户:4 办理业务
class java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy:
pool-2-thread-1	 给用户:0 办理业务
pool-2-thread-2	 给用户:1 办理业务
pool-2-thread-1	 给用户:2 办理业务
pool-2-thread-3	 给用户:5 办理业务
pool-2-thread-3	 给用户:7 办理业务
pool-2-thread-3	 给用户:9 办理业务
pool-2-thread-4	 给用户:6 办理业务
pool-2-thread-2	 给用户:3 办理业务
pool-2-thread-5	 给用户:8 办理业务
main	 给用户:10 办理业务
pool-2-thread-1	 给用户:4 办理业务
pool-2-thread-3	 给用户:11 办理业务
pool-2-thread-4	 给用户:13 办理业务
main	 给用户:14 办理业务
pool-2-thread-1	 给用户:12 办理业务
pool-2-thread-5	 给用户:15 办理业务
pool-2-thread-2	 给用户:17 办理业务
main	 给用户:18 办理业务
pool-2-thread-3	 给用户:16 办理业务
pool-2-thread-4	 给用户:19 办理业务



class java.util.concurrent.ThreadPoolExecutor$DiscardOldestPolicy:
pool-3-thread-1	 给用户:0 办理业务
pool-3-thread-2	 给用户:1 办理业务
pool-3-thread-1	 给用户:2 办理业务
pool-3-thread-2	 给用户:3 办理业务
pool-3-thread-3	 给用户:5 办理业务
pool-3-thread-5	 给用户:8 办理业务
pool-3-thread-2	 给用户:7 办理业务
pool-3-thread-4	 给用户:6 办理业务
pool-3-thread-1	 给用户:4 办理业务
pool-3-thread-3	 给用户:9 办理业务



class java.util.concurrent.ThreadPoolExecutor$DiscardPolicy:
pool-4-thread-1	 给用户:0 办理业务
pool-4-thread-2	 给用户:1 办理业务
pool-4-thread-1	 给用户:2 办理业务
pool-4-thread-2	 给用户:3 办理业务
pool-4-thread-3	 给用户:5 办理业务
pool-4-thread-3	 给用户:9 办理业务
pool-4-thread-1	 给用户:4 办理业务
pool-4-thread-5	 给用户:8 办理业务
pool-4-thread-4	 给用户:6 办理业务
pool-4-thread-2	 给用户:7 办理业务




生产上你如何设置合理的参数?

合理配置线程池你是如何考虑的?

1.CPU密集型

CPU密集的意思是该任务需要大量的运算而没有阻塞,CPU一直全速运行。

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务配置尽可能少的线程数量:

一般公式:(CPU核数+1)个线程的线程池

2.lO密集型

由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数 * 2。

IO密集型,即该任务需要大量的IO,即大量的阻塞。

在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。

所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型时,大部分线程都阻塞,故需要多配置线程数:

参考公式:CPU核数/ (1-阻塞系数)

阻塞系数在0.8~0.9之间

比如8核CPU:8/(1-0.9)=80个线

shutdown和shutdownnow,两个有什么区别?

线程池调用了shutdown方法,只是拒绝新任务的提交,那些已经存放在等待队列待执行的任务一样会被执行,而shutdownNow方法则不同,一旦调用后,线程池就立即关闭了,等待队列的任务不会再被执行,新任务的提交也会被拒绝。

16、JVM+GC相关

回顾基础:

https://blog.csdn.net/Mcdull__/article/details/112170487

面试题:

1、JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots?(GC Root如何确定,哪些对象可以作为GC Root?)

什么是垃圾?

简单的说就是内存中已经不再被使用到的空间就是垃圾。

要进行垃圾回收,如何判断一个对象是否可以被回收?

  1. 引用计数法
  2. 枚举根节点,做可达性分析(根搜索路径)

什么是引用计数法?(上面的链接里也有)

给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加一,每当有一个引用失效时,计数器值减一。任何时刻计数器值为零的对象就是不可能在被使用的,那么这个对象就是可回收对象。

缺点:很难解决对象之间相互循环引用的问题。目前无人用,了解即可。

java基础面经--上_第23张图片

枚举根节点,做可达性分析(根搜索路径)

为了解决引用计数法的循环引用问题,java使用了可达性分析的方法。

java基础面经--上_第24张图片

java基础面经--上_第25张图片

所谓"GC roots'或者说tracing GC的“根集合”就是一组必须活跃的引用

基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。(也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然判定为死亡。)

java中可以作为GC Roots的对象?

  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(Native方法)引用的对象

2.你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值?

JVM的参数类型: 标配参数 ,X参数(了解),XX参数

(1)标配参数

  • -version      java -version
  •  -help 

(2)X参数:(了解)

  • -Xint  解释执行
  • -Xcomp 第一次使用就编译成本地代码
  • -Xmixed 混合模式

(3)XX参数:(重要)

Boolean类型:公式 -XX:+或者- 某个属性值(+表示开启,-表示关闭)

如何查看一个正在运行中的java程序?它的某个jvm参数是否开启?具体值是多少?

  1. jps -l查看一个正在运行中的java程序,得到java进程号。
  2. jinfo -flag 配置项 进程号  查看它的某个jvm参数(如PrintGCDetails)是否开启。
  3. jinfo -flags 进程号            查看它的所有jvm参数

Case:

是否打印GC收集细节

  • -XX:-PrintGCDetails
  • -XX:+PrintGCDetails

是否使用串行垃圾回收器

  • -XX:-UseSerialGC
  • -XX:+UseSerialGC

KV设值类型:

公式: -XX:属性key=属性值value

Case:

  • -XX:MetaspaceSize=128m
  • -XX:MaxTenuringThreshold=15

面试题(大坑)

两个经典参数:-Xms和-Xmx

  • -Xms等价于 -XX:InitialHeapSize 初始大小内存,默认物理内存的1/64
  • -Xmx等价于 -XX:MaxHeapSize 最大分配内存,默认为物理内存的1/4

JVM盘点家底查看初始默认值:

查看初始默认参数值    公式:java -XX:+PrintFlagsInitial

C:\Users\abc>java -XX:+PrintFlagsInitial
[Global flags]
      int ActiveProcessorCount                     = -1                                        {product} {default}
    uintx AdaptiveSizeDecrementScaleFactor         = 4                                         {product} {default}
    uintx AdaptiveSizeMajorGCDecayTimeScale        = 10                                        {product} {default}
    uintx AdaptiveSizePolicyCollectionCostMargin   = 50                                        {product} {default}
    uintx AdaptiveSizePolicyInitializingSteps      = 20                                        {product} {default}
    uintx AdaptiveSizePolicyOutputInterval         = 0                                         {product} {default}
    uintx AdaptiveSizePolicyWeight                 = 10                                        {product} {default}
... 

查看修改更新参数值  公式: java -XX:+PrintFlagsFinal

C:\Users\abc>java -XX:+PrintFlagsFinal
...
   size_t HeapBaseMinAddress                       = 2147483648                             {pd product} {default}
     bool HeapDumpAfterFullGC                      = false                                  {manageable} {default}
     bool HeapDumpBeforeFullGC                     = false                                  {manageable} {default}
     bool HeapDumpOnOutOfMemoryError               = false                                  {manageable} {default}
    ccstr HeapDumpPath                             =                                        {manageable} {default}
    uintx HeapFirstMaximumCompactionCount          = 3                                         {product} {default}
    uintx HeapMaximumCompactionInterval            = 20                                        {product} {default}
    uintx HeapSearchSteps                          = 3                                         {product} {default}
   size_t HeapSizePerGCThread                      = 43620760                                  {product} {default}
     bool IgnoreEmptyClassPaths                    = false                                     {product} {default}
     bool IgnoreUnrecognizedVMOptions              = false                                     {product} {default}
    uintx IncreaseFirstTierCompileThresholdAt      = 50                                        {product} {default}
     bool IncrementalInline                        = true                                   {C2 product} {default}
   size_t InitialBootClassLoaderMetaspaceSize      = 4194304                                   {product} {default}
    uintx InitialCodeCacheSize                     = 2555904                                {pd product} {default}
   size_t InitialHeapSize                          := 268435456                                 {product} {ergonomic}
...

=表示默认,:=表示修改过的。

JVM盘点家底查看修改变更值

PrintFlagsFinal举例,运行java命令的同时打印出参数

java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m HelloWorld

...
   size_t MetaspaceSize                            := 536870912                               {pd product} {default}
...

打印命令行参数

-XX:+PrintCommandLineFlags

C:\Users\abc>java -XX:+PrintCommandLineFlags -version
-XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=266613056 -XX:MarkStackSize=4
194304 -XX:MaxHeapSize=4265808896 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+Seg
mentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
openjdk version "15.0.1" 2020-10-20
OpenJDK Runtime Environment (build 15.0.1+9-18)
OpenJDK 64-Bit Server VM (build 15.0.1+9-18, mixed mode)

3.你平时工作中用过的JVM常用基本配置参数有哪些?

JDK 1.8之后将最初的永久代取消了,由元空间取代。

java基础面经--上_第26张图片

在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。

元空间(Java8)与永久代(Java7)之间最大的区别在于:永久带使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存

因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
 

public class JVMMemorySizeDemo {
    public static void main(String[] args) throws InterruptedException {
        // 返回Java虚拟机中内存的总量
        long totalMemory = Runtime.getRuntime().totalMemory();

        // 返回Java虚拟机中试图使用的最大内存量
        long maxMemory = Runtime.getRuntime().maxMemory();

        System.out.println(String.format("TOTAL_MEMORY(-Xms): %d B, %.2f MB.", totalMemory, totalMemory / 1024.0 / 1024));
        System.out.println(String.format("MAX_MEMORY(-Xmx): %d B, %.2f MB.", maxMemory, maxMemory / 1024.0 / 1024));
    }
}

输出结果:

TOTAL_MEMORY(-Xms): 257425408 B, 245.50 MB.
MAX_MEMORY(-Xmx): 3793747968 B, 3618.00 MB.

JVM常用基本配置参数

(1)Xss: 设置单个线程栈的大小,等价于 -XX:ThreadStackSize,一般默认是512k-1024k       

(2)Xmn: 设置年轻代的大小 ,一般默认不用改

(3)-XX:MetaspaceSize 设置元空间大小

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制

经典设置案例:

-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails-XX:+UseSerialGC

(4)-XX:+PrintGCDetails输出详细GC收集日志信息

设置参数 -Xms10m -Xmx10m -XX:+PrintGCDetails运行以下程序

import java.util.concurrent.TimeUnit;

public class PrintGCDetailsDemo {

	
	public static void main(String[] args) throws InterruptedException {
		byte[] byteArray = new byte[10 * 1024 * 1024];
		
		TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
	}
}

输出结果:

[GC (Allocation Failure) [PSYoungGen: 778K->480K(2560K)] 778K->608K(9728K), 0.0029909 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 480K->480K(2560K)] 608K->616K(9728K), 0.0007890 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 136K->518K(7168K)] 616K->518K(9728K), [Metaspace: 2644K->2644K(1056768K)], 0.0058272 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 518K->518K(9728K), 0.0002924 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 518K->506K(7168K)] 518K->506K(9728K), [Metaspace: 2644K->2644K(1056768K)], 0.0056906 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.lun.jvm.PrintGCDetailsDemo.main(PrintGCDetailsDemo.java:9)
Heap
 PSYoungGen      total 2560K, used 61K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 3% used [0x00000000ffd00000,0x00000000ffd0f748,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 506K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 7% used [0x00000000ff600000,0x00000000ff67ea58,0x00000000ffd00000)
 Metaspace       used 2676K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

java基础面经--上_第27张图片

java基础面经--上_第28张图片

(5) -XX:SurvivorRatio

java基础面经--上_第29张图片

(6) -XX:NewRatio

java基础面经--上_第30张图片

新生代特别小,会造成频繁的进行GC收集。

(7)-XX:MaxTenuringThreshold

设置垃圾最大年龄

java基础面经--上_第31张图片

To和From互换,原To成为下一次GC时的From区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15,并且设置的值范围在0-15之间),最终如果还是存活,就存入到老年代。

-XX:MaxTenuringThreshold=0,如果设置为0的话,则年轻对象不经过Survivor区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大的值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活的时间,增加在年轻代即被回收的概率。

4.强引用,软引用,弱引用,虚引用分别是什么?

java基础面经--上_第32张图片

(1)强引用:

当内存不足时,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收死都不收

// 这样定义的默认就是强应用
Object obj1 = new Object();

在java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即该对象以后永远都不会用到JVM,也不会回收。因此强引用是造成java内存泄漏的主要原因之一。

(2)软引用:SoftReference

软引用是一种相对强引用弱化了一些的引用,需要用java.labg.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。

对于只有软引用的对象来说,

当系统内存充足时 它 不会 被回收

当系统内存不足时 它 会   被回收

软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。

public class SoftReferenceDemo {

    /**
     * 内存够用的时候
     * -XX:+PrintGCDetails
     */
    public static void softRefMemoryEnough() {
        // 创建一个强应用
        Object o1 = new Object();
        // 创建一个软引用
        SoftReference softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;
        // 手动GC
        System.gc();

        System.out.println(o1);
        System.out.println(softReference.get());
    }

    /**
     * JVM配置,故意产生大对象并配置小的内存,让它的内存不够用了导致OOM,看软引用的回收情况
     * -Xms5m -Xmx5m -XX:+PrintGCDetails
     */
    public static void softRefMemoryNoEnough() {

        System.out.println("========================");
        // 创建一个强应用
        Object o1 = new Object();
        // 创建一个软引用
        SoftReference softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;

        // 模拟OOM自动GC
        try {
            // 创建30M的大对象
            byte[] bytes = new byte[30 * 1024 * 1024];
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(o1);
            System.out.println(softReference.get());
        }
    }

    public static void main(String[] args) {
        softRefMemoryEnough();
        //softRefMemoryNoEnough();
    }
}
 
  

内存充足输出结果:

java.lang.Object@15db9742
java.lang.Object@15db9742
[GC (System.gc()) [PSYoungGen: 2621K->728K(76288K)] 2621K->736K(251392K), 0.0011732 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 728K->0K(76288K)] [ParOldGen: 8K->519K(175104K)] 736K->519K(251392K), [Metaspace: 2646K->2646K(1056768K)], 0.0048782 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
null
java.lang.Object@15db9742
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b380000,0x000000076b56ba70,0x000000076f380000)
  from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
  to   space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
 ParOldGen       total 175104K, used 519K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
  object space 175104K, 0% used [0x00000006c1a00000,0x00000006c1a81e88,0x00000006cc500000)
 Metaspace       used 2653K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 282K, capacity 386K, committed 512K, reserved 1048576K

内存不充足,软引用关联对象会被回收:

========================
java.lang.Object@15db9742
java.lang.Object@15db9742
[GC (Allocation Failure) [PSYoungGen: 756K->496K(1536K)] 756K->600K(5632K), 0.0009017 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 496K->480K(1536K)] 600K->624K(5632K), 0.0006772 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(1536K)] [ParOldGen: 144K->519K(4096K)] 624K->519K(5632K), [Metaspace: 2646K->2646K(1056768K)], 0.0055489 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 519K->519K(5632K), 0.0002674 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 519K->507K(4096K)] 519K->507K(5632K), [Metaspace: 2646K->2646K(1056768K)], 0.0052951 secs] [Times: user=0.11 sys=0.00, real=0.01 secs] 
null
null
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.lun.jvm.SoftReferenceDemo.softRefMemoryNotEnough(SoftReferenceDemo.java:44)
	at com.lun.jvm.SoftReferenceDemo.main(SoftReferenceDemo.java:58)
Heap
 PSYoungGen      total 1536K, used 30K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 2% used [0x00000000ffe00000,0x00000000ffe07ac8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 4096K, used 507K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 12% used [0x00000000ffa00000,0x00000000ffa7edd0,0x00000000ffe00000)
 Metaspace       used 2678K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

回收后,内存依然不足的话,还是会抛异常。

(3)弱引用:WeakReference

弱引用需要用java.lang.WeakReference类来实现,它比软引用生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

import java.lang.ref.WeakReference;

public class WeakReferenceDemo {
    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference weakReference = new WeakReference<>(o1);
        System.out.println(o1);
        System.out.println(weakReference.get());
        o1 = null;
        System.gc();
        System.out.println(o1);
        System.out.println(weakReference.get());
    }
}
 
  

输出结果:

java.lang.Object@15db9742
java.lang.Object@15db9742
null
null

(4)虚引用 PhantomReference

虚引用需要java.lang.ref.PhantomReference类来实现

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用

主要作用:跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事的机制,PhantomReference的get方法总是返回null,因此无法访问对应的引用对象,其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,以来实现比finalization机制更灵活的回收操作。

换句话说,这只虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

(5)引用队列

创建引用的时候可以指定关联的队列,当Gc释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动这相当于是一种通知机制。

当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVW允许我们在对象被销毁后,做一些我们自己想做的事情。

简单来讲:回收前需要被引用的,用队列保存下。

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

public class ReferenceQueueDemo {
    public static void main(String[] args) {
        Object o1 = new Object();

        // 创建引用队列
        ReferenceQueue referenceQueue = new ReferenceQueue<>();

        // 创建一个弱引用
        WeakReference weakReference = new WeakReference<>(o1, referenceQueue);

        System.out.println(o1);
        System.out.println(weakReference.get());
        // 取队列中的内容
        System.out.println(referenceQueue.poll());

        System.out.println("==================");
        
        o1 = null;
        System.gc();
        System.out.println("执行GC操作");

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(o1);
        System.out.println(weakReference.get());
        // 取队列中的内容
        System.out.println(referenceQueue.poll());

    }
}
 
  

输出结果:

java.lang.Object@15db9742
java.lang.Object@15db9742
null
==================
执行GC操作
null
null
java.lang.ref.WeakReference@6d06d69c

虚引用和引用队列配合使用:

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceDemo {
	public static void main(String[] args) throws InterruptedException {
		Object o1 = new Object();
		ReferenceQueue referenceQueue = new ReferenceQueue<>();
		PhantomReference phantomReference = new PhantomReference<>(o1, referenceQueue);
		System.out.println(o1);
		System.out.println(phantomReference.get());
		System.out.println(referenceQueue.poll());
		
		System.out.println("==================");
		o1 = null;
		System.gc();
		Thread.sleep(500) ;
		
		System.out.println(o1);
		System.out.println(phantomReference.get());
		System.out.println(referenceQueue.poll());
	}
}
 
  

输出结果:

java.lang.Object@15db9742
null
null
==================
null
null
java.lang.ref.PhantomReference@6d06d69c

软引用的适用场景

场景:假如有一个应用需要读取大量的本地图片

  • 如果每次读取图片都从硬盘读取则会严重影响性能
  • 如果一次性全部加载到内存中,又可能造成内存溢出

此时使用软引用可以解决这个问题。

设计思路:使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有效地避免了OOM的问题

Map> imageCache = new HashMap>();

你知道弱引用的话,能谈谈WeakHashMap吗?

import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapDemo {
    public static void main(String[] args) {
        myHashMap();
        System.out.println("==========");
        myWeakHashMap();
    }

    private static void myHashMap() {
        Map map = new HashMap<>();
        Integer key = new Integer(1);
        String value = "HashMap";

        map.put(key, value);
        System.out.println(map);

        key = null;

        System.gc();

        System.out.println(map);
    }

    private static void myWeakHashMap() {
        Map map = new WeakHashMap<>();
        Integer key = new Integer(1);
        String value = "WeakHashMap";

        map.put(key, value);
        System.out.println(map);

        key = null;

        System.gc();

        System.out.println(map);
    }
}

输出结果:

{1=HashMap}
{1=HashMap}
==========
{1=WeakHashMap}
{}

5.请谈谈你对OOM的认识?

JVM中常见的两种错误

StackoverFlowError

  • java.lang.StackOverflowError

OutofMemoryError

  • java.lang.OutOfMemoryError:java heap space
  • java.lang.OutOfMemoryError:GC overhead limit exceeded
  • java.lang.OutOfMemoryError:Direct buffer memory
  • java.lang.OutOfMemoryError:unable to create new native thread
  • java.lang.OutOfMemoryError:Metaspace
     

(1)StackOverflowError的展现

public class StackOverflowErrorDemo {

	public static void main(String[] args) {
		main(args);
	}//栈 递归
}

输出结果:

Exception in thread "main" java.lang.StackOverflowError
	at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
	at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
	at com.lun.jvm.StackOverflowErrorDemo.main(StackOverflowErrorDemo.java:6)
	...

(2)OOM之java heap space

public class OOMEJavaHeapSpaceDemo {

	/**
	 * 
	 * -Xms10m -Xmx10m
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		byte[] array = new byte[80 * 1024 * 1024];
	}

}

输出结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.lun.jvm.OOMEJavaHeapSpaceDemo.main(OOMEJavaHeapSpaceDemo.java:6)

(3)OOM之GC overhead limit exceeded

GC回收时间过长会抛出OutOfMemoryError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。

假如不抛出GC overhead limit错误会发生什么情况呢?那就是GC清理的这么点内存会很快再次填满,迫使gc再次执行。这样就形成恶性循环,CPU使用率一直是100%,而gc却没有任何成果。

java基础面经--上_第33张图片

import java.util.ArrayList;
import java.util.List;

public class OOMEGCOverheadLimitExceededDemo {

    /**
     * 
     * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m
     * 
     * @param args
     */
    public static void main(String[] args) {
        int i = 0;
        List list = new ArrayList<>();
        try {
            while(true) {
                list.add(String.valueOf(++i).intern());
            }
        } catch (Exception e) {
            System.out.println("***************i:" + i);
            e.printStackTrace();
            throw e;
        }
    }

}

输出结果:

[GC (Allocation Failure) [PSYoungGen: 2048K->498K(2560K)] 2048K->1658K(9728K), 0.0033090 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2323K->489K(2560K)] 3483K->3305K(9728K), 0.0020911 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2537K->496K(2560K)] 5353K->4864K(9728K), 0.0025591 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2410K->512K(2560K)] 6779K->6872K(9728K), 0.0058689 secs] [Times: user=0.09 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 512K->0K(2560K)] [ParOldGen: 6360K->6694K(7168K)] 6872K->6694K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0894928 secs] [Times: user=0.42 sys=0.00, real=0.09 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2048K->1421K(2560K)] [ParOldGen: 6694K->6902K(7168K)] 8742K->8324K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0514932 secs] [Times: user=0.34 sys=0.00, real=0.05 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2048K->2047K(2560K)] [ParOldGen: 6902K->6902K(7168K)] 8950K->8950K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0381615 secs] [Times: user=0.13 sys=0.00, real=0.04 secs] 
...省略89行...
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7044K->7044K(7168K)] 9092K->9092K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0360935 secs] [Times: user=0.25 sys=0.00, real=0.04 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7046K->7046K(7168K)] 9094K->9094K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0360458 secs] [Times: user=0.38 sys=0.00, real=0.04 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7048K->7048K(7168K)] 9096K->9096K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0353033 secs] [Times: user=0.11 sys=0.00, real=0.04 secs] 
***************i:147041
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7050K->7048K(7168K)] 9098K->9096K(9728K), [Metaspace: 2670K->2670K(1056768K)], 0.0371397 secs] [Times: user=0.22 sys=0.00, real=0.04 secs] 
java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) 	at java.lang.Integer.toString(Integer.java:401)
[PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7051K->7050K(7168K)] 9099K->9097K(9728K), [Metaspace: 2676K->2676K(1056768K)], 0.0434184 secs] [Times: user=0.38 sys=0.00, real=0.04 secs] 
	at java.lang.String.valueOf(String.java:3099)
	at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7054K->513K(7168K)] 9102K->513K(9728K), [Metaspace: 2677K->2677K(1056768K)], 0.0056578 secs] [Times: user=0.11 sys=0.00, real=0.01 secs] 
	at java.lang.Integer.toString(Integer.java:401)
	at java.lang.String.valueOf(String.java:3099)
	at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
Heap
 PSYoungGen      total 2560K, used 46K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0bb90,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 513K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 7% used [0x00000000ff600000,0x00000000ff6807f0,0x00000000ffd00000)
 Metaspace       used 2683K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K

(4)OOM之Direct buffer memory

导致原因:

写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

  • ByteBuffer.allocate(capability) 第一种方式是分配VM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
  • ByteBuffer.allocateDirect(capability) 第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。

但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。

java基础面经--上_第34张图片

import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;

public class OOMEDirectBufferMemoryDemo {

	/**
	 * -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
	 * 
	 * @param args
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException {
		System.out.println(String.format("配置的maxDirectMemory: %.2f MB",// 
				sun.misc.VM.maxDirectMemory() / 1024.0 / 1024));
		
		TimeUnit.SECONDS.sleep(3);
		
		ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
	}	
}

输出结果:

[GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->772K(5632K), 0.0014568 secs] [Times: user=0.09 sys=0.00, real=0.00 secs] 
配置的maxDirectMemory: 5.00 MB
[GC (System.gc()) [PSYoungGen: 622K->504K(1536K)] 890K->820K(5632K), 0.0009753 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 316K->725K(4096K)] 820K->725K(5632K), [Metaspace: 3477K->3477K(1056768K)], 0.0072268 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Exception in thread "main" Heap
 PSYoungGen      total 1536K, used 40K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 4% used [0x00000000ffe00000,0x00000000ffe0a3e0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 725K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 17% used [0x00000000ffa00000,0x00000000ffab5660,0x00000000ffe00000)
 Metaspace       used 3508K, capacity 4566K, committed 4864K, reserved 1056768K
  class space    used 391K, capacity 394K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.lun.jvm.OOMEDirectBufferMemoryDemo.main(OOMEDirectBufferMemoryDemo.java:20)

(5)OOM之unable to create new native thread  (非常重要)

高并发请求服务器时,经常出现如下异常:java.lang.OutOfMemoryError:unable to create new native thread 准确的讲 该native thread 异常与对应的平台有关

导致原因:

  1. 你的应用创建了太多线程,一个应用进程创建多个进程,超过系统承载极限
  2. 你的服务器不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报这个错误。

解决办法:

  1. 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低。
  2. 对于有的应用,确实需要创建很多进程,远超过linux系统的默认1024个线程的限制,可以通过修改linux服务器配置,扩大linux默认限制。
public class OOMEUnableCreateNewThreadDemo {
    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            System.out.println("************** i = " + i);
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

上面程序在Linux OS(CentOS)运行,会出现下列的错误,线程数大概在900多个。

Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread

上限调整:

非root用户登录Linux系统(CentOS)测试

服务器级别调参调优

查看系统线程限制数目: ulimit -u

修改系统线程限制数目:vim /etc/security/limits.d/90-nproc.conf
打开后发现除了root,其他账户都限制在1024个

假如我们想要张三这个用卢运行,希望他生成的线程多一些,我们可以如下配置

(6)OOM之Metaspace

使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:MetaspaceSize为21810376B(大约20.8M)

永久代(java8以后被元空间Metaspace取代了)存放了以下信息:

  • 虚拟机加载的类信息
  • 常量池
  • 静态变量
  • 即时编译后的代码

模拟Metaspace空间溢出,我们借助CGLib直接操作字节码运行时不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的。

首先添加CGLib依赖



    cglib
    cglib
    3.2.10

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class OOMEMetaspaceDemo {
    // 静态类
    static class OOMObject {}

    /**
     * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
     * 
     * @param args
     */
    public static void main(final String[] args) {
        // 模拟计数多少次以后发生异常
        int i =0;
        try {
            while (true) {
                i++;
                // 使用Spring的动态字节码技术
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invokeSuper(o, args);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable e) {
            System.out.println("发生异常的次数:" + i);
            e.printStackTrace();
        } finally {

        }

    }
}

输出结果:

发生异常的次数:569
java.lang.OutOfMemoryError: Metaspace
	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
	at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
	at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
	at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
	at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
	at com.lun.jvm.OOMEMetaspaceDemo.main(OOMEMetaspaceDemo.java:37)

6.GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈?

关系:GC算法(引用计数/复制拷贝/标记清除/标记压缩)是内存回收的方法论,垃圾收集器就是算法落地实现。因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集

4种主要的垃圾收集器

  1. Serial(串行垃圾回收器):它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。(一个清洁工打扫卫生)
  2. Parallel(并行垃圾回收器):多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理首台处理等弱交互场景。(多个清洁工打扫卫生)
  3. CMS(ConcMarkSweep并发标记清除垃圾回收器):用户线程和垃圾收集线程同时执行,(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用对响应时间有要求的场景。
  4. G1(G1垃圾回收器):G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。
  5. ZGC(java11的,了解)java基础面经--上_第35张图片

7.怎么查看服务器默认的垃圾收集器是哪个?生产上如何配置垃圾收集器的?谈谈你对垃圾收集器的理解?

(1)查看默认的垃圾回收器:

java -XX:+PrintCommandLineFlags -version

输出结果

C:\Users\abc>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=266613056 -XX:MaxHeapSize=4265808896 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

从结果看到-XX:+UseParallelGC,也就是说默认的垃圾收集器是并行垃圾回收器。

或者

jps -l

得出Java程序号

jinfo -flags (Java程序号)

(2)7种垃圾收集器

Java中一共有7大垃圾收集器

  • 年轻代GC

    • UserSerialGC:串行垃圾收集器
    • UserParallelGC:并行垃圾收集器
    • UseParNewGC:年轻代的并行垃圾回收器
  • 老年代GC

    • UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
    • UseParallelOldGC:老年代的并行垃圾回收器
    • UseConcMarkSweepGC:(CMS)并发标记清除垃圾收集器
  • 老嫩通吃

    • UseG1GC:G1垃圾收集器

不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:

java基础面经--上_第36张图片

新生代

  • 串行GC(Serial)/(Serial Copying)

  • 并行GC(ParNew)

  • 并行回收GC(Parallel)/(Parallel Scavenge)

java基础面经--上_第37张图片

java基础面经--上_第38张图片

Server/Client模式分别是什么意思?

使用范围:一般使用Server模式,Client模式基本不会使用

操作系统

  • 32位的Window操作系统,不论硬件如何都默认使用Client的JVM模式
  • 32位的其它操作系统,2G内存同时有2个cpu以上用Server模式,低于该配置还是Client模式
  • 64位只有Server模式
     
C:\Users\abc>java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

java基础面经--上_第39张图片

<1>GC之Serial收集器

一句话:一个单线程的收集器,在进行垃圾收集的时候,必须暂停其他所有的工作线程直到它收集结束。

STW: Stop The World

串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World”状态)。虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。

对应JVM参数是:-XX:+UseSerialGC

开启后会使用:Serial(Young区用) + Serial Old(Old区用)的收集器组合

表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-压缩算法

public class GCDemo {

	public static void main(String[] args) throws InterruptedException {
		
		Random rand = new Random(System.nanoTime());
		
		try {
			String str = "Hello, World";
			while(true) {
				str += str + rand.nextInt(Integer.MAX_VALUE) + rand.nextInt(Integer.MAX_VALUE);
			}
		}catch (Throwable e) {
			e.printStackTrace();
		}
		
	}
}

设置VM参数:(启用UseSerialGC)

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC

输出结果:

-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
[GC (Allocation Failure) [DefNew: 2346K->320K(3072K), 0.0012956 secs] 2346K->1030K(9920K), 0.0013536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2888K->0K(3072K), 0.0013692 secs] 3598K->2539K(9920K), 0.0014059 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2065K->0K(3072K), 0.0011613 secs] 4604K->4550K(9920K), 0.0011946 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2056K->0K(3072K), 0.0010394 secs] 6606K->6562K(9920K), 0.0010808 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2011K->2011K(3072K), 0.0000124 secs][Tenured: 6562K->2537K(6848K), 0.0021691 secs] 8574K->2537K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0024399 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2059K->2059K(3072K), 0.0000291 secs][Tenured: 6561K->6561K(6848K), 0.0012330 secs] 8620K->6561K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0012888 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [Tenured: 6561K->6547K(6848K), 0.0017784 secs] 6561K->6547K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0018111 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOfRange(Arrays.java:3664)
	at java.lang.String.(String.java:207)
	at java.lang.StringBuilder.toString(StringBuilder.java:407)
	at com.lun.jvm.GCDemo.main(GCDemo.java:23)
Heap
 def new generation   total 3072K, used 105K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
  eden space 2752K,   3% used [0x00000000ff600000, 0x00000000ff61a7c8, 0x00000000ff8b0000)
  from space 320K,   0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
  to   space 320K,   0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
 tenured generation   total 6848K, used 6547K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
   the space 6848K,  95% used [0x00000000ff950000, 0x00000000fffb4c30, 0x00000000fffb4e00, 0x0000000100000000)
 Metaspace       used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K

  • DefNew:Default New Generation
  • Tenured:Old

<2>GC之ParNew收集器

一句话:使用多线程进行垃圾回收,在垃圾收集时,会Stop-The-World暂停其他所有的工作线程直到它收集结束。

ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的场景是配合老年代的CMS GC工作,其余的行为和Seria收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多java虚拟机运行在server模式下新生代的默认垃圾收集器。

常用对应JVM参数:-XX:+UseParNewGC启用ParNew收集器,只影响新生代的收集,不影响老年代。

开启上述参数后,会使用:ParNew(Young区)+ Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法

但是,ParNew+Tenured这样的搭配,Java8已经不再被推荐,推荐使用ParNew + CMS

Java HotSpot™64-Bit Server VM warning:
Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release.

备注:-XX:ParallelGCThreads限制线程数量,默认开启和CPU数目相同的线程数。

例:设置VM参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC

输出结果:

-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC 
[GC (Allocation Failure) [ParNew: 2702K->320K(3072K), 0.0007029 secs] 2702K->1272K(9920K), 0.0007396 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 2292K->37K(3072K), 0.0010829 secs] 3244K->2774K(9920K), 0.0011000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 2005K->9K(3072K), 0.0008401 secs] 4742K->5624K(9920K), 0.0008605 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 1974K->1974K(3072K), 0.0000136 secs][Tenured: 5615K->3404K(6848K), 0.0021646 secs] 7589K->3404K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0022520 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 1918K->2K(3072K), 0.0008094 secs] 5322K->5324K(9920K), 0.0008273 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 1970K->1970K(3072K), 0.0000282 secs][Tenured: 5322K->4363K(6848K), 0.0018652 secs] 7292K->4363K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0019205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [Tenured: 4363K->4348K(6848K), 0.0023131 secs] 4363K->4348K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0023358 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
 par new generation   total 3072K, used 106K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
  eden space 2752K,   3% used [0x00000000ff600000, 0x00000000ff61a938, 0x00000000ff8b0000)
  from space 320K,   0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
  to   space 320K,   0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
 tenured generation   total 6848K, used 4348K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
   the space 6848K,  63% used [0x00000000ff950000, 0x00000000ffd8f3a0, 0x00000000ffd8f400, 0x0000000100000000)
 Metaspace       used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release

<3>GC之Parallel 收集器

Parallel / Parallel Scavenge

一句话:串行收集器在新生代和老年代的并行化

Parallel Scavenge 收集器类似ParNew,也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器

它关注的重点是:

可控制的吞吐量(Thoughput=运行用户代码时间(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99% )。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。

自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量)。

常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器。

开启该参数后:新生代使用复制算法,老年代使用标记-整理算法

多说一句:-XX:ParallelGCThreads=数字N 表示启动多少个GC线程

  • cpu>8 N= 5/8

  • cpu<8 N=实际个数

设置VM参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC

输出结果:

-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
[GC (Allocation Failure) [PSYoungGen: 2009K->503K(2560K)] 2009K->803K(9728K), 0.7943182 secs] [Times: user=0.00 sys=0.00, real=0.79 secs] 
[GC (Allocation Failure) [PSYoungGen: 2272K->432K(2560K)] 2572K->2214K(9728K), 0.0020218 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2448K->352K(2560K)] 4230K->3122K(9728K), 0.0017173 secs] [Times: user=0.11 sys=0.02, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1380K->0K(2560K)] [ParOldGen: 6722K->2502K(7168K)] 8102K->2502K(9728K), [Metaspace: 2657K->2657K(1056768K)], 0.0039763 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2016K->0K(2560K)] [ParOldGen: 6454K->6454K(7168K)] 8471K->6454K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0049598 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6454K->6454K(9728K), 0.0008614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6454K->6440K(7168K)] 6454K->6440K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0055542 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOfRange(Arrays.java:3664)
	at java.lang.String.(String.java:207)
	at java.lang.StringBuilder.toString(StringBuilder.java:407)
	at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
 PSYoungGen      total 2560K, used 82K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd14810,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 6440K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 89% used [0x00000000ff600000,0x00000000ffc4a1c8,0x00000000ffd00000)
 Metaspace       used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K

java基础面经--上_第40张图片

<4>GC之ParallelOld 收集器

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6之后才开始提供。

在JDK1.6之前,新生代使用的Parallel Scavenge收集器只能搭配老年代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。在JDK1.6之前(Parallel Scavenge + Serial Old)

Parallel Old正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以优先考虑新生代Parallel Scavenge和老年代Parallel Old收集器的搭配策略。在JDK1.8及以后(Parallel Scavenge +Parallel Old)

JVM常用参数:-XX:+UseParallelOldGC使用Parallel Old收集器,设置该参数后,新生代Parallel+老年代Parallel Old。

设置VM参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC

输出结果:

-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelOldGC 
[GC (Allocation Failure) [PSYoungGen: 1979K->480K(2560K)] 1979K->848K(9728K), 0.0007724 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2205K->480K(2560K)] 2574K->2317K(9728K), 0.0008700 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2446K->496K(2560K)] 4284K->3312K(9728K), 0.0010374 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1499K->0K(2560K)] [ParOldGen: 6669K->2451K(7168K)] 8168K->2451K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0043327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1966K->0K(2560K)] [ParOldGen: 6304K->6304K(7168K)] 8270K->6304K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0021269 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6304K->6304K(9728K), 0.0004841 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6304K->6290K(7168K)] 6304K->6290K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0058149 secs] [Times: user=0.11 sys=0.00, real=0.01 secs] 
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOfRange(Arrays.java:3664)
	at java.lang.String.(String.java:207)
	at java.lang.StringBuilder.toString(StringBuilder.java:407)
	at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
 PSYoungGen      total 2560K, used 81K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 3% used [0x00000000ffd00000,0x00000000ffd14768,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 6290K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 87% used [0x00000000ff600000,0x00000000ffc24b70,0x00000000ffd00000)
 Metaspace       used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K

<5>GC之CMS 收集器

CMS(Concurrent Mark Sweep)并发标记清除,是一种以获取最短回收停顿时间为目标的收集器。适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿的时间最短。

CMS非常适合地内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。

java基础面经--上_第41张图片

Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行

开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC开启该参数后会自动将-XX:+UseParNewGC打开。

开启该参数后,使用ParNew(Young区用)+ CMS(Old区用)+ Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器。

4步过程:

  • 初始标记(CMS initial mark) - 只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
  • 并发标记(CMS concurrent mark)和用户线程一起 - 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。
  • 重新标记(CMS remark)- 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。
  • 并发清除(CMS concurrent sweep) - 清除GCRoots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
     

优点:并发收集低停顿。

缺点:并发执行,对CPU资源压力大,采用的标记清除算法会导致大量碎片。

  1. 由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间。
  2. 标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认O,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。
     

设置VM参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC

输出结果:

-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=3497984 -XX:MaxTenuringThreshold=6 -XX:NewSize=3497984 -XX:OldPLABSize=16 -XX:OldSize=6987776 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC 
[GC (Allocation Failure) [ParNew: 2274K->319K(3072K), 0.0016975 secs] 2274K->1043K(9920K), 0.0017458 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 2844K->8K(3072K), 0.0010921 secs] 3568K->2287K(9920K), 0.0011138 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 2040K->2K(3072K), 0.0037625 secs] 4318K->4257K(9920K), 0.0037843 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (CMS Initial Mark) [1 CMS-initial-mark: 4255K(6848K)] 6235K(9920K), 0.0003380 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start]
[GC (Allocation Failure) [ParNew: 2024K->2K(3072K), 0.0013295 secs] 6279K->6235K(9920K), 0.0013596 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 1979K->1979K(3072K), 0.0000116 secs][CMS[CMS-concurrent-mark: 0.001/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 (concurrent mode failure): 6233K->2508K(6848K), 0.0031737 secs] 8212K->2508K(9920K), [Metaspace: 2657K->2657K(1056768K)], 0.0032232 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 2025K->2025K(3072K), 0.0000154 secs][CMS: 6462K->6461K(6848K), 0.0020534 secs] 8488K->6461K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0021033 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [CMS: 6461K->6448K(6848K), 0.0020383 secs] 6461K->6448K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0020757 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (CMS Initial Mark) [1 CMS-initial-mark: 6448K(6848K)] 6448K(9920K), 0.0001419 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (CMS Final Remark) [YG occupancy: 50 K (3072 K)][Rescan (parallel) , 0.0002648 secs][weak refs processing, 0.0000173 secs][class unloading, 0.0002671 secs][scrub symbol table, 0.0004290 secs][scrub string table, 0.0001593 secs][1 CMS-remark: 6448K(6848K)] 6499K(9920K), 0.0012107 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-sweep-start]
java.lang.OutOfMemoryError: Java heap space
[CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
	at java.util.Arrays.copyOfRange(Arrays.java:3664)
	at java.lang.String.(String.java:207)
	at java.lang.StringBuilder.toString(StringBuilder.java:407)
	at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
 par new generation   total 3072K, used 106K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
  eden space 2752K,   3% used [0x00000000ff600000, 0x00000000ff61a820, 0x00000000ff8b0000)
  from space 320K,   0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
  to   space 320K,   0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
 concurrent mark-sweep generation total 6848K, used 6447K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K

<6>GC之SerialOld 收集器

SerialOld是Serial垃圾收集器的老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要运行在Client默认的java虚拟机默认的老年代垃圾收集器。

在server模式下,主要有两个用途(了解,版本已经到8及以后)

  • 1.在JDK1.5以前版本中与新生代的Parallel Scavenge收集器搭配使用。
  • 2.作为老年代版中使用CMS收集器的后备垃圾收集方案。

设置VM参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC

 输出结果:

Unrecognized VM option 'UseSerialOldGC'
Did you mean '(+/-)UseSerialGC'?

在Java8中,-XX:+UseSerialOldGC不起作用。 

<7> G1垃圾收集器

下面有详细介绍

(3)如何选择垃圾收集器?

1.单CPU或小内存,单机程序

  • -XX:+UseSerialGC

2.多CPU,需要最大吞吐量,如后台计算型应用

  • -XX:+UseParallelGC 或 -XX:+UseParallelOldGC

3.多CPU,追求低停顿时间,需快速响应如互联网应用

  • -XX:+UseConcMarkSweepGC    -XX:+ParNewGC

小总结:

java基础面经--上_第42张图片

  8.G1垃圾收集器

例:

设置VM参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC

输出结果:

-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation 
[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0015787 secs]
   [Parallel Time: 0.8 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 106.4, Avg: 106.5, Max: 106.5, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 0.2, Avg: 0.3, Max: 0.5, Diff: 0.4, Sum: 2.2]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.0, Avg: 0.3, Max: 0.3, Diff: 0.3, Sum: 2.1]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
         [Termination Attempts: Min: 1, Avg: 5.3, Max: 10, Diff: 9, Sum: 42]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.3]
      [GC Worker Total (ms): Min: 0.6, Avg: 0.6, Max: 0.7, Diff: 0.1, Sum: 4.9]
      [GC Worker End (ms): Min: 107.1, Avg: 107.1, Max: 107.1, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.3 ms]
   [Other: 0.5 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.2 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.3 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 4096.0K(4096.0K)->0.0B(4096.0K) Survivors: 0.0B->1024.0K Heap: 7073.4K(10.0M)->2724.8K(10.0M)]
 [Times: user=0.02 sys=0.02, real=0.00 secs] 
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0004957 secs]
[GC concurrent-mark-start]
[GC concurrent-mark-end, 0.0001071 secs]
[GC remark [Finalize Marking, 0.0001876 secs] [GC ref-proc, 0.0002450 secs] [Unloading, 0.0003675 secs], 0.0011690 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC cleanup 4725K->4725K(10M), 0.0004907 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC pause (G1 Humongous Allocation) (young), 0.0009748 secs]
   [Parallel Time: 0.6 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 111.8, Avg: 111.9, Max: 112.2, Diff: 0.5]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 1.7]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
         [Termination Attempts: Min: 1, Avg: 3.3, Max: 5, Diff: 4, Sum: 26]
      [GC Worker Other (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.0, Sum: 0.8]
      [GC Worker Total (ms): Min: 0.1, Avg: 0.5, Max: 0.6, Diff: 0.5, Sum: 3.6]
      [GC Worker End (ms): Min: 112.3, Avg: 112.3, Max: 112.4, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.2 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.1 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 1024.0K(4096.0K)->0.0B(4096.0K) Survivors: 1024.0K->1024.0K Heap: 6808.1K(10.0M)->2595.2K(10.0M)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC pause (G1 Humongous Allocation) (young) (initial-mark), 0.0006211 secs]
   [Parallel Time: 0.2 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 113.3, Avg: 113.3, Max: 113.4, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 1.0]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.1, Max: 1, Diff: 1, Sum: 1]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [GC Worker Total (ms): Min: 0.1, Avg: 0.2, Max: 0.2, Diff: 0.1, Sum: 1.4]
      [GC Worker End (ms): Min: 113.5, Avg: 113.5, Max: 113.5, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.3 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.1 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 0.0B(4096.0K)->0.0B(2048.0K) Survivors: 1024.0K->1024.0K Heap: 4595.9K(10.0M)->4557.3K(10.0M)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC concurrent-root-region-scan-start]
[GC pause (G1 Humongous Allocation) (young)[GC concurrent-root-region-scan-end, 0.0001112 secs]
[GC concurrent-mark-start]
, 0.0006422 secs]
   [Root Region Scan Waiting: 0.0 ms]
   [Parallel Time: 0.2 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 114.2, Avg: 114.3, Max: 114.4, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.7]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.1, Max: 1, Diff: 1, Sum: 1]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [GC Worker Total (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.9]
      [GC Worker End (ms): Min: 114.4, Avg: 114.4, Max: 114.4, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.3 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.1 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 0.0B(2048.0K)->0.0B(2048.0K) Survivors: 1024.0K->1024.0K Heap: 4557.3K(10.0M)->4547.6K(10.0M)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure)  4547K->4527K(10M), 0.0023437 secs]
   [Eden: 0.0B(2048.0K)->0.0B(3072.0K) Survivors: 1024.0K->0.0B Heap: 4547.6K(10.0M)->4527.6K(10.0M)], [Metaspace: 2658K->2658K(1056768K)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure)  4527K->4513K(10M), 0.0021281 secs]
   [Eden: 0.0B(3072.0K)->0.0B(3072.0K) Survivors: 0.0B->0.0B Heap: 4527.6K(10.0M)->4514.0K(10.0M)], [Metaspace: 2658K->2658K(1056768K)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC concurrent-mark-abort]
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
 garbage-first heap   total 10240K, used 4513K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
  region size 1024K, 1 young (1024K), 0 survivors (0K)
 Metaspace       used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K

<1> 以前收集器的特点:

  • 年轻代和老年代都是各自独立且连续的内存块;
  • 年轻代收集使用单eden+s0+s1进行复制算法;
  • 老年代收集必须扫描整个老年代区域;
  • 都是以尽可能少儿快速地执行GC为设计原则。

<2> G1是什么?

G1(Garbage-First)收集器是一款面向服务端应用的收集器;应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。

具有以下特性

  • 像CMS收集器一样,能与应用程序线程并发执行。
  • 整理空闲空间更快。
  • 需要更多的时间来预测GC停顿时间。
  • 不希望牺牲大量的吞吐性能。
  • 不需要更大的Java Heap。

<3> G1收集器的设计目标是取代CMS收集器,它与CMS相比,在以下方面表现的更出色:

G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。

G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器。

G1是在2012年才在jdk1.7u4中可用。oracle官方计划在JDK9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CMS收集器。

主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region ,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。

<4> 特点:

  • 1.G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW。
  • 2.G1整体上采用标记-整理(压缩)算法,局部是通过复制算法,不会产生内存碎片。
  • 3.宏观上看G1中不在区分年轻代和老年代。把内存划分为多个独立的子区域(region),可以近似理解为一个围棋的棋盘。
  • 4.G1收集器里面将整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但他们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。
  • 5.G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。

<5> G1底层原理

Region区域化垃圾收集器 - 最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。

区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。

核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为: 32MB*2048=65536MB=64G内存

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。

这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。

这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。

在G1中,还有一种特殊的区域,叫Humongous区域。

如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。

为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
 

回收步骤:

G1收集器下的Young GC

针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片

  • Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部会晋升到Old区。
  • Survivor区的数据移动到新的Survivor区,部会数据晋升到Old区。
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。

4步过程:

  1. 初始标记:只标记GC Roots能直接关联到的对象
  2. 并发标记:进行GC Roots Tracing的过程
  3. 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
  4. 筛选回收:根据时间来进行价值最大化的回收

<6> G1参数配置即和CMS的比较

-XX:+UseG1GC
-XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域。
-XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间。
-XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45。
-XX:ConcGCThreads=n:并发GC使用的线程数。
-XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%。

开发人员仅仅需要声明以下参数即可:

三步归纳:开始G1+设置最大内存+设置最大停顿时间

-XX:+UseG1GC
-Xmx32g
-XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间
 

G1和CMS比较

  • G1不会产生内存碎片
  • G1是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分为多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

9.JVM+GC结合SpringBoot微服务优化简介

  1. IDEA开发微服务工程。
  2. Maven进行clean package。
  3. 要求微服务启动的时候,同时配置我们的JVM/GC的调优参数。
  4. 公式:java -server jvm的各种参数 -jar 第1步上面的jar/war包名

10.实例对象是怎样存储的?

https://www.nowcoder.com/discuss/644046

对象的实例存储在空间;

对象的类元信息存储在方法区(元空间),被对象头中的类型指针所指向;

对象的引用存储在虚拟机栈

11.一个对象有哪些部分组成(堆内存中)?

对象头、实例数据、对齐填充

12、对象头包含哪些信息?

运行时元数据markword、类型指针、如果是数组对象还会包含数组长度

13、markword包含哪些信息?

锁的状态标识(如偏向锁,无锁,轻量级锁等),分代年龄,锁记录record,如果是重量级锁的话还保存monitor对象、线程id,hashcode等。

17、生产环境服务器变慢,诊断思路和性能评估谈谈?

Linux命令之top 整机性能查看

主要看load average, CPU, MEN三部分

load average表示系统负载,即任务队列的平均长度。 三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值。

load average: 如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了。

Linux之cpu查看vmstat

procs

  • r:运行和等待的CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不超过总核数的2倍,否则代表系统压力过大,我们看蘑菇博客测试服务器,能发现都超过了2,说明现在压力过大
  • b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O等

cpu

  • us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,优化程序
  • sy:内核进程消耗的CPU时间百分比
  • us + sy 参考值为80%,如果us + sy 大于80%,说明可能存在CPU不足,从上面的图片可以看出,us + sy还没有超过百分80,因此说明蘑菇博客的CPU消耗不是很高
  • id:处于空闲的CPU百分比
  • wa:系统等待IO的CPU时间百分比
  • st:来自于一个虚拟机偷取的CPU时间比
     

Linux之cpu查看pidstat

查看看所有cpu核信息

mpstat -P ALL 2

每个进程使用cpu的用量分解信息

pidstat -u 1 -p 进程编号

java基础面经--上_第43张图片

Linux之内存查看free和pidstat

应用程序可用内存数

经验值

  • 应用程序可用内存l系统物理内存>70%内存充足

  • 应用程序可用内存/系统物理内存<20%内存不足,需要增加内存

  • 20%<应用程序可用内存/系统物理内存<70%内存基本够用

m/g:兆/G

查看额外

pidstat -p 进程号 -r 采样间隔秒数

Linux之硬盘查看df

查看磁盘剩余空间数

Linux之磁盘IO查看iostat和pidstat

磁盘I/O性能评估

磁盘块设备分布

  • rkB/s每秒读取数据量kB;wkB/s每秒写入数据量kB;
  • svctm lO请求的平均服务时间,单位毫秒;
  • await l/O请求的平均等待时间,单位毫秒;值越小,性能越好;
  • util一秒中有百分几的时间用于I/O操作。接近100%时,表示磁盘带宽跑满,需要优化程序或者增加磁盘;
  • rkB/s、wkB/s根据系统应用不同会有不同的值,但有规律遵循:长期、超大数据读写,肯定不正常,需要优化程序读取。
  • svctm的值与await的值很接近,表示几乎没有IO等待,磁盘性能好。
  • 如果await的值远高于svctm的值,则表示IO队列等待太长,需要优化程序或更换更快磁盘。
     

Linux之网络IO查看ifstat

默认本地没有,下载ifstat

wget http://gael.roualland.free.fr/lifstat/ifstat-1.1.tar.gz
tar -xzvf ifstat-1.1.tar.gz
cd ifstat-1.1
./configure
make
make install

查看网络IO

各个网卡的in、out

观察网络负载情况程序

网络读写是否正常

  • 程序网络I/O优化
  • 增加网络I/O带宽

18、CPU占用过高的定位分析思路(记一次排错经历

结合Linux和JDK命令一块分析

案例步骤

  • 先用top命令找出CPU占比最高的

  • ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序作搞屎棍

  • 定位到具体线程或者代码

    • ps -mp 进程 -o THREAD,tid,time
      • -m 显示所有的线程
      • -p pid进程使用cpu的时间
      • -o 该参数后是用户自定义格式

  • 将需要的线程ID转换为16进制格式(英文小写格式),命令printf %x 172 将172转换为十六进制
  • jstack 进程ID | grep tid(16进制线程ID小写英文)-A60

19、GitHub面试相关

GitHub骚操作之常用词

常用词含义

  • watch:会持续收到该项目的动态
  • fork:复制其个项目到自己的Github仓库中
  • star,可以理解为点赞
  • clone,将项目下载至本地
  • follow,关注你感兴趣的作者,会收到他们的动态

GitHub骚操作之in限制搜索

in关键词限制搜索范围:

公式 :xxx(关键词) in:name或description或readme
xxx in:name 项目名包含xxx的
xxx in:description 项目描述包含xxx的
xxx in:readme 项目的readme文件中包含xxx的组合使用
组合使用
搜索项目名或者readme中包含秒杀的项目
xxx in:name,readme

GitHub骚操作之star和fork范围搜索

公式:
xxx关键字 stars 通配符 :> 或者 :>=
区间范围数字: stars:数字1…数字2
案例
查找stars数大于等于5000的springboot项目:springboot stars:>=5000
查找forks数在1000~2000之间的springboot项目:springboot forks:1000…5000
组合使用
查找star大于1000,fork数在500到1000的springboot项目:springboot stars:>1000 forks:500…1000
 

GitHub骚操作之awesome搜索

  • 公式:awesome 关键字:awesome系列,一般用来收集学习、工具、书籍类相关的项目
  • 搜索优秀的redis相关的项目,包括框架,教程等 awesome redis

GitHub骚操作之代码高亮

一行:地址后面紧跟 #L10
https://github.com/abc/abc/pom.xml#L13
多行:地址后面紧跟 #Lx - #Ln

GitHub骚操作之T搜索

在项目仓库下按键盘T,进行项目内搜索

GitHub骚操作之搜索区域活跃用户

  • location:地区
  • language:语言
  • 例如:location:beijing language:java

20、字符串常量池(intern()方法?- 是否读过经典JVM书籍?)

暖身面试题

  • Redis默认端口是多少?- 6379 
  • Spring官网地址 - https://spring.io
  • 经典计算机图书看过吗?

字符串常量Java内部加载

public native String intern();

String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此对象包含的字符串添加到常量池中,并且返回此String对象的引用。

58同城的一道面试题、(同时也是深入理解java虚拟机经典图书中的一道案例):

考查点 - intern()方法,判断true/false?- 《深入理解java虚拟机》书原题是否读过经典JVM书籍

public class StringInternDemo {

	public static void main(String[] args) {
		
		String str1 = new StringBuilder("58").append("tongcheng").toString();
		System.out.println(str1);
		System.out.println(str1.intern());
		System.out.println(str1 == str1.intern());

		System.out.println();
		
		String str2 = new StringBuilder("ja").append("va").toString();
		System.out.println(str2);
		System.out.println(str2.intern());
		System.out.println(str2 == str2.intern());
		
	}

}

输出结果:

58tongcheng
58tongcheng
true

java
java
false

按照代码结果,Java字符串答案为false必然是两个不同的java,那另外一个java字符串如何加载进来的?

答:有一个初始化的Java字符串(JDK出娘胎自带的),在加载sun.misc.Version这个类的时候进入常量池。

  • System代码解析 System -> initializeSystemClass() -> Version
package java.lang;

public final class System {

    /* register the natives via the static initializer.
     *
     * VM will invoke the initializeSystemClass method to complete
     * the initialization for this class separated from clinit.
     * Note that to use properties set by the VM, see the constraints
     * described in the initializeSystemClass method.
     */
    private static native void registerNatives();
    static {
        registerNatives();
    }
    
    //本地方法registerNatives()将会调用initializeSystemClass()
    private static void initializeSystemClass() {

		...
        
        sun.misc.Version.init();

		...
    }
    ...
}
package sun.misc;

//反编译后的代码
public class Version {
	private static final String launcher_name = "java";
	...
}
  • 类加载器和rt.jar - 根加载器提前部署加载rt.jar
  • OpenJDK8源码
  •               http://openjdk.java.net/
  •               openjdk8\jdk\src\share\classes\sun\misc

sun.misc.Version类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue〉做默认初始化,此时被sun.misc.Version.launcher静态常量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池——StringTable里了。

请你解释一下类加载机制,双亲委派模型,好处是什么?为什么那么要有扩展类加载器这一层?

类加载器的种类:

启动类加载器(bootstrap class loader):它用来加载 Java 的核心库,也就是java最开始被设计时的一些java类库。

扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。也就是随着jdk不断更新后加进去的一些类库。

系统类加载器(App class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,我们自己平时书写的类都是它加载的。

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

java基础面经--上_第44张图片

好处:

避免重复加载 + 避免核心类篡改

  1. 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父加载器已经加载了该类时,就没有必要子加载器再加载一次。
  2. 其次是考虑到安全因素,java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心API库被随意篡改。

缺点:

顶层的ClassLoader无法访问底层的ClassLoader所加载的类。

21、AQS(AbstractQueuedSynchronizer)

回顾synchronized:

java基础面经--上_第45张图片

第二个monitorexit是为了异常的时候保证它彻底释放锁和退出。

synchronized的重入实现机理:

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
 

LockSupport是什么?

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程,是wait/notify,await/signal的加强版。

3种让线程等待和唤醒的方法

  • 方式1:使用Object中的wait()方法让线程等待,使用object中的notify()方法唤醒线程
  • 方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
  • 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
     

方式一:wait/notify

public class WaitNotifyDemo {

	static Object lock = new Object();
	
	public static void main(String[] args) {
		new Thread(()->{
			synchronized (lock) {
				System.out.println(Thread.currentThread().getName()+" come in.");
				try {
					lock.wait();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName()+" 换醒.");
		}, "Thread A").start();
		
		new Thread(()->{
			synchronized (lock) {
				lock.notify();
				System.out.println(Thread.currentThread().getName()+" 通知.");
			}
		}, "Thread B").start();
	}
}

小总结:

  • wait和notify方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException
  • 必须先wait后notify

wait和sleep区别:

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁

wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。调用wait会释放锁。

方式二:await/signal

Condition接口中的await和signal方法实现线程的等待和唤醒,与Object类中的wait和notify方法线程等待和唤醒类似。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionAwaitSignalDemo {
		
	public static void main(String[] args) {
		
		ReentrantLock lock = new ReentrantLock();
		Condition condition = lock.newCondition();
		
		new Thread(()->{
			
			try {
				System.out.println(Thread.currentThread().getName()+" come in.");
				lock.lock();
				condition.await();				
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
			
			System.out.println(Thread.currentThread().getName()+" 换醒.");
		},"Thread A").start();
		
		new Thread(()->{
			try {
				lock.lock();
				condition.signal();
				System.out.println(Thread.currentThread().getName()+" 通知.");
			}finally {
				lock.unlock();
			}
		},"Thread B").start();
	}
	
}

输出结果:

Thread A come in.
Thread B 通知.
Thread A 换醒.

同理:

  • await和signal方法必须要在同步块或者方法里面成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。
  • 调用顺序需要先await后signal.

方式三:LockSupport

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是0.可以把许可看成是一种(0,1)信号量(semaphore),但和信号量不同的是,许可的累加上限是1。

通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

park()/park(Object blocker) - 阻塞当前线程阻塞传入的具体线程

public class LockSupport {

    ...
    
    public static void park() {
        UNSAFE.park(false, 0L);
    }

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    
    ...
    
}

permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。

unpark(Thread thread) - 唤醒处于阻塞状态的指定线程

public class LockSupport {
 
    ...
    
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
    
    ...

}

调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,pemit值还是1)会自动唤醒thead线程,即之前阻塞中的LockSupport.park()方法会立即返回。

例:

public class LockSupportDemo {

	public static void main(String[] args) {
		Thread a = new Thread(()->{
//			try {
//				TimeUnit.SECONDS.sleep(2);
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
			System.out.println(Thread.currentThread().getName() + " come in. " + System.currentTimeMillis());
			LockSupport.park();
			System.out.println(Thread.currentThread().getName() + " 换醒. " + System.currentTimeMillis());
		}, "Thread A");
		a.start();
		
		Thread b = new Thread(()->{
			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			LockSupport.unpark(a);
			System.out.println(Thread.currentThread().getName()+" 通知.");
		}, "Thread B");
		b.start();
	}
	
}

输出结果:

Thread A come in.
Thread B 通知.
Thread A 换醒.

小总结:

  • 正常 + 无锁块要求。
  • 先前错误的先唤醒后等待顺序,LockSupport可无视这顺序。

重点:

  • LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用Unsafe中的native代码。
  • LockSupport和每个使用它的线程都有一个许可(permit)关联,permit相当于1,0的开关,默认是0,调用一次park会消费permit,将1变成0,同时park立即返回。如果再次调用park会变成阻塞(因为permit为0会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark不会积累凭证

面试题:

为什么可以先唤醒线程后阻塞线程?

因为unpack()获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多是1,连续调用两次unpack和调用一次unpack效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

1.AQS理论初步

AbstractQueuedSynchronizer  抽象的队列同步器

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    ...
    
}

是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。

CLH:Craig、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO

AQS为什么是JUC内容中最重要的基石?

java基础面经--上_第46张图片

进一步理解锁和同步器的关系?

  • 锁,面向锁的使用者,定义了程序员和锁交互的使用层api,隐藏了实现细节,你调用即可。
  • 同步器,面向锁的实现者,比如java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制的等。

能干嘛?

加锁会导致阻塞 - 有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理

解释说明

抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。

既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupportpark)的方式,维护state变量的状态,使并发达到同步的控制效果。
 

2.AQS源码体系

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private static final long serialVersionUID = 7373984972572414691L;

     * Creates a new {@code AbstractQueuedSynchronizer} instance
    protected AbstractQueuedSynchronizer() { }

     * Wait queue node class.
    static final class Node {

     * Head of the wait queue, lazily initialized.  Except for
    private transient volatile Node head;

     * Tail of the wait queue, lazily initialized.  Modified only via
    private transient volatile Node tail;

     * The synchronization state.
    private volatile int state;

     * Returns the current value of synchronization state.
    protected final int getState() {

     * Sets the value of synchronization state.
    protected final void setState(int newState) {

     * Atomically sets synchronization state to the given updated
    protected final boolean compareAndSetState(int expect, int update) {
         
    ...
}         

java基础面经--上_第47张图片

AQS自身

AQS的int变量 - AQS的同步状态state成员变量

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    ...

     * The synchronization state.
    private volatile int state;
    
    ...
}

state成员变量相当于银行办理业务的受理窗口状态。

  • 零就是没人,自由状态可以办理

  • 大于等于1,有人占用窗口,等着去

AQS的CLH队列

  • CLH队列(三个大牛的名字组成),为一个双向队列

  • 银行候客区的等待顾客

小总结

  • 有阻塞就需要排队,实现排队必然需要队列

  • state变量+CLH变种的双端队列

AbstractQueuedSynchronizer内部类Node源码

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    ...

     * Creates a new {@code AbstractQueuedSynchronizer} instance
    protected AbstractQueuedSynchronizer() { }

     * Wait queue node class.
    static final class Node {
        //表示线程以共享的模式等待锁
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        
        //表示线程正在以独占的方式等待锁
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        //线程被取消了
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;

        //后继线程需要唤醒
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        
        //等待condition唤醒
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        
        //共享式同步状态获取将会无条件地传播下去
        * waitStatus value to indicate the next acquireShared should     
        static final int PROPAGATE = -3;

        //当前节点在队列中的状态(重点)
        //说人话:
        //等候区其它顾客(其它线程)的等待状态
        //队列中每个排队的个体就是一个Node
        //初始为0,状态值就是上面的几种
         * Status field, taking on only the values:
        volatile int waitStatus;

        //前驱节点(重点)
         * Link to predecessor node that current node/thread relies on
        volatile Node prev;

        //后继节点(重点)
         * Link to the successor node that the current node/thread
        volatile Node next;

        //表示处于该节点的线程
         * The thread that enqueued this node.  Initialized on
        volatile Thread thread;

        //指向下一个处于CONDITION状态的节点
         * Link to next node waiting on condition, or the special
        Node nextWaiter;

         * Returns true if node is waiting in shared mode.
        final boolean isShared() {

        //返回前驱节点,没有的话抛出npe
         * Returns previous node, or throws NullPointerException if null.
        final Node predecessor() throws NullPointerException {

        Node() {    // Used to establish initial head or SHARED marker

        Node(Thread thread, Node mode) {     // Used by addWaiter

        Node(Thread thread, int waitStatus) { // Used by Condition
    }
	...
}

AQS同步队列的基本结构

java基础面经--上_第48张图片

源码深度解析:(重要)

例:从ReentrantLock开始解读AQS

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类(Sync)完成线程访问控制的。

 * A reentrant mutual exclusion {@link Lock} with the same basic
public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

     * Base of synchronization control for this lock. Subclassed
    abstract static class Sync extends AbstractQueuedSynchronizer {

     * Sync object for non-fair locks
    static final class NonfairSync extends Sync {

     * Sync object for fair locks
    static final class FairSync extends Sync {

     * Creates an instance of {@code ReentrantLock}.
    public ReentrantLock() {
        sync = new NonfairSync();
    }

     * Creates an instance of {@code ReentrantLock} with the
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

     * Acquires the lock.
    public void lock() {
        sync.lock();//<------------------------注意,我们从这里入手
    }
        
    * Attempts to release this lock.
    public void unlock() {
        sync.release(1);
    }
    ...
}

从最简单的lock方法开始看看公平和非公平,先浏览下AbstractQueuedSynchronizer,FairSync,NonfairSync类的源码。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

	...

     * Acquires in exclusive mode, ignoring interrupts.  Implemented
    public final void acquire(int arg) {//公平锁或非公平锁都会调用这方法
        if (!tryAcquire(arg) &&//0.
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//1. 2.
            selfInterrupt();//3.
    }
    
    //0.
    * Attempts to acquire in exclusive mode. This method should query
    protected boolean tryAcquire(int arg) {//取决于公平锁或非公平锁的实现
        throw new UnsupportedOperationException();
    }
	
    
    //1.
    * Acquires in exclusive uninterruptible mode for thread already in
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    //2.
    * Creates and enqueues node for current thread and given mode.
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    
    //3.
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
    
    //这个方法将会被公平锁的tryAcquire()调用
    * Queries whether any threads have been waiting to acquire longer
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    
    
	...         
}
public class ReentrantLock implements Lock, java.io.Serializable {
    
    ...
    
    //非公平锁与公平锁的公共父类
     * Base of synchronization control for this lock. Subclassed
    abstract static class Sync extends AbstractQueuedSynchronizer {
    
    	...
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        } 
        ...
    
    }
        
    //非公平锁
	static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {//<---ReentrantLock初始化为非公平锁时,ReentrantLock.lock()将会调用这
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);//调用父类AbstractQueuedSynchronizer的acquire()
        }

        //acquire()将会间接调用该方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);//调用父类Sync的nonfairTryAcquire()
        }
    }
    
    * Sync object for fair locks
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {//<---ReentrantLock初始化为非公平锁时,ReentrantLock.lock()将会调用这
            acquire(1);调用父类AbstractQueuedSynchronizer的acquire()
        }

        //acquire()将会间接调用该方法
         * Fair version of tryAcquire.  Don't grant access unless
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&//<---公平锁与非公平锁的唯一区别,公平锁调用hasQueuedPredecessors(),而非公平锁没有调用
                    							//hasQueuedPredecessors()在父类AbstractQueuedSynchronizer定义
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
    
	...

}

可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()

hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:

公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;

非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)

接下来讲述非公平锁的lock()

整个ReentrantLock 的加锁过程,可以分为三个阶段:

  1. 尝试加锁;
  2. 加锁失败,线程入队列;
  3. 线程入队列后,进入阻塞状态。

例:带入一个银行办理业务的案例来模拟我们的AQS 如何进行线程的管理和通知唤醒机制,3个线程模拟3个来银行网点受理窗口办理业务的顾客。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class AQSDemo {
	
	public static void main(String[] args) {
		ReentrantLock lock = new ReentrantLock();
		
		//带入一个银行办理业务的案例来模拟我们的AQs 如何进行线程的管理和通知唤醒机制
		//3个线程模拟3个来银行网点,受理窗口办理业务的顾客

		//A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
		new Thread(()->{
			lock.lock();
			try {
				System.out.println(Thread.currentThread().getName() + " come in.");
				
				try {
					TimeUnit.SECONDS.sleep(5);//模拟办理业务时间
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			} finally {
				lock.unlock();
			}
		}, "Thread A").start();
		
		//第2个顾客,第2个线程---->,由于受理业务的窗口只有一个(只能一个线程持有锁),此代B只能等待,
		//进入候客区
		new Thread(()->{
			lock.lock();
			try {
				System.out.println(Thread.currentThread().getName() + " come in.");
				
			} finally {
				lock.unlock();
			}
		}, "Thread B").start();
		
		
		//第3个顾客,第3个线程---->,由于受理业务的窗口只有一个(只能一个线程持有锁),此代C只能等待,
		//进入候客区
		new Thread(()->{
			lock.lock();
			try {
				System.out.println(Thread.currentThread().getName() + " come in.");
				
			} finally {
				lock.unlock();
			}
		}, "Thread C").start();
	}
}

程序初始状态方便理解图

启动程序,首先是运行线程A,ReentrantLock默认是选用非公平锁。

public class ReentrantLock implements Lock, java.io.Serializable {
    
    ...
        
    * Acquires the lock.
    public void lock() {
        sync.lock();//<------------------------注意,我们从这里入手,一开始将线程A的
    }
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        
        ...

        //被NonfairSync的tryAcquire()调用
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
        ...

    }
    
    
	//非公平锁
	static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {//<----线程A的lock.lock()调用该方法
            if (compareAndSetState(0, 1))//AbstractQueuedSynchronizer的方法,刚开始这方法返回true
                setExclusiveOwnerThread(Thread.currentThread());//设置独占的所有者线程,显然一开始是线程A
            else
                acquire(1);//稍后紧接着的线程B将会调用该方法。
        }

        //acquire()将会间接调用该方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);//调用父类Sync的nonfairTryAcquire()
        }
        

        
    }
    
    ...
}
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /**
     * The synchronization state.
     */
    private volatile int state;

    //线程A将state设为1,下图红色椭圆区
    /*Atomically sets synchronization state to the given updated value 
    if the current state value equals the expected value.
    This operation has memory semantics of a volatile read and write.*/
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

}

线程A开始办业务了。

轮到线程B运行

public class ReentrantLock implements Lock, java.io.Serializable {
    
    ...
        
    * Acquires the lock.
    public void lock() {
        sync.lock();//<------------------------注意,我们从这里入手,线程B的执行这
    }
    
	//非公平锁
	static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {//<-------------------------线程B的lock.lock()调用该方法
            if (compareAndSetState(0, 1))//这是预定线程A还在工作,这里返回false
                setExclusiveOwnerThread(Thread.currentThread());//
            else
                acquire(1);//线程B将会调用该方法,该方法在AbstractQueuedSynchronizer,
            			   //它会调用本类的tryAcquire()方法
        }

        //acquire()将会间接调用该方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);//调用父类Sync的nonfairTryAcquire()
        }
    }

    //非公平锁与公平锁的公共父类
     * Base of synchronization control for this lock. Subclassed
    abstract static class Sync extends AbstractQueuedSynchronizer {
    
        //acquire()将会间接调用该方法
    	...
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//这里是线程B
            int c = getState();//线程A还在工作,c=>1
            if (c == 0) {//false
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//(线程B == 线程A) => false
                int nextc = c + acquires;//+1
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;//最终返回false
        } 
        ...
    
    }
    
    ...
}
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

	...

     * Acquires in exclusive mode, ignoring interrupts.  Implemented
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&//线程B调用非公平锁的tryAcquire(), 最终返回false,加上!,也就是true,也就是还要执行下面两行语句
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//下一节论述
            selfInterrupt();
    }
    
    ...
}

另外  插一句

假设线程B,C还没启动,正在工作线程A重新尝试获得锁(可重入锁),也就是调用lock.lock()多一次

    //非公平锁与公平锁的公共父类fa
     * Base of synchronization control for this lock. Subclassed
    abstract static class Sync extends AbstractQueuedSynchronizer {
    
    	...
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//这里是线程A
            int c = getState();//线程A还在工作,c=>1;如果线程A恰好运行到在这工作完了,c=>0,这时它又要申请锁的话
            if (c == 0) {//线程A正在工作为false;如果线程A恰好工作完,c=>0,这时它又要申请锁的话,则为true
                if (compareAndSetState(0, acquires)) {//线程A重新获得锁
                    setExclusiveOwnerThread(current);//这里相当于NonfairSync.lock()另一重设置吧!
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//(线程A == 线程A) => true
                int nextc = c + acquires;//1+1=>nextc=2
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//state=2,说明要unlock多两次吧(现在盲猜)
                return true;//返回true
            }
            return false;
        } 
        ...
    
    }

继续上一节

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

	...

     * Acquires in exclusive mode, ignoring interrupts.  Implemented
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&//线程B调用非公平锁的tryAcquire(), 最终返回false,加上!,也就是true,也就是还要执行下面两行语句
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//线程B加入等待队列
            selfInterrupt();//下一节论述
    }
    
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {//根据上面一句注释,本语句块的意义是将新节点快速添加至队尾
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);//快速添加至队尾失败,则用这方法调用(可能链表为空,才调用该方法)
        return node;
    }
    
    //Inserts node into queue, initializing if necessary.
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))//插入一个哨兵节点(或称傀儡节点)占位
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {//真正插入我们需要的节点,也就是包含线程B引用的节点
                    t.next = node;
                    return t;
                }
            }
        }
    }
    
    //CAS head field. Used only by enq.
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }

    //CAS tail field. Used only by enq.
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }

    
    ...
}

线程B加入等待队列。

线程A依然工作,线程C如线程B那样炮制加入等待队列。

双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

	...

     * Acquires in exclusive mode, ignoring interrupts.  Implemented
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&//线程B调用非公平锁的tryAcquire(), 最终返回false,加上!,也就是true,也就是还要执行下面两行语句
            //线程B加入等待队列,acquireQueued本节论述<--------------------------
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();//
    }
    
    //Acquires in exclusive uninterruptible mode for thread already inqueue. 
    //Used by condition wait methods as well as acquire.
    //
    //return true if interrupted while waiting
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//1.返回前一节点,对与线程B来说,p也就是傀儡节点
				//p==head为true,tryAcquire()方法说明请转至 #21_AQS源码深度解读03
                //假设线程A正在工作,现在线程B只能等待,所以tryAcquire(arg)返回false,下面的if语块不执行
                //
                //第二次循环,假设线程A继续正在工作,下面的if语块还是不执行
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //请移步到2.处的shouldParkAfterFailedAcquire()解说。第一次返回false, 下一次(第二次)循环
                //第二次循环,shouldParkAfterFailedAcquire()返回true,执行parkAndCheckInterrupt()
                if (shouldParkAfterFailedAcquire(p, node) && 
                    //4. 
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    
    static final class Node {

        ...
        //1.返回前一节点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
        
        ...

    }
    
    //2. 
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//此时pred指向傀儡节点,它的waitStatus为0
        //Node.SIGNAL为-1,跳过
        //第二次调用,ws为-1,条件成立,返回true
        if (ws == Node.SIGNAL)//-1
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {//跳过
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            //3. 傀儡节点的WaitStatus设置为-1//下图红圈
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;//第一次返回
    }
    
    /**
     * CAS waitStatus field of a node.
     */
    //3.
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }
    
    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    //4.
    private final boolean parkAndCheckInterrupt() {
        //前段章节讲述的LockSupport,this指的是NonfairSync对象,
        //这意味着真正阻塞线程B,同样地阻塞了线程C
        LockSupport.park(this);//线程B,C在此处暂停了运行<-------------------------
        return Thread.interrupted();
    }
    
}

图中的傀儡节点的waitStatus由0变为-1(Node.SIGNAL)。

接下来讨论ReentrantLock.unLock()方法。假设线程A工作结束,调用unLock(),释放锁占用。

public class ReentrantLock implements Lock, java.io.Serializable {
    
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        
        ...
        //2.unlock()间接调用本方法,releases传入1
        protected final boolean tryRelease(int releases) {
            //3.
            int c = getState() - releases;//c为0
            //4.
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//c为0,条件为ture,执行if语句块
                free = true;
                //5.
                setExclusiveOwnerThread(null);
            }
            //6.
            setState(c);
            return free;//最后返回true
        }
    	...
    
    }
    
    static final class NonfairSync extends Sync {...}
    
    public ReentrantLock() {
        sync = new NonfairSync();//我们使用的非公平锁
    }
    					//注意!注意!注意!
    public void unlock() {//<----------从这开始,假设线程A工作结束,调用unLock(),释放锁占用
        //1.
        sync.release(1);//在AbstractQueuedSynchronizer类定义
    }
    
    ...
 
}
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    ...
    //1.
    public final boolean release(int arg) {
        //2.
        if (tryRelease(arg)) {//该方法看子类NonfairSync实现,最后返回true
            Node h = head;//返回傀儡节点
            if (h != null && h.waitStatus != 0)//傀儡节点非空,且状态为-1,条件为true,执行if语句
                //7.
                unparkSuccessor(h);
            return true;
        }
        return false;//返回true,false都无所谓了,unlock方法只是简单调用release方法,对返回结果没要求
    }
    
    /**
     * The synchronization state.
     */
    private volatile int state;

    //3.
    protected final int getState() {
        return state;
    }

    //6.
    protected final void setState(int newState) {
        state = newState;
    }
    
    //7. Wakes up node's successor, if one exists.
    //传入傀儡节点
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;//傀儡节点waitStatus为-1
        if (ws < 0)//ws为-1,条件成立,执行if语块
            compareAndSetWaitStatus(node, ws, 0);//8.将傀儡节点waitStatus由-1变为0

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;//傀儡节点的下一节点,也就是带有线程B的节点
        if (s == null || s.waitStatus > 0) {//s非空,s.waitStatus非0,条件为false,不执行if语块
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)//s非空,条件为true,不执行if语块
            LockSupport.unpark(s.thread);//唤醒线程B。运行到这里,线程A的工作基本告一段落了。
    }
    
    //8.
    private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
    }
    
    
}

 java基础面经--上_第49张图片

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    ...

    protected AbstractOwnableSynchronizer() { }

    private transient Thread exclusiveOwnerThread;

    //5.
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
    
    //4.
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

线程A结束工作,调用unlock()的tryRelease()后的状态,state由1变为0,exclusiveOwnerThread由线程A变为null。

线程B被唤醒,即从原先park()的方法继续运行

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

     private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//线程B从阻塞到非阻塞,继续执行
        return Thread.interrupted();//线程B没有被中断,返回false
    }
    
	...
 
    //Acquires in exclusive uninterruptible mode for thread already inqueue. 
    //Used by condition wait methods as well as acquire.
    //
    //return true if interrupted while waiting
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();//线程B所在的节点的前一节点是傀儡节点
                //傀儡节点是头节点,tryAcquire()的说明请移步至#21_AQS源码深度解读03
                //tryAcquire()返回true,线程B成功上位
                if (p == head && tryAcquire(arg)) {
                    setHead(node);//1.将附带线程B的节点的变成新的傀儡节点
                    p.next = null; // help GC//置空原傀儡指针与新的傀儡节点之间的前后驱指针,方便GC回收
                    failed = false;
                    return interrupted;//返回false,跳到2.acquire()
                }
               
                if (shouldParkAfterFailedAcquire(p, node) && 
                    //唤醒线程B继续工作,parkAndCheckInterrupt()返回false
                    //if语块不执行,跳到下一循环
                    parkAndCheckInterrupt())//<---------------------------------唤醒线程在这里继续运行
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    //1. 
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
    
    //2.
    * Acquires in exclusive mode, ignoring interrupts.  Implemented
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            //acquireQueued()返回fasle,条件为false,if语块不执行,acquire()返回
            //也就是说,线程B成功获得锁,可以展开线程B自己的工作了。
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();//
    }
    
}

最后,线程B上位成功。

java基础面经--上_第50张图片

23、Redis

redis基础:观看的是b站狂神的视频,pdf笔记见下载资源

学习包括:
1.Nosql概述
2.redis的linux和windows的安装
3.String ,List, Set ,Hash ,Zset(有序集合)五种数据类型的基本redis命令
4.Geospatial(地理位置),Hyperloglog(基数统计),Bitmap(位图,位存储)三种特殊大的数据类型的基本redis命令
5.redis中的事务机制(不保证原子性!没有隔离级别的概念!multi开启事务,exec执行事务)
6.redis中使用watch进行监控
7.Jedis(官方推荐的redis与java连接开发工具)
8.redis和springboot进行整合(在这里提供了一个工具类:RedisUtils)
9.redis的配置文件 redis.conf详解
10.redis的两种持久化方式(RDB和AOF,默认使用RDB)
11.redis发布订阅功能
12.redis主从复制(最小需要三台服务器构成一主二从,哨兵模式)
13.redis缓存穿透和雪崩穿透
 

redis面试题:(一定要看)

https://blog.csdn.net/Mcdull__/article/details/118440180

redis传统五大数据类型的落地应用?

你知道分布式锁吗?有哪些实现方案?你谈谈对redis分布式锁的理解,删key的时候有什么问题?

redis缓存过期淘汰策略?

redis的LRU算法简介?

你可能感兴趣的:(面经,面经)