文章目录
- 1. Java8新特性
- 1.1 Lambda表达式
- 1.2 函数式接口
- 1.3 Stream流式计算,应用了上述函数式接口能力
- 1.4 接口增强
- 2. 常用原子类
- 3. 多线程与高并发-juc
- 3.1 谈一谈对volatile的理解
- 3.2 谈一谈对JMM的理解
- 3.3 谈一谈对CAS及底层原理的理解
- 3.4 谈一谈对ABA问题及原子更新引用的理解
- 3.5 谈一谈集合不安全的情况
- 3.6 谈一谈Java常用锁
- 3.7 谈一谈常见的线程计数器
- 3.8 谈一谈阻塞队列
- 3.9 谈一谈线程池以及ThreadPoolExecutor
- 3.10 谈一谈死锁以及定位分析
- 3.11 谈一谈强引用、软引用、弱引用和虚引用
- 3.12 谈一谈六种常见的OOM
- 4. JVM核心知识点
- 4.1 JVM垃圾回收如何确定垃圾及GC Roots
- 4.2 常用的JVM基本配置参数
- 4.3 垃圾收集算法与垃圾回收器
- 4.4 Linux系统变慢如何定位
- 4.5 Github骚操作
1. Java8新特性
1.1 Lambda表达式
java8引入了一个新的操作符"->",该操作符将表达式拆分为两部分:左侧:Lambada表示表达式的参数列表右侧:Lambda表示所需要执行的功能规则:(左侧)->(右侧)new Thread( ()-> System.out.println("abc")).start();
1.2 函数式接口
package com.gwolf;
@FunctionalInterface
public interface MyPredicate<T> {public boolean test(T t);
}
1.3 Stream流式计算,应用了上述函数式接口能力
StreamAPI是java8中处理集合的关键抽象概念,它可以指定您希望对集合进行的操作,
可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询。
1.4 接口增强
Java8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示例如下:
2. 常用原子类
引入原子操作类的原因?
- 在多线程环境下,对于Java中的运算操作(例如自增或自减),例如num++解析为num=num+1,显然这个操作不具备原子性,多线程并发共享这个变量时必然会出现问题,线程不安全。
- 因此需要引入各种原子操作类。AtomicInteger提供线程安全的原子操作方式来进行相关运算(包括加减),因此原子操作类十分适合多线程、高并发情况下的使用。
(1)AtomicBoolean: 原子更新布尔类型
(2)AtomicInteger: 原子更新整型
(3)AtomicLong: 原子更新长整型以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:
AtomicInteger构造方法
1、public AtomicInteger(int initialValue) 创建具有给定初始值的新 AtomicInteger
2、public AtomicInteger() 创建具有初始值0的新 AtomicInteger AtomicInteger常用API:
1、public final int get() 获取当前值
2、public final void set(int newValue) 设置为给定值
3、public final boolean compareAndSet(int expect, int update) (CAS)参数:expect - 预期值 update - 新值 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值,返回 true如果当前值 != 预期值,返回 false,说明实际值与预期值不相等
4、public final int getAndIncrement() 以原子方式将当前值加 1。
5、public final int getAndDecrement() 以原子方式将当前值减 1。
6、public final int incrementAndGet() 以原子方式将当前值加 1。
7、public final int decrementAndGet() 以原子方式将当前值减 1。
8、public final int getAndAdd(int delta) 以原子方式将给定值与当前值相加。
9、public final int addAndGet(int delta) 以原子方式将给定值与当前值相加。
3. 多线程与高并发-juc
主要包括volatile、jmm 、可见性、原子性、有序性 、AtomicInteger、 CAS 、ABA问题、Unsafe类、自旋锁 、 ABA问题、原子引用更新、如何规避ABA问题
3.1 谈一谈对volatile的理解
A、volatile是Java虚拟机提供的轻量级的线程同步机制(轻量级的synchronized)
特性:
1、保证可见性 testvolatile01.java
2、保证有序性(禁止指令重排列) testvolatile03.java
3、不保证原子性 testvolatile02.javas
可见性:某一线程中的变量发生变化,其他线程可立刻得到通知,拿到该变量的最新值
原子性:也叫做完整性,不可分割,即某个线程在做某个具体操作时,要么不执行,要么彻底执行,中间不可以加塞或者被分割。
有序性(禁止指令重排列):有序性只针对多线程,计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排列,一般分为三种:
单线程环境可以确保程序最终执行结果和代码顺序执行的结果一致,但在多线程环境中,多个线程交替执行,由于编译器优化重排列的结果,两个线程使用的变量能否保证一致性是无法确定的。(类似于做题,先做简单题,后做难题。多线程环境下程序执行也不是按顺序执行的,但满足happens before原则(指令之间的数据依赖性))
在多线程环境下,指令重排会导致变量值不一致,必须禁止指令重排列,volatile修饰的变量就可以禁止指令重排。
B、volatile为什么不能保证原子性?
一个被volatile修饰的变量,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行n++这样的非原子操作,如果多个线程操作n++,就会出现写值丢失的情况,线程不安全,也就不会保证这个变量的原子性了。
C、既然volatile不保证原子性,如何解决?
1、synchronized方法进行加锁,效率低;
2、使用原子包装类Atomic,保证线程安全。
例如:AtomicInteger n=new AtomicInteger(),默认为0,
通过其方法atomic_i.getAndIncrement(); // 原子操作+1 实现原子操作
D、你在哪些地方使用过volatile?
1、单例模式:并发多线程环境下,懒汉单例模式,按道理应该实例化一次,实际却实例化很多次(而且不确定次数)【8种单例模式】
如何解决:
a、方法1 synchronized: public synchronized static testSingletonDemo getInstance()
b、方法2:DCL(Double Check Lock 双端检锁机制)+ volatile关键字
2、CAS
示例:
- 饿汉单例模式: 类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快(线程安全)
- 懒汉单例模式(线程不安全)
- 懒汉单例模式优化-001(线程安全:加锁synchronized)
- 懒汉单例模式优化-002(线程安全:volatile+双端检锁机制)
3.2 谈一谈对JMM的理解
JMM(Java内存模型)本身是一种抽象的概念,并不真实存在,它描述的是一组规范或规则,通过这组规范定义了程序中各个遍历的访问方式。
JMM特性:可见性、原子性、禁止指令重排列(保证有序性)
JMM关于线程同步的规定:
1、线程加锁前,必须读取主内存中共享变量的最新值读到自己的工作内存
2、线程解锁前,必须把共享变量的值刷新到主内存。
3、加锁、解锁是同一把锁。
由于JVM运行程序的实体是线程,而每个线程创建时JVM会为其分配一个工作内存(也叫做栈空间),工作内存是每一个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享区域,所有线程都可以访问。但线程对变量的读写操作必须在自己的工作内存中进行,因此,
1、首先线程要将变量从主内存拷贝到自己的工作内存
2、然后线程在自己的工作内存对变量进行操作
3、操作完成后再将变量的值写回主内存
各个线程的工作内存存储的是主内存的变量副本拷贝,不能直接操作主内存的变量。因此不同线程无法访问对方的的工作内存,线程间的通信必须通过主内存来完成,过程如下:
JMM线程安全性如何得到保证?
1、工作内存与主内存同步延迟导致的可见性问题,可以通过synchronized或volatile关键字解决,可以使一个线程修改后的变量立即对其他线程可见。
2、对于指令重排导致的有序性问题,可以通过volatile关键字解决,它可以禁止指令重排优化。
3.3 谈一谈对CAS及底层原理的理解
CAS是Compare-And-Swap,它是一条CPU并发原语,它的功能是判断内存某个变量的值是否为期望值,如果是则返回true,并更改为新值,如果不是则返回false,这个过程是原子操作过程。
典型应用是原子操作类AtomicInteger的compareAndSet方法:
compareAndSet(int expect, int update)
如果主内存真实值 == 线程期望值,则以原子方式将该值设置为给定的更新值,返回 true
如果主内存真实值 != 线程期望值,返回 false,说明实际值与预期值不相等
再次强调,CAS是一种系统原语,由若干条指令组成,原语的执行必须是连续的,在执行过程中不可以中断,也就是说CAS是一条CPU的原子指令,不存在数据不一致的情况,也就是线程安全。
CAS底层原理:Unsafe类与自旋锁
- Unsafe是CAS核心类,由于Java无法直接访问底层操作系统,需要调用native本地方法来访问,Unsafe相当于一个后门,基于Unsafe类可以直接像C语言指针一样操作特定内存中的数据,并直接调用底层资源执行相应任务。
- Unsafe类中的所有方法都是native修饰,Java中CAS操作的执行都依赖于Unsafe中的方法。
// 举例:getAndIncrement底层实现
getAndIncrement底层实现:底层getAndAddInt()采用CAS思想
this: new* 出来的对象
VALUE: 内存地址(内存偏移量)
1: 变动的数量
执行过程:假设线程A和线程B并发执行getAndAddInt操作:
1、AtomicInteger里面的value值原始值为3,根据JMM,线程A、B各自持有一份值为3的变量副本在其工作内存。
2、线程A通过getIntVolatile得到value的值为3,此时线程A被挂起。
3、线程B也通过getIntVolatile得到value的值为3,并立即执行compareAndSetInt比较内存的值也为3,成功修改value=4,结束收工。
4、此时线程A恢复,执行compareAndSetInt,发现自己工作内存的value值为3,与主内存的4不一致,修改失败,只能重新读取主内存的value值。
5、线程A重新获取value值,因为value被volatile修饰,根据可见性原理它可以立即知道vlaue最新值,然后执行compareAndSetInt进行比较替换,知道成功。
总结:因为每个线程执行过程中多了一步将本线程的变量值与主内存变量值比较的过程,所以保证了线程安全。
CAS缺点1、由于通过循环do..while来实现,没有加锁,如果CAS长时间不成功,会给CPU带来很大的开销2、只能保证一个共享变量的原子操作3、引发ABA问题AtomicInteger为什么使用CAS不使用synchronized?
1、synchronized是对方法加锁,同一时间段只能有一个线程访问,数据一致性得到保证,但是并发性下降;
2、CAS使用do while语句,没有加锁,反复将期望值与真实值对比,即保证了数据一致性,也提高了并发性
3.4 谈一谈对ABA问题及原子更新引用的理解
ABA问题:俗称狸猫换太子,也即是说,CAS算法实现的一个重要前提是需要取出主内存某时刻的数据并在当下时刻比较并替换,那么在这个时间差内出现数据的变化,比如下面这种情况,尽管线程Thread01操作成功,但是并不代表这个过程没有问题。
那如何解决ABA问题?
带时间戳的原子引用AtomicStampedReference
// todo 原子引用
3.5 谈一谈集合不安全的情况
- HashMap
put(K, V) // 将键/值映射存放到Map集合中,key不允许重复,否则会覆盖已有的key对应的值,key、value允许为null
remove(key) //删除Map集合中键为key的数据并返回其所对应value值。
get(key) //返回指定键所映射的值,没有该key对应的值则返回null
isEmpty () //判断Map集合中是否有数据,如果没有则返回true,否则返回false
containsKey(Object key) //判断集合中是否包含指定键
containsValue(Object value) //判断集合中是否包含指定值
size() //返回Map集合中数据数量
1.底层数据结构:
jdk7:数组+链表,数组初始长度16,扩容数组大小增加为原来的2倍
jdk8:数组+链表+红黑树,数组初始长度16,当链表长度大于8,会将链表转为红黑树
2.实现原理
JAVA7:数组+链表
大方向上,HashMap 底层是一个数组,然后数组中每个元素是一个单向链表
put(key, value)执行过程:
1、根据输入的key,计算key的哈希值Hash(key),并得到对应数组下标:Hash(key)%N。
2、当计算得到的下标出现一致时,即哈希冲突,此时需要存储到上一个entry链表的下一个节点,HashMap通过链址法解决哈希冲突。get(key) 执行过程:
1、首先根据key得到一个数组下标Hash(key)%N
2、然后比较该下标hash值与hash(key)值是否一致,若key值也一致,则返回对应的value(key、hash(key)都一致)
JAVA8:数组+链表+红黑树
根据 Java7 的介绍,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。
为了解决链表过长查询效率过低的问题,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,降低时间复杂度为 O(logN)。
put(key, value)执行过程:
1、根据输入的key,计算key的哈希值Hash(key),并得到对应数组下标:Hash(key)%N。
2、当计算得到的下标出现一致时,即哈希冲突,此时需要存储到上一个entry链表的下一个节点,HashMap通过链址法解决哈希冲突。 3、当链表长度大于8时,将链表改为红黑树!!
// java8默认类成员变量值static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量16(必须是2的倍数),也就是数组初始化容量为16static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量 1073741824 2^30static final float DEFAULT_LOAD_FACTOR = 0.75F; // 默认扩充因子static final int TREEIFY_THRESHOLD = 8; // 当链表长度大于此值时,将链表转成红黑树static final int UNTREEIFY_THRESHOLD = 6; // 当红黑树元素小于此值时,将红黑树转换成链表static final int MIN_TREEIFY_CAPACITY = 64; // 当链表长度大于此值且容量大于64时,才允许树形化链表(即将链表转换成红黑树)int threshold; // threshold=capacity*loadFactor扩容阈值 表示当HashMap的size大于threshold时会执行resize(扩容)操作 final float loadFactor;// 装载因子=size/capacity,是用来衡量 HashMap 满的程度,而不是占用桶的数量去除以capacity
PS: 二叉搜索树-平衡二叉树-红黑树
口诀:黑根黑叶红不邻【2-3-4】,同祖等高只数黑【5】
- 为什么ArrayList是线程不安全的?
ArrayList的底层实现:
1、用了一个Object[]的数组,用来保存所有的元素;
2、用了一个size变量,用来保存当前数组中已经添加了多少元素。
add方法线程不安全:
add方法添加一个元素的时候,它可能会有两步来完成:
1. 在elementData[size]的位置存放此元素;
2. 集合个数加1:size+1在单线程运行的情况下,如果size=0,添加一个元素后,此元素在位置elementData[0],而且size=1,没有问题
但在多线程情况下,比如有两个线程,1. 线程A先将元素存放在位置0,但还没有执行size+1操作,随后线程A暂停,线程B得到运行机会。【elementData[0]=A,size=0】2. 线程B也添加元素,因为此时size=0,所以线程B也将元素存放在位置0,并执行size+1操作。【elementData[0]=B,size=1】3. 线程A继续运行,执行size+1操作【此时elementData[0]=B,size=2】此时集合元素实际上只有一个,存放在位置0,而 size=2。这就是“线程不安全”了。
- 为什么HashMap是线程不安全的?
Java7 HashMap线程不安全体现在:由于多线程对HashMap进行扩容,调用了HashMap#transfer(),导致线程死循环、数据丢失具体原因:某个线程执行过程中被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
Java8 HashMap线程不安全体现在:由于多线程对HashMap进行put操作,调用了HashMap#putVal(),导致数据覆盖1. 假设线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的2. 当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,3. 然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
1、故障现象java.util.ConcurrentModificationException2、导致原因并发线程争抢资源,出现数据不一致,导致写值丢失的现象。类似于花名册签字案例3、解决方案解决方案一: 使用各自线程安全的集合List<Object> al=new Vector<>(); // ArrayList -> Vector(synchronized)Map<String,String>map=new ConcurrentHashMap<>(); // HashMap -> ConcurrentHashMap、HashTablePS:一般不采用HashTable,其效率低下,需要线程安全就使用concurrentHashMap,不需要线程安全就使用HashMap解决方案二:Collections的静态方法,同步线程Collections.synchronizedList(al);Collections.synchronizedMap(mp);Collections.synchronizedSet(st);解决方案三: CopyOnWrite容器,没有CopyOnWriteMapList<Object> al=new CopyOnWriteArrayList<>()Set<Object> set=new CopyOnWriteArraySet<>();
CopyOnWriteArrayList原理:写时复制、读写分离、lock
CopyOnWrite容器是一个写时复制的容器,往容器内添加元素的时候,不直接在容器内添加,而是1、先将当前容器Object[]进行复制copy,复制得到一个新的容器Object[] newElements2、然后向新容器添加元素,并将原容器的引用指向当前容器。(好处:可以对CopyOnWrite并发的读,而不需要加锁。因为当前容器不会添加任何元素)3、其他线程使用新的容器newElements。所有CopyOnWrite是一种读写分离的思想,读和写不同的容器。
3.6 谈一谈Java常用锁
主要包括:公平锁/非公平锁/可重入锁(递归锁)/自旋锁/独占锁(写锁)/共享锁(读锁)/读写锁
1. 公平锁: 在多线程环境下,按照线程申请锁的顺序来获取锁,性能不高,类似排队打饭,先来后到
2. 非公平锁: 指线程获取锁的顺序不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获得锁。线程执行效率提高,吞吐量大,但是在高并发的环境下,有可能会造成优先级反转或饥饿现象。
3. 可重入锁: 又称为递归锁,指同一线程外层函数获得锁之后,内层递归函数仍然能获取锁的代码,即同一线程在外层方法获取锁后,进入内层方法自动获取锁,可以防止死锁
4. 自旋锁: 指尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁,这样可以减少线程上下文切换的消耗,缺点是循环会消耗CPU资源。典型Unsafe.getAndAddInt()
5. 读写锁:读时共享、写时独占可重入读写锁ReentrantReadWriteLock,基本上可以取代synchronizedprivate ReentrantReadWriteLock rwlock=new ReentrantReadWriteLock();rwlock.writeLock().lock(); //加写锁rwlock.readLock().lock(); //加读锁锁升级:1. 锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。2. 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级为重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
3.7 谈一谈常见的线程计数器
a、CountDownLatch:让一些线程阻塞直到另一些线程完成一系列操作才被唤醒,初始值为target,计数器=0再执行(做减法):(以前我们都是第七个线程睡眠一段时间确保前六个线程执行完)CountDownLatch countDownLatch = new CountDownLatch(6); countDownLatch.countDown();//计数器减1countDownLatch.await();//调用线程会被阻塞,当计数器为0时,才会被唤醒b、CyclicBarrier:字面意思是可循环使用的屏障,让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门。初始值==0,计数器==target再执行(做加法)CyclicBarrier c=new CyclicBarrier(7,()->{System.out.println("最后执行操作,召唤神龙");}); c.await();//先来的线程,被阻塞c、Semaphore信号量机制:多个线程争抢多个共享资源。主要用于两个目的,一是用于多个共享资源的互斥使用,另一个是用于控制并发线程数目(争车位)Semaphore s=new Semaphore(3);//共享资源是3个停车位s.acquire();//占到车位s.release();//释放资源
3.8 谈一谈阻塞队列
阻塞队列:首先是一个队列,当阻塞队列为空时,从队列中获取元素的操作会被阻塞;当阻塞队列为满时,往队列里添加元素的操作将会被阻塞。
阻塞队列的优点:我们不需要决定什么时候去阻塞线程,什么时候去唤醒线程(不需要考虑wait、notify),因为这一切阻塞队列BlockingQueue一手包办了。在并发concurrent包发布之前,在多线程环境下,程序员需要控制这些线程同步的细节,并且兼顾系统的效率和安全性。
如何高效管理阻塞队列:ArrayBlockingQueue: 由数组结构组成的有界阻塞队列LinkedBlockingQueue: 由链表结构组成的有界阻塞队列(但大小默认为Integre.MAX_VALUE)也可以认为是无界阻塞队列(谨慎使用)SynchronousQueue: 不存储元素的阻塞队列,也即单个元素的队列(有且仅有1个元素)PriorityBlockingQueue: 支持优先级排序的阻塞队列DelayQueue: 使用优先级队列实现的延迟无界阻塞队列LinkedTransferQueue: 由链表结构组成的无界阻塞队列LinkedBlockingDeque: 由链表结构组成的无向阻塞队列
核心方法:
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
增加 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
删除 | remove() | poll() | take() | poll(time ,unit) |
查询 | element() | peek() | NO | NO |
抛出异常:当阻塞队列满时,再添加元素add(e)会抛出异常:lleagalStateException:Queue Full当阻塞队列空时,在删除元素remove()、remove(e) 会抛出异常:NoSuchElementException特殊值:插入方法offer(), 成功为true,失败为false删除方法poll(), 成功为队内元素,失败为null阻塞:当阻塞队列满时,生产者再继续往队列添加元素put(e),队列会一直阻塞直到成功或者响应中断推出。当阻塞队列空时,消费者线程试图从队列take元素,队列会一直阻塞消费者进程直到队列可用。超时:建议使用阻塞队列用在哪里?1、生产者消费者模式(传统版、阻塞队列版)待定!!!2、线程池3、消息中间件
3.9 谈一谈线程池以及ThreadPoolExecutor
线程池就是容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象、销毁线程的操作,避免系统资源的浪费。
线程池的优势?线程复用: 通过线程复用降低线程的创建和销毁造成的消耗提升响应速度: 当任务到达时,任务可以不需要等到线程创建就可以执行。提高线程的可管理性: 控制线程的并发数量,统一分配、管理线程常用的线程池
1、Executors.newCachedThreadPool(int) 执行短期异步的小程序SynchronousQueue
2、Executors.newFixedThreadPool(int) 执行长期的任务 LinkedBlockingQueue
3、Executors.newSingleThreadExecutor(int) 任务按序执行 LinkedBlockingQueue
4、Executors.newScheduledThreadPool(int) 延迟重复执行线程synchronized保证三大性:原子性,有序性,可见性
volatile保证有序性,可见性,不能保证原子性
- 常用线程池底层实现
- 自定义线程池ThreadPoolExecutor
1:核心线程数(corePoolSize): 线程池中常驻核心线程数
2:最大线程数(maximumPoolSize): 线程池能够容纳同时执行的最大线程数
3:keepAliveTime: 非核心线程最大存活时间,若当前线程池数量超过核心线程数corePoolSize,当空闲时间达到keepAliveTime,多余非核心线程会被销毁直到只剩下corePoolSize个线程。
4:unit: keepAliveTime的单位
5:任务队列(workQueue): 被提交但尚未执行的任务,存放在阻塞队列中
6:线程工厂(threadFactory): 用于创建线程,一般默认即可。
7:拒绝策略(handler): 表示当等待队列满了并且工作线程大于等于线程池的最大线程数常见拒绝策略:AbortPolicy(默认): 直接抛出异常RejectedExecutionException,阻止系统正常运行CallerRunsPolicy: 调用者优先策略,该策略不会抛弃任务,也不会抛出异常,而是将某些任务回退DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务DiscardPolicy: 直接丢弃任务,不做任何处理也不抛出异常。如果允许任务丢失,可以使用
- 底层原理
1、在创建了线程池之后,等待提交过来的任务请求
2、当调用execute()方法后添加一个请求任务2.1 如果正在运行的线程数量小于corePoolSize,则立即执行该任务2.2 如果正在运行的线程数量大于等于corePoolSize,则将该任务放进队列 workQueue2.3 如果任务队列满了,并且正在运行的线程数量小于maximumPoolSize,那么线程池创建非核心线程执行任务2.4 如果任务队列满了,并且正在运行的线程数量大于等于maximumPoolSize,那么线程池启动拒绝策略执行。3、当一个线程完成任务时,它会从队列取下一个任务执行
4、当一个线程无事可做超过一定的时间keepAliveTime,线程池会判断4.1 如果运行的线程数大于corePoolSize,那么会停掉该非核心线程4.2 最后线程池的线程数目收缩到corePoolSize的大小
3.10 谈一谈死锁以及定位分析
死锁:指的是两个或两个以上进程在执行过程中,因争夺资源而造成的互相等待的现象,若无外力干涉,那他们将无法继续推进下去,如果系统资源充足,进程的资源请求都能得到满足,死锁出现的可能性较低,否则会因争夺有限的资源而陷入死锁。(系统资源不足、进程推进顺序不合适、资源分配不当)
故障分析:
1、打开IDEA控制台
2、定位Java线程:jps -l
3、查看异常进程号:jstack 531
常用命令行:
1、jps: 查看本机后台java进程信息。 jps -l
2、jinfo: 查看正在运行Java程序相关信息jinfo -flag 配置项 进程号jinfo -flags 进程号
3、jstack: 打印线程的栈信息,制作线程dump文件(发现死锁)
4、jmap: 打印内存映射,制作堆dump文件
5、jstat: 性能监控工具
6、jhat: 内存分析工具
7、jconsole: 简易的可视化控制台
8、jvisualvm: 功能强大的控制台
问题一:synchronized和volatile区别1、synchronized可以修饰方法,代码块,而volatile只能用于修饰变量。2、sychronized可以保证线程的原子性、可见性、有序性,而volatile能保证变量在多个线程之间的可见性、有序性,但不能保证原子性。3、多线程访问sychronized会出现阻塞,而访问volatile不会发生阻塞。4、volatile是线程同步的轻量级实现,所以volatile性能比synchronized要好,但是sychronized关键字在执行效率上逐渐得到提升,在开发中使用synchronized关键字的比率还是比较大。问题二:synchronized和lock区别1、synchronized是Java中的关键字,是内置的语言实现,而Lock是一个接口;2、synchronized在发生异常时,会自动释放线程占有的锁,因此不会发生死锁;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此需要在finally块中释放锁;3、通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。4、Lock可以提高多个线程进行读操作的效率(读写锁)。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
3.11 谈一谈强引用、软引用、弱引用和虚引用
强引用:默认支持模式,当内存不足时,JVM会进行垃圾回收,对于强引用的对象,就算是出现了OOM,也不会对该对象进行回收。只要有一个强引用指向一个对象,就说明对象还活着,垃圾回收器不会回收这类对象。因此强引用是造成Java内存泄露的主要原因之一。【JVM永不回收】
软引用:是一种相对强引用弱化了一些的引用,需要用java.lang.SoftReference类来实现,当系统内存充足时,该对象不会被回收,当系统内存不足时,该对象会被垃圾收集器回收。软引用通常应用在对内存敏感的程序中,比如高速缓存。【JVM内存够用的时候就保留,不够用就回收】
弱引用:弱引用需要用java.lang.weakReference类来实现,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会立即回收该对象占有的内存。【JVM立即回收】
虚引用:虚引用需要java.lang.ref.PhantomReference类来实现,就是形同虚设,虚引用不会决定对象的生命周期**,在任何时候都会被垃圾回收器回收**,并且不能单独使用,必须要和引用队列ReferenceQueue联合使用。换句话说,虚引用的唯一作用,就是在这个对象被垃圾收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
软引用应用场景:
假如有一个应用需要读取大量的本地图片:如果每次读取图片都从硬盘读取则会严重影响性能,如果一次性全部加载到内存中又可能造成内存溢出。此时使用软引用可以解决这个问题。设计思路:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
3.12 谈一谈六种常见的OOM
java.lang.StackOverFlowError 栈溢出:方法递归
java.lang.OutOfMemoryError:Java heap space 堆溢出:new大对象
java.lang.OutOfMemoryError:Metaspace 元空间异常, 存储类信息、常量池、静态类
java.lang.OutOfMemoryError:Direct buffer memory 直接内存溢出(本地内存溢出),主要由NIO引起,ByteBuffer.allocate(capacity); // 分配JVM堆内存,属于GC管辖范围,由于需要拷贝速度较慢ByteBuffer.allocateDirect(capacity); // 分配本地内存,不属于GC管辖范围,若本地内存用光,那程序崩溃,抛出该异常java.lang.OutofMemoryError:GC overhead limit exceeded GC回收时间过长,超过98%时间进行垃圾回收GC,并且回收不到2%的堆内存
java.lang.OutOfMemoryError:unable to create native thread 无法创建新的native线程,准确来讲,该异常与对应的平台有关
导致原因:1、你的应用创建的太多的线程,超过系统承载极限2、你的服务器并不允许创建那么多的线程,如Linux默认单个进程只可以创建1024个线程
解决方法:1、降低线程的数量2、扩大Linux默认线程设置的限制
4. JVM核心知识点
4.1 JVM垃圾回收如何确定垃圾及GC Roots
垃圾:内存中已经不再被使用的对象被成为垃圾
- 如何判断一个对象是否可以被回收?
**引用计数法:**简单来说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器加一,每当有一个引用失效,计数器减一,任何时刻对象的计数器的值为0,那么这个对象就是可回收对象。一般该方法已被舍弃,主要原因在于很难解决对象之间互相循环引用的问题。
根搜索路径(枚举根节点做可达性分析):为了解决引用计数法的循环引用问题,Java使用通过GC Roots做可达性分析的根搜索路径方法。通过一系列名为“GC roots”的对象作为起点,从GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明该对象可回收。
GC Roots的对象:1、虚拟机栈中引用的对象 局部变量表2、方法区中的类静态属性引用的对象 static3、方法区中常量引用的对象 final4、本地方法栈native引用的对象 native
4.2 常用的JVM基本配置参数
JVM的参数类型1、标配参数:-version -help -showversion2、X参数(了解): -Xint:解释执行 -Xcomp:第一次使用就编译成本地代码 -Xmixed:混合模式3、XX参数:重点!!!Boolean类型:-XX: + 属性值、-XX: - 属性值(+:开启、-:关闭)例:-XX:+PrintGCDetails 打印GC日志-XX:+UseSerialGC 、-XX:-UseG1GC 打开/关闭某垃圾收集器java -XX:+PrintFlagsInitial 查看JVM所有初始化参数java -XX:+PrintFlagsFinal 查看JVM所有修改、更新后的参数java -XX:+PrintCommandLineFlags -version 查看内存空间初始值KV设值类型:-XX:key=value常见参数:
-XX:NewSize=10m 新生代
-XX:OldSize=60m 老年代
-XX:PermSize=50m 永久代
-XX:MaxPermSize=50m 永久代最大内存
-XX:MetaspaceSize=128 元空间, 理论上取决于物理内存,实际只有20MB左
-XX:MaxTenuringThreshold=15: 控制对象在经过多少次minor GC之后进入老年代,此参数只有在Serial 串行GC时有效。-Xms: 堆初始内存大小,默认为物理内存的1/64,等价于 -XX:InitialHeapSize
-Xmx: 堆最大内存大小,默认为物理内存的1/4, 等价于 -XX:MaxHeapSize
-Xmn: 设置年轻代的大小,默认为堆内存的1/3 等价于 -XX:NewSize=n
-XX:SurvivorRatio=3: Eden区 / Survivor区=3 (PS:年轻代有2个Survivor区)
-XX:NewRatio=3: 年轻代Young / 老年代Old=3
案例一:查看某个程序配置项信息:jinfo -flag 配置项 进程号
查看某个程序所有配置项信息:jinfo -flags 进程号
案例二:查看本机MetaspaceSize大小,并修改为256MB
1、jps -l 查询后台运行的进程号码
2、jinfo -flag MetaspaceSize 1030 指定进程号的配置项信息
3、修改
4、再次测试(修改成功)
查看系统默认参数
第一种:
1、jps -l 查看本机后台java进程信息
2、jinfo -flag 配置项 进程号 查看某个进程配置项相关信息
3、jinfo -flags 进程号 查看某个进程所有配置项信息第二种:
1、Java -XX:+PrintFlagsInitial 查看JVM所有初始化参数
2、Java -XX:+PrintFlagsFinal 查看JVM所有修改、更新后的参数= JVM默认加载 := 用户修改的
3、java -XX:+PrintFlagsFinal -XX MetaspaceSize=512m T 运行时参数修改
4、java -XX:+PrintCommandLineFlags -version 查看内存空间初始值,初始垃圾回收器
总结:
查看初始垃圾回收器jinfo -flags进程号java -XX:+PrintCommandLineFlags -version 查看某个初始配置项参数值jps -l 查看本机后台java进程信息jinfo -flag 配置项 进程号 查看某个进程配置项相关信息
4.3 垃圾收集算法与垃圾回收器
四大垃圾回收算法:引用计数法、复制算法、标记清除、标记整理
落地实现:串行Serial、并行Parallel、并发CMS、G1七大垃圾回收器:UseSerialGC、UseParNewGC、UseParallelGC –> 新生代
UseSerialOldGC、UseConcMarkSweepGC、UseParallelOldGC、UseG1GC ->老年代
case1:查看本机使用的垃圾收集器(java -XX:+PrintCommandLineFlags -version)case2:配置串行Serial收集器 (年轻代:UseSerialGC串行 老年代:UseSerialOldGC串行)-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGCcase3:配置并行ParNew收集器:不推荐使用,后老年代使用CMS(年轻代:UseParNewGC并行 老年代:UseSerialOldGC串行) -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGCcase4:配置并行Parallel收集器(年轻代:UseParallelGC并行 老年代:UseParallelOldGC并行) -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGCcase5:配置并发CMS收集器(年轻代:UseParNewGC并行 老年代:UseConcMarkSweepGC并行) -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGCcase6:配置G1集器-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC怎么选择垃圾收集器?
1.优先调整堆的大小让服务器自己来选择
2.如果单核CPU或小内存(<100m), 使用串行收集器Serial、Serial old
3.如果多核CPU需要最大吞吐量(如后台计算型引用) 使用并行收集器Parallel、Parallelold
4.如果多核CPU需要低停顿时间(如需快速响应的电商) 使用并发收集器CMS、ParNew
官方推荐G1,性能高。
4.4 Linux系统变慢如何定位
(整机->内存->磁盘->CPU->IO)
a、查看整体机器性能: top 分析过程:(cpu/mem->id->load avg)1、先整体查看cpu 与mem内存占用率:是否存在某个线程占用的cpu或内存太高。2、再单个CPU占用率:因为现在都是多核cpu,按数字1,着重查看id(idle) cpu空闲率,id越大越好3、最后查看load average系统负载率:分别是1分钟,5分钟,15分钟的系统平均负载率,load average:2.33,2.51,2.12 ,如果(a+b+c)/3>6,说明系统负载率太高。b、查看内存信息 free -mfreefree -gfree -m(以MB字节查看内存信息)c、查看磁盘信息 df -h
d、查看包含但不限于CPU也有内存 vmstat -n 2 3
e、查看磁盘IO信息 iostat -xdk 2 3
4.5 Github骚操作
in、stars、forks、#L、location、language
1、通过in关键词限制搜索范围 java in:name,description,readme 查找项目名name、描述description、readme中包含关键字java的github项目2、通过 stars 或者forks数 去查找项目java stars:2000..5000 forks:20..50 查找点赞数stars在[2000,5000],收藏fork数[20,50]的java项目java stars:>5000 forks:>20 查找点赞数stars大于5000,收藏fork数大于20的java项目
综合:springboot项目 in name,description stars:>5000 forks:>20 pushed:>2020-01-01 3、awesome 关键字awesome java 查找关于java的教程精品4、分享项目中某一行的代码
https://github.com/MrZhu1/solidity/blob/master/10-test_Array.sol#L23 高亮显示github关键代码的行号 #L23
https://github.com/MrZhu1/solidity/blob/master/10-test_Array.sol#L23-L27高亮显示github关键代码的段落 #L23-L275、搜索某个地区内的大佬
location:nanjing language:java 查找活跃用户