volatile
volatile是什么
volatile是JVM提供的轻量级的同步机制
保证可见性
不保证原子性
禁止指令重排(保证有序性)
JMM内存模型之可见性
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
线程解锁前,必须把共享变量的值刷新回主内存
线程加锁前,必须读取主内存的最新值到自己的工作内存
加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
可见性
通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
/**
* 测试可见性
*/
class MyData{
int number = 0;
public void addT030(){
this.number += 30;
}
}
public class VolatileVisibility {
public static void main(String[] args) {
// 共享资源
MyData data = new MyData();
// 线程处理业务,3秒后修改共享变量
new Thread(()->{
System.out.println(Thread.currentThread().getName()+ " come in");
try {
TimeUnit.SECONDS.sleep(3);
data.addT030();
System.out.println(Thread.currentThread().getName()+ " updated");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AAA").start();
// 主线程 如果发现共享变量改变了,立即退出
while (data.number == 0){}
System.out.println(Thread.currentThread().getName()+ " mission success");
}
}
结果
即使线程AAA修改了变量写回了内存,但是主线程工作空间中,变量值一直是0 无法退出,因为线程间变量的修改是不可见的,没有通知主线程,内存中的共享变量发生了改变。
因此,需要有一种机制,能够是线程间能够相互看到共享变量。
volatile保证可见性
/**
* 测试可见性
*/
class MyData{
volatile int number = 0;
public void addT030(){
this.number += 30;
}
}
使用volatile。轻量级锁。使得共享资源可以相互可见。
volatile不保证原子性
原子性指的是什么意思?
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。
volatile不保证原子性案例演示:
/**
* 测试可见性
*/
class MyData{
volatile int number = 0;
public void addT030(){
this.number += 30;
}
public void increase(){
this.number++;
}
}
public class VolatileTest {
public static void main(String[] args) {
MyData data = new MyData();
// 启动20个线程 对同一变量操作
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
data.increase();
}
},String.valueOf(i)).start();
}
// 当活跃的线程数量 大于2时,主线程进入就绪状态,等待cpu执行
// 默认两个线程是 main,gc垃圾回收 两个线程。只有只剩两个线程时,主线程继续往下执行
while (Thread.activeCount()>2){
Thread.yield();
}
// 按理说值是20000 但是大部分情况下值不是20000
System.out.println("value "+data.number);
}
}
volatile不保证原子性理论解释
number++在多线程下是非线程安全的。
我们可以将代码编译成字节码,可看出number++被编译成3条指令。
假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,**但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,**这样也就让最终的结果少于20000。
volatile不保证原子性问题解决(Atomic原子类),多线程环境下i++等问题解决
可加synchronized解决,但它是重量级同步机制,性能上有所顾虑。
如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger。
class MyData{
volatile int number = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
public void addT030(){
this.number += 30;
}
public void increase(){
this.number++;
}
}
public class VolatileTest {
public static void main(String[] args) {
MyData data = new MyData();
// 启动20个线程 对同一变量操作
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
data.increase();
data.addAtomic();
}
},String.valueOf(i)).start();
}
// 当活跃的线程数量 大于2时,主线程进入就绪状态,等待cpu执行
// 默认两个线程是 main,gc垃圾回收 两个线程。只有只剩两个线程时,主线程继续往下执行
while (Thread.activeCount()>2){
Thread.yield();
}
// 按理说值是20000 但是大部分情况下值不是20000
System.out.println(" volatile value "+data.number);
System.out.println(" atomicInteger value "+data.atomicInteger);
}
}
结果:
volatile指令重排案例1
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
重排案例1
public void mySort{
int x = 11;//语句1
int y = 12;//语句2
× = × + 5;//语句3
y = x * x;//语句4
}
可重排序列:
1234
2134
1324
问题:请问语句4可以重排后变成第一个条吗?答:不能。
重排案例2
int a,b,x,y = 0
这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
volatile指令重排案例2
以下程序在高并发的情况下,执行结果不一样
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或5
}
}
}
多线程环境中线程交替执行method01()和method02(),由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
禁止指令重排小总结
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的执行顺序,
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。
对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
线性安全性获得保证
工作内存与主内存同步延迟现象导致的可见性问题 - 可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题 - 可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。
单例模式也会出现并发安全问题
public class SingletonTest {
private static SingletonTest instance = null;
private SingletonTest(){
System.out.println(Thread.currentThread().getName()+ "构造方法被执行");
}
public static SingletonTest getInstance(){
if(instance == null){
instance = new SingletonTest();
}
return instance;
}
}
多线程下,单例模式出现了并发安全问题
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonTest.getInstance();
},String.valueOf(i)).start();
}
}
执行结果
按理说,单例模式,应该只出现一个实例,但实际上出现了多个(多线程下)
解决方法:
方法一:使用synchronized
public class SingletonTest {
private static SingletonTest instance = null;
private SingletonTest(){
System.out.println(Thread.currentThread().getName()+ "构造方法被执行");
}
public static SingletonTest getInstance(){
// dcl 双端检锁机制
// 内层检查控制并发情况下,单实例问题
// 外层检查控制并发情况下,效率问题
if(instance == null){
synchronized (SingletonTest.class){
if(instance == null){
instance = new SingletonTest();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonTest.getInstance();
},String.valueOf(i)).start();
}
}
}
注意:这里采用了dcl双端检锁机制,保证并发的同时,保证效率
方法二: synchronized+ volatile
为什么要用volatile
我们看看 创建对象的过程,在并发环境下,是否会有问题
instance = new SingletonTest();可以分为以下3步完成(伪代码):
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //1.分配对象内存空间
instance = memory;//3.设置instance指向刚分配的内存地址,此时
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
在多线程情况下,可能没有实例化完成, 对象引用就指向了对应地址。对象!=null,但是内部没有数据,调用对应方法时,得不到对应结果。因此我们要禁止指令重排
public class SingletonTest {
private static volatile SingletonTest instance = null;
private SingletonTest(){
System.out.println(Thread.currentThread().getName()+ "构造方法被执行");
}
public static SingletonTest getInstance(){
// dcl 双端检锁机制
// 内层检查控制并发情况下,单实例问题
// 外层检查控制并发情况下,效率问题
if(instance == null){
synchronized (SingletonTest.class){
if(instance == null){
instance = new SingletonTest();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonTest.getInstance();
},String.valueOf(i)).start();
}
}
}
CAS
CAS是什么
Compare And Swap 比较并交换。CAS是一个cpu原语,该原子性操作不可被中断。
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。(原子性)
CAS原理
下面我们举一个例子来演示CAS原理
atomiclnteger.getAndIncrement();
相关参数介绍
1 Unsafe
Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。
Unsafe类中的方法主要是直接调用操作系统底层资源执行相应任务。
2 变量valueOffset
表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3 变量value
用volatile修饰,保证了多线程之间的内存可见性。
UnSafe.getAndAddInt()源码解释:
var1 AtomicInteger对象本身。
var2 该对象值得引用地址。
var4 需要变动的数量。
var5是用过var1,var2找出的主内存中真实的值。
用该对象当前的值与var5比较:
如果相同,更新var5+var4并且返回true,
如果不同,继续取值然后再比较,直到更新完成。(非常占用cpu)
举例:
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :
Atomiclnteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
线程B也通过getintVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行**compareAndSwapInt方法(CAS原语)**比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。
底层汇编
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)
UnsafeWrapper("Unsafe_CompareAndSwaplnt");
oop p = JNlHandles::resolve(obj);
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e))== e;
UNSAFE_END
//先想办法拿到变量value在内存中的地址。
//通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。
CAS缺点
1、占用过多CPU(循环取值比较)
// 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;
}
当我们的预期值,与valueOffSet实际指向的值不同时,进行do-while循环,直到 预期值和内存中实际值 相同
2、CAS只能对一个变量进行原子性操作,而synchronized可以对一段代码进行锁操作
3、CAS存在ABA问题
存在两个线程t1,t2,同时访问共享变量A,将共享变量拷贝到线程自己的工作空间。由于t2处理速度快,可能直接修改A为B,再修改B为A。此时,如果t1要修改变量A,对变量A进行compareAndSwap。发现是A没变,直接修改。但是A其实已经被动过,A-B-A的过程可以发生很多事,A已经不再是原来那个单纯的A了
ABA问题的产生
存在两个线程t1,t2,同时访问共享变量A,将共享变量拷贝到线程自己的工作空间。由于t2处理速度快,可能直接修改A为B,再修改B为A。此时,如果t1要修改变量A,对变量A进行compareAndSwap。发现是A没变,直接修改。但是A其实已经被动过,A-B-A的过程可以发生很多事,A已经不再是原来那个单纯的A了
AtomicReference原子引用
之前我们了解过AtomicInteger是对Integer类型的共享变量,要保证数据一致性(原子性)问题时,采取的方案。
如果说我们需要将引用类型作为共享变量,保证原子性。可以使用AtomicReference。
class Person{
String name;
Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
public class CASTest {
public static void main(String[] args) {
Person p1 = new Person("小明", 15);
Person p2 = new Person("小红", 16);
Person p3 = new Person("小张", 18);
// 共享资源p1
AtomicReference<Person> personAtomicReference = new AtomicReference<>(p1);
System.out.println("修改成功否" + personAtomicReference.compareAndSet(p1, p2));
System.out.println("修改成功否" + personAtomicReference.compareAndSet(p1, p3));
}
}
结果:
ABA问题解决(AtomicStampedReference)
public static void main(String[] args) {
AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>("A",1);
AtomicReference<String> stringAtomicReference = new AtomicReference<>("A");
System.out.println("----------ABA 问题产生-----------");
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " 初始共享变量值为:"+stringAtomicReference.get());
stringAtomicReference.compareAndSet("A","B");
System.out.println(Thread.currentThread().getName() + " 共享变量值为:"+stringAtomicReference.get());
stringAtomicReference.compareAndSet("B","A");
System.out.println(Thread.currentThread().getName() + " 共享变量值为:"+stringAtomicReference.get());
},"t1").start();
new Thread(()->{
// 睡眠3秒确保 ,出现ABA问题
try { TimeUnit.MICROSECONDS.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); }
stringAtomicReference.compareAndSet("A","C");
System.out.println(Thread.currentThread().getName() + " 共享变量值为:"+stringAtomicReference.get());
},"t2").start();
System.out.println("----------ABA 问题解决-----------");
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+ stampedReference.getStamp());
// 睡眠一秒确保拿到初始值版本
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
stampedReference.compareAndSet("A","B",stamp,stamp+1);
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+ stampedReference.getStamp() +" 共享变量值为:"+stampedReference.getReference());
stampedReference.compareAndSet("B","A",stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+ stampedReference.getStamp() +" 共享变量值为:"+stampedReference.getReference());
},"t3").start();
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+stamp);
// 睡眠3秒确保 ,出现ABA问题,获取内存变量的版本号
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
boolean result = stampedReference.compareAndSet("A", "C", stamp, stamp+1);
System.out.println("修改成功否 "+result);
System.out.println(Thread.currentThread().getName() + " 当前的版本号为: "+
stampedReference.getStamp() +
" 我的版本号为: "+ stamp +
" 共享变量值为:"+stampedReference.getReference());
},"t4").start();
}
ArrayList不是线程安全的
怎么证明呢?
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}finally {
}
},String.valueOf(i+1)).start();
}
}
运行结果:
List<String> list = new ArrayList<>();
list.add("AA");
list.add("BB");
list.add("CC");
for (String s : list) {
System.out.println(s);
list.remove(s);
}
两个示例都抛出了并发修改异常。第二个示例foreach 会产生一个iterator进行迭代。
解决方法
1、synchronized
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
synchronized(list){
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}
},String.valueOf(i+1)).start();
}
}
2、Reentrantlock
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Lock lock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
new Thread(()->{
lock.lock();
try {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
}finally {
lock.unlock();
}
},String.valueOf(i+1)).start();
}
}
3、collections
看对应api即可
4、copyOnWriteAraaylist
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i+1)).start();
}
}
CopyOnWriteArrayList原理
CopyOnWriteArrayList遵循的是读写分离的思想。读取数据的时候,可以直接读取。但是写数据在多线程环境下,可能出现线程安全问题。
我们可以看下对应源码
add()
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();
}
}
步骤
1、add对象时,获取可重入锁,只有线程获取到该锁,才能进行写操作
2、获取原对象数组,通过Arrays拷贝生成一个原数组的副本。将add的对象放入数组末尾,将原数组引用指向该进行了写操作的数组副本(这一部分保证原子性)
3、操作完后释放锁,其它线程可访问该方法进行写操作
get()
// Positional Access Operations
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
可以看到get方法并没有加锁,因此线程无需互斥访问get方法,提高了并发处理速度
HashMap原理
hashMap底层由哈希表构成。 在jdk1.8中 hashMap由数组+链表+红黑树构成
在介绍HashMap之前,有必要知道
1、什么hash
Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
2、什么是hashCode
我们以String为例
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
上面的计算将字符串的每一个字符+ hash*31然后相加得到新的,以此类推返回最终的hash值。
3、为什么要用31相乘
在名著 《Effective Java》第 42 页就有对 hashCode 为什么采用 31 做了说明:
之所以使用 31, 是因为他是一个奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算(低位补0)。使用素数的好处并不很明显,但是习惯上使用素数来计算散列结果。 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i, 现代的 VM 可以自动完成这种优化。这个公式可以很简单的推导出来。
这个问题在 SO 上也有讨论: https://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier)
可以看到,使用 31 最主要的还是为了性能。当然用 63 也可以。但是 63 的溢出风险就更大了。那么15 呢?仔细想想也可以。
在《Effective Java》也说道:编写这种散列函数是个研究课题,最好留给数学家和理论方面的计算机科学家来完成。我们此次最重要的是知道了为什么使用31。
我们以hashMap中的put方法为例,看看源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
1、当我们put一个k-v进来的时候,首先对key进行hash算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看到key为null时,hash值为0,这就是为什么hashMap可以运行key为null。接下来,获取key的hashCode 与 hashcode右位移16位进行异或运算,返回hash值
2、判断数组(bucket数组)存不存在,大小是不是为0。如果没有数组,或者数组长度为0,进行resize()扩容。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
数组默认大小16,负载因子0.75,阈值12(capacity*loadFactory)
3、通过(n-1)& hash 获取数组下标,取出对应位置的值。如果为null,可以放入数组。如果不为null,即hash冲突,通过equals比较key是否相等。如果相等则替换,不相等放入链表尾部。如果链表个数大于8,转红黑树
放入数组
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
不相等放入链表尾部。如果链表个数大于8,转红黑树
4、如果当前kv个数大于阈值,进行扩容
注意: modCount为结构上的修改次数
++modCount;
if (++size > threshold)
resize();
hash算法的大致过程:
1:由键key得出hashCode
2:由hashCode算出hash值,
3:由hash算出于数组中的下标
如下图所示:
1-我们知道无论是键还是值,都应该是一个对象,不同的对象具有不同的存储地址,hashCode得到的哈希值就是根据对象的地址转化而来的。
2:在hashmap中,对hashcode方法进行了重写,重写的时候用到了hash算法,即把hashcode的高16位与低16位进行异或运算,得到hash值。
3:把hash值与数组长度减一进行“与”运算得到下标。(这里需要注意的是,因为数组的长度始终是2的次方幂,这里的与运算,就相当于给length-1取模运算)
java锁
java公平锁和非公平锁(Reentrantlock)
-
公平锁―是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
-
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁。
The constructor for this class accepts an optional fairness parameter. When set true, under contention, locks favor granting access to the longest-waiting thread. Otherwise this lock does not guarantee any particular access order. Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting, but have smaller variances in times to obtain locks and guarantee lack of starvation.
Note however, that fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock. Also note that the untimed tryLock() method does not honor the fairness setting. It will succeed if the lock is available even if other threads are waiting.
此类的构造函数接受可选的公平性参数。当设置为true时,在争用下,锁有利于向等待时间最长的线程授予访问权限。否则,此锁不保证任何特定的访问顺序。与使用默认设置的程序相比,使用由许多线程访问的公平锁的程序可能显示出较低的总体吞吐量(即,较慢;通常要慢得多),但是在获得锁和保证没有饥饿的时间上差异较小。
但是请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的多个线程中的一个线程可以连续多次获得公平锁,而其他活动线程则没有进行并且当前没有持有该锁。还要注意,不计时的 tryLock()方法不支持公平性设置。如果锁可用,即使其他线程正在等待,它也会成功。
两者区别
关于两者区别:
公平锁
Threads acquire a fair lock in the order in which they requested it.
公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁
a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lockhappens to be available when it is requested.
非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。
非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁
值得注意的是:jvm对线程的调度策略是抢占式的,优先级越高的线程,可能越会获取cpu的处理权
java可重入锁和递归锁(Reentrantlock/synchronized)
可重入锁(也叫做递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁(也即线程对于内存锁代码,不需要再次争抢锁)。
也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
ReentrantLock/synchronized就是一个典型的可重入锁。
可重入锁最大的作用是避免死锁。
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().getId() + "\t invoked sendEmail()");
}
}
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendSMS();
},"t1").start();
new Thread(()->{
phone.sendSMS();
},"t2").start();
}
输出结果
t1 invoked sendSMS()
11 invoked sendEmail()
t2 invoked sendSMS()
12 invoked sendEmail()
ReentrantLock可重入锁
public class ReentrantLockTest {
public static void sendSMS(){
System.out.println(Thread.currentThread().getName()+" sendSMS");
sendEmail();
}
public static void sendEmail(){
System.out.println(Thread.currentThread().getName()+" sendEmail");
}
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(()->{
lock.lock();
try {
sendSMS();
}finally {
lock.unlock();
}
},"t1").start();
new Thread(()->{
lock.lock();
try {
sendSMS();
}finally {
lock.unlock();
}
},"t2").start();
}
}
输出结果
t1 invoked sendSMS()
11 invoked sendEmail()
t2 invoked sendSMS()
12 invoked sendEmail()
java自旋锁(spinLock)
自旋锁(Spin Lock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU (以循环方式消耗cpu的代价,不让线程堵塞(挂起))
unsafe中的getAndAddInt就是一个良好示例
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
提到了互斥同步对性能最大的影响阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
《深入理解JVM.2nd》Page 398
为什么要使用自旋锁,就是多个线程对同一变量进行访问,为了线程安全加锁,但是由于线程使用共享变量很短一段时间,挂起线程进入堵塞状态,然后回复,消耗大。因此采用循环访问锁的方式,获取锁。
public class SpinLockTest {
AtomicReference<String> atomicReference = new AtomicReference<>("unlock");
public void lock(){
System.out.println(Thread.currentThread().getName()+ " come in");
// 如果值不为unlock时, 进行当前线程进行循环获取锁。值为unlock时,获取该锁并退出
while (!atomicReference.compareAndSet("unlock","lock")){
}
System.out.println(Thread.currentThread().getName()+ " lock");
}
public void unlock(){
atomicReference.compareAndSet("lock","unlock");
System.out.println(Thread.currentThread().getName()+ " unlock");
}
public static void main(String[] args) {
// 假设我们希望当原子引用共享对象为unlock值时,才可以访问这个共享对象
SpinLockTest spinLock = new SpinLockTest();
new Thread(()->{
spinLock.lock();
try {
TimeUnit.SECONDS.sleep(8); }catch (InterruptedException e){
}finally {
spinLock.unlock();
}
},"t1").start();
try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){}
new Thread(()->{
spinLock.lock();
try {
}finally {
spinLock.unlock();
}
},"t2").start();
}
}
java读写锁(ReentrantReadWriteLock)
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可被多个线程所持有。
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源来,就不应该再有其它线程可以对该资源进行读或写。
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。
堵塞队列BlockingQueue
什么是堵塞队列
堵塞队列本质就是队列,底层数据结构 通常是由数组,或者链表构成。实现FIFO思想
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。
注意:bolckingQueue是在多线程环境下提供的线程安全的队列
与ArrayList区别
1、ArrayList线程不安全,blockingQueue线程安全
2、ArrayList可以扩容,blockingQueue队列不能
为什么要使用堵塞队列
1、我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,BlockingQueue都给你一手包办了
2、如果有很多任务要处理,我们当前处理不了,总不能不处理。我们可以延迟处理,总比不处理要好
阻塞队列使用场景
1、生产者消费者模式
传统版(synchronized, wait, notify)
阻塞队列版(lock, await, signal)
2、线程池
3、消息中间件
怎么使用堵塞队列
blockingQueue实现类
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现延迟无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列(生产一个消费一个)。
LinkedTransferQueue:由链表结构绒成的无界阻塞队列。
LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
BlockingQueue核心方法组
offer和poll组
public static void main(String[] args) {
ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
new Thread(()->{
System.out.println("元素成功入队否 "+blockingQueue.offer("1"));
System.out.println("元素成功入队否 "+blockingQueue.offer("2"));
},"producer1").start();
new Thread(()->{
System.out.println("元素成功入队否 "+blockingQueue.offer("3"));
System.out.println("元素成功入队否 "+blockingQueue.offer("4"));
System.out.println("阻塞队列中当前拥有数据个数: "+blockingQueue.size());
},"producer2").start();
new Thread(()->{
System.out.println("成功消费数据 "+blockingQueue.poll());
System.out.println("阻塞队列中当前拥有数据个数: "+blockingQueue.size());
},"consumer").start();
}
结果:
阻塞队列中只能存放指定个数的数据,如果使用offer(),将数据放入队列,当前队列已满,消费线程没有来得及消费,那么offer放入数据会失败
超时的 offer和poll组 与上面代码类似,只不过加了时间限制
put和take组
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
new Thread(()->{
try {
blockingQueue.put("1");
blockingQueue.put("2");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"producer1").start();
new Thread(()->{
try {
blockingQueue.put("3");
blockingQueue.put("4");
System.out.println("阻塞队列中当前拥有数据个数: "+blockingQueue.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"producer2").start();
}
结果:
使用put将数据消息进入队列,如果队列满了,并且没有消费者线程进行消费,那么一直会堵塞线程,只要队列不为满时,将元素放入才不会堵塞线程
SynchronousQueue队列
SynchronousQueue没有容量。
与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。
每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t put A ");
blockingQueue.put("A");
System.out.println(Thread.currentThread().getName() + "\t put B ");
blockingQueue.put("B");
System.out.println(Thread.currentThread().getName() + "\t put C ");
blockingQueue.put("C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
new Thread(() -> {
try {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
blockingQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take A ");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
blockingQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take B ");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
blockingQueue.take();
System.out.println(Thread.currentThread().getName() + "\t take C ");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
}
放一个拿一个,存在一个就不能放了哦
传统模式下的生产者消费者
1、synchronized控制的
class Data{
int number = 0;
AtomicInteger atomicInteger = new AtomicInteger(0);
public void increment(){
synchronized (this){
// 不等于0进行,等待消费者消费
while (number != 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产
number++;
System.out.println(Thread.currentThread().getName()+" 生产了一个产品: " +number);
// 通知消费者消费
this.notify();
}
}
public void decrement(){
synchronized (this){
// 等待生产者生产
while (number == 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费
number--;
System.out.println(Thread.currentThread().getName()+" 消费了一个产品: " +number);
// 通知生产者生产
this.notify();
}
}
}
public static void main(String[] args) {
// 任务: 生产一个消费一个
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.increment();
}
},"producer").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.decrement();
}
},"consumer").start();
}
2、lock(ReentrantLock)
class Data{
int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment(){
lock.lock();
try {
// 不等于0进行,等待消费者消费
while (number != 0){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产
number++;
System.out.println(Thread.currentThread().getName()+" 生产了一个产品: " +number);
// 通知消费者消费
condition.signal();
}finally {
lock.unlock();
}
}
public void decrement(){
lock.lock();
try {
// 等待生产者生产
while (number == 0){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费
number--;
System.out.println(Thread.currentThread().getName()+" 消费了一个产品: " +number);
// 通知生产者生产
condition.signal();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
// 任务: 生产一个消费一个
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.increment();
}
},"producer").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.decrement();
}
},"consumer").start();
}
两者运行结果:
虚假唤醒问题
存在多个线程并发争抢一个资源。以生产者消费者为例:
我们任务要求,只能生产一个产品消费一个产品。两个生产者生产,两个消费者消费。
当生产者生产完一个产品时,要唤醒等待的线程(notify是随机唤醒)。注意此时有两个消费者线程,一个生产者线程等待。如果cpu的调度权被等待的生产者获取到了,此时生产者在 wait()方法处 会直接往下执行,实际上就生产了两个产品。同理消费者也可能同时消费两个产品
根源在于:换性的线程是直接往下执行的并没有判断是否满足对应条件
产生虚假唤醒的源码
class Data{
int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment(){
lock.lock();
try {
// 不等于0进行,等待消费者消费
if (number != 0){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产
number++;
System.out.println(Thread.currentThread().getName()+" 生产了一个产品: " +number);
// 通知消费者消费
condition.signal();
}finally {
lock.unlock();
}
}
public void decrement(){
lock.lock();
try {
// 等待生产者生产
if (number == 0){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费
number--;
System.out.println(Thread.currentThread().getName()+" 消费了一个产品: " +number);
// 通知生产者生产
condition.signal();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
// 任务: 生产一个消费一个
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.increment();
}
},"producer").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.decrement();
}
},"consumer").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.increment();
}
},"producer1").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
data.decrement();
}
},"consumer2").start();
}
可能的结果
解决:
if该while即可,唤醒的同时,进行再次判断
总结具有 await/wait方法时,需要使用while
Synchronized和Lock区别
1、synchronized属于JVM层面,属于java的关键字
monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象 只能在同步块或者方法中才能调用 wait/ notify等方法)
Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁
2、使用方法:
synchronized:不需要用户去手动释放锁,当synchronized代码执行后,系统会自动让线程释放对锁的占用。
ReentrantLock:则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁的现象,需要lock() 和 unlock() 配置try catch语句来完成
3、等待是否中断
synchronized:不可中断,除非抛出异常或者正常运行完成。
ReentrantLock:可中断,可以设置超时方法
设置超时方法,trylock(long timeout, TimeUnit unit)
lockInterrupible() 放代码块中,调用interrupt() 方法可以中断
4、加锁是否公平
synchronized:非公平锁
ReentrantLock:默认非公平锁,构造函数可以传递boolean值,true为公平锁,false为非公平锁
5、锁绑定多个条件Condition
synchronized:没有,要么随机,要么全部唤醒
ReentrantLock:用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized那样,要么随机,要么全部唤醒
Condition实现精准唤醒线程
任务:
多线程之间按顺序调用,实现 A-> B -> C 三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
class ShareData{
// 1,2,3 分别标识3个不同的线程A,B,C
int number = 1;
Lock lock = new ReentrantLock();
// condition在哪个线程中就表示是哪个线程的条件
Condition c1 =lock.newCondition();
Condition c2 =lock.newCondition();
Condition c3 =lock.newCondition();
// 功能聚合 任务写在共享资源中
public void print5(){
lock.lock();
try {
// 判断
while (number != 1){
try {
// 当前线程等待
c1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行任务
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
System.out.println();
// 唤醒 (干完活后,需要通知B线程执行)
number = 2;
// 通知2号去干活了
c2.signal();
}finally {
lock.unlock();
}
}
public void print10(){
lock.lock();
try {
// 判断
while (number != 2){
try {
// 当前线程等待
c2.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行任务
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
System.out.println();
// 唤醒 (干完活后,需要通知C线程执行)
number = 3;
// 通知3号去干活了
c3.signal();
}finally {
lock.unlock();
}
}
public void print15(){
lock.lock();
try {
// 判断
while (number != 3){
try {
// 当前线程等待
c3.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行任务
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
System.out.println();
// 唤醒 (干完活后,需要通知A线程执行)
number = 1;
// 通知1号去干活了
c1.signal();
}finally {
lock.unlock();
}
}
}
/**
* 1、多线程操作资源类
* 2、判断需不需要等待
* 3、执行任务
* 4、通知其它线程执行
*/
public class ConditionTest {
public static void main(String[] args) {
ShareData shareData = new ShareData();
// Condition在哪个线程,表示是哪个线程的条件
new Thread(()->{
shareData.print5();
},"A").start();
new Thread(()->{
shareData.print10();
},"B").start();
new Thread(()->{
shareData.print15();
},"C").start();
}
}
执行结果
A 0
A 1
A 2
A 3
A 4
B 0
B 1
B 2
B 3
B 4
B 5
B 6
B 7
B 8
B 9
C 0
C 1
C 2
C 3
C 4
C 5
C 6
C 7
C 8
C 9
C 10
C 11
C 12
C 13
C 14
注意: Condition在哪个线程,表示是哪个线程的条件,其它线程可以使用其线程的对应condition精准控制线程调用
BlockingQueue队列下的生产者和消费者
class MyResource {
// 默认开启,进行生产消费
// 这里用到了volatile是为了保持数据的可见性,也就是当TLAG修改时,要马上通知其它线程进行修改
private volatile boolean FLAG = true;
// 使用原子包装类,而不用number++
private AtomicInteger atomicInteger = new AtomicInteger();
// 这里不能为了满足条件,而实例化一个具体的SynchronousBlockingQueue
BlockingQueue<String> blockingQueue = null;
// 而应该采用依赖注入里面的,构造注入方法传入
public MyResource(BlockingQueue<String> 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 BlockingQueueProducerConsumer {
public static void main(String[] args) {
// 传入具体的实现类, ArrayBlockingQueue
MyResource myResource = new MyResource(new ArrayBlockingQueue<String>(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();
}
}
线程池
Callable接口
相比于runnable接口而言,callable可以抛出异常,并且返回返回值
callable接口基本使用
class Task implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(5);
return "aaa";
}
}
public class ThreadPoolTest {
public static void main(String[] args)throws Exception {
FutureTask futureTask = new FutureTask(new Task());
Thread thread = new Thread(futureTask);
thread.start();
// futureTask获取不到线程的返回值结果,会一直堵塞线程
System.out.println(futureTask.get());
}
}
为什么要使用线程池
缺点:
我们知道创建线程需要时间和空间。如果使用一次就不在使用,会等待 young GC 回收。当有大量的异步任务时,创建大量线程对象,消耗了时间和堆空间,会导致eden区 更早触发young gc,进一步降低效率。
优点:
降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
创建线程的四种方式
1、继承Thread
2、实现Runnable
3、实现Callable
4、使用线程池
线程池七大参数
我们以Executors为例
Executors为线程工具类,使用时可以便捷的创建线程。
public static void main(String[] args)throws Exception {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
System.out.println("固定线程的线程池");
for (int i = 0; i < 5; i++) {
fixedThreadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" "+ fixedThreadPool.getClass().getName());
});
}
System.out.println("---------------------------");
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
System.out.println("单一线程的线程池");
for (int i = 0; i < 5; i++) {
singleThreadExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+" "+ singleThreadExecutor.getClass().getName());
});
}
System.out.println("---------------------------");
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
System.out.println("缓存线程的线程池");
for (int i = 0; i < 5; i++) {
singleThreadExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+" "+ cachedThreadPool.getClass().getName());
});
}
}
1、Executors.newFixedThreadPool(3);
固定大小的线程池
2、Executors.newSingleThreadExecutor();
单一线程的线程池
3、Executors.newCachedThreadPool();
可缓存线程的线程池
线程池七大参数
1、corePoolSize 核心线程数(常驻线程)
2、maximumPoolSize 最大线程数 (线程池容纳的最大线程,当堵塞队列和常驻线程都没有空间时,会开启最大线程数-核心线程数得到的线程)
3、keepAliveTime 最大线程数-核心线程数得到的线程 存活的时间
4、TimeUnit 时间单元
5、BlockingQueue 存放线程执行任务的堵塞队列(该队列是线程安全的,当线程池中线程没时间处理任务时,任务进入堵塞队列)
6、ThreadFactory 生产线程的工厂
7、RejectedExecutionHandler 拒绝执行的处理器策略,当maximumPoolSize对应的线程都在执行任务,堵塞队列也满了,对后来的线程的拒绝执行的处理策略
不能使用Executors创建线程
Executors创建的线程,底层都是通过ThreadPoolExecutor创建的,并且使用的是LinkedBlockingQueue队列,默认最大Integer.MAX_VALUE。这意味这,除非没有内存了,不然队列永远满不了。在高并发下,内存中会囤积大量的异步任务,容易发生OOM,线程的执行效率也会变低。
线程池的4种拒绝策略
等待队列也已经排满了,再也塞不下新任务了同时,线程池中的max线程也达到了,无法继续为新任务服务。
这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK拒绝策略:
1、AbortPolicy(默认):直接抛出 RejectedExecutionException异常阻止系统正常运知。
2、CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
3、DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
4、DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
手写一个线程池
注意:开发中常见的应该是scheduledThreadPoolExecutor+ timerTask
class Task extends TimerTask{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +"执行任务");
}
}
public static void main(String[] args)throws Exception {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(5),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());
// 核心线程 和 blockingQueue 已满会创建 剩下的线程执行
for (int i = 0; i < 8 ; i++) {
threadPoolExecutor.execute(new Task());
}
}
核心线程 和 blockingQueue 已满会创建 剩下的线程执行。
如果max线程 和 blockingQueue 已满会 还有任务要处理,该任务直接丢弃
合理线程池线程数
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:
一般公式:(CPU核数+1)个线程的线程池
例如:8核+8 = 16
lO密集型
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数 * 2。
IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/ (1-阻塞系数)
阻塞系数在0.8~0.9之间
比如8核CPU:8/(1-0.9)=80个线程数
死锁
简单点来说,两个或两个以上的线程情景下,线程A持有锁资源A,但是还想要资源B,于是请求B锁,线程B持有锁资源B,但是还想要资源A,于是请求A锁。两者互不释放锁,又想获得对方资源。
死锁条件
1、请求保持
2、资源不可剥夺
3、资源互斥访问
4、循环等待
实例
public class DeadLock {
public static void main(String[] args){
String resourceA = "resourceA";
String resourceB = "resourceB";
new Thread(()->{
synchronized (resourceA){
System.out.println(Thread.currentThread().getName()+" 已经获取到资源 "+resourceA);
// 执行任务
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+" 正在获取资源 "+resourceB);
synchronized (resourceB){
System.out.println(Thread.currentThread().getName()+" 已经获取到资源 "+resourceB);
}
}
},"A").start();
new Thread(()->{
synchronized (resourceB){
System.out.println(Thread.currentThread().getName()+" 已经获取到资源 "+resourceB);
// 执行任务
try { TimeUnit.MILLISECONDS.sleep(1200); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+" 正在获取资源 "+resourceA);
synchronized (resourceA){
System.out.println(Thread.currentThread().getName()+" 已经获取到资源 "+resourceA);
}
}
},"B").start();
}
}
结果:
破环死锁,只需要破坏四个条件之一即可
查看进程的执行栈
以演示死锁为例
- jps -l 查看java中进程列表
- jstack 查看对应进程的执行栈
E:\Intellij IDEA\IDEAProject\test-01>jps -l
13920 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
7796 com.xiaoxu.test_thread_pool.DeadLock
8948
16056 sun.tools.jps.Jps
12476 org.jetbrains.jps.cmdline.Launcher
E:\Intellij IDEA\IDEAProject\test-01>jstack 7796
2021-07-23 11:19:15
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b15 mixed mode):
"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x00000000027e4000 nid=0x3770 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"B" #13 prio=5 os_prio=0 tid=0x000000001a55b800 nid=0x3b0 waiting for monitor entry [0x000000001ae4f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.xiaoxu.test_thread_pool.DeadLock.lambda$main$1(DeadLock.java:34)
- waiting to lock <0x00000000d67b58a8> (a java.lang.String)
- locked <0x00000000d67b58e8> (a java.lang.String)
at com.xiaoxu.test_thread_pool.DeadLock$$Lambda$2/1702297201.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"A" #12 prio=5 os_prio=0 tid=0x000000001a557800 nid=0x2524 waiting for monitor entry [0x000000001ad4f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.xiaoxu.test_thread_pool.DeadLock.lambda$main$0(DeadLock.java:21)
- waiting to lock <0x00000000d67b58e8> (a java.lang.String)
- locked <0x00000000d67b58a8> (a java.lang.String)
at com.xiaoxu.test_thread_pool.DeadLock$$Lambda$1/1896277646.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x0000000019931000 nid=0x322c runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x0000000019915800 nid=0x1910 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001990f800 nid=0x288 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000000019908800 nid=0x1ad0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000000019906000 nid=0x1c10 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000000019904800 nid=0x980 runnable [0x0000000019e4e000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x00000000d6850918> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x00000000d6850918> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x00000000196df800 nid=0x3534 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001968a000 nid=0x3f3c runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000000019670800 nid=0x342c in Object.wait() [0x0000000019b4f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6588ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x00000000d6588ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x00000000028da000 nid=0x3228 in Object.wait() [0x000000001964f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6586b50> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000d6586b50> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=2 tid=0x0000000017788800 nid=0x331c runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000027fa000 nid=0xf4c runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000027fc000 nid=0xfd0 runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000027fd800 nid=0x1234 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000027ff000 nid=0x376c runnable
"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x0000000002802000 nid=0x30c8 runnable
"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000002803000 nid=0x2a74 runnable
"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000002806800 nid=0x1200 runnable
"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000002807800 nid=0x120 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x0000000019935800 nid=0x3e60 waiting on condition
JNI global references: 335
Found one Java-level deadlock:
=============================
"B":
waiting to lock monitor 0x0000000017793608 (object 0x00000000d67b58a8, a java.lang.String),
which is held by "A"
"A":
waiting to lock monitor 0x0000000017790d78 (object 0x00000000d67b58e8, a java.lang.String),
which is held by "B"
Java stack information for the threads listed above:
===================================================
"B":
at com.xiaoxu.test_thread_pool.DeadLock.lambda$main$1(DeadLock.java:34)
- waiting to lock <0x00000000d67b58a8> (a java.lang.String)
- locked <0x00000000d67b58e8> (a java.lang.String)
at com.xiaoxu.test_thread_pool.DeadLock$$Lambda$2/1702297201.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"A":
at com.xiaoxu.test_thread_pool.DeadLock.lambda$main$0(DeadLock.java:21)
- waiting to lock <0x00000000d67b58e8> (a java.lang.String)
- locked <0x00000000d67b58a8> (a java.lang.String)
at com.xiaoxu.test_thread_pool.DeadLock$$Lambda$1/1896277646.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
JVM内存模型和JVM调优
JVM内存模型
注意:方法区的实现是MetaSpace,JVM中方法区和堆是共享的,每一个线程的自己独立的执行栈,pc计数器,本地方法栈均为线程私有
JVM堆的划分
1、创建的对象都会放入heap中的Eden区
2、当Eden区满时,会开启youngGC,存活对象放入from区
3、下次扫描时,就会扫描Eden区和from区,将存活的对象放入To区,From区和To区进行交换(复制交换)
4、当对象经过15次交换后,将对象放入oldGen(full GC 主要是回收oldGen对象)
GC回收四大算法
1、引用计数
2、复制交换
3、标记-清除
4、标记-清除-压缩
优点:无内存碎片
缺点:三次扫描,耗时大
GC root原理
1、GC root原理
GC root原理:通过对枚举GCroot对象做引用可达性分析,即从GC root对象开始,向下搜索,形成的路径称之为 引用链。如果一个对象到GC roots对象没有任何引用,没有形成引用链,那么该对象等待GC回收。
2、GC root对象是什么?
Java中可以作为GC Roots的对象
1、虚拟机栈(javaStack)(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
2、方法区中的类静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中JNI(Native方法)引用的对象。
JVM参数类型
1、标准参数
- version java -version
- help
2、X参数
- Xint:解释执行
- Xcomp:第一次使用就编译成本地代码
- Xmixed:混合模式
3、XX参数
xx参数分为boolean类型参数 和 kv类型参数
3.1、boolean类型参数参数
-XX:+ 或者 - 某个属性值(+表示开启,-表示关闭)
是否打印GC收集细节
- -XX:-PrintGCDetails
- -XX:+PrintGCDetails
是否使用串行垃圾回收器
- -XX:-UseSerialGC
- -XX:+UserSerialGC
3.2、KV类型参数参数
- -XX:InitialHeapSize=xxxx
- -XX:maxHeapSize=xxx
实例:
查看一个正在运行的java应用,jvm配置如何?
- jps -l 查看正在运行中的java程。
- jinfo -flag PrintGCDetails 进程pid 查看它的某个jvm参数(如PrintGCDetails )是否开启。
- jinfo -flags 进程pid 查看它的所有jvm参数
E:\Intellij IDEA\IDEAProject\test-01>jinfo -flags 3112
Attaching to process ID 3112, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.91-b15
Non-default VM flags: -XX:CICompilerCount=4 -XX:InitialHeapSize=132120576 -XX:MaxHeapSize=2097152000 -XX:MaxNewSize=698875904 -XX:MinHeapDeltaBytes=52428
8 -XX:NewSize=44040192 -XX:OldSize=88080384 -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -X
X:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line: -XX:+PrintGCDetails -javaagent:E:\Intellij IDEA\IntelliJ IDEA 2019.1.4\lib\idea_rt.jar=5181:E:\Intellij IDEA\IntelliJ IDEA 2019.1.4\bin -D
file.encoding=UTF-8
-XX:InitialHeapSize=132120576 JVM最初堆内存大小(默认是物理内存的64分之一)
-XX:MaxHeapSize=2097152000 JVM最大堆大小(默认是物理内存的4分之一)
-XX:+PrintGCDetails 打印输出GC回收信息
Heap
PSYoungGen total 37888K, used 4592K [0x00000000d6580000, 0x00000000d8f80000, 0x0000000100000000)
eden space 32768K, 14% used [0x00000000d6580000,0x00000000d69fc330,0x00000000d8580000)
from space 5120K, 0% used [0x00000000d8a80000,0x00000000d8a80000,0x00000000d8f80000)
to space 5120K, 0% used [0x00000000d8580000,0x00000000d8580000,0x00000000d8a80000)
ParOldGen total 86016K, used 0K [0x0000000083000000, 0x0000000088400000, 0x00000000d6580000)
object space 86016K, 0% used [0x0000000083000000,0x0000000083000000,0x0000000088400000)
Metaspace used 3226K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 357K, capacity 388K, committed 512K, reserved 1048576K
JVM查看初始默认值
-XX:+PrintFlagsInitial
使用方式:java -XX:+PrintFlagsInitial 也可以将XX参数配置在VM option中启动引用查看
数据过多,有需要自己查看
uintx MetaspaceSize = 21810376 {pd product}
元空间大小为21M左右
JVM查看修改更新参数值
-XX:+PrintFlagsFinal
使用方式:java -XX:+PrintFlagsFinal 可以将XX参数配置在VM option中启动引用查看
JVM常用参数
-XX:ThreadStackSize 配置线程栈大小
-Xmn:设置年轻代大小 (这是简写形式,我个人喜欢用全称,好记)
-XX:MetaspaceSize 设置元空间大小
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
java中的引用对象类型
java中的引用类型分别是强引用,SoftReference,WeakReference,PhantomReference。
我们平常中产生的对象绝大部分是强引用类型,下来来分别介绍。
java强引用对象
Object o = new Object;
String s = "aa";
等等。。。。。。。
特点
当内存不足,JVM开始通过垃圾回收器进行垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,因为存在引用指向这它,我们平常创建的对象都是强引用对象。要将强引用对象回收,只需引用指向null即可,等待gc回收
java软引用对象(SoftReference)
这里我们启动程序的时候,为了出现内存不足现象,使用了jvm -XX参数。只需vm option配置即可
-XX:InitialHeapSize=10m -XX:MaxHeapSize=10m -XX:+PrintGCDetails
特点
1、当内存充足时,只要软引用对象还有引用指向,就不会被gc回收
2、当内存不充足时,不管软引用对象有没有被指向,都会被回收
public static void main(String[] args) {
// 强引用对象
Object s = new Object();
// 软引用对象
SoftReference<Object> softReference = new SoftReference<>(s);
// 手动GC
System.gc();
System.out.println("内存充足时");
System.out.println(s);
System.out.println(softReference.get());
try {
s = null;
// 当jvm堆内存不足时,会垃圾回收软引用对象
byte[] buffer = new byte[35*1024*1024];
}catch (Error e){
e.printStackTrace();
}finally {
System.out.println("内存不充足时");
System.out.println(s);
System.out.println(softReference.get());
}
}
结果:
[GC (Allocation Failure) [PSYoungGen: 2048K->512K(2560K)] 2048K->842K(9728K), 0.0013049 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 1084K->512K(2560K)] 1414K->962K(9728K), 0.0010431 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 512K->0K(2560K)] [ParOldGen: 450K->853K(7168K)] 962K->853K(9728K), [Metaspace: 3313K->3313K(1056768K)], 0.0062308 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
内存充足时
java.lang.Object@6b884d57
java.lang.Object@6b884d57
[GC (Allocation Failure) [PSYoungGen: 81K->64K(2560K)] 934K->917K(9728K), 0.0007031 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 64K->128K(2560K)] 917K->981K(9728K), 0.0005246 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 128K->0K(2560K)] [ParOldGen: 853K->770K(5632K)] 981K->770K(8192K), [Metaspace: 3317K->3317K(1056768K)], 0.0077545 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 770K->770K(9728K), 0.0003751 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 770K->752K(7168K)] 770K->752K(9728K), [Metaspace: 3317K->3317K(1056768K)], 0.0096544 secs] [Times: user=0.08 sys=0.00, real=0.01 secs]
java.lang.OutOfMemoryError: Java heap space
at com.xiaoxu.test_reference.ReferenceTest.main(ReferenceTest.java:28)
内存不充足时
null
null
Heap
PSYoungGen total 2560K, used 143K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 7% used [0x00000000ffd00000,0x00000000ffd23ee8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 752K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 10% used [0x00000000ff600000,0x00000000ff6bc158,0x00000000ffd00000)
Metaspace used 3360K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 369K, capacity 388K, committed 512K, reserved 1048576K
java弱引用对象(WeakReference)
特点
只要gc就会回收弱引用对象,不管弱引用对象,有没有引用指向
public static void main(String[] args) {
// 强引用对象
Object s = new Object();
// weak引用对象
Reference<Object> weakReference = new WeakReference<>(s);
s=null;
// 手动GC
System.gc();
System.out.println("内存充足时");
System.out.println(s);
System.out.println(weakReference.get());
}
结果:
[GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->856K(9728K), 0.0010334 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 1015K->496K(2560K)] 1368K->976K(9728K), 0.0011791 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 480K->849K(7168K)] 976K->849K(9728K), [Metaspace: 3273K->3273K(1056768K)], 0.0051562 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
内存充足时
null
null
Heap
PSYoungGen total 2560K, used 102K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 5% used [0x00000000ffd00000,0x00000000ffd19b38,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 849K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 11% used [0x00000000ff600000,0x00000000ff6d4630,0x00000000ffd00000)
Metaspace used 3303K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K
软引用和弱引用的适用场景
需要对图片进行大量缓存
1、如果每次读取图片都从硬盘读取则会严重影响性能
2、如果一次性全部加载到内存中,又可能造成内存溢出
此时使用软引用可以解决这个问题。
设计思路:使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有效地避免了OOM的问题
Map<String, SoftReference<BitMap>> imageCache
= new HashMap<String, SoftReference<Bitmap>>();
WeakHashMap存储虚引用对象
public static void main(String[] args) {
myHashMap();
System.out.println("==========");
myWeakHashMap();
}
private static void myHashMap() {
Map<Integer, String> 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<Integer, String> 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);
}
一旦回收,map为空
java虚引用对象(PhantomReference)
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
JVM中常见的两类错误
StackoverFlowError
- java.lang.StackOverflowError (线程执行栈,栈溢出)
OutofMemoryError
- java.lang.OutOfMemoryError:java heap space (堆空间不足)
- java.lang.OutOfMemoryError:GC overhead limit exceeded (gc垃圾收集器负载过重,就是gc也回收不了垃圾)
- java.lang.OutOfMemoryError:Direct buffer memory (jvm堆内存之外的物理内存不足)
- java.lang.OutOfMemoryError:unable to create new native thread (线程的创建数量过多导致,内存不足)
- java.lang.OutOfMemoryError:Metaspace (元空间内存不足)
StackoverFlowError错误复现
public static void main(String[] args) {
recursion();
}
public static void recursion(){
recursion();
}
结果:
线程的栈大小默认是1024kb,一旦超出该大小就会产生stackOverFlow
java.lang.OutOfMemoryError:java heap space (堆空间不足)错误复现
配置java程序启动jvm参数
-XX:+PrintGCDetails -XX:InitialHeapSize=5m -XX:MaxHeapSize=5m
public static void main(String[] args) throws Exception {
byte[] bytes = new byte[10 * 1024 * 1024];
}
[GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->660K(5632K), 0.0010736 secs] [Times: user=0.05 sys=0.05, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1528K->504K(1536K)] 1684K->871K(5632K), 0.0012427 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 945K->504K(1536K)] 1312K->967K(5632K), 0.0011252 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 504K->504K(1536K)] 967K->991K(5632K), 0.0008079 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 487K->767K(4096K)] 991K->767K(5632K), [Metaspace: 3230K->3230K(1056768K)], 0.0065384 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 767K->767K(5632K), 0.0004416 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 767K->749K(4096K)] 767K->749K(5632K), [Metaspace: 3230K->3230K(1056768K)], 0.0073540 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.xiaoxu.test_error.ErrorTest.main(ErrorTest.java:11)
Heap
PSYoungGen total 1536K, used 118K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 11% used [0x00000000ffe00000,0x00000000ffe1db68,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 4096K, used 749K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
object space 4096K, 18% used [0x00000000ffa00000,0x00000000ffabb670,0x00000000ffe00000)
Metaspace used 3333K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 366K, capacity 388K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError:GC overhead limit exceeded (gc垃圾收集器负载过重,就是gc也回收不了垃圾)
配置java程序启动jvm参数
-XX:+PrintGCDetails -XX:InitialHeapSize=5m -XX:MaxHeapSize=5m
产生此次错误,主要是jvm堆中存在大量对量被引用,gc一直无法回收垃圾,导致gc负载过重
public static void main(String[] args) throws Exception {
ArrayList<Object> list = new ArrayList<>();
while (true){
list.add(new Object());
}
}
值得注意的是:如果是创建字符串常量的话,放在常量池中,常量池位于方法区,对应实现是MetaSpace。对象是放在物理jvm之外的物理内存中
配置程序启动时jvm参数
-XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
public static void main(String[] args) throws Exception {
ArrayList<String> list = new ArrayList<>();
int i = 0;
try {
while(true) {
list.add(String.valueOf(++i).intern());
}
} catch (Exception e) {
System.out.println("***************i:" + i);
e.printStackTrace();
throw e;
}
}
java.lang.OutOfMemoryError:Direct buffer memory (jvm堆内存之外的物理内存不足)
导致原因:
写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避兔了在Java堆和Native堆中来回复制数据。
ByteBuffer.allocate(capability) 第一种方式是分配VM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
ByteBuffer.allocateDirect(capability) 第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。
但如果不断分配本地内存,堆内存很少使用,那么JV就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。
jvm启动时附带的参数
-XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
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);
}
结果
配置的maxDirectMemory: 5.00 MB
[GC (System.gc()) [PSYoungGen: 5906K->1496K(37888K)] 5906K->1504K(123904K), 0.0019607 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1496K->0K(37888K)] [ParOldGen: 8K->1359K(86016K)] 1504K->1359K(123904K), [Metaspace: 4182K->4182K(1056768K)], 0.0078596 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:693)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.xiaoxu.test_error.ErrorTest.main(ErrorTest.java:17)
Heap
PSYoungGen total 37888K, used 874K [0x00000000d6580000, 0x00000000d8f80000, 0x0000000100000000)
eden space 32768K, 2% used [0x00000000d6580000,0x00000000d665a8d8,0x00000000d8580000)
from space 5120K, 0% used [0x00000000d8580000,0x00000000d8580000,0x00000000d8a80000)
to space 5120K, 0% used [0x00000000d8a80000,0x00000000d8a80000,0x00000000d8f80000)
ParOldGen total 86016K, used 1359K [0x0000000083000000, 0x0000000088400000, 0x00000000d6580000)
object space 86016K, 1% used [0x0000000083000000,0x0000000083153e38,0x0000000088400000)
Metaspace used 4213K, capacity 4678K, committed 4864K, reserved 1056768K
class space used 465K, capacity 494K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError:unable to create new native thread (线程的创建数量过多导致,内存不足)
导致原因:
应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
服务器并不允许你的应用程序创建这么多线程**,linux系统默认运行单个进程可以创建的线程为1024个**,如果应用创建超过这个数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread
配置java程序启动jvm参数
-XX:+PrintGCDetails -XX:InitialHeapSize=5m -XX:MaxHeapSize=5m
注意:可能会导致死机
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();
}
}
java.lang.OutOfMemoryError:Metaspace (元空间内存不足)
元空间主要是方法区的实现,存放常量池,静态变量,类相关信息等
默认的元空间大小为21M左右
通过jvm参数 -XX:PrintFlagsFinal 或者 jinfo -flag MetaspaceSize 查看
一旦元空间满了,要注意元空间中存放的信息
java中的七大GC收集器和四大GC算法
垃圾收集器的种类和介绍
1、serial 串行垃圾收集器,采用单线程收集垃圾
2、parallel 并行垃圾收集器,采用多线程收集垃圾
3、CMS(concurrentMarkSweep)并发标记收集 垃圾收集器,串行和并行同在,是前两种垃圾收集器的优化,较短时间进行STW(stop the world),保证较快的响应速度。
4、G1 新一代垃圾收集器,采用的region分区
java8中默认的垃圾收集器
jps -l #查看java进程列表
jinfo -flags pid # 查看对应java进程的相关jvm参数
java8中默认使用的是ParallelGC
java中七大垃圾收集器
年轻代GC
1、UserSerialGC:串行垃圾收集器
2、UserParallelGC:并行垃圾收集器
3、UseParNewGC:年轻代的并行垃圾回收器
新生代中采用的垃圾收集算法 基本都是 标记复制算法/复制拷贝
老年代GC
1、UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
2、UseParallelOldGC:老年代的并行垃圾回收器
3、UseConcMarkSweepGC:(CMS)并发标记清除
老年代中采用的来及收集算法 基本都是 标记清除,标记清除压缩
youngGen 和 oldGen 使用的GC
UseG1GC:G1垃圾收集器
七大垃圾收集器搭配和介绍
下面两个图很重要
注意:下面图中指的是可以这样搭配,但是jvm中会有默认搭配机制
Serial收集器
一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程(STW)直到它收集结束。
新生代老年代都是单线程的垃圾回收
STW: Stop The World
串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World”状态)。虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。
如果让jvm使用Serial垃圾收集器
-XX:+UseSerialGC
实例:
jvm配置参数
-XX:+UseSerialGC -XX:+PrintGCDetails -XX:MaxHeapSize=5m -XX:InitialHeapSize=5m
public static void main(String[] args) throws InterruptedException {
List<Object> list = new ArrayList<>();
while (true){
list.add(new Object());
}
}
-XX:InitialHeapSize=5242880 -XX:MaxHeapSize=5242880 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
[GC (Allocation Failure) [DefNew: 1664K->191K(1856K), 0.0017261 secs] 1664K->730K(5952K), 0.0017851 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 1855K->192K(1856K), 0.0029624 secs] 2394K->1366K(5952K), 0.0030089 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 1856K->192K(1856K), 0.0028893 secs] 3030K->2443K(5952K), 0.0029367 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 1856K->191K(1856K), 0.0042756 secs] 4107K->4107K(5952K), 0.0043167 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew: 1855K->1855K(1856K), 0.0000200 secs][Tenured: 3915K->3195K(4096K), 0.0125114 secs] 5771K->4728K(5952K), [Metaspace: 3329K->3329K(1056768K)], 0.0125823 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 3715K->3715K(4096K), 0.0106739 secs] 5571K->5571K(5952K), [Metaspace: 3329K->3329K(1056768K)], 0.0107996 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 3715K->3610K(4096K), 0.0125134 secs] 5571K->5466K(5952K), [Metaspace: 3329K->3329K(1056768K)], 0.0125436 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.xiaoxu.test_error.ErrorTest.main(ErrorTest.java:16)
Heap
def new generation total 1856K, used 1855K [0x00000000ffa00000, 0x00000000ffc00000, 0x00000000ffc00000)
eden space 1664K, 100% used [0x00000000ffa00000, 0x00000000ffba0000, 0x00000000ffba0000)
from space 192K, 99% used [0x00000000ffba0000, 0x00000000ffbcfff8, 0x00000000ffbd0000)
to space 192K, 0% used [0x00000000ffbd0000, 0x00000000ffbd0000, 0x00000000ffc00000)
tenured generation total 4096K, used 3640K [0x00000000ffc00000, 0x0000000100000000, 0x0000000100000000)
the space 4096K, 88% used [0x00000000ffc00000, 0x00000000fff8e108, 0x00000000fff8e200, 0x0000000100000000)
Metaspace used 3362K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 369K, capacity 388K, committed 512K, reserved 1048576K
ParNew收集器
paraNew是在新生代中的并行多线程垃圾收集器,老年代采用单线程垃圾收集器。
ParNew(Young区)+ Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法
jvm参数配置
-XX:+UseParNewGC -XX:+PrintGCDetails -XX:MaxHeapSize=5m -XX:InitialHeapSize=5m
public static void main(String[] args) throws InterruptedException {
List<Object> list = new ArrayList<>();
while (true){
list.add(new Object());
}
}
[GC (Allocation Failure) [ParNew: 1664K->192K(1856K), 0.0017274 secs] 1664K->774K(5952K), 0.0017788 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1856K->192K(1856K), 0.0018861 secs] 2438K->1443K(5952K), 0.0019318 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1439K->192K(1856K), 0.0022849 secs] 2690K->2497K(5952K), 0.0023308 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1856K->192K(1856K), 0.0029927 secs] 4161K->3778K(5952K), 0.0030483 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 990K->990K(1856K), 0.0000341 secs][Tenured: 3586K->4095K(4096K), 0.0135080 secs] 4576K->4174K(5952K), [Metaspace: 3329K->3329K(1056768K)], 0.0135917 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 4095K->3399K(4096K), 0.0126730 secs] 5951K->5176K(5952K), [Metaspace: 3329K->3329K(1056768K)], 0.0127294 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 3732K->3732K(4096K), 0.0117807 secs] 5587K->5587K(5952K), [Metaspace: 3329K->3329K(1056768K)], 0.0118350 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 3732K->3608K(4096K), 0.0141602 secs] 5587K->5464K(5952K), [Metaspace: 3329K->3329K(1056768K)], 0.0142101 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.xiaoxu.test_error.ErrorTest.main(ErrorTest.java:16)
Heap
par new generation total 1856K, used 1855K [0x00000000ffa00000, 0x00000000ffc00000, 0x00000000ffc00000)
eden space 1664K, 99% used [0x00000000ffa00000, 0x00000000ffb9fff8, 0x00000000ffba0000)
from space 192K, 100% used [0x00000000ffba0000, 0x00000000ffbd0000, 0x00000000ffbd0000)
to space 192K, 0% used [0x00000000ffbd0000, 0x00000000ffbd0000, 0x00000000ffc00000)
tenured generation total 4096K, used 3637K [0x00000000ffc00000, 0x0000000100000000, 0x0000000100000000)
the space 4096K, 88% used [0x00000000ffc00000, 0x00000000fff8d728, 0x00000000fff8d800, 0x0000000100000000)
Metaspace used 3362K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 369K, capacity 388K, 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
Parallel/Parallel Scavenge收集器(java8默认的垃圾收集器)
新生代和老年代都采用多线程的垃圾收集器
jvm参数配置
-XX:+UseParallelGC -XX:+PrintGCDetails -XX:MaxHeapSize=5m -XX:InitialHeapSize=5m
public static void main(String[] args) throws InterruptedException {
List<Object> list = new ArrayList<>();
while (true){
list.add(new Object());
}
}
[Full GC (Ergonomics) [PSYoungGen: 1023K->1023K(1536K)] [ParOldGen: 4026K->4026K(4096K)] 5050K->5050K(5632K), [Metaspace: 3463K->3463K(1056768K)], 0.0296184 secs] [Times: user=0.14 sys=0.00, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 1023K->1023K(1536K)] [ParOldGen: 4027K->4027K(4096K)] 5051K->5051K(5632K), [Metaspace: 3463K->3463K(1056768K)], 0.0288021 secs] [Times: user=0.09 sys=0.02, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 1023K->1023K(1536K)] [ParOldGen: 4028K->4028K(4096K)] 5052K->5052K(5632K), [Metaspace: 3463K->3463K(1056768K)], 0.0301605 secs] [Times: user=0.13 sys=0.00, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 1023K->0K(1536K)] [ParOldGen: 4036K->880K(4096K)] 5060K->880K(5632K), [Metaspace: 3464K->3464K(1056768K)], 0.0078608 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at com.xiaoxu.test_error.ErrorTest.main(ErrorTest.java:16)
Heap
PSYoungGen total 1536K, used 127K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 12% used [0x00000000ffe00000,0x00000000ffe1fd78,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 4096K, used 880K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
object space 4096K, 21% used [0x00000000ffa00000,0x00000000ffadc3a0,0x00000000ffe00000)
Metaspace used 3496K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 380K, capacity 388K, committed 512K, reserved 1048576K
ParallelOld收集器
ParallelOld收集器是老年代采用的垃圾收集器,使用的算法是标记清除, 与之对应的新生代垃圾收集器ParallelScavenge
上文介绍过
即使我配置-XX:UseParallelOldGC,新生代默认也会配置成ParallelScavenge 注意这是默认的。新生代配置ParallelScavenge,老年代还是配置成这个
CMS(ConcurrentMarkSweep)收集器(建议使用)
CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。
适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
CMS非常适合地内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
注意
新生代使用ParNew收集器,老年代会使用收集器SerialOldGC
使用jvm参数配置
-XX:+UseConcMarkSweepGC 新生代会自动使用ParNew
开启该参数后,使用ParNew(Young区用)+ CMS(Old区用)+ Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器。
4步过程:
1、初始标记(CMS initial mark) - 只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
2、并发标记(CMS concurrent mark)和用户线程一起 - 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。
3、重新标记(CMS remark)- 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正。
4、并发清除(CMS concurrent sweep) - 清除GCRoots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
优点:并发收集低停顿,响应速度快。
缺点:并发执行,对CPU资源压力大,采用的标记清除算法会导致大量碎片。
jvm参数配置
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:MaxHeapSize=5m -XX:InitialHeapSize=5m
public static void main(String[] args) throws InterruptedException {
List<Object> list = new ArrayList<>();
while (true){
list.add(new Object());
}
}
[GC (Allocation Failure) [ParNew: 1088K->128K(1216K), 0.0030865 secs] 1088K->621K(6016K), 0.0031883 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1216K->128K(1216K), 0.0011983 secs] 1709K->848K(6016K), 0.0012514 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1216K->128K(1216K), 0.0026329 secs] 1936K->1461K(6016K), 0.0026799 secs] [Times: user=0.06 sys=0.03, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1216K->128K(1216K), 0.0026583 secs] 2549K->2637K(6016K), 0.0026936 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 2509K(4800K)] 2644K(6016K), 0.0002571 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
[GC (Allocation Failure) [ParNew: 1216K->128K(1216K), 0.0036117 secs] 3725K->4048K(6016K), 0.0036503 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1216K->1216K(1216K), 0.0000303 secs][CMS[CMS-concurrent-mark: 0.005/0.011 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
(concurrent mode failure): 3920K->4513K(4800K), 0.0150135 secs] 5136K->4513K(6016K), [Metaspace: 3329K->3329K(1056768K)], 0.0151086 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
[GC (Allocation Failure) [ParNew: 486K->486K(1216K), 0.0000183 secs][CMS: 4513K->4799K(4800K), 0.0167265 secs] 4999K->5050K(6016K), [Metaspace: 3329K->3329K(1056768K)], 0.0168074 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[Full GC (Allocation Failure) [CMS: 4799K->4799K(4800K), 0.0141188 secs] 5050K->5038K(6016K), [Metaspace: 3329K->3329K(1056768K)], 0.0141803 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 4799K(4800K)] 5038K(6016K), 0.0004316 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[CMS-concurrent-mark-start]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.xiaoxu.test_error.ErrorTest.main(ErrorTest.java:16)
Heap
par new generation total 1216K, used 291K [0x00000000ffa00000, 0x00000000ffb50000, 0x00000000ffb50000)
eden space 1088K, 26% used [0x00000000ffa00000, 0x00000000ffa48ef0, 0x00000000ffb10000)
from space 128K, 0% used [0x00000000ffb30000, 0x00000000ffb30000, 0x00000000ffb50000)
to space 128K, 0% used [0x00000000ffb10000, 0x00000000ffb10000, 0x00000000ffb30000)
concurrent mark-sweep generation total 4800K, used 4799K [0x00000000ffb50000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 3362K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 369K, capacity 388K, committed 512K, reserved 1048576K
G1垃圾收集器
后面讲
SerailOldGC收集器
单线程,标记整理算法,不再赘述
GC如何选择垃圾收集器
组合的选择
单CPU或者小内存,单机程序
-XX:+UseSerialGC
多CPU,需要最大的吞吐量,如后台计算型应用(java8默认)
-XX:+UseParallelGC(这两个相互激活)
-XX:+UseParallelOldGC
多CPU,追求低停顿时间,需要快速响应如互联网应用
-XX:+UseConcMarkSweepGC
-XX:+ParNewGC
G1收集器
G1 (Garbage-First)收集器,是一款面向服务端应用的收集器
特点:
G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW。
G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。
宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘。
G1收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。
G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。
目的
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内存区域。
回收步骤
G1收集器下的Young GC
针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片
Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部会晋升到Old区。
Survivor区的数据移动到新的Survivor区,部会数据晋升到Old区。
最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。
4步过程:
初始标记:只标记GC Roots能直接关联到的对象
并发标记:进行GC Roots Tracing的过程
最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
筛选回收:根据时间来进行价值最大化的回收
G1参数配置及和CMS的比较
-XX:+UseG1GC
-XX:G1HeapRegionSize=n:设置的G1区域的大小。值是2的幂,范围是1MB到32MB。目标是根据最小的Java堆大小划分出约2048个区域。
-XX:MaxGCPauseMillis=n:最大GC停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间。
-XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45。
-XX:ConcGCThreads=n:并发GC使用的线程数。
-XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%。
开发人员仅仅需要声明以下参数即可:
三步归纳:开始G1+设置最大内存+设置最大停顿时间
-XX:+UseG1GC
-Xmx32g
-XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间
G1和CMS比较
G1不会产生内碎片
是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。
java程序启动,配置jvm参数
java -server jvm的各种参数 -jar jar/war包名
linux查看系统性能负载常用命令
top 查看系统整体性能/使用情况
用法
top [-d number] | top [-bnp]
-d:number代表秒数,表示top命令显示的页面更新一次的间隔。默认是5秒。
-b:以批次的方式执行top。
-n:与-b配合使用,表示需要进行几次top命令的输出结果
-p:指定特定的pid进程号进行观察
vmstat 查看系统cpu使用情况
用法
[root@VM-8-9-centos ~]# vmstat --help
Usage:
vmstat [options] [delay [count]]
Options:
-a, --active active/inactive memory
-f, --forks number of forks since boot
-m, --slabs slabinfo
-n, --one-header do not redisplay header
-s, --stats event counter statistics
-d, --disk disk statistics
-D, --disk-sum summarize disk statistics
-p, --partition <dev> partition specific statistics
-S, --unit <char> define display unit
-w, --wide wide output
-t, --timestamp show timestamp
-h, --help display this help and exit
-V, --version output version information and exit
vmstat
vmstat -n 2 3
# vmstat -n 每隔多少秒进行采样 采样多少次
procs
-
r:运行和等待的CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不超过总核数的2倍,否则代表系统压力过大,我们看蘑菇博客测试服务器,能发现都超过了2,说明现在压力过大
-
b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O等
cpu
-
us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,优化程序
-
sy:内核进程消耗的CPU时间百分比
-
us + sy 参考值为80%,如果us + sy 大于80%,说明可能存在CPU不足,从上面的图片可以看出,us + sy还没有超过百分80,因此说明蘑菇博客的CPU消耗不是很高
-
id:处于空闲的CPU百分比
-
wa:系统等待IO的CPU时间百分比
-
st:来自于一个虚拟机偷取的CPU时间比
pidstat 查看系统cpu/process使用情况
用法
pidstat -u 5 -p pid
# pidstat -u 多少秒刷新一次 -p pid
注意:默认是没有pidstat命令的,需要 安装一下
yum install pidstat
free 查看系统内存使用情况
用法
free -m
# 单位是M
经验值
应用程序可用内存l系统物理内存>70%内存充足
应用程序可用内存/系统物理内存<20%内存不足,需要增加内存
20%<应用程序可用内存/系统物理内存<70%内存基本够用
df 查看硬盘使用情况
df -h
# h参数为 : 以人类能看懂的方式呈现
iostat 查看网络io情况
iostat -xdk 2 3
rkB/s每秒读取数据量kB;wkB/s每秒写入数据量kB;
svctm lO请求的平均服务时间,单位毫秒;
await l/O请求的平均等待时间,单位毫秒;值越小,性能越好;
util一秒中有百分几的时间用于I/O操作。接近100%时,表示磁盘带宽跑满,需要优化程序或者增加磁盘;
rkB/s、wkB/s根据系统应用不同会有不同的值,但有规律遵循:长期、超大数据读写,肯定不正常,需要优化程序读取。
svctm的值与await的值很接近,表示几乎没有IO等待,磁盘性能好。
如果await的值远高于svctm的值,则表示IO队列等待太长,需要优化程序或更换更快磁盘。
CPU占用过高的定位分析思路
1、先用top命令找出CPU占比最高的
2、ps -ef或者jps -l 进一步定位,定位进程
ps -ef | grep 3928
3、定位到具体线程或者代码
ps -mp 进程 -o THREAD,tid,time
-m 显示所有的线程
-p pid进程使用cpu的时间
-o 该参数后是用户自定义格式 要显示的内容
ps -mp 3928 -o THREAD,tid,time
4、jstack 进程ID | grep tid -A60
查看对应进程中,对应线程的栈信息
Github搜索好的开源项目
GitHub关键字
watch:会持续收到该项目的动态
fork:复制其个项目到自己的Github仓库中
star,可以理解为点赞
clone,将项目下载至本地
follow,关注你感兴趣的作者,会收到他们的动态
Github中使用in限制搜索
关键词 in:name/description/readme
例如:springboot 存在于name,readme中的项目
springboot in:name,readme
Github中star和fork搜索
关键字 stars: fork:
实例
springboot stars:>=2000 fork:>=200
Github中awesome搜索
awesome 项目/教程/案例
用来收集一系列较好的项目
awesome springboot
Github中#L数字
#L 主要是用来查看代码,进行高亮的
实例
GitHub中搜索区域活跃用户
location:hangzhou language:java