1.请你谈谈你对volatile的理解?
volatile是JVM提供的轻量级的同步机制
1.保证可见性
2.不保证原子性
3.禁止指令重排
说一说java内存模型?
Java语言为了保证并发编程中可以满足原子性,可见性及有序性,于是推出了一个概念就是JMM内存模型。JMM内存模型,目的是为了在多线程条件下,使用共享内存进行数据通信时,通过对多线程程序读操作,写操作行为规范约束,来尽量避免多次内存数据读取不一致,编译器对代码指令重排序、处理器对代码乱序执行带来的问题。
- JMM 内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。
- JMM 内存模型将内存主要划分为主内存和工作内存两种。规定 所有的变量都存储在主内存中,每条线程都拥有自己的工作内存,线程的工作内存中保存了该线程所需要用到的变量在主内存中的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读、写主内存。
- 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要线程自己的工作内存和主存之间进行数据交互。
JMM 内存模型工作内存、主内存和 JVM 内存有什么关系?
JMM 内存模型中,工作内存和主内存其实跟JVM内存的划分是在不同层次上进行的,是自己的一套抽象概念,大概可以理解为,主内存对应的是 Java 堆中的对象实例部分,而工作内存对应的则是栈中的部分区域。
JMM(Java内存模型java memory model)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范。
JMM关于同步的规定:
由于JVM运行程序的实体是线程,而每个线程创建是JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有的线程都可以访问。线程对变量的操作(读取赋值等)都必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,不同的线程之间无法访问对方发的工作内存。
而这就可能存在 当一个线程AAA修改了共享变量X的值但还未写回主内存时,另一个线程BBB又对主内存中同一个变量X进行操作,但此时AAA线程工作内存中的变量X对线程BBB来讲并不课件,这种工作内存与主内存同步延迟现象就造成了可见性问题。
原子性:不可分割,完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者分割,需要整体完成,要么同时成功要么同时失败。
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条指令。
假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。
问题解决:
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种:
单线程环境里确保程序最终执行结果和代码顺序执行的结果一致。
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线性中使用的变量能否保障一致性是无法确定的,结果无法预测。
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指令,它的作用有两个:
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。
对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
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的原子指令,不会造成所谓的数据不一致问题。(原子性)
上面类似自旋锁
UnSafe.getAndAddInt()源码解释:
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
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的七条原则:
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的。
解决方法:
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
解决方法:
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. 值传递
(2)要打印的是main方法中的person,引用传递,传递内存地址,changeValue2和main两个引用指向同一个内存地址abc,修改之后,变成xxx,故输出的是xxx
(3)这是面试官故意挖的坑,要打印的是main方法的str,由于String方法的特殊性,String str = "abc";会在字符串常量池中先找,没有就新建.changeValue3方法开始会和main方法指向同一个地abc,当执行 changeValue3 中的 str = "xxx"时,由于string的特殊性,会先在常量池中找有没有xxx,没有就新建,随后changeValue3方法就指向xxx了,而main依然指向abc。
10.java的锁
公平锁、非公平锁、可重入锁(递归锁)、自旋锁(要会手写自旋锁),独占锁(写锁,互斥锁、排他锁、X锁)、共享锁(读锁、S锁),乐观锁、悲观锁。
公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁(类似加塞)。在高并发情况下,有可能造成优先级反转或者饥饿现象。
区别:
公平锁很公平,按照FIFO的规则从队列中取到自己,非公平锁比较粗鲁,上来就尝试直接占有锁,如果尝试失败,就再采用类似公平锁那种方式。非公平锁的优点在于吞吐量比公平锁大。
题外话:
ReentrantLock 通过传递true/false来指定该锁是否是公平锁,默认是非公平锁。
Synchronized 是一种非公平锁。
这也是lock和synchronized之间的一点区别
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();
自旋的反义词叫做阻塞
定义:自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
优点:减少线程上下文切换的消耗
缺点:循环会消耗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()
独占锁(写锁)
指该锁一次只能被一个线程锁持有。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
java的乐观锁机制:
乐观锁体现的是悲观锁的反面,他是一种积极的思想,总是认为数据时不会被修改的,所以是不会对数据上锁的。但是乐观锁在更新的时候会去判断数据是否被修改过。乐观锁的实现方案一般有两种(版本号机制和CAS)。乐观锁适用于读多写少的场景,这样可以提高系统的并发量。
乐观锁大多是基于数据版本记录机制实现。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个version字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本和数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
java的悲观锁机制:
假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作,在查询完数据的时候就把事务锁起来,直到提交事务,实现方式:使用数据库中的锁机制。一般多写的场景下用悲观锁就比较合适。
锁优化?自适应自旋锁?锁消除?偏向锁?轻量级锁?重量级锁?
1.什么是锁优化?
2.自适应自旋锁:
我们都知道如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销;自旋锁默认的自旋次数是10
对自旋锁方式进行优化,使它的自旋次数不再固定,自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
3.锁消除:锁消除是Java虚拟机在JIT(即时)编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
4.偏向锁:
所谓的偏向,就是偏心,锁会偏向于当前已经占有锁的线程;也就是说,这个线程已经占有这个锁,当他再次试图去获取这个锁的时候,他会以最快的方式去拿到这个锁,而不需要再进行一些monitor操作,因此在这方面是会对性能有所提升的,因为在大部分情况下是没有竞争的,所以锁此时是没用的,所以使用偏向锁是可以提高性能的。
5.重量级锁:
重量级锁的加锁,解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下。
说说锁升级?
一开始是无锁的状态,一上来会先去判断一下有没有锁,有锁的话最开始的时候锁是支持偏向锁的。偏向锁当前获取到锁资源的这个线程,我会优先让他再去获取这个锁,如果它没获取到这个锁,就升级为一个轻量级的,一个cas锁,即乐观锁,乐观锁的时候它是一个比较和交换的过程,如果没有设置成功的话,它会进行一个自旋,然后自旋到一定次数之后才会升级成一个synchronized的这样一个重量级的锁,这样的话他就保证了性能的问题。你想想如果一开始就是synchronized这样一个重量级的锁,那性能就比较差了。
锁状态一共有四种:无锁,偏向锁,轻量级锁,重量级锁。锁升级的过程是单向的,不能退化,只能是从偏向锁到轻量级锁再到重量级锁的过程,记录这几种锁状态的标记是在对象头的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(信号量)使用过吗?
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
枚举相当于数据库中的表
例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 秦国统一中原。
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颗龙珠
召唤神龙
多个线程枪多份资源 JUC下面的包,功能类似于争车位:20个车抢30个车位
正常的锁(lock或synchronized)在任何时刻都只允许一个任务访问一个资源,而Semaphore允许n个任务同时访问n个资源
信号量主要用于两个目的:
主要有两个方法: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、阻塞队列知道吗?
定义:
为什么用BlockingQueue?有什么好处?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这些BlockingQueue都一手包办了。
在cuncurrent包发布以前,在多线程环境下,程序员必须自己控制这些细节,尤其还要兼顾效率和线程安全,而这会带给我们的程序不小的复杂度。
架构介绍:
种类分析:
BlockingQueue的核心方法
竖着看(一组一组的)
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
面试题:在Queue中poll()和remove()有什么区别?
poll()和remove()都将移除并且返回队头,但是在poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常。
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();
}
}
实现一个简单的生产者消费者模式(传统版)
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.原始构成
2.使用方法
3.等待是否中断
4.加锁是否公平
5.锁绑定多个条件Condition
用新的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、线程启动的四种方式:
步骤
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()方法正在执行...
步骤
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()方法执行中...
步骤
为什么使用FutureTask?(适配器模式)(spring bean 是单例模式,beanfactory是简单工厂模式,factorybean是抽象工厂模式)
FutureTask 实现了RunnableFuture接口,而RunnableFuture又继承了Runnable 和Future接口,
而有FutureTask(Callable callable),即FutureTask可以直接将实现了Runnable和Callable接口的对象封装成FutureTask对象。从而调用FutureTask对象的方法。
代码:
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()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。所以建议放在最后。
相同点
主要区别
Executors提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
主要有newFixedThreadPool(int)(一池固定个处理线程),newCachedThreadPool()(一池N个处理线程),newSingleThreadExecutor()(一池一个处理线程),newScheduledThreadPool()(带时间调度的线程池),后续详细介绍这四种线程池
15、线程池用过吗?生产上你如何设置合理的参数?ThreadPoolExecutor谈谈你的理解?
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用;控制最大并发数;管理线程
优势:
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。其中Executors是Executor的资源类
了解
重点
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
主要特点如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
主要特点如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
主要特点如下:
代码验证:
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:线程池中的常驻核心线程数(今日当值窗口)
2、maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
3、keepAliveTime:多余的空闲线程的存活时间,当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下coolPoolSize个线程为止
4、unit:keepAliveTime的单位
5、workQueue:任务队列,被提交但是尚未被执行的任务(阻塞队列,候客区)
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可。
7、handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程时(maximumPoolSize)时,如何来拒绝(最大线程也满了,阻塞队列也满了,就去拒绝来访)
例:
(就是上面说的七个参数)
重要重要重要
1.在创建了线程池后,等待提交过来的任务请求。
2.当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
(1)如果正在正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务。
(2)如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列。
(3)如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务。
(4)如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3.当一个线程完成任务时,他会从队列中取出下一个任务来执行。
4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
(1)如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉
(2)所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小
下面有具体代码验证
正确答案:一个都不用,我们生产上只能使用自定义的
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?)
什么是垃圾?
简单的说就是内存中已经不再被使用到的空间就是垃圾。
要进行垃圾回收,如何判断一个对象是否可以被回收?
什么是引用计数法?(上面的链接里也有)
给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加一,每当有一个引用失效时,计数器值减一。任何时刻计数器值为零的对象就是不可能在被使用的,那么这个对象就是可回收对象。
缺点:很难解决对象之间相互循环引用的问题。目前无人用,了解即可。
枚举根节点,做可达性分析(根搜索路径)
为了解决引用计数法的循环引用问题,java使用了可达性分析的方法。
所谓"GC roots'或者说tracing GC的“根集合”就是一组必须活跃的引用。
基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。(也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然判定为死亡。)
java中可以作为GC Roots的对象?
2.你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值?
(1)标配参数:
(2)X参数:(了解)
(3)XX参数:(重要)
Boolean类型:公式 -XX:+或者- 某个属性值(+表示开启,-表示关闭)
如何查看一个正在运行中的java程序?它的某个jvm参数是否开启?具体值是多少?
Case:
是否打印GC收集细节
是否使用串行垃圾回收器
KV设值类型:
公式: -XX:属性key=属性值value
Case:
面试题(大坑)
两个经典参数:-Xms和-Xmx
查看初始默认参数值 公式: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}
...
=表示默认,:=表示修改过的。
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之后将最初的永久代取消了,由元空间取代。
在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.
(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
(5) -XX:SurvivorRatio
(6) -XX:NewRatio
新生代特别小,会造成频繁的进行GC收集。
(7)-XX:MaxTenuringThreshold
设置垃圾最大年龄
To和From互换,原To成为下一次GC时的From区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15,并且设置的值范围在0-15之间),最终如果还是存活,就存入到老年代。
-XX:MaxTenuringThreshold=0,如果设置为0的话,则年轻对象不经过Survivor区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大的值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活的时间,增加在年轻代即被回收的概率。
4.强引用,软引用,弱引用,虚引用分别是什么?
当内存不足时,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
// 这样定义的默认就是强应用
Object obj1 = new Object();
在java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即该对象以后永远都不会用到JVM,也不会回收。因此强引用是造成java内存泄漏的主要原因之一。
软引用是一种相对强引用弱化了一些的引用,需要用java.labg.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说,
当系统内存充足时 它 不会 被回收
当系统内存不足时 它 会 被回收
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。
public class SoftReferenceDemo {
/**
* 内存够用的时候
* -XX:+PrintGCDetails
*/
public static void softRefMemoryEnough() {
// 创建一个强应用
Object o1 = new Object();
// 创建一个软引用
SoftReference
内存充足输出结果:
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
回收后,内存依然不足的话,还是会抛异常。
弱引用需要用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
虚引用需要java.lang.ref.PhantomReference类来实现
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
主要作用:跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事的机制,PhantomReference的get方法总是返回null,因此无法访问对应的引用对象,其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,以来实现比finalization机制更灵活的回收操作。
换句话说,这只虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
创建引用的时候可以指定关联的队列,当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>();
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
OutofMemoryError
(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却没有任何成果。
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堆中来回复制数据。
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。
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 异常与对应的平台有关
导致原因:
解决办法:
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种主要的垃圾收集器
7.怎么查看服务器默认的垃圾收集器是哪个?生产上如何配置垃圾收集器的?谈谈你对垃圾收集器的理解?
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程序号)
Java中一共有7大垃圾收集器
年轻代GC
老年代GC
老嫩通吃
不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:
新生代
串行GC(Serial)/(Serial Copying)
并行GC(ParNew)
并行回收GC(Parallel)/(Parallel Scavenge)
Server/Client模式分别是什么意思?
使用范围:一般使用Server模式,Client模式基本不会使用
操作系统
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)
<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
<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
<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出现之前大型应用的首选收集器。
Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行
开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC开启该参数后会自动将-XX:+UseParNewGC打开。
开启该参数后,使用ParNew(Young区用)+ CMS(Old区用)+ Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器。
4步过程:
优点:并发收集低停顿。
缺点:并发执行,对CPU资源压力大,采用的标记清除算法会导致大量碎片。
设置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及以后)
设置VM参数:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC
输出结果:
Unrecognized VM option 'UseSerialOldGC'
Did you mean '(+/-)UseSerialGC'?
在Java8中,-XX:+UseSerialOldGC不起作用。
<7> G1垃圾收集器
下面有详细介绍
1.单CPU或小内存,单机程序
2.多CPU,需要最大吞吐量,如后台计算型应用
3.多CPU,追求低停顿时间,需快速响应如互联网应用
小总结:
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> 以前收集器的特点:
<2> G1是什么?
G1(Garbage-First)收集器是一款面向服务端应用的收集器;应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。
具有以下特性:
<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> 特点:
<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区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片
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比较
9.JVM+GC结合SpringBoot微服务优化简介
java -server jvm的各种参数 -jar 第1步上面的jar/war包名
。10.实例对象是怎样存储的?
https://www.nowcoder.com/discuss/644046
对象的实例存储在堆空间;
对象的类元信息存储在方法区(元空间),被对象头中的类型指针所指向;
对象的引用存储在虚拟机栈
11.一个对象有哪些部分组成(堆内存中)?
对象头、实例数据、对齐填充
12、对象头包含哪些信息?
运行时元数据markword、类型指针、如果是数组对象还会包含数组长度
13、markword包含哪些信息?
锁的状态标识(如偏向锁,无锁,轻量级锁等),分代年龄,锁记录record,如果是重量级锁的话还保存monitor对象、线程id,hashcode等。
17、生产环境服务器变慢,诊断思路和性能评估谈谈?
主要看load average, CPU, MEN三部分
load average表示系统负载,即任务队列的平均长度。 三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值。
load average: 如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了。
procs
cpu
查看看所有cpu核信息
mpstat -P ALL 2
每个进程使用cpu的用量分解信息
pidstat -u 1 -p 进程编号
应用程序可用内存数
经验值
应用程序可用内存l系统物理内存>70%内存充足
应用程序可用内存/系统物理内存<20%内存不足,需要增加内存
20%<应用程序可用内存/系统物理内存<70%内存基本够用
m/g:兆/G
查看额外
pidstat -p 进程号 -r 采样间隔秒数
查看磁盘剩余空间数
磁盘I/O性能评估
磁盘块设备分布
默认本地没有,下载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
观察网络负载情况程序
网络读写是否正常
18、CPU占用过高的定位分析思路(记一次排错经历)
结合Linux和JDK命令一块分析
案例步骤
定位到具体线程或者代码
19、GitHub面试相关
常用词含义
in关键词限制搜索范围:
公式 :xxx(关键词) in:name或description或readme
xxx in:name 项目名包含xxx的
xxx in:description 项目描述包含xxx的
xxx in:readme 项目的readme文件中包含xxx的组合使用
组合使用
搜索项目名或者readme中包含秒杀的项目
xxx in:name,readme
公式:
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
一行:地址后面紧跟 #L10
https://github.com/abc/abc/pom.xml#L13
多行:地址后面紧跟 #Lx - #Ln
在项目仓库下按键盘T,进行项目内搜索
20、字符串常量池(intern()方法?- 是否读过经典JVM书籍?)
暖身面试题
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这个类的时候进入常量池。
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";
...
}
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 类。一般来说,我们自己平时书写的类都是它加载的。
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
好处:
避免重复加载 + 避免核心类篡改
缺点:
顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
21、AQS(AbstractQueuedSynchronizer)
第二个monitorexit是为了异常的时候保证它彻底释放锁和退出。
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport中的park()和unpark()的作用分别是阻塞线程和解除阻塞线程,是wait/notify,await/signal的加强版。
3种让线程等待和唤醒的方法
方式一: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和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 换醒.
同理:
方式三: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 换醒.
小总结:
重点:
因为unpack()获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
因为凭证的数量最多是1,连续调用两次unpack和调用一次unpack效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
AbstractQueuedSynchronizer 抽象的队列同步器。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
}
是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。
CLH:Craig、Landin and Hagersten队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO。
AQS为什么是JUC内容中最重要的基石?
进一步理解锁和同步器的关系?
能干嘛?
加锁会导致阻塞 - 有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
解释说明
抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupportpark)的方式,维护state变量的状态,使并发达到同步的控制效果。
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) {
...
}
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同步队列的基本结构
例:从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 的加锁过程,可以分为三个阶段:
例:带入一个银行办理业务的案例来模拟我们的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);
}
}
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上位成功。
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缓存穿透和雪崩穿透
https://blog.csdn.net/Mcdull__/article/details/118440180
redis传统五大数据类型的落地应用?
你知道分布式锁吗?有哪些实现方案?你谈谈对redis分布式锁的理解,删key的时候有什么问题?
redis缓存过期淘汰策略?
redis的LRU算法简介?