Java面试题第二季

一、JUC多线程及高并发

1.谈谈你对volatile的理解

线程安全获得保证

1.volatile是Java虚拟机提供的轻量级的同步机制

  • 保证可见性

  • 不保证原子性

    1号和2号线程同时修改各自工作空间中的内容,因为可见性,需要重写入内存,但是1号在写入的时候,2号也同时写入,导致1号线程的写入操作被挂起,这样造成了在2号线程的写入后,1号线程覆盖了2号线程的值,造成了数据丢失的问题

    image-20231204164617009

  • 禁止指令重排

2.JMM你谈谈

JMM(Java内存模型,简称JMM)本身是一种抽象的概念并不真实存在
,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

JMM关于同步的规定:

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

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储到主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取、复制等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

image-20231204164949133

2.1可见性

通过前面对JMM的介绍,我们知道
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回主内存中的。

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

package Volatile;

import java.util.concurrent.TimeUnit;
//volatile保证可见性 演示
public class VolatileDemo {
    public static void main(String[] args) {
        myData myData = new myData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addTo60();
            System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.num);
        }, "Thread01").start();

        while (myData.num == 0) {

        }
        System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.num);
    }
}
class myData {
    volatile int num;

    void addTo60() {
        this.num += 60;
    }
}
2.2原子性

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

数据库也经常提到事务具备原子性
volatile不保证原子性,但是可以使用synchronized关键字,一般使用原子类

private static void atomicJMM() {
    myData myData = new myData();
    for (int i = 1; i <= 20; i++) {
        new Thread(() -> {
            for (int j = 0; j < 1000; j++) {
                myData.addPlusPlus();
                myData.addMyAtomic();
            }
        }, String.valueOf(i)).start();
    }
    while (Thread.activeCount() > 2) {
        Thread.yield();
    }
    System.out.println(Thread.currentThread().getName() + "\t int type update" + myData.num);
    System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type update" + myData.atomicInteger);
}
 
class myData {
    volatile int num;

    void addTo60() {
        this.num += 60;
    }

    //此时加了volatile关键字,volatile不保证原子性
    public void addPlusPlus() {
        num++;
    }
    //利用AtomicInteger
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAtomic(){
        atomicInteger.getAndIncrement();
    }
} 
2.3 有序性

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

image-20231204165309945

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

处理器在进行重排序时必须考虑指令之间的数据依赖性

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

重排1:image-20231204165506676

不能,因为处理器在进行重排序时必须考虑指令之间的数据依赖性。

重排2:image-20231204165543980

image-20231204165555478

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

禁止指令重排小总结(了解)

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

先了解一个概念,内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个:
一是保证特定操作的执行顺序
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

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

image-20231204165819559

线程安全获得保证

1.工作内存和主内存同步延迟现象导致的可见性问题
可以使用synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其他线程可见

2.对于指令重排导致的可见性问题和有序性问题可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

3.你在哪些地方用过volatile?

3.1单例模式DCL代码
public class SingletonDemo {
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo()");
    }
    private static Volatile SingletonDemo instance = null;

    public static SingletonDemo getSingletonDemo(){
        if (instance == null){
            synchronized (SingletonDemo.class){
                if (instance == null){
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 10 ; i++) {
            new Thread(() -> {
                SingletonDemo.getSingletonDemo();
            },String.valueOf(i)).start();
        }
    }
}
3.2单例模式volatile分析

DCL(双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。

原因在于某一个线程执行到第一个检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化

指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程间的语义一致性。

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。

DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排

原因是在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。因为 instance = new SingletonDemo();可以分为以下三步进行完成:

- memory = allocate();   // 1、分配对象内存空间
- instance(memory);   // 2、初始化对象
- instance = memory;  // 3、设置instance指向刚刚分配的内存地址,此时instance != null

但是我们通过上面的三个步骤,能够发现,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

- memory = allocate();   // 1、分配对象内存空间
- instance = memory;  // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
- instance(memory);   // 2、初始化对象

这样就会造成什么问题呢?

也就是当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例

指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性

所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题

所以需要引入volatile,来保证禁止出现指令重排的问题,从而保证单例模式的线程安全性

2.CAS你知道吗

1.比较并交换==>compareAndSet

CASDemo代码

public class CASDemo {
    public static void main(String[] args) {
        //设置主物理内存中数据
        AtomicInteger atomicInteger = new AtomicInteger(5);

        //main do thing.....
        System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data :" + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data :" + atomicInteger.get());
    }
}

2.CAS底层原理?如果知道,谈谈你对Unsafe的理解

atomicInteger.getAndIncrement();

image-20231204170237334

Unsafe:

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

注意Unsafe类中的所有方法都是native修饰的,也就是说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的原子指令,不会造成所谓的数据不一致性问题。

unsafe.getAndAddInt

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

1、 AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。

2、线程A通过getIntVolatile(var1,var2)拿到value值3,这时线程A被挂起。

3、线程B也通过getIntVolatile(var1,var2)方法获得value值3,此时刚好线程B没有被挂起并执行compareAndSwap方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。

4、这时线程A恢复,执行compareAndSwapInt方法比较,发现手里的值3与内存值4不一致,说明该值已经被其他线程抢险异步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。

5、线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

底层汇编

image-20231204170541753

简单版小总结

CAS(CompareAndSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。

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

3.CAS缺点

  • 循环时间长开销大

    image-20231204170637092

    如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

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

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

  • 引出来ABA问题???

    通过原子引用解决ABA问题:AtomicStampedReference

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

ABA问题怎么产生的

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

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

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

原子引用解决ABA问题

时间戳原子引用

AtomicReferenceDemo:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {
 
  public static void main(String[] args) {
    User z3 = new User("z3", 22);
    User l3 = new User("l3", 25);
 
    AtomicReference<User> atomicReference = new AtomicReference<>();
    atomicReference.set(z3);
 
    System.out.println(atomicReference.compareAndSet(z3, l3) + "\t" + atomicReference.get().toString());
  }
 
}
 
class User {
  String username;
  int age;
 
  public String getUsername() {
    return username;
  }
 
  public void setUsername(String username) {
    this.username = username;
  }
 
  public int getAge() {
    return age;
  }
 
  public void setAge(int age) {
    this.age = age;
  }
 
  public User(String username, int age) {
    super();
    this.username = username;
    this.age = age;
  }
}

ABADemo:

package com.luojia.interview.study.thread;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
 
  static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
  static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100, 1);
 
  public static void main(String[] args) {
    
    System.out.println("===========以下是ABA问题的产生============");
    new Thread(() -> {
      atomicReference.compareAndSet(100, 101);
      atomicReference.compareAndSet(101, 100);
    }, "t1").start();
 
    new Thread(() -> {
      // 线程t2暂停1秒钟,保证线程t1完成一次ABA操作
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (Exception e) {
        e.printStackTrace();
      }
 
      System.out.println(atomicReference.compareAndSet(100, 2020) + "\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() + "第一次版本号" + stamp);
      
      // 暂停线程1秒 t3线程
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (Exception e) {
        e.printStackTrace();
      }
      atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
      System.out.println(Thread.currentThread().getName() + "第二次版本号" + atomicStampedReference.getStamp());
      
      atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
      System.out.println(Thread.currentThread().getName() + "第三次版本号" + atomicStampedReference.getStamp());
    }, "t3").start();
    
    new Thread(() -> {
      int stamp = atomicStampedReference.getStamp();
      System.out.println(Thread.currentThread().getName() + "第一次版本号" + stamp);
      
      // 暂停线程3秒 t4线程, 保证线程t3完成一次ABA操作
      try {
        TimeUnit.SECONDS.sleep(3);
      } catch (Exception e) {
        e.printStackTrace();
      }
      boolean result = atomicStampedReference.compareAndSet(100, 2020, 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();
    
  }
 
}

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

解决方案1:Vector、Collections.synchronizedList

限制不可以用Vector和Collections工具类解决方案2:

  • new CopyOnWriteArrayList<>()

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

    image-20231204171215167

    这样做的好处是:
    可以对copyonwrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 copyonwrite 容器也是一种
    读写分离的思想,读和写不同的容器。

    image-20231204171229893

  • HashSet对应安全版本CopyOnWriteArraySet<>()

  • HashMap对应ConcurrentHashMap<>()

5.公平锁/非公平锁/可重入锁/递归锁/自旋锁谈谈你的理解?请手写一个自旋锁

公平锁和非公平锁

是什么?

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

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

两者区别

公平锁/非公平锁:并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁。

关于两者区别:
公平锁:Threads acquire a fair lock in the order in which they requested it.
公平锁,就是很公平,在并发情况下,每个线程在获取锁时会查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

非公平锁:非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采取类似公平锁那种方式。

题外话

Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

对于Synchronized而言,也是一种非公平锁。

可重入锁(又名递归锁)

是什么

可重入锁(也就是递归锁):指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,同一线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

也就是说,

线程可以进入任何一个它已经拥有的锁所有同步着的代码块。

ReentrantLock/Synchronized就是一个典型的可重入锁
可重入锁最大的作用是避免死锁
ReenterLockDemo
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class LockDemo {
 
  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();
    
    
    System.out.println("---------------------------");
    Thread t3 = new Thread(phone, "t3");
    Thread t4 = new Thread(phone, "t4");
    t3.start();
    t4.start();
    
  }
  
}
 
class Phone implements Runnable{
  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()");
  }
 
  Lock lock = new ReentrantLock();
  
  @Override
  public void run() {
    get();
  }
  
  public void get() {
    // 可以同时加多把锁,但是一定是加锁次数和解锁次数相同才可以,否则死锁
    lock.lock();
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + "\t invoked get()");
      set();
    } finally {
      lock.unlock();
      lock.unlock();
    }
  }
  
  public void set() {
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + "\t invoked set()");
    } finally {
      lock.unlock();
    }
  }
}

结果:

image-20240329160723379

或者

image-20240329160733024

自旋锁

自旋锁:是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下切换的消耗,缺点是循环会消耗CPU。

image-20240329160809233

SpinLockDemo
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
 
/**
 * 实现一个自旋锁
 * 自旋锁好处:循环比较获取直到成功为止,没有类似wait的阻塞
 * 
 * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后发现
 * 当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁B抢到为止
 */
public class SpinLockDemo {
  
  // 原子引用线程
  AtomicReference<Thread> atomicReference = new AtomicReference<>();
  
  public void myLock() {
    Thread thread = Thread.currentThread();
    System.out.println(Thread.currentThread().getName()+"\t come in");
    
    while (!atomicReference.compareAndSet(null, thread)) {
//      System.out.println("阻塞");
    }
  }
  
  public void myUnlock() {
    Thread thread = Thread.currentThread();
    atomicReference.compareAndSet(thread, null);
    System.out.println(Thread.currentThread().getName()+"\t invoked myUnlock()");
  }
  
  public static void main(String[] args) {
    SpinLockDemo spinLockDemo = new SpinLockDemo();
    
    new Thread(() -> {
      try {
        spinLockDemo.myLock();
        TimeUnit.SECONDS.sleep(4);
      } catch (Exception e) {
        e.printStackTrace();
      }
      
      spinLockDemo.myUnlock();
    }, "AA").start();
    
    // 保证A线程先进入
    try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { e.printStackTrace(); }
    
    new Thread(() -> {
      spinLockDemo.myLock();
      spinLockDemo.myUnlock();
    }, "BB").start();  
  }
}

运行结果:

AA	 come in
BB	 come in
AA	 invoked myUnlock()
BB	 invoked myUnlock()

独占锁(写锁)/共享锁(读锁)/互斥锁

独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁。

共享锁:指该锁可被多个线程所持有。
对ReentrantReadWriteLock,其读锁是共享锁,其写锁是独占锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

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

//资源类
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public void put(String key, Object value) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在写入:" + key);
            //暂停一会儿线程
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (Exception e) { e.printStackTrace(); }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "\t写入完成:");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }

    }
    public void get(String key) {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t正在读取...");
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (Exception e) { e.printStackTrace(); }
            Object result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "\t读取完成" + result);
        } catch (Exception exception) {
            exception.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }
}

/**
 * 多个线程同时读一个资源类没有任何问题,所有为了满足并发量,读取共享资源应该可以同时进行
 * 但是
 * 如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写
 * 小总结
 * 读 - 读能共存
 * 读 - 写不能共存
 * 写 - 写不能共存
 * <p>
 * 写操作:原子-独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(() ->{
                myCache.put(finalI +"", finalI + "");
            },String.valueOf(i)).start();
        }
        for (int i = 1; i <= 5; i++) {
            int finalI = i;
            new Thread(() ->{
                myCache.get(finalI +"");
            },String.valueOf(i)).start();
        }
    }
}

运行结果:

image-20240329161624705

Lock和synchronized的区别?

image-20240329161702612

Lock的优势可以绑定多个条件Condition举例:

image-20240329161908004

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

class ShareResource {
    private int number = 1; //A:1 B:2 C:3
    private Lock lock = new ReentrantLock();
    Condition c1 = lock.newCondition();
    Condition c2 = lock.newCondition();
    Condition c3 = lock.newCondition();
    public void print5() {
        lock.lock();
        try {
            //1.判断
            while (number != 1) {
                c1.await();
            }
            //2.干活
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //3.通知
            number = 2;
            c2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print10() {
        lock.lock();
        try {
            //1.判断
            while (number != 2) {
                c2.await();
            }
            //2.干活
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //3.通知
            number = 3;
            c3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void print15() {
        lock.lock();
        try {
            //1.判断
            while (number != 3) {
                c3.await();
            }
            //2.干活
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            //3.通知
            number = 1;
            c1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class CompareLockAndsynchronized {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(() ->{
            for (int i = 1; i < 10; i++) {
                shareResource.print5();
            }
        },"A").start();
        new Thread(() ->{
            for (int i = 1; i < 10; i++) {
                shareResource.print10();
            }
        },"B").start();
        new Thread(() ->{
            for (int i = 1; i < 10; i++) {
                shareResource.print15();
            }
        },"C").start();
    }
}

结果:

A	1
A	2
A	3
A	4
A	5
B	1
B	2
B	3
B	4
B	5
synchronized和lock有什么区别?用新的lock有什么好处?你举例说说

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

2.使用方法

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

3.等待是否可中断

synchronized不可中断,除非抛出异常或者正常运行完成。
ReentrantLock可中断,

​ 1.设置超时方法 tryLock(long timeout,TimeUnit unit);

​ 2.lockInterruptibly()放代码块中,调用interrupt()方法可中断。

4.加锁是否公平

synchronized非公平锁
ReentrantLock两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁。

5.锁绑定多个条件Condition

synchronized没有
ReentrantLock用来实现分组唤醒需要唤醒的线程,可以精确唤醒
,而不是像synchronized要么随机唤醒一个要么唤醒全部线程。

6.CountDownLatch/CyclicBarrier/Semaphore

CountDownLatch

让一些线程阻塞直到另一个线程完成一系列操作后才被唤醒

CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。其他线程调用countDown方法会将计数器减1(调用CountDown方法的线程不会阻塞),当计数器的值变为0时,调用await方法被阻塞的线程会被唤醒,继续执行。

CountDownLatchDemo
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; 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*****************班长最后关门走人");
    }
}

结果:

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

CyclicBarrier

CyslicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会打开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法。

CyclicBarrierDemo
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        //CyclicBarrier(int parties, Runnable barrierAction) 
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()-> System.out.println("***召唤神龙"));
        for (int i = 1; i <= 7; i++) {
            int finalI = i;
            new Thread(() ->{
                System.out.println(Thread.currentThread().getName()+"\t收集到第:"+ finalI +"龙珠");
                try {
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                } 
            },String.valueOf(i)).start();
        }
    }
}
2	收集到第:2龙珠
6	收集到第:6龙珠
4	收集到第:4龙珠
5	收集到第:5龙珠
3	收集到第:3龙珠
1	收集到第:1龙珠
7	收集到第:7龙珠
***召唤神龙

Semaphore

信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

SemaphoreDemo

类似于争车位

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

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);//模拟3个停车位

        for (int i = 1; i <= 6; i++) {  //模拟6辆车
            new Thread(() ->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"\t抢到车位");
                    try { TimeUnit.SECONDS.sleep(3); } catch (Exception e) { e.printStackTrace(); }
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                     System.out.println(Thread.currentThread().getName()+"\t停车3s后离开车位");
                }
            },String.valueOf(i)).start();
        }
    }
}
1	抢到车位
3	抢到车位
2	抢到车位
2	停车3s后离开车位
4	抢到车位
3	停车3s后离开车位
1	停车3s后离开车位
5	抢到车位
6	抢到车位
6	停车3s后离开车位
5	停车3s后离开车位
4	停车3s后离开车位

7.阻塞队列知道吗

队列+阻塞队列

阻塞队列,首先它是一个队列,而一个阻塞队列在数据结构中所起的作用大致是:线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素。

image-20240329163501662

当阻塞队列是空时,从队列中获取元素的操作将被阻塞。
当阻塞队列是满时,往队列里添加元素的操作将被阻塞。

试图从空的阻塞队列中获取元素的线程将被阻塞,直到其他的线程往空队列中插入新的元素。
试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列变得空闲起来并后续新增。

为什么用?有什么好处?

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦满足条件,被挂起的线程又会自动被唤醒。

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

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

BlockingQueue的核心方法

image-20240329163653675

抛出异常当阻塞队列满时:再往队列中add插入元素会抛出 IIIegalStateExceptionQueue full
当阻塞队列空时:再往队列中remove移除元素,会抛出NoSuchException
特殊性插入方法,成功true,失败false
移除方法:成功返回出队列元素,队列没有就返回空
一直阻塞当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出, 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。
超时退出当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出

架构梳理+种类分析

架构介绍

BlockingQueue 和 list 都是 Collections 的接口。

image-20240329164834324

种类分析
  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列

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

  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。

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

  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。

    理论:

​ SynchronousQueue没有容量。
​ 与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。每一个put操 作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

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

/**
 * 阻塞队列SychronousQueueDemo演示
 * SynchronousQueue是一个不存储元素的BlockingQueue。
 * 每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
 */
public class SychronousQueueDemo {

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

        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName()+"\t put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName()+"\t put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName()+"\t put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AAA").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"\t "+blockingQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"\t "+blockingQueue.take());
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"\t "+blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "BBB").start();
    }

}

结果:

AAA	 put 1
BBB	 1
AAA	 put 2
BBB	 2
AAA	 put 3
BBB	 3
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

用在哪里

生产者消费者模式
  • 传统版

    ProdConsumer_TraditionDemo

    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 InterruptedException {
            lock.lock();
            try {
                //1.判断
                while (number!= 0){
                    //等待,不能生产
                    condition.await();
                }
                //2.干活
                number++;
                System.out.println(Thread.currentThread().getName() + "\t" + number);
                //3.通知唤醒
                condition.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        public void decrement() throws InterruptedException {
            lock.lock();
            try {
                //1.判断
                while (number == 0){
                    //等待,不能生产
                    condition.await();
                }
                //2.干活
                number--;
                System.out.println(Thread.currentThread().getName() + "\t" + number);
                //3.通知唤醒
                condition.signalAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    /**
     * 题目:一个初始为0的变量,两个线程对其交错操作,一个加1,一个减1,来五轮
     * 1   线程    操作     资源类
     * 2   判断    干活     通知
     * 3    防止虚假唤醒
     */
    public class ProdConsumer_TraditionDemo {
        public static void main(String[] args) {
            ShareData shareData = new ShareData();
                new Thread(() ->{
                    for (int i = 1; i <= 5; i++) {
                        try {
                            shareData.increment();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                },"AA").start();
            new Thread(() ->{
                for (int i = 1; i <= 5; i++) {
                    try {
                        shareData.decrement();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            },"BB").start();
        }
    }
    
  • 阻塞队列版

    ProdConsumer_BlockQueueDemo

    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    
    class MyResource{
        private boolean FLAG = true;//默认开启,进行生产+消费
        private AtomicInteger atomicInteger = new AtomicInteger();
        private BlockingQueue blockingQueue = null;
    
        public MyResource(BlockingQueue blockingQueue) {
            this.blockingQueue = blockingQueue;
            System.out.println(blockingQueue.getClass().getName());
        }
        public void myProd() throws Exception {
            String data = null;
            boolean retValue;
            while (FLAG){
                data = atomicInteger.incrementAndGet()+"";
                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+"失败");
                }
                TimeUnit.SECONDS.sleep(1);
            }
            System.out.println(Thread.currentThread().getName()+"\t大老板叫停,表示FLAG=false,生产动作结束");
        }
        public void myConsumer() throws Exception{
            String result = null;
            while (FLAG){
                result = blockingQueue.poll(2L, TimeUnit.SECONDS) + "";
                if (result == null || result.equalsIgnoreCase("")){
                    FLAG = false;
                    System.out.println(Thread.currentThread().getName()+"\t超过两秒钟没有取到蛋糕,消费退出");
                    System.out.println();
                    System.out.println();
                    return;
                }
                System.out.println(Thread.currentThread().getName()+"\t消费队列蛋糕"+result+"成功");
            }
        }
    
        public void stop(){
            this.FLAG = false;
        }
    
    }
    public class ProdConsumer_BlockQueueDemo {
        public static void main(String[] args) {
            MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
            new Thread(() ->{
                try {
                    System.out.println(Thread.currentThread().getName()+"\t生产者线程启动");
                    myResource.myProd();
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
            },"Prod").start();
            new Thread(() ->{
                try {
                    System.out.println(Thread.currentThread().getName()+"\t消费者线程启动");
                    System.out.println();
                    System.out.println();
                    myResource.myConsumer();
                    System.out.println();
                    System.out.println();
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
            },"Consumer").start();
    
            //暂停一会线程
            try { TimeUnit.SECONDS.sleep(5); } catch (Exception e) { e.printStackTrace(); }
            System.out.println();
            System.out.println();
    
            System.out.println("5秒钟时间到,大老板main线程叫停,活动结束");
            myResource.stop();
        }
    }
    

    image-20240329165543283

线程池
消息中间件

8.线程池用过吗?ThreadPoolExecutor谈谈你的理解?

为什么用线程池,优势

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

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

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

线程池如何使用?

架构说明

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

image-20240329170723724

编码实现
了解

Executors.newScheduledThreadPool()

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

重点
public class MyThreadPoolDemo {
    public static void main(String[] args) {
        // 一个连接池处理5个线程
        // ExecutorService threadPool = Executors.newFixedThreadPool(5);
        // 一个连接池只有一个线程
        // ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 一个连接池n个线程
        ExecutorService pool = Executors.newCachedThreadPool();
        try {
            for (int i = 0; i < 10; i++) {
                pool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            pool.shutdown();
        }
    }
}
  • Executors.newFixedThreadPool(int)

    执行长期的任务,性能好很多

    image-20240329170900210

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

  • Executors.newSingleThreadExecutor()

    一个任务一个任务执行的场景

    image-20240329173122354

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

  • Executors.newCachedThreadPool()

    适用:执行很多短期异步的小程序或者负载较轻的服务器

    image-20240329173216141

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

ThreadPoolExcutor

image-20240329173314763

线程池的几个重要参数介绍

7大参数

image-20240329173348058

1.corePoolSize:线程池中的常驻核心线程数

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

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

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

默认情况下:
只有当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize。

4.unit: keepAliveTime的单位

5.workQueue:任务队列,被提交但尚未被执行的任务。

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

7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数。

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

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

image-202403291748172121.在创建了线程池后,等待提交过来的任务请求。
2.当调用execute()方法添加一个请求任务时,线程池会做如下判断:
2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务。
2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列。
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务。
2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒线程无事可做绝策略来执行。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程超过一定的时间(keepAlilveTime)时,线程池会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后它
最终会收缩到corePoolSize的大小。

9.线程池用过吗?生产上你是如何设置合理参数?

线程池的拒绝策略你谈谈

是什么

等待队列也满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新的任务服务。这时候就需要拒绝策略机制合理的处理这个问题。

JDK内置的拒绝策略

AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。

CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。

DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。

DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了RejectedExecutionHandler接口

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

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

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

image-20240329181545524

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

Case:MyThreadPoolDemo

public static void main(String[] args) {
    
    System.out.println(Runtime.getRuntime().availableProcessors());
    
    ExecutorService threadPool = new ThreadPoolExecutor(2, 5, 1, TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>(3), Executors.defaultThreadFactory(),
        // new ThreadPoolExecutor.AbortPolicy() // 当一个线程进来发现最大线程数和线程等待数都已占满直接报错
        // new ThreadPoolExecutor.CallerRunsPolicy()
        // new ThreadPoolExecutor.DiscardOldestPolicy()// 抛弃队列中等待最久的,即只处理8(最大线程数+最多等待线程数)个线程
        new ThreadPoolExecutor.DiscardPolicy());// 抛弃最新来的线程
    try {
      // 模拟10个顾客来办理业务,受理窗口只有5个
      for (int i = 1; i <= 10; i++) {
        final int temp = i;
        threadPool.execute(() -> {
          System.out.println(Thread.currentThread().getName() + "号窗口," + "服务顾客" + temp);
          // try {
          // TimeUnit.SECONDS.sleep(4);
          // } catch (InterruptedException e) {
          // e.printStackTrace();
          // }
        });
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      threadPool.shutdown();
    }

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

CPU密集型

IO密集型

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

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

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

IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间。

比如8核CPU: 8 / 1 - 0.9 = 80个线程数

10.死锁编程及定位分析

是什么

死锁是指两个或者两个以上的进程在执行过程中,因抢夺资源而造成的一种互相等待的现象
,若无外力干涉它们将都无法推进下去,如果系统资源充足,

image-20240329181846946

产生死锁的主要原因

  • 系统资源不足
  • 进程运行推进的顺序不合适
  • 资源分配不当

代码:DeadLockDemo

import java.util.concurrent.TimeUnit;
/**
 * 死锁是指两个或两个以上的进程在执行中,
 * 因争夺资源造成的一种互相等待的现象,
 * 若无外力干涉那他们都将无法推进下去
 * */
class HoldLoadThread implements Runnable{
    private String lockA;
    private String lockB;

    public HoldLoadThread(String lockA, String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }

    @Override
    public void run() {
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName()+"\t自己持有:"+lockA+"尝试获得:"+lockB);
            try { TimeUnit.SECONDS.sleep(2); } catch (Exception e) { e.printStackTrace(); }

            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"\t自己持有:"+lockB+"\t尝试获得"+lockA);
            }
        }
    }
}
public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA = "lockA";
        String lockB = "lockB";
        new Thread(new HoldLoadThread(lockA,lockB),"ThreadA").start();
        new Thread(new HoldLoadThread(lockB,lockA),"ThreadB").start();
    }
}

解决

jps命令定位进程号

jstack找到死锁查看

11.Java里面锁请谈谈你的理解,能说多少说多少

二、JVM+GC解析

前提复习

1.8之前的JVM内存结构

image-20240329191551939

Java8以后的JVM

image-20240329191622513

image-20240329191631444

GC的作用域

image-20240329191644869

常见的垃圾回收算法

  1. 引用计数

    image-20240329191708257

  2. 复制

    image-20240329191749565

  3. 标记整理

    image-20240329191909870

  4. 标记清除

    image-20240329191919599

    image-20240329191923770

1.JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots

什么是垃圾?内存中已经不再被使用到的空间就是垃圾

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

  • 引用计数法

    Java中,引用和对象是有关联的。如果要操作对象则必须引用进行。

    因此,简单的办法是通过引用计数来判断一个对象是否可以回收。简单的说,给对象中添加一个引用计数,每当有一个引用失效时,计数器值减1.

    任何时刻计数器值为0的对象就是不可能再被利用的,那么这个对象就是可回收对象。

    那么为什么主流的Java虚拟机里面都没有选择这种算法呢?主要的原因是它很难解决对象之间相互循环引用的问题。

    image-20240329192054455

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

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

    image-20240329192125121

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

    case

    image-20240329192222050

    Java中可以作为GC Roots的对象

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

    标记清除

    最基础的收集算法是“标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分为“标记”和“清除”两个阶段:
      * 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
      * 它的主要不足有两个:
        * 一个是效率问题,标记和清除两个过程的效率都不高;
            * 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

    image-20240329192337298

​ 复制

  • 为甚么出现复制算法?
      * 为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按量划分为大小相等的两块,每次只使用其中的一块
      * 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

  • 现在的商业虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。 Survivor from 和Survivor to ,内存比例 8:1:1

image-20240329192443491

​ 标记整理

  • 标记-整理
      * 根据老年代的特点,有人提出了另外一种“标记-整理(Mark- Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

*-分代收集
  * 一般把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法
  * 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,标记整理只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记一整理”算法来进行回收

image-20240329192514442

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

JVM的参数类型

标配参数

-version

-help

java -showversion

x参数(了解)

-Xint:解释执行

-Xcomp:第一次使用就编译成本地代码

-Xmixed:混合模式

xx参数

Boolean类型

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

Case

是否打印GC收集细节

-XX:-PrintGCDetails

-XX:+PrintGCDetails

是否使用串行垃圾回收器

-XX:-UseSerialGC

-XX:+UseSerialGC

KV设值类型

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

Case

-XX:MetaspaceSize=128m

-XX:MaxTenuringThreadhold=15

jinfo举例,如何查看当前运行程序的配置

jinfo -flag 配置项 进程编号

Case:

image-20240329193058310

image-20240329193115942

image-20240329193131835

题外话(坑题)

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

这个你如何解释:-Xms:等价于-XX:InitialHeapSize ,-Xmx:等价于-XX:MaxHeapSize

盘点家底查看JVM默认值

-XX:+PrintFlagsInitial 主要查看初始默认

java -XX:+PrintFlagsInitial -version

java -XX:+PrintFlagsInitial

-XX:+PrintFlagsFinal 主要查看修改更新

java -XX:+PrintFlagsFinal -version

java -XX:+PrintFlags -version

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

-XX:+PrintCommandLineFlags

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

image-20240329193447103

image-20240329193503451

常用参数

-Xms初始化大小内存,默认为物理内存1/64
等价于-XX:InitialHeapSize
-Xmx最大分配内存,默认物理内存的1/4
等价于-XX:MaxHeapSize
-Xss设置单个线程栈的大小,一般默认为512K~1024K
等价于-XX:ThreadStackSize
-Xmn设置年轻代大小
-Xms128m -Xmx4096m -Xss1024k -XX: MetaSpaceSize=512m-XX:+PrintCommandFlags -XX:+PrintGCDetails -XX:+UseSerialGC
-XX:MetaspaceSize元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:
永久代在堆里面
元空间并不在虚拟机中,而是使用本地内存。
因此,在默认情况下,元空间的大小仅受本地内存限制

典型设置案例

-XX:+PrintGCDetails

GC

image-20240329193831023

FullGC

image-20240329193841500

-XX:SurvivorRatio

image-20240329193852218

image-20240329193857891

-XX:NewRatio

image-20240329193925116-XX:MaxTenuringThreshold 设置垃圾的最大年龄

image-20240329193953258

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

整体架构

image-20240329194039788

强引用(默认支持模式)

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

强引用是最常见的普通对象引用,只要强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰到这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾机制回收的,即使该对象以后永远都不能被用到,JVM也不会回收。因此强引用是造成Java内存泄露的主要原因之一。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式的将相应(强)引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收时还要看垃圾收集策略)。

软引用

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

对于只有软引用的对象来说,
当系统内存充足时,它不会被回收 当系统内存不足时,它会被回收

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

Case:SoftReferenceDemo
import java.lang.ref.SoftReference;
/**
 * 软引用
 */
public class SoftReferenceDemo {
  
  // 内存够用的时候保留,不够用就回收
  public static void softRef_Memory_Enough() {
    Object obj1 = new Object();// 这样定义的默认就是强引用
    SoftReference<Object> softReference = new SoftReference<>(obj1);
    System.out.println(obj1);// java.lang.Object@7852e922
    System.out.println(softReference.get());// java.lang.Object@7852e922
    
    obj1 = null;
    System.gc();
    
    System.out.println(obj1);// null
    System.out.println(softReference.get());//java.lang.Object@7852e922
  }
  
  // -Xms5m -Xmx5m -XX:+PrintGCDetails
  public static void softRef_Memory_NotEnough() {
    Object obj1 = new Object();// 这样定义的默认就是强引用
    SoftReference<Object> softReference = new SoftReference<>(obj1);
    System.out.println(obj1);// java.lang.Object@7852e922
    System.out.println(softReference.get());// java.lang.Object@7852e922
    
    obj1 = null;
    
    try {
      Byte[] bytes = new Byte[50*1024*1024];
    } catch (Exception e) {
      e.printStackTrace(); // java.lang.OutOfMemoryError: Java heap space
    }finally {    System.out.println(obj1);// null
      System.out.println(softReference.get());// null
    }
  }
  
  public static void main(String[] args) {
//    softRef_Memory_Enough();
  softRef_Memory_NotEnough();
  }
}
软引用和弱引用的使用场景

假如有一个应用需要读取大量的本地图片:
如果每次读取图片都从硬盘读取则会严重影响性能。
如果一次性全部加载到内存中有可能造成内存泄露。此时使用软引用可以解决这个问题。

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

Map<String,SoftReference> imageCache = new HashMap<String,SoftReference>();

弱引用

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短。

对于软引用对象来说,只要垃圾回收机制一运行,
不管JVM的内存空间是否足够,都会回收该对象占用的内存

Case:WeakReferenceDemo
package com.luojia.interview.study;

import java.lang.ref.WeakReference;

/**
 * 弱引用
 */
public class WeakReferenceDemo {
    public static void main(String[] args) {
        Object obj = new Object();
        WeakReference<Object> weakReference = new WeakReference<>(obj);

        System.out.println(obj);// java.lang.Object@7852e922
        System.out.println(weakReference.get());// java.lang.Object@7852e922

        obj = null;
        System.gc();

        System.out.println(obj);// null
        System.out.println(weakReference.get());// null

    }
}
你知道弱引用的话,能谈谈weakHashMap吗

Case:WeakHashMapDemo

import java.util.HashMap;
import java.util.WeakHashMap;
public class WeakHashMapDemo {

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

    public static void myHashMap() {
        HashMap<Integer, String> map = new HashMap<>();
        Integer key = new Integer(1);
        String value = "hashMap";

        map.put(key, value);
        System.out.println(map);// {1=hashMap}

        key = null;
        System.out.println(map);// {1=hashMap}
        System.gc();
        System.out.println(map);// {1=hashMap}
    }

    public static void myWeakHashMap() {
        WeakHashMap<Integer, String> map = new WeakHashMap<>();
        Integer key = new Integer(2);
        String value = "hashMap";

        map.put(key, value);
        System.out.println(map);// {2=hashMap}

        key = null;
        System.out.println(map);// {2=hashMap}
        System.gc();
        System.out.println(map);// {}
    }
}

虚引用

虚引用需要java.lang.refPhantonReference类来实现。

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

虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。

PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象那个已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

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

引用队列

被回收前需要被引用队列保存下。

case:ReferenceQueueDemo

package com.luojia.interview.study;

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

public class ReferenceQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        WeakReference<Object> weakReference = new WeakReference<>(obj, referenceQueue);
        System.out.println(obj);// java.lang.Object@7852e922
        System.out.println(weakReference.get());// java.lang.Object@7852e922
        System.out.println(referenceQueue.poll());// null
        System.out.println("=====================");

        obj = null;
        System.gc();
        TimeUnit.MILLISECONDS.sleep(500);

        System.out.println(obj);// null
        System.out.println(weakReference.get());// null
        System.out.println(referenceQueue.poll());// java.lang.ref.WeakReference@4e25154f
    }

}

case:PhantomReferenceDemo

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<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<Object> 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@372f7a8d
null
null
======================
null
null
java.lang.ref.PhantomReference@2f92e0f4

GCRoots和四大引用小总结

image-20240329210846963

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

java.lang.StackOverFlowError

/**
 * StackOverflowError是Error不是exception
 */
public class StackOverflowErrorDemo {
  public static void main(String[] args) {
    StackOverflowError();
  }
  public static void StackOverflowError() {
    StackOverflowError();// StackOverflowError
  }
}

java.lang.OutOfMemoryError:Java heap space

public class JavaHeapSpaceDemo {
  public static void main(String[] args) {
    String str = "learning";
    
    while(true) {
      str += str + new Random().nextInt(10000)+new Random().nextInt(50000);
      str.intern();
      // Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    }
  }

java.lang.OutOfMemoryError:GC overhead limit exceeded

GC回收时间长时会抛出OutOfMemoryError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。
 
假设不抛出GC overhead limit错误会发生什么情况呢?
那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用率一直是100%,而GC却没有任何成果。
/**
 * 堆溢出
 * --Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
 */
public class GCOverheadDemo {
    public static void main(String[] args) {
        int i = 0;
        List<String> list = new ArrayList<>();

        try {
            while(true) {
                list.add(String.valueOf(++i).intern());
            }
        } catch (Throwable e) {
            System.out.println("*************i = " + i);
            // java.lang.OutOfMemoryError: GC overhead limit exceeded
            e.printStackTrace();
        }
    }
}

java.lang.OutOfMemoryError:Direct buffer memory

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

ByteBuffer.allocate(capability)第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。

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

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

/**
 * -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
 * 只给内存分配5M的空间,但是使用的时候,需要6M,运行会报内存溢出
 * @author Romantic-Lei
 * @create 2020年5月15日
 */
public class DirectBufferMemoryDemo {
 
  public static void main(String[] args) {
    
    // ByteBuffer.allocateDirect(capability)方式是分配OS本地内存,不属于GC管辖范围
    // ,由于不需要内存拷贝,所以速度相对较快。
    // Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
    ByteBuffer bb = ByteBuffer.allocateDirect(6*1024*1024);
    
  }
  
}

java.langOutOfMemoryError:Metaspace

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

java.lang.OutOfMemoryError:unable to create new native thread

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

导致原因:
应用创建了太对线程,一个应用进程创建多个线程,超过系统承载极限。
服务器并不允许应用程序创建那么多线程,linux系统默认允许单个进程可以创建的线程数是1024个,如果应用创建超过这个数量,就会报java.lang.OutOfMemoryError:unable to create new native thread
解决办法:
想办法降低应用程序创建线程的数量,分析应用是否真的需要创建那么多线程,如果不是,改代码将线程数降到最低。
对于有的应用,确实需要创建多个线程,远超过linux系统默认的1024个线程的限制,可以通过修改linux服务器配置,扩大linux默认限制。

非root用户登录linux系统测试

查看最大线程数量命令:ulimit -u

服务器级别调参调优

扩大服务器线程数:
vim/etc/security/limits.d/90-nproc.conf

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

GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现。

因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集。

4种主要垃圾收集器

image-20240329211547642

串行垃圾回收器(Serial)

它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。(程序->GC->程序)

并行垃圾回收器(Parallel)

多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理平台处理等弱交互场景。

并发垃圾回收器(CMS)( Concurrent Mark Sweep)

用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,使用对响应时间有要求的场景。

上述3个小总结,G1特殊后面说

image-20240329211716110

GI垃圾回收器

G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。

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

怎么查看默认的垃圾收集器是哪个?

JVM参数:
java -XX:+PrintCommandLineFlags -version

默认的垃圾收集器有哪些?

java的gc回收的类型主要有几种:
UseSerialGC
UseParallelGC
UseConcMarkSweepGC
UseParNewGC
UseParallelOldGC
UseG1GC

垃圾收集器

image-20240329211922250

image-20240329211928828

部分参数预先说明

  1. DefNew : Default New Generation
  2. Tenured : Old
  3. ParNew : Parallel New Generation
  4. PSYoungGen : Parallel Scavenge
  5. ParOldGen : Parallel Old Generation

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

使用范围:只需要掌握Server模式即可,Client模式基本不会用。
操作系统:
2.1 32位操作系统,不论硬件如何都默认使用Client的JVM模式。
2.2 32位操作系统,2G内存同时有2个CPU以上用Server模式,低于该配置还是Client模式。
2.3 64为only server模式

新生代
串行GC(Serial)/(Serial Copying)

新生代和老年代都是只有一条线程

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

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

image-20240329215615567

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

开启后会使用:Serial(Young区用)+Serial Old(Old区用)的收集器组合。
表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法。

并行GC(ParNew)

新生代多个并行清理垃圾,老年代还是一个

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

ParNew(并行)收集器

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

image-20240329215711147

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

常用对应JVM参数:
-XX:+UseParNewGC 启用ParNew收集器,只影响新生代的收集,不影响老年代。
开启上述参数后,会使用:ParNew(Young区用)+Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法。

但是, ParNew+Tenured这样的搭配,java8已经不再推荐
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

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

新生代和老年代都是多个一起清理垃圾

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

Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量有限收集器。一句话:串行收集器在新生代和老年代的并行化。

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

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

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

老年代
串行GC(Serial Old)/(Serial MSC)

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

并行GC(Parallel Old)/(Parallel MSC)

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:+UseParallel Old 使用Parallel Old收集器,设置该参数后,新生代Parallel+老年代Parallel Old

并发标记清除GC(CMS)

CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。

适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。

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

image-20240329220222146

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

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

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

4步过程

image-20240330103933985

  • 初始标记(CMS initial mark)

    只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

  • 并发标记(CMS concurrent mark)和用户线程一起

    进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。

  • 重新标记(CMS remark)
    为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。

    由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。

  • 并发清除(CMS concurrent sweep)和用户线程一起
    清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象。
    由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发执行。

优缺点

优点:并发收集低停顿

缺点:

  1. 并发执行,对CPU资源压力大

由于并发进行,CMS在收集与应用线程会同时增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器会以STW的方式进行一次GC,从而造成较大停顿时间。

  1. 采用的标记清除算法会导致大量碎片

标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。

垃圾收集器配置代码总结
import java.util.Random;
 
/**
 * 
 * 1
 * -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
 * 
 * 2
 * -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
 * 
 * 3
 * -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
 */
public class GCDemo {
 
  public static void main(String[] args) {
    System.out.println("*************GCDemo hello");
    try {
      String str = "Hello World";
      while(true) {
        str += str + new Random().nextInt(10000);
        str.intern();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

如何选择垃圾收集器

image-20240330104446501

image-20240330104500240

7.G1垃圾收集器

以前收集器特点

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

G1是什么

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

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

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:
G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

特点

image-20240330104655875

底层原理

Region区域化垃圾收集器

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

核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小。

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

G1将新生代、老年代的物理空间取消了。

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

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

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

在G1中,还有一种特殊的区域,叫Humongous(巨大的)区域。如果一个对象占用的空间超过了分区容量的50%以上,,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在老年代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

image-20240330104816798

image-20240330104821053

image-20240330104827218

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

回收步骤

G1收集器下的Young GC

针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片。
Eden区的数据移动到新的Survivor区,部分数据晋升到Old区。
Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区。
最后Eden区收集干净了,GC结束,用户的应用程序继续执行。

4步过程

image-20240330104908080

常用配置参数(了解)

开发人员仅仅声明一下参数即可:
三步归纳:开始G1+设置最大内存+设置最大停顿时间

-XX:+UseG1GC
-Xmx32g
-XX:MaxGCPauseMillis=100

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

-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%

和CMS相比的优势

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

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

整机:top

image-20240330105036378

uptime,系统性能命令的精简版

CPU:vmstat

vmstat

  • 查看CPU(包含不限于)

vmstat -n 2 3
一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数单位是秒,第二个参数是采样的次数。

image-20240330105126828

-procs
r:运行等待CPU时间片的进程数,原则上1核的CPU运行队列不要超过2,整个系统的运行队列不能超过总核数的2倍,否则代表系统压力过大。
-cpu
us:用户进程消耗CPU时间百分比,us值高,用户进程小号CPU时间多,如果长期大于50%,优化程序;
sy:内核进程消耗的CPU百分比;

  • 查看额外
    查看所有cpu核信息
    mpstat -P ALL 2
  • 每个进程使用cpu的用量分解信息
    pidstat -u1 -p进程编号

内存:free

应用程序可用内存数

free
free -g
free -m

-经验值
应用程序可用内存/系统物理内存>70%内存充足。
应用程序可用内存/系统物理内存<20%内存不足,需要增加内存。
20%<应用程序可用内存/系统物理内存<70%内训基本够用

查看额外

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

硬盘:df

查看磁盘剩余空间数df -h

磁盘IO:iostat

磁盘I/O性能评估

iostat -xdk 2 3

image-20240330105321140

查看额外

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

网络IO:ifstat

默认本地没有,下载ifstat

查看网络IO

10.假设生产环境出现CPU占用过高,请谈谈你的分析思路和定位

结合Linux和JDK命令一块分析

案例步骤

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

2.ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序惹事

3.定位到具体线程或者代码

ps -mp进程 -o THREAD,tid,time

参数解释

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

4.将需要的线程ID转换为16进制格式(英文小写格式)

printf "%x\n"有问题的线程ID

5.jstack进程ID | grep(16进制线程ID小写英文) -A60

11.对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的?

性能监控工具

  • jps
  • jinfo
  • jmap
  • jstat
  • jstack

三、github骚操作

常见词含义

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

in关键字限制搜索范围

公式:xxx关键词 in:name或description或readme

xxx in:name项目名包含xxx的

xxx in:description项目描述包含xxx的

xxx in:readme项目的readme文件中包含xxx的

组合使用:

​ 搜索项目名或者readme中包含秒杀的项目

​ seckill in:name,readme

stars或fork数量关键词去查找

公式:xxx关键词 star通配符:>或者:>=

区间范围数字:数字1…数字2

​ 查找stars数大于等于5000的springboot项目: springboot stars:>=5000

​ 查找forks数大于500的springcloud项目: springcloud forks:>500

组合使用

查找fork在100到200之间并且stars数在80到100之间的springboot项目

springboot forks:100…200 stars:80…100

awesome加强搜索

公式:awesome关键字

awesome系列一般是用来学习、工具、书籍类相关的项目。搜索优秀的redis相关的项目,包括框架、教程等

高亮显示某一行代码

公式

  • 1行
    地址后面紧跟#L数字
  • 多行
    地址后面紧跟#L数字-L数字2

项目内搜索

英文t

搜索某个地区内的大佬

公式:location:地区,language:语言

地区北京的Java方向的用户

location:beijing language:java

  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值