尚硅谷面试第二季(周阳主讲)
尚硅谷面试第二季
- 1.volatile关键字
- volatile是什么
- volatile的作用
- 1.保证可见性
- 2.不保证原子性
- 3.禁止指令重排
- DCL(单例模式双重锁)
- JMM模型
- JMM是什么
- JMM关于同步规定
- JMM的工作流程
- 2.CAS
- CAS是什么
- CAS的功能
- 为什么使用CAS来代替Synchronized
- CAS底层原理
- CAS的缺点
- 3.集合类不安全问题
- ArrayList线程安全问题
- 如何解决并发修改异常?
- HashSet线程安全问题
- Map线程安全问题
- 4.Java锁
- 公平锁和非公平锁
- 公平锁
- 非公平锁
- 可重入锁(又名递归锁)
- 自旋锁
- 独占锁(写锁)/共享锁(读锁)/互斥锁
- CountDownLatch
- CyclicBarrier
- Semaphore
- 5.阻塞队列
- 阻塞队列是什么?
- 阻塞队列的好处?
- 阻塞队列的种类
- SynchronousQueue
- 阻塞队列常用四组API
- 生产者消费者实现
- 6.synchronized与lock的区别
- 7.Callable
- 8.线程池
- 8.1为什么使用线程池?
- 8.2线程池的架构
- 8.3线程池的五大种类
- 源码解析
- 8.4线程池的七大参数
- 8.5线程池的处理流程
- 8.6线程池的拒绝策略
- JDK内置的四种拒绝策略
- 8.7自定义线程池
- 9.死锁编码及定位分析
- 死锁的产生
- 死锁的定位
- 10.JVM部分
- GC Roots理解
- JVM参数配置
- 查看JVM默认参数
- 常用参数
- GC日志阅读
- 11.四大引用
- 强引用
- 软引用
- 弱引用
- 虚引用
- 软引用和弱引用使用场景
- WeakHashMap
- 12.OOM
- Java异常和错误的继承体系
- Java中常见错误
- StackOverflowError
- OOM:java heap space
- OOM:GC overhead limit exceeded
- OOM:direct buffer memory
- OOM:unable to create new native thread
- OOM:metaspace
- 13.垃圾回收器
- 如何确定垃圾?哪些对象已经死亡?
- GC有哪些垃圾回收算法?
- 垃圾回收器
- 垃圾收集器的四大类
- 默认垃圾回收器、JVM参数配置配置
- JVM垃圾回收日志参数说明
- 七大垃圾回收器详解
- 串行垃圾回收器(Serial/Serial Copying)(1:1)
- 并行垃圾回收器(ParNew)(n:1)
- 并行收集器(Parallel Scavenge)(n:n)
- Parallel Old收集器
- 并发标记清除收集器(CMS)
- Serial Old收集器
- G1垃圾回收器
- 垃圾回收器选择
- 14.Linux常用命令
- 生产服务器变慢,谈谈诊断思路和性能评估?
- 整机
- CPU
- 内存
- 硬盘
- 磁盘IO
- 网络IO
- 假如生产环境出现CPU占用过高,谈谈分析思路以及定位
- 15.Github骚操作
- in操作
- star/fork操作
- awesome加强搜索
- #L操作
- 项目内搜索
- 搜索某语言某地区的活跃大佬
1.volatile关键字
volatile是什么
volatile是Java虚拟机提供的轻量级同步机制。
volatile的作用
1.保证可见性
解释:当某个线程从主内存中获取到共享数据的变量副本,并进行修改,其他的线程能狗第一时间感知到。
代码验证:
import java.util.concurrent.TimeUnit;/*** @author: LiuBen* @Date: 2021/10/11 13:52:08* @Description:** 验证Volatile可见性*/
public class VolatileDemo1 {public static void main(String[] args) {MyData myData = new MyData();new Thread(()->{System.out.println(Thread.currentThread().getName()+"进来了~");try {TimeUnit.SECONDS.sleep(3);}catch (Exception e){e.printStackTrace();}myData.addTo60();System.out.println(myData.number);},"线程1").start();while (myData.number == 0){//没有感知到,main线程一直等待,知道number不等于0}System.out.println(Thread.currentThread().getName()+"线程感知到number="+myData.number);}
}
class MyData{volatile int number = 0;public void addTo60(){this.number = 60;}
}
2.不保证原子性
解释:多个线程对volatile修饰的变量进行修改时,可能会出现覆盖的问题。(i++是线程不安全的)
代码验证:
public class VolatileDemo1 {public static void main(String[] args) {MyData myData = new MyData();for (int i = 0; i < 20; i++) {new Thread(()->{for (int j = 0; j < 100; j++) {myData.addPlus();myData.addAtomic();}},String.valueOf(i)).start();}while (Thread.activeCount() > 2){Thread.yield();}System.out.println(myData.number);System.out.println(myData.atomicInteger);}
}
class MyData{volatile int number = 0;AtomicInteger atomicInteger = new AtomicInteger();public void addTo60(){this.number = 60;}public void addPlus(){number++;}public void addAtomic(){atomicInteger.getAndIncrement();//此处详见CAS}
}
3.禁止指令重排
解释:计算机执行程序时,为了提高性能,会对指令进行重新排序。
源代码———>编译器优化的重排———>指令并行的重排———>内存系统的重排———>最终执行的命令。处理器在进行重排时,必须要考虑的因素是指令之间的数据依赖性。多线程环境下,由于编译器指令重排,导致两个线程中使用的变量一致性无法保证。volatile实现禁止指令重排,避免了多线程下程序乱序执行的结果。
volatile为什么能进行指令重排:
内存屏障(Memory Barrier):是一个CPU命令。由于编译器和处理器都能进行重排优化,如果插入一条Memory Barrier则会告诉编译器和处理器,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说插入内存屏障指令能够禁止在内存屏障前后的指令执行重排序优化。另一个作用是:强制刷出各种CPU的缓存数据,因此CPU上的线程能够读取到这些数据的最新版本。
DCL(单例模式双重锁)
public class SingletonDemo {private static volatile SingletonDemo instance = null; //使用volatile可以防止下面3条语句发生指令重排变成:1 -> 3 -> 2private SingletonDemo() {System.out.println(Thread.currentThread().getName()+"我是构造方法SingletonDemo()");}public static SingletonDemo getInstance() {if (instance == null) {synchronized (SingletonDemo.class) {if (instance == null) {// 以下语句实际等同于3条语句:// 1. memory = allocate(); //分配对象内存空间// 2. instance(memory); //初始化对象// 3. instance = memory //设置instance指向分配的内存地址,此时INSTANCE != nullinstance = new SingletonDemo();}}}return instance;}public static void main(String[] args) {for (int i = 0; i < 20; i++) {new Thread(()->{for (int j = 0; j < 100; j++) {SingletonDemo.getInstance();}},String.valueOf(i)).start();}}
}
JMM模型
JMM是什么
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
JMM关于同步规定
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存
3.加锁解锁是同一把锁
JMM的工作流程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成在这里插入代码片
2.CAS
CAS是什么
CAS(Compare And Swap):比较并交换,cas是一条CPU并发原语(涉及到汇编语言Atomic::cmpxchg(x, addr, e) == e),原语就是一条CPU的原子指令,必须是连续的不允许被打断,不会造成数据不一致问题。
CAS的功能
它的功能是判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个操作是原子的。
就JMM模型分析,线程1从主内存读取变量为变量副本A,判断从主内存的预期值,与获取的变量副本是否相同,相同则修改。
为什么使用CAS来代替Synchronized
Synchronized加锁在同一个时间内只允许一个线程方法,能够保证一致性,但是并发性大大下降。使用CAS是使用do-while,既能够保证一致性也能够保证并发性。
CAS底层原理
//此处调用atomicInteger.getAndIncrement()底层用到CAS
AtomicInteger atomicInteger = new AtomicInteger(5);
atomicInteger.getAndIncrement();`
Unsafe是CAS的核心累类,Java无法访问底层系统,所以需要通过本地native方法来访问,Unsafe类可以调用native方法,来达到访问底层的目的。
通过偏移量来获取内存中的值。
通过volatile修饰value,从而保证了可见性。为后面do-while获取内存的值能够被感知到。
此处调用getAndAddInt,使用到了位于jdk中jre/lib/rt.jar(runtime)中的sun.misc.Unsafe类,里面大部分都是native方法。
/*** Atomically increments by one the current value.** @return the previous value* this 是当前对象 valueOffset 内存偏移量 1 加一*/public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);}
使用do-while,当预期值和从内存获取的值不相等时,重新do,因为value是volatile修饰,所以当内存值被修改导致不一致的时候,value是可见的,能够获取到最新 的value值进行比较。
用到了Unsafe类中的getIntVolatile(Object o, long offset);方法来获取内存中指定偏移量的值。compareAndSwapInt(Object o, long offset,int expected, int x);来判断期望值与内存中的值是否相等,相等则增加。
/*** Atomically adds the given value to the current value of a field* or array element within the given object <code>o</code>* at the given <code>offset</code>.** @param o object/array to update the field/element in* @param offset field/element offset* @param delta the value to add* @return the previous value* @since 1.8*/public final int getAndAddInt(Object o, long offset, int delta) {int v;do {v = getIntVolatile(o, offset);} while (!compareAndSwapInt(o, offset, v, v + delta));return v;}
CAS的缺点
- 并发性能加强,但是每次do - while的时候,数据被修改,导致不停的自旋,导致CPU压力变大。
- 只能保证一个变量的原子操作。
- ABA问题
ABA问题:
线程A某一时间从主内存中获取值1,此时线程B对主内存数据修改为2,此时线程A比较慢,线程B又将数据修改为1,此时线程A从内存中获取值1,发现是一样的进行修改。
ABA问题的解决:
1.采用AtomicStampedReference,每次修改时间戳都会改变,根绝时间戳不同,就算期望值相同也不能修改。
2.采用AtomicMarkableReference,每次修改都会改变是否被标记,来比较每次是否被标记,来看能否修改。
3.集合类不安全问题
ArrayList线程安全问题
ArrayList是线程不安全的。在并发写的时候会出现java.util.ConcurrentModificationException(并发修改异常)。因为arrayList底层是没有加锁的。
如何解决并发修改异常?
- List list = new Vector<>();使用了synchronized加锁。
- List list = Collections.synchronizedList(new ArrayList<>());底层直接将list传递给静态内部类SynchronizedList,调用静态内部类里面的add方法,里面加了synchronized修饰。类似于装饰器模式。
- List list = new CopyOnWriteArrayList<>();底层采用lock的方式,将当前容器复制到新的容器,操作之后,将指向原容器的引用指向新容器。
HashSet线程安全问题
HashSet底层就是一个HashMap,默认的HashSet是一个初始大小为16,负载因子为0.75的HashMap,所以HashSet的多线程安全问题实际上就是HashMap的多线程安全问题。
问?HashMap存储的是k,v键值对,那么你说HashSet底层是HashMap,到底是如何存储的?
HashSet存储的值就是HashMap存储的key,value则是一个固定常量。
线程不安全的解决方案?
- Set set = Collections.synchronizedSet(new HashSet<>());
- Set set = new CopyOnWriteArraySet<>();
Map线程安全问题
Map<String, Object> map = Collections.synchronizedMap(new
HashMap<>());Map<String, Object> map = new ConcurrentHashMap<>();
4.Java锁
公平锁和非公平锁
ReentrantLock和Synchronized默认都是非公平锁。
ReentrantLock可以通过构造函数参数来指定公平锁还是非公平锁。
公平锁
多个线程按照申请锁的顺序来获取锁。按照FIFO规则从等待队列中拿到等待线程获取相应锁。
非公平锁
多个线程不是按照申请锁的顺序来获取锁,可能后面的线程优先拿到锁。非公平锁的吞吐量大。
可重入锁(又名递归锁)
简单来说,就是访问锁住的方法,可以访问方法里面调用的也被锁住的方法。就称为可重入锁。
synchronized 和 ReentrantLock都是可重入锁。
public static void main(String[] args) {methodA();}private static synchronized void methodA() {methodB();}private static synchronized void methodB() {System.out.println("synchronized-methodA ------> synchronized-methodB");}
在使用ReentrantLock类演示可重入锁时,lock.lock()和lock.unlock()数量一定要匹配,否则:
- 当lock.lock()数量 > lock.unlock():程序一直运行
- 当lock.lock()数量 <lock.unlock():抛出java.lang.IllegalMonitorStateException异常
自旋锁
自旋锁的好处:尝试获取自旋锁的线程不会立即阻塞,而是通过不断的尝试去执行,减少了不必要的上下文的切换。
坏处:当处于一直自旋的状态下,会消耗CPU性能。
代码实例:
public class SpinLockDemo {AtomicReference<Thread> atomicReference = new AtomicReference<>();public void myLock(){Thread thread = Thread.currentThread();System.out.println("当前线程:"+Thread.currentThread().getName());while (!atomicReference.compareAndSet(null,thread)){}}public void myUnLck(){Thread thread = Thread.currentThread();System.out.println("当前线程"+Thread.currentThread().getName()+"正在解锁");atomicReference.compareAndSet(thread,null);}public static void main(String[] args) {SpinLockDemo spinLockDemo = new SpinLockDemo();new Thread(()->{spinLockDemo.myLock();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}spinLockDemo.myUnLck();},"线程1").start();try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->{spinLockDemo.myLock();spinLockDemo.myUnLck();},"线程2").start();}
}
独占锁(写锁)/共享锁(读锁)/互斥锁
- 写锁,指该锁只能被一个线程所持有,ReentrantLock和Synchronized都是独占锁。
- 读锁,可以被多个线程所持有。
- 读锁可以保证较大的并发性,读写,写写是互斥的。
public class ReadWriteLockDemo {public static void main(String[] args) {MyCache myCache = new MyCache();for (int i = 0; i < 5; i++) {int finalI = i;new Thread(() -> {myCache.put("key" + finalI, "value" + finalI);}, "线程" + i).start();}for (int i = 0; i < 5; i++) {int finalI = i;new Thread(() -> {myCache.get("key" + finalI);}, "线程" + i).start();}}
}//资源类
class MyCache {Map<String, Object> map = new HashMap<String, Object>();ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();public void put(String key, Object value) {rwLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "正在写入");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}map.put(key, value);System.out.println(Thread.currentThread().getName() + "写入\tkey:"+key+"\tvalue:"+value+"完成");} catch (Exception e) {e.printStackTrace();} finally {rwLock.writeLock().unlock();}}public void get(String key) {rwLock.readLock().lock();try {System.out.println(Thread.currentThread().getName() + "正在读取");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}Object value = map.get(key);System.out.println(Thread.currentThread().getName() + "读取完成\tkey:"+key+"\tvalue:"+value);} finally {rwLock.readLock().unlock();}}
}
CountDownLatch
CountDownLatch来给定一个置顶计数,使用 await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放。
public class CountDownLatchDemo {public static void main(String[] args) {CountDownLatch countDownLatch = new CountDownLatch(5);for (int i = 1; i <= 5; i++) {int finalI = i;new Thread(()->{System.out.println(StudentEnum.getStudentEnum(Integer.valueOf(finalI)).getMsg()+"离开教师");countDownLatch.countDown();},"线程"+i).start();}try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("班长锁门");}
}
CyclicBarrier
CyclicBarrier的意思是循环屏障,使一组线程到达一个屏障是被阻塞,只有最后一个线程到达屏障,屏障才会被打开。所有被屏障阻塞的方法都会被打开。
public class CyclicBarrierDemo {public static void main(String[] args) {CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{System.out.println("召唤神龙");});for (int i = 0; i < 7; i++) {int j = i;new Thread(()->{System.out.println("收集到第"+j+"颗龙珠");try {cyclicBarrier.await();} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}},String.valueOf(i)).start();}}
}
Semaphore
Semaphore信号量主要有两个目的:
- 用于多个共享资源的互斥使用;
- 用于并发数量的控制(是synchronized的加强版,当并发数量为1时就退化成synchronized);
Semaphore是一个计数信号量,可以指定信号量,通过acquire():请求一个信号量,导致信号量的数量减1,通过release():释放一个信号量,信号量加1;
public class SemaphoreDemo {public static void main(String[] args) {Semaphore semaphore = new Semaphore(5);for (int i = 0; i < 10; i++) {int finalI = i;new Thread(()->{//占车位try {semaphore.acquire();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("车"+ finalI +"进入车库");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}semaphore.release();System.out.println("车"+ finalI +"离开车库");},String.valueOf(i)).start();}}
}
5.阻塞队列
阻塞队列是什么?
当队列为空时,线程进来取数据会被挂起(阻塞),当队列中有数据时,线程会被唤醒。
阻塞队列的好处?
我们不需要关心什么时候需要取阻塞线程,什么时候需要去唤醒线程。
阻塞队列的种类
Queue接口和List接口都是在Collection接口下的。
- ArrayBlockingQueue:由数组组成的有界阻塞队列。
- LinkedBlockingQueue:由链表组成的有界(Integer.MAX_VALUE())阻塞队列。
- LinkedTransferQueue:由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先队列实现延迟无界阻塞队列
- SynchronizedQueue:不存储元素的阻塞队列,也即单个元素的队列
SynchronousQueue
每一个put操作必须等待一个take操作,否则就不能继续添加元素,反之亦然。总之,SynchronizedQueue,生产一个,消费一个。
阻塞队列常用四组API
抛出异常
当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
特殊值
插入方法,成功ture失败false
移除方法,成功返回出队列的元素,队列里没有就返回null
一直阻塞
当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出
当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用
超时退出
当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出
移除方法,成功返回出队列的元素,当阻塞队列空时,队列会阻塞生产者线程一定时间,队列里没有就返回null
生产者消费者实现
生产者-消费者模型:
1.线程操纵资源类
2.先判断,后干活,再通知
3.多线程用while代替if防止虚假唤醒
简单版本代码实现:
public class ProdConsumer_TraditionDemo {public static void main(String[] args) {ShareData shareData = new ShareData();new Thread(()->{try {for (int i = 0; i < 10; i++) {shareData.increment();}} catch (Exception e) {e.printStackTrace();}},"线程2").start();new Thread(()->{try {for (int i = 0; i < 10; i++) {shareData.reduce();}} catch (Exception e) {e.printStackTrace();}},"线程1").start();}
}class ShareData{private int num = 0;public synchronized void increment()throws Exception{//1.判断while (num != 0){//等待,不能生产this.wait();}num++;System.out.println(Thread.currentThread().getName()+"新增为1");this.notifyAll();}public synchronized void reduce()throws Exception{if (num == 0){this.wait();}num--;System.out.println(Thread.currentThread().getName()+"减少为0");this.notifyAll();}
}
Lock结合Condition 版本:
当使用多个Condition可以做到精准通知顺序访问
public class PCDemo2 {public static void main(String[] args) {DataNum dataNum = new DataNum();for (int i = 0; i < 10; i++) {new Thread(() -> {try {dataNum.add();} catch (InterruptedException e) {e.printStackTrace();}}).start();}for (int i = 0; i < 10; i++) {new Thread(() -> {try {dataNum.reduce();} catch (InterruptedException e) {e.printStackTrace();}}).start();}}
}class DataNum {private int num = 0;private Lock lock = new ReentrantLock();Condition condition = lock.newCondition();public void add() throws InterruptedException {lock.lock();try {//判断while (num == 1) {condition.await();}//干活num++;System.out.println("当前num为" + num);//通知condition.signal();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}public void reduce() throws InterruptedException {lock.lock();try {while (num == 0) {condition.await();}num--;System.out.println("当前num为" + num);condition.signal();} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}
}
阻塞队列实现:
public class ProdConsumerDemo {public static void main(String[] args) {PC pc = new PC(new LinkedBlockingDeque<String>());for (int i = 0; i < 5; i++) {new Thread(() -> {pc.produce();}, "生产者" + i).start();}new Thread(() -> {pc.consume();}, "消费者").start();try {TimeUnit.MINUTES.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}pc.stop();System.out.println("终止生产者生产,消费者消费");}
}//生产者消费者资源类
class PC {private volatile Boolean flag = true; //控制阻塞队列生产者消费者状态private AtomicInteger atomicInteger = new AtomicInteger(); //进行消费的队列private BlockingQueue<String> blockingQueue = null;//构造方法注入,通过传入BlockingQueue接口对BlockingQueue的实现都适配public PC(BlockingQueue<String> blockingQueue) {this.blockingQueue = blockingQueue;}public void stop() {flag = false;}//生产public void produce() {String data = null;boolean returnValue;while (flag) {data = atomicInteger.incrementAndGet() + "";returnValue = blockingQueue.offer(data);if (returnValue) {System.out.println(Thread.currentThread().getName() + "\t添加元素:" + data + "\t成功");} else {System.out.println(Thread.currentThread().getName() + "\t添加元素:" + data + "\t成功");}try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("此时falg = false,生产条件结束");}//消费public void consume() {String data = null;String poll = null;while (flag) {try {TimeUnit.SECONDS.sleep(1);poll = blockingQueue.poll(2, TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}if (StringUtils.isEmpty(poll)) {System.out.println(Thread.currentThread().getName() + ":超过两秒为获取数据,即将退出");return;}System.out.println(Thread.currentThread().getName() + ":取出数据成功,数据为:" + poll);}System.out.println("此时falg = false,消费条件结束");}}
6.synchronized与lock的区别
存在存次:
- Synchronized时关键字,属于JVM层面,底层通过 monitorenter 和 monitorexit 两个指令实现。
- Lock是JUC包(java.util.concurrent.locks)中的类。是API层面。
Synchronized底层是通过monitor对象来完成,monitorenter进入,monitorexit 退出,其实wait和notify也依赖于monitor对象,只有在同步代码块和方法中才能调用wait和notify等方法。
锁的释放:
- Synchronized是执行完锁住的代码,释放锁。或者出现异常时,JVM会让线程释放锁。不可中断锁。
- Lock必须在finally中释放锁,不然容易造成线程死锁。可中断锁。设置超时方法tryLock(time, unit);使用lockInterruptibly,调用iterrupt方法可中断;
适用场景:
- Synchronized适用于少量同步。
- Lock适用于大量同步。
是否公平:
- Synchronized是非公平锁
- Lock可以设置公平非公平锁。默认非公平。
Lock可以使用Condition可以做到精准通知顺序访问
7.Callable
Java创建线程的4中方式以及区别:
- 继承Thread类创建线程。
- 实现Runnable接口创建线程。
- 使用Callable和FutureTask创建线程。
- 使用线程池,例如用Executor框架创建线程。实际开发中采用自定义线程池。
Callable和FutureTask创建线程:FutureTask采用了适配器模式,实现了Runnable接口,构造方法支持Runnable和Callable,能够很好的适配Callable。
实现Runnable和Callable接口与继承Thread类区别?
- 线程类实现Runnable和Callable接口还能继承其他类。方便扩展
- 继承Thread类,获取当前线程可以直接用this。不用Thread.currentThread()。继承Thread类不能继承其他父类。
实现Runnable和Callable接口的区别?
- 实现Runnable接口重写里面的run方法。实现Callable接口重写里面的call方法。
- run方法没有返回值。call方法必须有返回值。
- run方法无法抛出异常。call方法可以抛出异常。
- Thread类构造方法只支持Runnable。
Callable接口通过 FutureTask 接口的适配:
FutureTask是Future、Runnable接口的实现类,其构造函数接受Callable接口参数,Future接口主要的方法有:
- isCancel()
- isDone()
- get()
也正因为Future接口的这些方法,相对于Runnable接口而言,Callable接口的优势在于:
- 判断线程任务是否完成或者被取消
- 能够中断任务
- 能够获得返回结果,且可以抛出异常
Callable配合FutureTask代码实现:
public class CallableDemo implements Callable<Integer> {public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<Integer> futureTask = new FutureTask(new CallableDemo());//多个线程同用一个FutureTask对象,对应的call()方法只会被执行一次new Thread(futureTask,"线程1").start();new Thread(futureTask,"线程2").start();// get()方法尽量推迟使用,过早使用可能会阻塞主线程Integer result = futureTask.get();//判断call()方法是否已经结束可以通过while&isDone()方法,类似于自旋锁形式while (!futureTask.isDone()){}System.out.println(result + 200);}@Overridepublic Integer call() throws Exception {System.out.println("进入到call方法中");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}return 100;}
}
8.线程池
8.1为什么使用线程池?
主要特点为:线程复用;控制最大并发数;管理线程。
- 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
- 第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
8.2线程池的架构
8.3线程池的五大种类
- Executors.newFixedThreadPool:固定线程数量的线程池。使用的阻塞队列是 new LinkedBlockingQueue()。
- Executors.newSingleThreadExecutor:固定数量为一的线程池。使用的阻塞队列是 new LinkedBlockingQueue()。
- Executors.newCacheThreadPool:缓存线程池,如果当前任务少,就会回收部分线程,当前任务多,线程池会增加线程数量。使用的阻塞队列是new SynchronousQueue()。
- Executors.newScheduledThreadPool:延迟时间,定时执行周期性任务,参数为线程池数量。使用的阻塞队列是new DelayedWorkQueue()。
- Executors.newWorkStealingPool:会根据所需的并行层次来动态创建和关闭线程,通过使用多个队列减少竞争,底层用的ForkJoinPool来实现的。
源码解析
- newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize是相等的,阻塞队列用的是new LinkedBlockingQueue()。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(),threadFactory);
}
- newSingleThreadExecutor创建的线程池corePoolSize和maximumPoolSize都是1,阻塞队列用的是new LinkedBlockingQueue()。
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(),threadFactory));
}
- newCachedThreadPool创建的线程池corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,阻塞队列用的是new SynchronousQueue()。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>(),threadFactory);
}
注意:
线程池使用完毕之后需要关闭,应该配合try-finally代码块,将线程池关闭的代码放在finally代码块中;
8.4线程池的七大参数
ThreadPoolExecutor对构造函数进行了重载,实际内部使用了7个参数:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
corePoolSize:线程池中常驻核心线程数
maximumPoolSize:线程池中能够容纳的最大线程数,必须大于0
keepAliveTime:多余的空闲线程的存活时间,当前线程数超过corePoolSize,当超过的线程空闲时间超过keepAliveTime,超过的线程数就会被销毁。恢复到corePoolSize线程数
unit:keepAliveTime的单位
workQueue:任务队列,任务被提交,但是未被执行的任务
threadFactory:线程工厂,用于创建线程,一般默认即可
handler: 拒绝策略,当任务队列已满,并且当前线程数超过maximumPoolSize时,用来执行的Runnable拒绝策略。
8.5线程池的处理流程
- 创建线程池,等待请求。
- 当调用execute()方法时,添加请求任务,线程池会作出判断:
2.1. 如果当前线程数量小于corePoolSize,马上创建线程运行这个任务。
2.2.如果当前线程数量大于corePoolSize,那么这个任务先放入阻塞队列中。
2.3.如果阻塞队列也满了,就需要进行扩容,线程池中线程数量变为maximumPoolSize来运行。
2.4.如果线程池中线程数量大于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行 - 当一个线程完成时,他会从队列中取出一个任务来执行。
- 当一个线程无事可做超过一定时间(keepAliveTime)时,线程会判断:
4.1.如果当前线程数 大于corePoolSize,他就会停掉这些无事可做的线程,收缩线程池大小为corePoolSize。
8.6线程池的拒绝策略
什么时候会启动拒绝策略?
等待队列已经排满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务。这个是时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的四种拒绝策略
- AbortPolicy策略:默认的,直接抛出异常,RejectedExecutionException,阻止系统正常运行。
- CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中(使用线程池的那个线程),运行当前的被丢弃的任务。
- DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
- DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。
我们可以根据自己的业务需求去自定义拒绝策略,自定义的方式很简单,直接实现RejectedExecutionHandler接口即可。
8.7自定义线程池
在工作当中,由单一的,固定数量的,可变的几种创建线程池的方法,但是都不用,工作中用自定义的。
自定义线程池配置线程数量
判断是CPU密集型还是IO密集型:
CPU密集型:
# CPU核数 + 1个线程的线程池
Runtime.getRuntime().availableProcessors()+1
IO密集型:
# CPU核数 / (1 - 阻塞系数) 阻塞系数在0.8-0.9
# CPU核数 * 2Runtime.getRuntime().availableProcessors()/(1-0.9)
Runtime.getRuntime().availableProcessors()*2
实际情况得通过压测去确定
9.死锁编码及定位分析
死锁的产生
两个及以上线程在执行过程当中,因为互相争夺资源而导致一种相互等待的现象,如果没有外力干涉,他们将无法继续运行下去。
public class DeadLockDemo implements Runnable{public static void main(String[] args) {String lockA = "lockA";String lockB = "lockB";new Thread(new DeadLockDemo(lockA,lockB)).start();new Thread(new DeadLockDemo(lockB,lockA)).start();}private String lockA;private String lockB;public DeadLockDemo(String lockA, String lockB) {this.lockA = lockA;this.lockB = lockB;}@Overridepublic void run() {synchronized (lockA){System.out.println(Thread.currentThread().getName()+"\t持有:"+lockA+"\t尝试获取"+lockB);try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB){System.out.println(Thread.currentThread().getName()+"\t持有:"+lockB+"\t尝试获取"+lockA);}}}
}
死锁的定位
Java提供了类似于ps的命令jps来查看当前Java程序及其进程号,再通过命令jstack能够产看具体进程号的Java程序栈使用情况。
liubendeMacBook-Pro:ProdCons liuben$ jps -l
22960 com.liu.guigumianshitwo.DeadLock.DeadLockDemo
22976 sun.tools.jps.Jps
21767
22524 org.jetbrains.idea.maven.server.RemoteMavenServer36
22959 org.jetbrains.jps.cmdline.Launcher
liubendeMacBook-Pro:ProdCons liuben$ jstack 22960
...
Java stack information for the threads listed above:
===================================================
"Thread-1":at com.liu.guigumianshitwo.DeadLock.DeadLockDemo.run(DeadLockDemo.java:42)- waiting to lock <0x000000076acfb6f0> (a java.lang.String)- locked <0x000000076acfb728> (a java.lang.String)at java.lang.Thread.run(Thread.java:748)
"Thread-0":at com.liu.guigumianshitwo.DeadLock.DeadLockDemo.run(DeadLockDemo.java:42)- waiting to lock <0x000000076acfb728> (a java.lang.String)- locked <0x000000076acfb6f0> (a java.lang.String)at java.lang.Thread.run(Thread.java:748)Found 1 deadlock. #找到一个死锁
10.JVM部分
GC Roots理解
1.什么是垃圾?
内存中已经不在被使用到的空间就是垃圾,要进行垃圾回收,首先需要判断一个对象是否可以被回收。
2.如何确定垃圾?哪些对象已经死亡?
- 引用计数法
给对象添加一个引用计数器,引用一次就加1,引用失效就减1;计数器为0时,对象就是不可能再被使用。缺点:无法解决对象之间相互循环引用的问题。 - 可行性分析
以GC Roots集合中的对象为根节点向下进行搜索,如果对象到GC Roots没有任何引用链相连接,说明该对象不可达。解决了引用计数法的缺点。
3.哪些对象可以作为GC Roots的对象,什么是GC Roots
- 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈JNI(Native方法)引用的对象
JVM参数配置
1. 标配参数
-version、-help、java -showversion,标配参数从Java1.0开始就有
liubendeMacBook-Pro:~ liuben$ java -version
liubendeMacBook-Pro:~ liuben$ java -help
2. X参数
-Xint:解释执行.
-Xcomp:第一次使用就编译成本地代码.
-Xmixed:混合模式.
3. XX参数
- Boolean类型:-XX:+|-属性值
// 开启/关闭打印GC详细信息属性
XX:+PrintGCDetails
XX:-PrintGCDetails
- KV键值设置属性。-XX:MetaspaceSize=1024m
设置java元空间大小
-XX:MetaspaceSize=1024m // 元空间默认大小21m左右
// 设置从Young升到老年代区的年龄
-XX:MaxTenuringThreshold=10 // 默认年龄15
//查看当前运行的java进程编号
jps -l
//查看当前java进程是否开启某个JVM参数
jinfo -flag MetaspaceSize 11111
jinfo -flag JVM参数 进程编号
//查看当前java进程所有JVM参数
jinfo -flag 进程编号
注意:-Xmx1024m 和 -Xms1024 是- XX:InitialHeapSize=1024m 和 -XX:MaxHeapSize=1024m的缩写
查看JVM默认参数
- -XX:+PrintFlagsInitial:查看JVM默认参数,能够在java程序未运行时查看。
- -XX:_PrintFlagsFinal:查看JVM修改更新的参数。
liubendeMacBook-Pro:ProdCons liuben$ java -XX:+PrintFlagsInitial
[Global flags]
...
liubendeMacBook-Pro:ProdCons liuben$ java -XX:+PrintFlagsFinal -version
注意::=代表被JVM修改过或者认为修改过后的值
常用参数
- -Xms:初始化堆内存大小,默认为物理内存的1/64。等价于-XX:InitialHeadSize=128m。
- -Xmx:堆内存最大分配大小,默认为物理内存的1/4。等价于-XX:MaxHeapSize=4096m。
- -Xss:设置单个线程栈大小,一般是1024k,主要依赖于平台,Windows使用的是JVM该参数的默认值。等价于-XX:ThreadStackSize=1024k。
- -Xmn:设置年轻代大小,一般不调节该参数。
- -XX:MetaspaceSize:设置元空间大小,元空间并不在虚拟机当中,而是使用本地内存,元空间的大小受本地内存限制。
- -XX:+UseSerialGC:使用串行化GC,默认是并行化GC
- -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例。默认是-XX:SurvivorRatio=8,即Eden:S0:S1 = 8:1:1。
- -XX:NewRatio:设置年轻代和老年代在堆结构中的占比。默认-XX:NewRatio=2,即新生代占1/3,老年代占2/3。-XX:NewRatio所设置的数值为老年代占的比例。新生代始终为1。
- -XX:MaxTenuringThreshold:设置垃圾的最大年龄,默认为15,不能超过15。
推荐配置(典型):
-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC
GC日志阅读
上面图是在JDK1.8之前的,没有元空间的GC日志信息,在JDK1.8及之后,GC日志一共分为4部分:
- 新生代
- 老年代
- 元空间
- GC时间
11.四大引用
强引用,软引用,弱引用,虚引用是位于java.lang.ref包下的。组织架构如下:
强引用
当JVM进行垃圾回收时,不会回收强引用对象,就算出现OOM也不会对该对象进行回收,强引用是造成Java内存泄露的主要原因之一。
/*** @author: LiuBen* @Date: 2021/10/20 10:23:06* @Description:* 强引用:最常用的就是强引用,就算出现OOM也不会对该对象进行回收。强引用是造成java内存泄露的主要原因。*/
public class StrongReferenceDemo {public static void main(String[] args) {Object obj1 = new Object();Object obj2 = obj1;obj1 = null;//手动GCSystem.gc();System.out.println(obj1);System.out.println(obj2);}
}
软引用
对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之内。
如果这次回收后还没有足够的内存,才会抛出异常。即:
- 当系统内存充足时,软引用不会回收;
- 当系统内存不足时,软引用会被回收,回收后内存仍然不足,就抛出异常;
/*** @author: LiuBen* @Date: 2021/10/20 10:25:34* @Description: VM: -Xms5m -Xmx5m -XX:+PrintGCDetails* 软引用:当内存不足时,软引用会被回收,当内存充足时内存不会被回收*/
public class SoftReferenceDemo {public static void main(String[] args) {Object obj1 = new Object();SoftReference<Object> softReference = new SoftReference<>(obj1);System.out.println(obj1);System.out.println(softReference);obj1 = null;try {byte[] bytes = new byte[6 * 1024 * 1024];} catch (Exception e) {e.printStackTrace();}finally {System.out.println(obj1);System.out.println(softReference.get());}}
}
弱引用
当JVM进行GC垃圾回收时,无论内存是否充足,被弱引用关联的对象都会被回收,即弱引用关联的对象活不到下一次GC的时候。
/*** @author: LiuBen* @Date: 2021/10/20 10:35:17* @Description:* 弱引用:当JVM进行GC时,无论内存是否充足,被弱引用关联的对象都会被回收。被弱引用关联的对象活不到下一轮的GC*/
public class WeakReferenceDemo {public static void main(String[] args) {Object obj1 = new Object();WeakReference<Object> weakReference = new WeakReference<>(obj1);System.out.println(obj1);System.out.println(weakReference.get());obj1 = null;System.gc();System.out.println(obj1);System.out.println(weakReference.get());}
}
虚引用
虚引用必须和引用队列(ReferenceQueue)联合使用,一个对象仅持有虚引用,那么它和没有任何引用一样,调用get()方法
总返回null,在任何时候都可能被垃圾回收器回收。
创建引用时候可以指定关联的队列,当GC释放对象的时候会将引用的对象添加到引用队列中,如果程序发现某个虚引用对象已经被加入到引用队列中,那么就可以在引用对象的内存回收之前采取必要的措施,这就相当于一种通知机制;
public class PhantomReferenceDemo {public static void main(String[] args) {Object obj1 = new Object();ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();PhantomReference<Object> phantomReference = new PhantomReference<>(obj1,referenceQueue);System.out.println(obj1);System.out.println(phantomReference.get());System.out.println(referenceQueue.poll());obj1 = null;System.gc();System.out.println(obj1);System.out.println(phantomReference.get());System.out.println(referenceQueue.poll());}
}
软引用和弱引用使用场景
场景:
有一个应用需要读取大量的本地图片:如果每次读取图片都从硬盘读取则会严重影响性能;如果一次都加载到内存中有可能造成内存溢出,此时可以通过软引用或者弱引用来解决该问题。
设计思路:
用一个HashMap来保存图片路径和相应图片对象关联的软引用之间的映射关系,当系统内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效的避免OOM:
Map<String, SoftReference> imgCache = new HashMap<String, SoftReference>();
WeakHashMap
WeakHashMap和HashMap一样也是一个散列表,但是它的键是“弱键”,其类型是WeakReference,对于“弱键”,其对应的映射的存在并不会阻止垃圾回收器对该键的丢弃,也就是说,该弱键是可被终止的。当某个键被终止(即被GC垃圾回收)时,其对应的WeakHashMap中键值对映射就会从散列表中移除
12.OOM
Java异常和错误的继承体系
注意:虽然StackOverflowError和OutOfMemoryError通常口语上都称为“异常”,但是实际上它们都是Error的子类,属于错误。
Java中常见错误
StackOverflowError
栈溢出异常,通常发生在递归调用时未加终止条件时。使用 -Xss 参数可以更改单个线程栈的大小。
/*** @author: LiuBen* @Date: 2021/10/20 14:44:12* @Description:** 无限递归调用会出现StackOverflowError** 堆管存储,栈管运行*/
public class StackOverflowErrorDemo {public static void main(String[] args) {stackOverflowError();}private static void stackOverflowError(){stackOverflowError(); // Exception in thread "main" java.lang.StackOverflowError}
}
OOM:java heap space
堆是负责存储的,当new出来的对象大小或者不停new对象,对象的内存超过堆内存,就会导致java.lang.OutOfMemoryError: Java heap space。可以通过-Xms5m -Xmx5m来设置堆的大小。
/*** @author: LiuBen* @Date: 2021/10/20 14:48:54* @Description:* 堆管存储,栈管运行*/
public class OutOfMemoryDemo01 {public static void main(String[] args) {byte[] bytes = new byte[80 * 1024 * 1024];//直接创建大对象,撑爆//Exception in thread "main" java.lang.OutOfMemoryError: Java heap space}
}
OOM:GC overhead limit exceeded
花费长时间GC但是回收的垃圾却只有一点,连续多次GC仍然出现这种情况时,才会抛出该异常。
如果不抛出该异常:GC清理的内存很快又会被再次填满,迫使再次GC,形成恶性循环,CPU使用率一直在100%,而GC却没有任何效果。
package com.liu.guigumianshitwo.OOM;import java.util.ArrayList;
import java.util.List;
//-Xms50m -Xmx50m -XX:+PrintGCDetails
public class OOMErrorDemo01 {public static void main(String[] args) {int i=0;List<String> list = new ArrayList<>();try {while(true){//list.add(String.valueOf(i++).intern());}} catch (Exception e) {e.printStackTrace();}}
}
OOM:direct buffer memory
在NIO当中,经常需要使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)和缓冲区(Buffer)的IO方式。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存引用进行操作。这样能够在一些场景中显著提高性能,因为可以避免在Java堆和Native堆中来回复制数据:
- ByteBuffer.allocate(capacity):第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度较慢;
- ByteBuffer.allocateDirect(capacity):这种方式是分配计算机本地系统内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快;
如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectBuffer对象们就不会被收,这时候堆内存充足,但是本地内存可能已经使用完毕,再次尝试分配本地内存就会出现OOM,程序直接崩溃:Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory
import java.nio.ByteBuffer;
//-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
public class DirectBufferMemory {public static void main(String[] args) {// 默认应该是1/4系统内存System.out.println("配置堆外内存为:"+(sun.misc.VM.maxDirectMemory())/(double) 1024/1024 + "MB");ByteBuffer.allocateDirect(7 * 1024 * 1024);}
}
OOM:unable to create new native thread
当高并发请求时,请求数量超过系统所允许的最大线程时,会出现unable to create new native thread
和对应的平台有关。linux默认允许单个进程可以创建线程数1024个
解决方案:
- 降低当前进程所使用的线程数量,分析程序是否真的需要创建那么多的线程。
- 修改linux默认配置,扩大linux默认限制。
# vim /etc/security/limits.d/90-nproc.conf:
* soft nproc 1024
root soft nproc unlimited
OOM:metaspace
Metaspace是Java8及其以后版本中使用的,用来代替永久代。Metaspace是方法区在HotSpot中的实现,它与永久带最大的区别是:
Metaspace并不在JVM内存中而是直接使用本地内存。也就是说在Java8中,class metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的Native Memory中。
永久代(Metaspace)存储的信息:
- JVM加载的类信息
- 常量池
- 静态变量
- 即时编译后的代码
查看默认MetaspaceSize:默认应该是物理内存的1/4:
# windows:java -XX:+PrintFlagsInitial | findstr -i metaspacesize
# linux: java -XX:+PrintFlagsInitial | grep -i metaspacesize
因为Metaspace中存储了类信息,所以不断产生新的类,就会不断向Metaspace中写入数据,就有可能导致Metaspace区域溢出。
Exception in thread “main” java.lang.OutOfMemoryError: Metaspace
package com.liu.guigumianshitwo.OOM;import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;
//-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=20m
public class MetaSpaceOOM {public static void main(String[] args) {int i = 0; //模拟计数多少次以后发生异常try {while (true){i++;Enhancer enhancer = new Enhancer();enhancer.setSuperclass(OOMTest.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {return methodProxy.invokeSuper(o,args);}});enhancer.create();}} catch (Exception e) {System.out.println("***********多少次后发生了异常:"+i);e.printStackTrace();}}static class OOMTest {}
}
13.垃圾回收器
如何确定垃圾?哪些对象已经死亡?
1.引用计数法
给对象添加一个引用计数器,引用一次就加1,引用失效就减1;计数器为0时,对象就是不可能再被使用。缺点:无法解决对象之间相互循环引用的问题。
2.可行性分析
以“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路称为引用链,当一个GC Roots没有任何引用链时,证明此对象是不可用的。解决了引用计数法的缺点。
GC有哪些垃圾回收算法?
1.复制算法
无碎片,耗内存
2.标记清除算法
节约了内存,碎片又出来了
3.标记压缩算法
多了整理的一步,时间消耗多了
4.分代收集算法(重要)
复制算法----》年轻代
标记清除和标记压缩的混合实现-----》老年代
没有最好的算法,只有最合适的算法
垃圾回收器
垃圾收集器的四大类
- 串行垃圾回收器(Serial):为单线程环境设置并且只使用一个线程进行垃圾回收,会暂停所有的用户线程,不适合服务器环境;
- 并行垃圾回收器(Parallel):Serial垃圾回收器的多线程版本,会开启多个线程进行垃圾回收,仍然会暂停所有的用户线程,速度较快,适用于科学计算或者大数据处理;
- 并发垃圾回收器(CMS):用户线程和垃圾回收线程同时执行(不一定是并行,可能是交替执行),不需要暂停用户线程,适用于对响应时间要求严格的应用;
- G1垃圾回收器⭐️:将堆内存分割成不同的区域(Region)然后进行垃圾回收,并不存在明显的新生代和老年代;
Serial和Parallel垃圾回收器都会产生STW(Stop The World)问题,CMS并不会产生该问题,但是CMS的GC过程可能更加复杂,导致抢占应用程序的CPU资源。
STW(Stop The World):暂停其他工作线程。
默认垃圾回收器、JVM参数配置配置
- 查看默认的垃圾回收器:java -XX:+PrintCommandLineFlags -version,Java8默认使用ParallelGC
- JVM默认的垃圾回收器(7种,有一种已经废弃),对应新生代老年代收集器。
- UseSerialGC <--------> UseSerialOldGC
- UseParallelGC <--------> UseParallelOldGC
- UseParNewGC <--------> UseConcMarkSweepGC
- UseG1GC
JVM垃圾回收日志参数说明
- DefNew: Default New Generation
- Tenured: 老年代
- ParNew: Parallel New Generation
- PSYoungGen: Parallel Scaveng
- ParOldGen: Parallel Old Generation
JVM中CS模式: - 32位Windows操作系统,无论硬件如何默认使用Client的JVM模式
- 32位其它操作系统,2G内存同时拥有2个cpu及以上的硬件资源使用的是Server模式,否则Client模式
- 64位操作系统只有Server模式
七大垃圾回收器详解
串行垃圾回收器(Serial/Serial Copying)(1:1)
-XX:+UseSerialGC
一个单线程的垃圾回收器并且只使用单个线程进行垃圾回收在进行垃圾回收时,必须暂停其他所有的工作线程(STW)直到它的垃圾回收结束。
它适合Client模式的应用,在单CPU环境下,它简单高效,由于没有线程交互的开销,专心垃圾收集自然可以获得最高的单线程效率。
- 开启SerialGC后,默认使用的配套GC是: Serial(Young区使用)+Serial Old(Old区使用)的收集器组合. 表示新生代和老年代都会使用
- 串行垃圾回收器,新生代使用复制算法,老年代使用标记-整理算法。
- 配置运行说明:DefNew&Tenured
并行垃圾回收器(ParNew)(n:1)
-XX:+UseParNewGC
使用多个线程进行垃圾回收,是Serial收集器新生代的并行多线程版本,在进行垃圾回收时仍然会出现STW问题直至它的回收工作结束。适合Server模式以及多CPU环境。一般会和jdk1.5之后出现的CMS搭配使用。
- 开启ParNewGC后,默认使用的配套GC是:ParNew(Young区使用)+Serial Old(Old区使用)的垃圾收集器组合,新生代是并行,老年代还是串行。
- 新生代使用复制算法,老年代使用标记-整理算法。
- 在Java8已经不推荐被使用,更加推荐的组合是:ParNew&CMS
- 备注:-XX:ParallelGCThreads参数可以限定回收线程的数量,默认开启和CPU数目相同的线程数
- 配置运行说明:ParNew&Tenured
并行收集器(Parallel Scavenge)(n:n)
-XX:+UseParallelGC
Parallel Scavenge收集器类似ParNew,也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。相当于是串行收集器在新生代和老年代的并行化。
重点关注可控制吞吐量,高吞吐量意味着高效利用CPU时间,它多用于在后台运算而不需要太多交互的任务。【吞吐量 = 用户代码运行时间/(用户代码运行时间+垃圾回收时间)】
自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。自适应调节策略就是JVM会根据当前系统的运行情况看收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX: MaxGCPauseMills)或最大吞吐量。
- 开启该参数后,新生代使用复制算法,老年代使用标记-整理算法
- -XX:ParallelGCThreads=N表示开启多少个GC线程,如果CPU核数>8, N = 5或者8,否则N=实际CPU核数;
- 配置运行说明:PSYoungGen&ParOldGen
Parallel Old收集器
-XX:+UseParallelOldGC, 和-XX:+UseParallelGC相互激活
Parallel Old收集器是PS的老年代版本,使用多线程的标记-整理算法,Parallel Old是JDK1.6才开始提供的。在JDK1.6之前,新生代使用PS收集器只能在老年代配合Serial Old收集器,只能保证在新生代的吞吐量有限,无法保证整体的吞吐量。
Parallel Old正是为了在老年代同样提供吞吐量优先的垃圾回收器,如果系统对吞吐量要求较高,在JDK1.8及以后版本都是使用Parallel Scavenge&Parallel Old垃圾收集器组合。
并发标记清除收集器(CMS)
-XX:+UseConcMarkSweepGC
会自动开启-XX:+UseParNewGC
开启该参数后,会自动使用: ParNew(Young区使用)&CMS(Old区使用)&Serial Old的收集器组合,Serial Old将作为CMS出错后的备用收集器;
CMS收集器是标记-清除GC算法的落地实现,是一种以获得最短停顿时间为目标的收集器,适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短CMS非常适合堆内存大, CPU核数多的服务器应用,也是G1收集器出现之前大型应用的首选收集器。
CMS内存回收一共有4个过程:
- 初始标记(CMS initial mark): 只有标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
- 并发标记(CMS concurrent mark): 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程,主要标记过程,标记全部对象。
- 重新标记(CMS remark): 修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前再做修正。
- 并发清除(CMS concurrent sweep): 清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户线程一起并发工作。所以总体上说CMS收集器的内存回收和用户线程是并发执行的(初始标记和重新标记虽然要暂停,但是用时很短)。
优点:并发收集,停顿次数少。
缺点:对CPU的压力大,CMS在收集和应用线程会同时增加对堆内存的占用,也就是i说CMS必须在老年代堆内存用完之前完成GC,否则CMS会回收失败,将触发担保机制,Serial Old会以STW(Stop The World,暂停所有工作线程)的方式进行依次GC,从而造成较大的停顿时间。而且采用标记清除算法会产生内存碎片。
- 配置运行说明:ParNew&CMS&Serial Old
Serial Old收集器
Serial Old收集器是Serial垃圾收集器老年代版本,同样是单线程的收集器,使用标记整理算法。
主要运行在Client默认的JVM老年代垃圾回收器。
在Server模式下,主要有两个用途:
- 在JDK1.5之前与新生代Parallel Scavenge收集器搭配使用。(Parallel Scavenge+Serial Old)
- 作为老年代版中使用CMS收集器的后备垃圾回收方案。、
注意:在Java8中不能使用JVM参数-XX:+UseSerialOldGC来开启Serial Old收集器。
G1垃圾回收器
-XX:+UseG1GC
-XX:G1HeapRegionSize=n G1Region的大小,值必须是2的幂,范围在1MB到32MB
-XX:MaxGCPauseMillis=n 最大GC停顿时间大小
-XX:ConcGCThreads=n 并发GC使用的线程数
G1垃圾回收器是在Java7 update 4之后引入的一个新的垃圾回收器。在JDK9中,G1是默认的垃圾回收器。
G1是什么?
G1收集器是一种面向服务端的垃圾回收器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能满足垃圾收集暂停时间的要求。
G1的特点?
- G1充分利用多CPU多核的硬件优势,尽量缩短STW。
- G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。
- G1将内存划分为一个个region,不存在物理上的年轻代与老年代的区别,只有逻辑上的年轻代和老年代的区别。
- G1除了追求低停顿时间之外,还建立了可预测的停顿时间模型,可以让用户明确指定一个长度为M毫秒的时间片段,消耗在垃圾
收集器上的时间不得超过N毫秒。
G1的原理:
G1收集器将区域化的内存分割成大小不等的Region,在堆内存使用上,G1并不要求对象一定存储在物理连续的内存上只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在新生代和老年代之间切换。
G1采用了分区(Region)的思路,Eden,Survivor,Tenured等内存区域不再是连续的,而是变成了一个个大小的region,每个region的大小可以是1MB~32MB,且必须是2的幂,默认将整堆划分为2048个分区。
G1中Region的一部分包含新生代,一部分包含老年代,一部分包含(Humongous)区域 。
在G1中还有一个特殊的区域(Humongous)区域,如果一个对象占用的内存超过了分区容量的50%,这样的巨型对象会直接被分配在老年代,但是它会对垃圾回收产生负面影响。为了解决这个问题,G1专门划分了一个Humongous区域,专门存放巨型对象。如果一个H区不能装下巨型对象,那么G1会寻找连续的H分区来存储,有时为了能够找到连续的H区,不得不启动一次Full GC。
G1收集器的运作步骤(不考虑Remembered Set):
- 初始标记: 只对GC Roots能够直接关联到的对象进行标记,存在STW;
- 并发标记: 对GC Roots关联的对象进行可达性分析,可以并发执行;
- 最终标记: 修正并发标记期间因为应用程序继续执行而导致变化的那一部分对象,存在STW;
- 筛选回收: 根据时间来进行价值最大化的回收;
G1对比CMS的优势:
- 不产生内存碎片
- 可以让用户指定期望的GC停顿时间,G1会根据允许的停顿时间区收集回收价值最高的垃圾
垃圾回收器选择
- 单CPU或小内存,单机程序:-XX:+UseSerialGC
- 多CPU,需要最大吞吐量,如后台计算型应用:-XX:+UseParallelGC或者-XX:+UseParallelOldGC
- 多CPU,追求低停顿时间,快速响应如互联网应用:-- XX:+UseConcMarkSweepGC或者-XX:+UseParNewGC
14.Linux常用命令
生产服务器变慢,谈谈诊断思路和性能评估?
整机
//查看整机的系统性能
top
[root@izwz94gb5cfw913042mfyxz ~]# uptime13:57:06 up 90 days, 17:54, 1 user, load average: 0.10, 0.31, 0.310.10, 0.31, 0.31。三个值分别代表一分钟,五分钟,十五分钟系统的平均负载值。
如果三个值相加除以3乘以100%,大于60%,系统的负担压力中。
CPU
//每两秒采样一次,攻击采样三次。
[root@izwz94gb5cfw913042mfyxz ~]# vmstat -n 2 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----r b swpd free buff cache si so bi bo in cs us sy id wa st2 0 0 106312 5772 322900 0 0 37 11 0 1 2 1 97 0 00 0 0 106328 5772 322932 0 0 0 0 2604 4965 1 1 98 0 00 0 0 106204 5796 322932 0 0 0 88 3171 6018 3 2 95 0 0一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔,间隔单位是秒,第二个参数是采样的次数。
procs
r:运行和等待CPU时间片的进程数,原则上一核的CPU的运行队列不要超过2,整个系统的运行队列不能超过总核数的两倍,否责代表系统压力过大。
b:等待资源的进程数。比如正在等待磁盘IO,网络IO等。------cpu-----us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,优化程序。sy:内核进程消耗CPU时间百分比。us+sy:参考值80%,如果 us+sy大于80%,说明可能存在CPU不足。id:处于空闲的CPU百分比。wa:系统等待IO的CPU时间百分比。st:来自于一个虚拟机偷取的CPU时间百分比。
//查看所有CPU核程信息
[root@izwz94gb5cfw913042mfyxz ~]# mpstat -P ALL 2
Linux 3.10.0-514.26.2.el7.x86_64 (izwz94gb5cfw913042mfyxz) 10/26/2021 _x86_64_ (2 CPU)02:16:33 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
02:16:35 PM all 1.02 0.00 0.77 0.00 0.00 0.00 0.00 0.00 0.00 98.21
02:16:35 PM 0 1.02 0.00 1.02 0.00 0.00 0.00 0.00 0.00 0.00 97.96
02:16:35 PM 1 1.53 0.00 0.51 0.00 0.00 0.00 0.00 0.00 0.00 97.96
//每个进程使用CPU的用量分解信息
[root@izwz94gb5cfw913042mfyxz ~]# pidstat -u 1 -p 1000
Linux 3.10.0-514.26.2.el7.x86_64 (izwz94gb5cfw913042mfyxz) 10/26/2021 _x86_64_ (2 CPU)02:19:03 PM UID PID %usr %system %guest %CPU CPU Command
内存
//free 查看内存,-g单位是g,-m单位是m
[root@izwz94gb5cfw913042mfyxz ~]# freetotal used free shared buff/cache available
Mem: 1777920 1345096 103748 130532 329076 131808
Swap: 0 0 0
[root@izwz94gb5cfw913042mfyxz ~]# free -gtotal used free shared buff/cache available
Mem: 1 1 0 0 0 0
Swap: 0 0 0
[root@izwz94gb5cfw913042mfyxz ~]# free -mtotal used free shared buff/cache available
Mem: 1736 1313 94 127 327 128
Swap: 0 0 0应用程序可用内存/系统物理内存小于20%需要加内存。//5101 进程号,2。每两秒采样一次 。 查看额外
[root@izwz94gb5cfw913042mfyxz ~]# pidstat -p 5101 -r 2
Linux 3.10.0-514.26.2.el7.x86_64 (izwz94gb5cfw913042mfyxz) 10/26/2021 _x86_64_ (2 CPU)02:25:05 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
硬盘
//查看磁盘剩余空间数
[root@izwz94gb5cfw913042mfyxz ~]# df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 59G 11G 46G 19% /
devtmpfs 859M 0 859M 0% /dev
tmpfs 869M 0 869M 0% /dev/shm
tmpfs 869M 89M 780M 11% /run
tmpfs 869M 0 869M 0% /sys/fs/cgroup
tmpfs 174M 0 174M 0% /run/user/0
tmpfs 174M 0 174M 0% /run/user/1002
overlay 59G 11G 46G 19% /var/lib/docker/overlay2/2e1d41bb84f9fb99cd59fb8a23a1109a2bf5dbd0a75c77d47028411aa5c3df9c/merged
overlay 59G 11G 46G 19% /var/lib/docker/overlay2/19d0ccd3a8704a753a8d25ef12a055b2947b072c3c4410ffb8ba4944fbec96f5/merged
overlay 59G 11G 46G 19% /var/lib/docker/overlay2/a7c285fe5d2c560484b414c7fe3dd30e7c661152f1ddda05cba98e9b7f899b0a/merged
overlay 59G 11G 46G 19% /var/lib/docker/overlay2/ab386309e81e650160b4f2367db497b1516e200d5e3b76f2720bd6907771407b/merged
磁盘IO
//参数 -d 表示,显示设备(磁盘)使用状态;-k某些使用block为单位的列强制使用Kilobytes为单位;1 10表示,数据显示每隔1秒刷新一次,共显示10次。使用-x参数表示获取更多统计信息。
[root@izwz94gb5cfw913042mfyxz ~]# iostat -xdk 2 3
Linux 3.10.0-514.26.2.el7.x86_64 (izwz94gb5cfw913042mfyxz) 10/26/2021 _x86_64_ (2 CPU)Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.13 2.90 2.64 1.70 73.20 20.91 43.41 0.04 9.88 15.52 1.12 0.22 0.10Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 0.00 18.50 0.00 82.00 0.00 8.86 0.02 1.22 1.22 0.00 0.05 0.10Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util
vda 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00util 长期高于 80%以上 磁盘数据过高,多半情况跟数据库读写有关,需要查看复杂的大 sqlrrqm/s: 每秒进行 merge 的读操作数目。即 delta(rmerge)/s
wrqm/s: 每秒进行 merge 的写操作数目。即 delta(wmerge)/s
r/s: 每秒完成的读 I/O 设备次数。即 delta(rio)/s
w/s: 每秒完成的写 I/O 设备次数。即 delta(wio)/s
rsec/s: 每秒读扇区数。即 delta(rsect)/s
wsec/s: 每秒写扇区数。即 delta(wsect)/s
rkB/s: 每秒读K字节数。是 rsect/s 的一半,因为每扇区大小为512字节。
wkB/s: 每秒写K字节数。是 wsect/s 的一半。
avgrq-sz: 平均每次设备I/O操作的数据大小 (扇区)。即 delta(rsect+wsect)/delta(rio+wio)
avgqu-sz: 平均I/O队列长度。即 delta(aveq)/s/1000 (因为aveq的单位为毫秒)。
await: 平均每次设备I/O操作的等待时间 (毫秒)。即 delta(ruse+wuse)/delta(rio+wio)
svctm: 平均每次设备I/O操作的服务时间 (毫秒)。即 delta(use)/delta(rio+wio)
%util: 一秒中有百分之多少的时间用于 I/O 操作,或者说一秒中有多少时间 I/O 队列是非空的。
即 delta(use)/s/1000 (因为use的单位为毫秒)
网络IO
[root@izwz94gb5cfw913042mfyxz ~]# ifstat 1
假如生产环境出现CPU占用过高,谈谈分析思路以及定位
- 先用top命令找出cpu占用最高的
- 痛过ps -ef 或者jps 进一步定位,得知是一个怎样的后台程序给我们惹事。
- 定位到具体线程。 ps -mp 进程ID -o THREAD,tid,time
- 将线程ID转换为16进制格式(英文小写格式)(linux命令printf ‘0x%x’ tid)
- jstack 进程ID|grep tid(16进制线程ID小写英文) -A60
1. 先用top命令找出cpu占用最高的PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 4087 root 20 0 90524 3204 2348 S 0.3 0.1 0:52.84 rngd
28982 root 20 0 3266512 492616 24724 S 0.3 12.7 42:05.41 java 2. 痛过ps -ef 或者jps 进一步定位,得知是一个怎样的后台程序给我们惹事。//定位到具体进程ID[root@fxiyun2_11_52 ~]# jps -l
29106 xxl-job-admin-2.3.0.jar
30706 sun.tools.jps.Jps
28982 /home/jars/com.cncbox.catalog/com.cncbox.catalog.jar
[root@fxiyun2_11_52 ~]# ps -ef|grep java|grep -v grep
root 28982 1 0 10月21 ? 00:42:06 /bin/java -Dsun.misc.URLClassPath.disableJarChecking=true -server -Xms215m -Xmx512m -Djava.io.tmpdir=/home/jars/com.cncbox.catalog/logs/ -jar /home/jars/com.cncbox.catalog/com.cncbox.catalog.jar --spring.profiles.active=test
root 29106 1 0 10月21 ? 00:13:58 java -jar xxl-job-admin-2.3.0.jar3. 定位到具体线程-m 显示所有的线程-p pid进程使用CPU的时间-o 该参数后是用户自定义格式[root@fxiyun2_11_52 ~]# ps -mp 28982 -o THREAD,tid,time
USER %CPU PRI SCNT WCHAN USER SYSTEM TID TIME
root 0.5 - - - - - - 00:42:07
root 0.0 19 - futex_ - - 28982 00:00:00
root 0.0 19 - futex_ - - 28984 00:00:15
root 0.0 19 - futex_ - - 28985 00:00:30
root 0.0 19 - futex_ - - 28986 00:00:00
root 0.0 19 - futex_ - - 28987 00:00:00
root 0.0 19 - futex_ - - 28988 00:00:00
root 0.0 19 - futex_ - - 28989 00:01:45
root 0.0 19 - futex_ - - 28990 00:00:39
root 0.3 19 - futex_ - - 29006 00:28:38
[root@fxiyun2_11_52 ~]# jstack 28982|grep 714e -A60
"com.alibaba.nacos.client.Worker.fixed-10.12.1.162_8848-test" #16 daemon prio=5 os_prio=0 cpu=1719405.56ms elapsed=451712.78s tid=0x00007fdf9438c000 nid=0x714e waiting on condition [0x00007fdf41d14000]java.lang.Thread.State: TIMED_WAITING (parking)at jdk.internal.misc.Unsafe.park(java.base@11-ea/Native Method)- parking to wait for <0x00000000e076c518> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)at java.util.concurrent.locks.LockSupport.parkNanos(java.base@11-ea/LockSupport.java:234)at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(java.base@11-ea/AbstractQueuedSynchronizer.java:2123)at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@11-ea/ScheduledThreadPoolExecutor.java:1182)at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(java.base@11-ea/ScheduledThreadPoolExecutor.java:899)at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@11-ea/ThreadPoolExecutor.java:1054)at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@11-ea/ThreadPoolExecutor.java:1114)at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@11-ea/ThreadPoolExecutor.java:628)at java.lang.Thread.run(java.base@11-ea/Thread.java:834)
15.Github骚操作
in操作
搜索内容 in:name
搜索内容 in:readme
搜索内容 in:description
搜索内容 in:name,readme,description
star/fork操作
搜索内容 star:>=500 搜索star大于等于500的项目
搜索内容 fork:>500搜索内容 star:500..1000 搜索star数在500带1000之间的项目搜索内容 star:500..1000 fork:500..1000
awesome加强搜索
一般用来收集学习,工具,书籍类相关的项目
awesome 搜索内容
#L操作
当分享指定项目行数给同事时,可以高亮显示。
https://gitee.com/alexyon/SpringSecurity/blob/master/test-sercurity-auth/src/main/java/com/liu/pojo/SysPermission.java#L25-L28
在网址后面加上#L25-L28
项目内搜索
小写字母t
搜索某语言某地区的活跃大佬
location:beijing language:java
尚硅谷面试第二季(周阳主讲)相关推荐
- 尚硅谷 SpringCloud 第二季学习笔记【已完结】
SpringCloud 一.介绍 (一)cloud和boot之间的依赖关系 https://spring.io/projects/spring-cloud#overview Finchley 是基于 ...
- 尚硅谷springcloud第二季笔记_外行人都能看懂的 Spring Cloud,错过了血亏
一.前言 这篇主要来讲讲SpringCloud的一些基础的知识(我就是现学现卖了,主要当做我学习SpringCloud的笔记吧!)当然了,我的水平是有限的,可能会有一些理解错的的概念/知识点,还请大家 ...
- 尚硅谷面试第一季-21消息队列在项目中的应用
背景:在分布式系统中是如何处理高并发的. 由于在高并发的环境下,来不及同步处理用户发送的请求,则会导致请求发生阻塞.比如说,大量的insert,update之类的请求同时到达数据库MYSQL, ...
- 尚硅谷2020最新版周阳SpringCloud(H版alibaba)框架开发教程 学习笔记
前言:今天看到周阳老师出了新课,十分欣喜,很喜欢周阳老师的讲课风格,内容也充实,我也算是周阳老师忠实粉丝啦. 新出的springcloud第二版很符合我现阶段的学习需求.但美中不足的是,目前只有视频资 ...
- 硅谷游记|机智云硅谷行第二季完美收官
Silicon Valley,全球高技术企业发源地,不管是技术发展.创业环境.投资机会,还是创新氛围.学术气氛,都是全球科技风向标,也一直是全球众多创业者和工程师梦寐以求的地方.为满足机智云开发者对硅 ...
- 尚硅谷面试(JUC)
01_本课程前提要求和说明 教学视频 https://www.bilibili.com/video/BV18b411M7xz 一些大厂的面试题 蚂蚁花呗一面: Java容器有哪些?哪些是同步容器,哪些 ...
- 尚硅谷-互联网大厂高频重点面试题 (第2季)JUC多线程及高并发
本期内容包括 JUC多线程并发.JVM和GC等目前大厂笔试中会考.面试中会问.工作中会用的高频难点知识. 斩offer.拿高薪.跳槽神器,对标阿里P6的<尚硅谷_互联网大厂高频重点面试题(第2季 ...
- 尚硅谷周阳_SpringCloud第二季脑图
尚硅谷周阳_SpringCloud第二季脑图 01_组件说明.架构构建 02_Eureka服务注册与发现.Zookeeper服务注册与发现 03_Consul服务注册与发现.Ribbon负载均衡服务调 ...
- 尚硅谷周阳老师 SpringCloud第二季学习笔记
前言:首先感谢尚硅谷周阳老师的讲解,让我对springcloud有了很好的理解,周阳老师的讲课风格真的很喜欢,内容充实也很幽默,随口一说就是一个段子,我也算是周阳老师的忠实粉丝啦. 先说说课程总体内容 ...
最新文章
- tar命令使用方法以及.tar.gz文件和.zip文件
- Ruby种的特殊变量
- java web--servlet(2)
- 每日站立会议12/19
- Spring Batch:多种格式输出编写器
- Jenkins:部署JEE工件
- JSP自定义标签_用简单标签控制标签体执行10次
- 成为高级网络管理员必学知识
- 利用melendy插入参考文献_如何利用mendeley搞定SCI论文参考文献,这篇一定要看
- 网规之路——强化项目管理知识点训练
- 【网络】为什么我执行了发布操作,但是线上的资源并没有更新?
- 华为交换机S3700基本配置
- 运行 java applet_创建运行第一个Java Applet程序
- vue 版的老虎机抽奖活动效果折腾小记
- UG二次开发装配篇 添加/拖动/删除组件方法的实现
- 数列极限四则运算误区
- 软件设计师---软件工程
- 蓝牙协议栈接收数据包流程1
- Java基础编程题(API阶段测试)(答案)
- 分享一波和黑客斗智斗勇的经历
热门文章
- speex speexdsp
- 贝叶斯分类器做文本分类案例
- QTP10破解方法及mgn-mqt82.exe下载
- Linux时钟管理clk
- 大数据告诉你何时何地买手机最划算!
- 机器学习强基计划6-2:详细推导马尔科夫随机场(MRF)及其应用(附例题)
- java二维数组添加数据_我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊...
- 编程之美 - 中国象棋将帅问题
- python中文词典构建_python-构建英语学习词典
- Ubuntu设置终端打开时的默认窗口大小和位置坐标