JUC
- JUC(java.util.concurrent)
- 进程和线程
- 进程:后台运行的程序(我们打开的一个软件,就是进程),进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域;
- 线程:轻量级的进程,并且一个进程包含多个线程(同在一个软件内,同时运行窗口,就是线程)线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
- 并发和并行
- 并发:同时访问某个东西,就是并发
- 并行:一起做某些事情,就是并行
- 进程和线程
- JUC下的三个包
在高内聚、低耦合的前提下: 线程 操作(资源类对外暴露的调用方法) 资源类
- 一言不合先创建一个资源类 ```java /**
- 题目:三个售票员 卖出 30张票
- 笔记:如何编写企业级的多线程代码
- 固定的变成套路+模板是什么?
- 在高内聚低耦合的前提下,线程 操作(资源类对外暴露的调用方法) 资源类
1.1 一言不合,先创建一个资源类 */ public class SaleTicketDemo1 { public static void main(String[] args) { // 主线程,一切程序的入口 Ticket ticket = new Ticket();
// Thread(Runnable target, String name) // 使用匿名内部类 /* new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.saleTicket();
}
}
}, “售票员1”).start(); new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.saleTicket();
}
}
}, “售票员2”).start(); new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 40; i++) {
ticket.saleTicket();
}
}
}, “售票员3”).start();*/ // 使用lambda表达式 new Thread(() -> {for (int i = 1; i <= 40; i++) ticket.saleTicket();}, “售票员1”).start(); new Thread(() -> {for (int i = 1; i <= 40; i++) ticket.saleTicket();}, “售票员2”).start(); new Thread(() -> {for (int i = 1; i <= 40; i++) ticket.saleTicket();}, “售票员3”).start(); } }
class Ticket { // 资源类 private int number = 30; private ReentrantLock lock = new ReentrantLock(); // 可重入锁
// public synchronized void saleTicket() {
public void saleTicket() {
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "\t 卖出的第"
+ number-- + "张票" + "还剩:" + number + "张");
}
} finally {
lock.unlock();
}
}
}
<a name="edpmV"></a>
## 1.2 Lambda表达式
- 口诀:拷贝小括号,写死右箭头,落地大括号
- 仅包含一个抽象方法的接口我们称为函数式接口(该接口可以通过lambda表达式实现匿名类),自动_加注解@FunctionalInterface_
- Java8的接口可以允许有默认方法和静态方法
```java
@FunctionalInterface // 有且仅有一个抽象方法的接口,会自动加上此注解
interface Foo { // 接口里面有且仅有一个抽象方法的,称为函数式接口
// void sayHello();
int add(int a, int b);
default int div (int x, int y) {
System.out.println("****hello div default method ******");
return x /y;
}
default int div2 (int x, int y) {
System.out.println("****hello div2 default method ******");
return x /y;
}
static int mv(int x, int y) {
return x * y;
}
static int mv1(int x, int y) {
return x * y;
}
}
/**
* 2. lambda表达式
* 2.1 口诀:拷贝小括号,写死右箭头,落地大括号
* 2.2 加注解@FunctionalInterface,这个注解只允接口许有一个抽象方法
* 2.3 java8接口可以有default方法
* 2.4 静态方法实现
*/
public class LambdaDemo {
public static void main(String[] args) {
// 通过lambda表达式实现匿名内部类接口
// Foo fo = () -> System.out.println("hello lambda expression");
// fo.sayHello();
Foo fo = (a, b) -> {
System.out.println("come in here -------");
return a + b;
};
System.out.println(fo.add(1, 2));
System.out.println(Foo.mv(1, 3));
}
}
三、*线程间通信
题目:两个线程,可以操作初始值为0的一个变量,实现一个线程对该变量+1,一个线程对该变量-1,实现交替,来10轮,变量初始值为0
3.1 两个线程synchronized写法
/**
* 题目:两个线程,可以操作初始值为0的一个变量
* 实现一个线程对该变量+1,一个线程对该变量-1
* 实现交替,来10轮,变量初始值为0
* 1. 高内聚第耦合的前提下,线程操作资源类
* 2. 判断/干活/通知
*/
public class ThreadWaitNotifyDemo {
public static void main(String[] args) {
AirConditioner airConditioner = new AirConditioner();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airConditioner.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airConditioner.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
class AirConditioner { // 资源类
private int number = 0;
public synchronized void increment() throws InterruptedException {
// 1. 判断
if (number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 1. 判断
if (number == 0) {
this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知
this.notifyAll();
}
}
3.2 四个线程synchronized写法
3.2.1 出现虚假唤醒和中断
换成4个线程会导致错误,虚假唤醒。
原因:在java多线程判断时,不能用if,程序出事出在了判断上面,突然有一添加的线程进到if了,突然中断了交出控制权,没有进行验证,而是直接走下去了,加了两次,甚至多次。
中断和虚假唤醒是可能产生的,所以要用loop循环,if只判断一次,while是只要唤醒就要拉回来再判断一次。if换成while。(也就是说在四个线程下,有可能两个increment线程都在**if**
中**wait**
,当其被唤醒时,不会再次判断number是否满足条件,而直接执行number++,因此会导致number大于1的情况,同理也会出现number小于0的情况)
3.2.2 使用while进行条件判断
- 高内聚第耦合的前提下,线程操作资源类
- 判断/干活/通知
- 多线程交互中,必须要防止多线程的虚假唤醒,也即(在多线程的判断中不许用if只能用while)
```java
/**
- 题目:两个线程,可以操作初始值为0的一个变量
- 实现一个线程对该变量+1,一个线程对该变量-1
- 实现交替,来10轮,变量初始值为0
- 高内聚第耦合的前提下,线程操作资源类
- 判断/干活/通知
- 多线程交互中,必须要防止多线程的虚假唤醒,也即(在多线程的判断中不许用if只能用while)
*/
public class ThreadWaitNotifyDemo {
public static void main(String[] args) {
AirConditioner airConditioner = new AirConditioner();
new Thread(() -> {
}, “A”).start(); new Thread(() -> {for (int i = 0; i < 10; i++) {
try {
airConditioner.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, “B”).start(); new Thread(() -> {for (int i = 0; i < 10; i++) {
try {
airConditioner.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, “C”).start(); new Thread(() -> {for (int i = 0; i < 10; i++) {
try {
airConditioner.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, “D”).start(); } }for (int i = 0; i < 10; i++) {
try {
airConditioner.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 多线程交互中,必须要防止多线程的虚假唤醒,也即(在多线程的判断中不许用if只能用while)
*/
public class ThreadWaitNotifyDemo {
public static void main(String[] args) {
AirConditioner airConditioner = new AirConditioner();
new Thread(() -> {
class AirConditioner { // 资源类 private int number = 0;
public synchronized void increment() throws InterruptedException {
// 1. 判断
while (number != 0) {
this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
// 1. 判断
while (number == 0) {
this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知
this.notifyAll();
}
}
<a name="Ks4mN"></a>
### 3.3.3 图示为什么会出现问题
在使用`if`判断两个线程的情况下,阻塞的线程只有两种情况,此时不会出现任何问题;<br />而使用`if`在四个线程的情况下,可能存在这种情况:
1. 最开始+线程进行了增加操作NotifyAll;
1. 此时+'线程抢占到执行权,进入if判断进入阻塞状态;
1. +线程又抢到了执行权,同样进入if判断阻塞;
1. -线程抢占执行权进行减操作,NotifyAll;
1. +'线程抢占执行权,进行增加操作,NotifyAll;
1. +线程抢占执行权,进行增加操作 (此时便出现了number=2的情况)
**使用while就不会出现这种问题,因为在NotifyAll线程激活运行后,会进行二次判断!**<br />
<a name="UsSjY"></a>
## 3.3 使用Lock和Condition实现线程间通信
通过Java8的Lock和Condition接口(await、signal、signalAll),可以替换synchronized与Object monitor方法(wait、notify、notifyAll)<br /><br />这里我们还是使用3.2中的例子,4个线程,两个打印1两个打印0,让其交替打印,分别打印十次
```java
public class ThreadWaitNotifyDemo {
public static void main(String[] args) {
AirConditioner airConditioner = new AirConditioner();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airConditioner.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airConditioner.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airConditioner.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airConditioner.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class AirConditioner { // 资源类
private int number = 0;
// 使用java8 lock 和 condition接口实现
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
// 1. 判断
while (number != 0) {
condition.await(); // this.wait();
}
// 2. 干活
number++;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知
condition.signalAll(); // this.notifyAll();
}catch (Exception e) {
}finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
// 1. 判断
while (number == 0) {
condition.await(); // this.wait();
}
// 2. 干活
number--;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3. 通知
condition.signalAll(); // this.notifyAll();
}catch (Exception e) {
}finally {
lock.unlock();
}
}
}
3.4 多线程之间精确唤醒
上面的例子只能说明Lock和Condition可以替代原来的同步方法和monitor方法,但是我们为什么要用Lock与Condition(与原来的方法相比,Lock和Condition好在哪里?他能解决哪些以前不能解决的问题?)
现在我们想实多个线程之间的顺序调用:三个线程启动,要求按照如下顺序AA打印5次,BB打印10次,CC打印15次循环十轮;
之前我们都是NotifyyAll和signalAll,这样会造成线程之间进行竞争,现在我们只想精确唤醒某个线程,所以我们需要用到Condition!
public class ThreadOrderAccess {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.printA();
}
}, "线程A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.printB();
}
}, "线程B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
shareResource.printC();
}
}, "线程C").start();
}
}
class ShareResource {
private int num = 1; // 1:A 2:B 3:C
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void printA() {
lock.lock();
try {
// 1.判断
while(num != 1) {
condition1.await();
}
// 2.干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
// 3.修改标志位 精确通知
num = 2;
condition2.signal();
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
// 1.判断
while (num != 2) {
condition2.await();
}
// 2.干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
// 3.修改标志位 精确通知
num = 3;
condition3.signal();
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
// 1.判断
while (num != 3) {
condition3.await();
}
// 2.干活
for (int i = 1; i <= 15; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
// 3.修改标志位 精确通知
num = 1;
condition1.signal();
}catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
对condition的一点理解
最开始我在使用Condition的时候一直在想,Condition是如何跟线程进行绑定的呢?其实可以这么理解,把condition看做是一个开关,在上面的例子中总共有三个开关,分别用于单独控制每个线程,当某个线程需要精确唤醒其他线程的时候,只需要将其开关打开即可conditionX.signal()。这样便能够实现线程的精确通信。
3.5 线程通信总结
- 高内聚第耦合的前提下,线程操作资源类
- 判断/干活/通知
- 多线程交互中,必须要防止多线程的虚假唤醒,也即**
- 注意标志位的修改和定位
四、多线程8锁
4.1 8锁演示
https://www.bilibili.com/video/BV1vE411D7KE?p=47
```java package com.juc;
import java.util.concurrent.TimeUnit;
/**
- 题目:多线程8锁
- 标准访问,请问先打印邮件还是短信? 邮件
- 邮件方法暂停4s,请问先打印邮件还是短信? 邮件
- 新增一个普通方法hello(),请问先打印邮件还是hello? hello
- 两部手机,请问先打印邮件还是短信? 短信
- 两个静态同步方法,同一部手机,请问先打印邮件还是短信? 邮件
- 两个静态同步方法,两部手机,请问先打印邮件还是短信? 邮件
- 1个普通同步方法,一个静态同步方法,1部手机,请问先打印邮件还是短信? 短信
1个普通同步方法,一个静态同步方法,2部手机,请问先打印邮件还是短信? 短信 */ public class Lock8 { public static void main(String[] args) throws InterruptedException{ Phone phone = new Phone(); Phone phone1 = new Phone(); new Thread(() -> {
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, “Thread A”).start();
// 1. 标准访问,请问先打印邮件还是短信? 邮件,因为主线程先创建A线程,然后sleep,那么A线程会抢占到Phone的锁 Thread.sleep(100);
new Thread(() -> {
try {
// phone.sendSMS(); // phone.hello(); // 3. 新增一个普通方法hello(),请问先打印邮件还是hello? hello 因为该方法没有加锁
phone1.sendSMS(); // 4. 两部手机,请问先打印邮件还是短信? 短信
} catch (Exception e) {
e.printStackTrace();
}
}, “Thread B”).start(); } }
class Phone { public static synchronized void sendEmail() throws Exception { // Thread.sleep(4000); // 2. 邮件方法暂停4s,请问先打印邮件还是短信? 邮件,sleep并不释放锁 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(“———-sendEmail”); } public synchronized void sendSMS() throws Exception { System.out.println(“———-sendSMS”); } public void hello() { System.out.println(“———-hello”); } }
<a name="si24d"></a>
## 4.2 8锁解释
1. 第1锁(主线程sleep 0.1s)和第2锁(邮件方法sleep 4s)的情况下,phone里面都是非静态同步方法,一个对象里面如果有多个非静态的synchronized方法,某一个时刻内,只要访问当前对象的所有线程中有一个线程去调用其中的一个synchronized方法了,其他的线程都只能等待。换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法;锁的是当前对象this,被锁定后,访问当前对象的其他线程都不能进入到当前对象的其他synchronized方法。
1. 第3锁(新增一个普通方法hello),普通方法在锁定当前对象this的时候,不会受到锁的影响;
1. 第4锁(两部手机分别调用邮件跟短信),当前情况下不存在线程争抢,分别锁住了自己的当前的对象,并不是同一把锁。
1. 第5锁(两静态同步方法,同一个手机)和第6锁(两静态同步方法,两个手机),静态同步方法锁的不是对象实例,而是Class对象(Phone.class),此时两个实例是共用一个Class模板的。
1. 第7锁(1普通同步、1静态同步、1手机)和第8锁(1普通同步、1静态同步、2手机),此时静态同步方法锁的是Class对象,同步方法锁的是实例对象,两个同步方法的锁不同,所以不会产生竞争。
总结:静态同步方法锁Class对象(不同实例对象共用一个Class对象),普通同步方法锁当前实例对象(不同实例对象锁不同),不同的锁之间不会产生竞争。
<a name="FxnZ4"></a>
# 五、集合类不安全问题
<a name="nNJ7l"></a>
## 5.1 List的线程不安全
```java
/**
* 题目:请举例说明集合类是不安全的
*/
public class NotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
5.1.1 故障现象
5.1.2 导致原因&解决方案
会出现ConcurrentModificationException是因为ArrayList的add方法不是线程安全的;当某个线程正在向List中写入数据时,另外一个线程同时进来写入,就会导致ConcurrentModificationException。
- 我们可以使用线程安全的集合类Vector,其add方法是同步方法(保证了数据一致性,但是访问性能下降);
- 使用Collections.synchronizedList(new ArrayList<>()) 创建一个线程安全的List (其实就是在add的时候使用了synchronized同步代码块);
- CopyOnWriteArrayList 写时复制ArrayList (多线程建议使用这个)
Vector是线程安全的,能够保证数据一致性但是性能低;ArrayList牺牲了数据一致性提升了读写效率;现在想要保证数据一致性的同时也要保证读写效率,那应该怎么办?因此出现了读写分离的CopyOnWriteArrayList 。
写时复制
我们可以先看看CopyOnWriteArrayList中add方法的源码
private transient volatile Object[] array;
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray(); // 获取当前List中的所有元素
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 拷贝旧元素到新的扩容的数组中
newElements[len] = e; // 添加新的值
setArray(newElements); // 更新
return true;
} finally {
lock.unlock();
}
}
CopyOnWrite容器即写时复制的容器,往一个容器中添加元素的时候,不直接往当前容器Object[]添加,而是现将当前容器Object[]进行Copy,而是复制出一个新的容器Object[] newElements向新容器添加元素,添加之后,再将原容器的引用指向新的容器setArray(newElements);这样做的好处时可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
5.2 Set的线程不安全
public class NotSafeDemo {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
}
上面这段代码同样会出现java.util.ConcurrentModificationException异常;同样可以使用Collections.synchronizedSet()和CopyOnWriteArraySet,其具体原理与List的一致。
这里说一下HashSet的源码,HashSet其实就是一个HashMap,HashSet的中存的值是HashMap的key,HashMap中的Value是一个固定对象PRESENT
5.3 Map的线程不安全
public class NotSafeDemo {
public static void main(String[] args) {
// HashMap<String, String> map = new HashMap<>();
// HashMap<String, String> map1 = (HashMap<String, String>) Collections.synchronizedMap(new HashMap<String, String>());
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
}
同样HashMap也是线程不安全的,可以使用集合工具类Collections.synchronizedMap(new HashMap
六、Callable
实现多线程的四种方式:继承Thread重写run、实现Runnable重写run、实现Callable重写call、线程池
6.1 与Runable对比
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
1. call()可以返回值的;
2. call()可以抛出异常,被外面的操作捕获,获取异常的信息;
3. Callable是支持泛型的;
6.2 通过Callable去开启一个线程—FutureTask
public class CallableDemo {
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask(new MyThreadCall());
new Thread(futureTask, "Thread A").start();
new Thread(futureTask, "Thread B").start(); // 不会再次执行call方法,因为是用一个futuretask
try {
System.out.println(futureTask.get()); // 获取call方法的返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "*****计算完成");
}
}
class MyThreadCall implements Callable<Integer> {
@Override
public Integer call() throws Exception {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("*************come in here");
return 1024;
}
}
原理
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,
当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。 一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。(会做的题目先做)
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。
只计算一次,如果通过同一个futuretask对象开启两个线程,call方法只会调用一次。
get方法一般请放在最后一行,因为get方法会阻塞main线程的运行。
七、JUC辅助类
7.1 CountDownLatch 减少技术
CountDownLatch内部维护了一个计数器,只有当计数器==0时,某些线程才会停止阻塞,开始执行。
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
- 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
案例:main线程是班长,6个线程是学生,只有6个线程运行完毕,都离开教室后,main线程班长才会关教室门。
public class CountDownDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i < 7; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t离开教室");
countDownLatch.countDown(); // 每输出一次 -1
}, String.valueOf(i)).start();
}
countDownLatch.await(); // 只有当count=0的时候才会唤醒main线程
System.out.println(Thread.currentThread().getName() + "\t班长关门走人");
}
}
7.2 CyclicBarrier循环栅栏
CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过CyclicBarrier的await()方法。(CyclicBarrier与CountDownLatch相反,CountDownLatch是加,CyclicBarrier是减)
案例:集齐7颗龙珠召唤神龙
public class CyclicBarrierDemo {
public static void main(String[] args) {
// CyclicBarrier(int parties, Runnable barrierAction)
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> System.out.println("召唤神龙"));
for (int i = 1; i < 8; i++) {
int finalI = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t收集到第" + finalI + "颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, "Thread" + i).start();
}
}
}
7.3 Semaphore信号量
CountDownLatch
的问题是不能复用。比如count=3,那么加到3,就不能继续操作了。而Semaphore
可以解决这个问题,比如6辆车3个停车位,对于CountDownLatch
只能停3辆车,而Semaphore
可以停6辆车,车位空出来后,其它车可以占有,这就涉及到了Semaphore.accquire()
和Semaphore.release()
方法。
在信号量上我们定义两种操作:
- acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
- release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
例子:6台车抢占3个车位
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3); // 模拟资源类有3个空车位
for (int i = 1; i < 7; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 当前线程抢占,信号量-1
System.out.println(Thread.currentThread().getName()+"\t抢占到了车位");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 当前线程释放,信号量+1
}
}, "Thread" + i).start();
}
}
}
八、ReadWriteLock 读写锁
ReentrantReadWriteLock可重入读写锁是读写锁的唯一实现。
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源类,就不应该再有其他线程可以对该资源进行读或写。
小总结: 读-读 能共存;读-写 不能共存;写-写 不能共存
8.1 有问题的例子
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache= new MyCache();
for (int i = 1; i < 6; i++) {
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt+"", tempInt+"");
}, "Thread " + i).start();
}
for (int i = 1; i < 6; i++) {
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt+"");
}, "Thread " + i).start();
}
}
}
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
public void put(String key, Object value) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t写入数据" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t写入完成");
}
public void get(String key) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t读取数据");
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t读取完成" + result);
}
}
查看输出会发现,当一个线程在写的时候另外一个线程同时也在写,这种情况会引起重大的问题(比如数据库事务的ACID)。
8.2 使用读写锁
我们改下一下MyCache类,使用ReentrantReadWriteLock在写的时候加上写锁,在读的时候加上读锁(之所以加读锁是因为写的时候不能够进行读操作)当某个写线程获取到锁的时候,其他读/写线程均阻塞,当某个读线程获取到锁的时候,其他写线程均阻塞,读线程可并发访问资源类数据。
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
readWriteLock.writeLock().lock(); // 加上写锁
try {
System.out.println(Thread.currentThread().getName() + "\t写入数据" + key);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key) {
readWriteLock.readLock().lock(); // 加上读锁
try {
System.out.println(Thread.currentThread().getName() + "\t读取数据");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t读取完成" + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
九、BlockingQueue
9.1 什么是阻塞队列,有什么用
阻塞队列是一个队列,在数据结构中起的作用如下图:
线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素
当队列是空的,从队列中获取元素的操作将会被阻塞;
当队列是满的,从队列中添加元素的操作将会被阻塞;
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素;
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增;
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
为什么需要BlockingQueue?
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了;
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。
9.2 种类分析以及核心方法
9.2.1 种类分析
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
- LinkedTransferQueue:由链表组成的无界阻塞队列。
LinkedBlockingDeque:由链表组成的双向阻塞队列。
9.2.2 核心方法
抛出异常:当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full;当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
- 特殊值:插入方法,成功ture失败false;移除方法,成功返回出队列的元素,队列里没有就返回null
- 一直阻塞:当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出;当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用
超时退出:当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出 ```java public class BlockingQueueDemo { public static void main(String[] args) throws InterruptedException {
// 有界阻塞队列,所以需要指定队列大小
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
// 抛出异常
/* System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// System.out.println(blockingQueue.add("x")); // java.lang.IllegalStateException: Queue full
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove()); // java.util.NoSuchElementException*/
// System.out.println(blockingQueue.add(“a”)); // System.out.println(blockingQueue.add(“b”)); // System.out.println(blockingQueue.element()); // 与peek一样输出队首元素但不出队,不同的是如果队列为空报异常
// 特殊值
/*System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("x")); // 队列满了继续添加不会抛出异常,而是输出false
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll()); // null*/
// 阻塞
/ blockingQueue.put(“a”); blockingQueue.put(“b”); blockingQueue.put(“c”); // blockingQueue.put(“d”); // 当队列满了,这个线程会被阻塞,程序不会结束执行 System.out.println(blockingQueue.take()); System.out.println(blockingQueue.take()); System.out.println(blockingQueue.take()); System.out.println(blockingQueue.take()); // 当队列为空 ,这个线程会被阻塞,程序不会结束执行/
// 超时
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("d", 2l, TimeUnit.SECONDS)); // 队列满了,阻塞2s后会返回false
}
}
<a name="qTWxY"></a>
# 十、线程池
<a name="OqyCn"></a>
## 10.1 为什么用线程池
线程池的优势(为什么要用线程池?):线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。避免了反复创建线程,减少了上下文的交换和资源消耗。<br />**它的主要特点为:线程复用、控制最大并发数、管理线程**。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
<a name="TV5f8"></a>
## 10.2 线程池的使用
<a name="Xgloz"></a>
### 10.2.1 架构说明
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors(线程池工具类),ExecutorService,**ThreadPoolExecutor**这几个类<br /><br />我们通过Executors工具类拿到**ThreadPoolExecutor**
<a name="x5XsL"></a>
### 10.2.2 编码实现
<a name="mMXl2"></a>
#### FixedThreadPool
FixedThreadPool执行长期的任务性能好,他是一个具有固定线程数量的线程池;newFixedThreadPool创建的线程池**corePoolSize和maximumPoolSize值是相等**的,它**使用的是LinkedBlockingQueue**。
<a name="RPboJ"></a>
#### SingleThreadExecutor
newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是**LinkedBlockingQueue**。一个线程一个线程的执行,线程池中只有一个线程。
<a name="s6eam"></a>
#### CachedThreadPool
执行很多短期异步任务,线程池根据需要创建新线程,在先前创建的线程可用时将重用它们,可扩容。<br />newCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它使用的是**SynchronousQueue**,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
<a name="or1uW"></a>
#### 代码
```java
public class MyThreadPoolDemo {
public static void main(String[] args) {
// 创建一个线程池,其中有5个线程;类似于一个银行有5个业务窗口
// FixedThreadPool适用与执行长期任务,性能好;创建一个线程池,其中有N个固定的线程
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 创建一个线程池,其中只有1个线程;类似于一个银行有1个业务窗口
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 创建一个线程池,会根据当前线程需求创建线程,已创建的线程可复用,空闲60s后销毁
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
10.2.3 源码
10.3 线程池的几个重要参数
线程池总共有7大参数;
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
1、corePoolSize:线程池中的常驻核心线程数;
2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1;
3、keepAliveTime:多余的空闲线程的存活时间当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止;
4、unit:keepAliveTime的单位 ;
5、workQueue:任务队列,被提交但尚未被执行的任务;(排号等位)
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可;
7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略;
10.4 线程池底层工作原理
工作流程
- 在创建了线程池后,开始等待请求
- 当调用execute()方法后,线程会作出如下判断:
- 如果正在运行的线程数小于corePoolSize,那么马上创建线程来运行当前任务;
- 如果正在运行的线程数等于或大于corePoolSize,那么尝试将当前任务放入队列;
- 如果这个时候队列满了且正在运行的线程小于maximumPoolSize,那么新建线程运行当前任务;
- 如果队列满了且正在运行的线程大于或者等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
- 当一个线程完成任务时,会从队列中取出下一个任务执行;
- 当一个线程无事可做,超过keepAliveTime后,线程会判断:
- 如果当前运行的线程数大于等于corePoolSize,那么这个线程会被停掉;
- 线程池中所有线程完成任务后,它最终会收缩到corePoolSize大小
10.5 线程池用哪个?生产中如设置合理参数
在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?超级大坑
答案是一个都不用,我们工作中只能使用自定义的,为什么呢?怎么定义maxPoolSize
这个要看是CPU密集型还是IO密集型,如果是CPU密集型那么将maxPoolSize设置为CPU核心数+1;System._out_.println(Runtime._getRuntime_().availableProcessors()); // 获取CPU核心数 (这里指的是线程数)
如果是IO密集型,maxPoolSize设置为:IO密集型核心线程数 = CPU核数 / (1-阻塞系数)
阻塞系数是指线程花在系统IO上的时间与CPU密集任务所耗时间的比值10.6 自定义线程池
线程池中能够容纳的最大线程数=maxPoolSize + workQueue队列容量;如果并发线程高于这个数也不一定会报java.util.concurrent.RejectedExecutionException异常,这与每个线程的执行速度有关。public class MyThreadPoolDemo {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors()); // 获取CPU核心数 (这里指的是线程数)
ExecutorService threadPool = new ThreadPoolExecutor(2,
Runtime.getRuntime().availableProcessors() + 1,
2L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3), // 默认是 Integer.MAX_VALUE
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// maxPoolSize + workQueue队列容量 = 线程池容纳的最大线程数
try {
for (int i = 0; i < 10; i++) { // 这里创建10个线程不一定会报java.util.concurrent.RejectedExecutionException,这个要看线程的执行速度
threadPool.execute(() -> System.out.println(Thread.currentThread().getName() + "\t办理业务"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
线程池的4大拒绝策略
等待队列已经排满了,再也塞不下新任务了。同时,线程池中的max线程也达到了,无法继续为新任务服务。这个是时候我们就需要拒绝策略机制合理的处理这个问题。
JDK中内置了如下4中拒绝策略:内置拒绝策略均实现了RejectedExecutionHandle接口
- AbortPolicy(默认):”中断策略”,直接抛出RejectedExecutionException异常阻止系统正常运行;
- CallerRunsPolicy:”调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量;
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
11. 分支合并框架&异步回调
11.1 Java8之流式计算复习
1. 四大函数式接口
```java Function
function = a -> a.length(); // lambda表达式实现函数式接口 函数型 // Function function = String::length; // 函数引用实现函数式接口 函数型 System.out.println(function.apply(“aaaaa”));
// Predicate
// Consumer
// Supplier
<a name="qDvEI"></a>
### 2. stream流
流(Stream) 到底是什么呢?是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据,流讲的是计算!”<br />Stream的特点:
- Stream 自己不会存储元素
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
class User {
private Integer id;
private String userName;
private int age;
}
/**
* 题目:请按照给出数据,找出同时满足
* 偶数ID且年龄大于24且用户名转为大写且用户名字母倒排序
* 最后只输出一个用户名字
*/
public class StreamDemo {
public static void main(String[] args) {
User u1 = new User(11, "a", 23);
User u2 = new User(12, "b", 24);
User u3 = new User(13, "c", 22);
User u4 = new User(14, "d", 28);
User u5 = new User(16, "e", 26);
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
list.stream().filter(p -> p.getAge() > 24 && (p.getAge() & 2) == 0).
map(p -> p.getUserName().toUpperCase()).
sorted(Comparator.reverseOrder()). // (o1, o2) -> o2.compareTo(o1)
limit(1).forEach(System.out::println);
}
}
11.2 分支合并框架ForkJoin
Fork:把一个复杂任务进行分拆,大事化小
Join:把分拆任务的结果进行合并
class MyTask extends RecursiveTask<Integer> {
private static final Integer ADJUST_VALUE = 10;
private int begin;
private int end;
private int result;
public MyTask(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
if ((end - begin) <= ADJUST_VALUE) {
for (int i = begin; i < end; i++) {
result += i;
}
} else {
int middle = (begin + end) / 2;
MyTask task01 = new MyTask(begin, middle);
MyTask task02 = new MyTask(middle + 1, end);
task01.fork(); // fork()方法,开启一个子线程进行调用compute方法
task02.fork();
result = task01.join() + task02.join(); // join()方法,返回子线程的结果
}
return result;
}
}
/**
* @author mrlinxi
* @create 2022-04-07 11:43
*
* 分支合并框架
* ForkJoinPool
* ForkJoinTask
* RecursiveTask
*/
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
MyTask myTask = new MyTask(0, 100);
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask); // 提交线程
System.out.println(forkJoinTask.get());
forkJoinPool.shutdown();
}
}
11.3 异步回调
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// runAsync 没有返回值,传入一个Runnable接口实现类
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() ->
System.out.println(Thread.currentThread().getName() + "\t没有返回值, update mysql ok"));
System.out.println(completableFuture.get());
//异步回调 supplyAsync有返回值
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"\t completableFuture2");
int i = 10/0;
return 1024;
});
System.out.println(completableFuture2.whenComplete((t, u) -> { // 当线程完成,不论是否发生异都会到这里
System.out.println("-------t=" + t); // 这个t就是正常执行得到的返回值 如果发生异常为null
System.out.println("-------u=" + u); // 正常执行时为null,发生异常为异常信息
}).exceptionally(f -> { // 异常完成,执行过程中发生异常
System.out.println("-----exception:" + f.getMessage());
return 444;
}).get()); // get()获取返回值,正常执行返回1024,异常执行返回444
}
}
JVM
请你谈谈对JVM的理解?JAVA8的虚拟机有什么更新?
什么是OOM?什么是StackOverFlowError?有哪些方法分析?
JVM的常用调优参数你知道哪些?
谈谈你对JVM中类加载器的认识?
一、JVM体系结构概述
JVM是运行在操作系统之上的,它与硬件没有直接的交互。
JVM体系结构概览:(灰色每个线程独有一份,方法区跟堆所有线程共享)
(方法区在jdk1.7之前是放在永久代中,jdk1.8取消了永久代,将其放入到了元空间)
1.1 ClassLoader 类装载器
负责加载.class文件(字节码文件),class文件在文件开头有特定的文件标识(cafe babe),将class文件字节码内容加载到内存中,并将这些呢绒转换成方法区中的运行时数据结构,并且ClassLoader只负责class文件的加载(类加载器只负责将class文件加载到JVM中),至于它是否可以运行,由Execution Engine决定。
类加载是一个将class字节码文件实例化成Class对象并进行相关初始化的过程。(from 硬盘 to 方法区,形成运行时数据结构,也就是类模板Class)
Book | b1= | new Book(); |
---|---|---|
方法区类模板 | java栈 | 堆 |
类加载器可以细分为四种
- 虚拟机自带的类加载器(BootstrapClassLoader):(启动类加载器/根加载器,通过C++编写)
- 拓展类加载器(Extension):在jdk9中称为Platform ClassLoader(平台类加载器);jdk8及之前的加载器是Extension ClassLoader;通过JAVA编写
- 应用程序类加载器(Application ClassLoader):Java也叫系统类加载器,加载当前应用的classpath的所有类;
用户自定义加载器(User ClassLoader):Java.lang.ClassLoader的子类,用户可以定制类的加载方式
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoaderTest classLoaderTest = new ClassLoaderTest();
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoaderTest.getClass().getClassLoader());
// sun.misc.Launcher$ExtClassLoader@1b6d3586
System.out.println(classLoaderTest.getClass().getClassLoader().getParent());
// 获取不到BootstrapClassloader,因为是用C++写的,并不属于Java体系内,所以为null
System.out.println(classLoaderTest.getClass().getClassLoader().getParent().getParent());
}
}
1.1.1 双亲委派模型+java的沙箱安全机制
java是如何加载一个类的?他有3个类加载器,如何做到不冲突却保证大家用到的都是同一个类?
当一个类加载器收到了加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次的类加载器都是如此,因此所有的家在请求都应该传送到BootStrap类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己加载。
采用双亲委派的一个好处时比如加载位于rt.jar(rt表示runtime)包中的类java.lang.Object,不管是哪个加载器加载这个类,最重都是委托给顶层的BootStrap进行加载,这样就保证了不同类加载器得到的都是同一个Object对象。
工作过程:
当App需要加载类时:App—>Exention—>Bootstrap;如果Bootstrap加载失败,Exention就加载;如果Exention失败,App加载;如果App还失败了,抛异常ClassNotFoundException;
这样就可以防止内存中出现多份同样的字节码——也就是所谓的沙箱安全机制
1.2 Execution Engine
Execution Engine执行引擎负责解释命令,提交操作系统执行。
1.3 本地方法栈和本地接口
比如Thread类中有一个native start0()方法;表面上通过.start()方法开启多线程,但实际上start方法调用的是start0本地方法。(进程线程和语言无关,只与操作系统有关)
native修饰的方法不在java管理范围之内,表示调用底层操作系统或者C语言编写的第三方函数库;
普通方法进入java栈,native方法进入本地方法栈
- Native Interface:融合不同的编程语言为Java所用(C/C++),在execution engine执行本地方法栈中登记的native方法时加载native interface,现在基本不使用了;
- Native Method Stack:登记native方法,在execution engine执行时加载本地方法库。
1.4 程序计数器(各线程隔离)
Program Counter Register程序计数器也称PC寄存器,每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
它是当前线程所执行的字节码的行号指示器;这块内存区域很小,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。1.5 方法区(多个线程共享)
供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool) 、字段和方法数据、构造函数和普通方法的字节码内容。
But上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space,jdk1.8以前) 和元空间(Metaspace,jdk1.8)。
实例变量存在堆内存中,和方法区无关。(方法区存在少量垃圾,但GC主要作用于堆)StackOverFlowEroor
StackOverFlowEroor是异常还是错误?1.6 类的加载顺序
普通代码块的加载顺序问题,JVM如何加载?
普通代码块的执行顺序由他们在代码块中的出现顺序决定(先出现先执行)
类中的非静态代码块称为构造代码块,其优先于构造函数执行,且每次创建对象的时候均会被调用,若存在显示赋值的非静态类变量;则非静态成员变量的赋值和构造代码块的执行顺序由出现顺序决定(先出现先执行)
同一个java程序中public类跟非public类中的静态代码块、构造块、构造方法在jvm中如何加载?优先级、加载顺序、加载次数?
jvm会首先加载public类的静态代码块(如果有显示赋值的静态类变量,也会一同加载,按出现顺序执行,且仅加载一次);非public的类只有在使用时才会去加载,加载顺序跟public一样。其他的跟上面讨论过的一致。
1.7 栈(Java栈)
栈管运行,堆管存储。
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
1.7.1 栈存储什么?
栈帧中主要保存3类数据:
- 本地变量(Local Variables) :输入参数和输出参数以及方法内的变量;
- 栈操作(Operand Stack) : 记录出栈、入栈的操作;
栈帧数据(Frame Data) :包括类文件、方法等等。.每一个方法就是一个栈帧
1.7.2 栈运行原理
栈中的数据都是以栈帧(Stack Frame) 的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈,….
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧….遵循“先进后出”/“后进先出”原则。
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256k~756K之 间,约等于1Mb左右。1.8 栈、堆、方法区的交互关系
二、堆体系结构概述
2.1 堆的逻辑结构
一个JVM实例只存在一一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
java7堆内存从逻辑上分为三部分:新生代、老年代、永久代
java8堆内存从逻辑上分为三部分:新生代、老年代、元空间新生代
- Eden区:新建对象会放在该区域内;
- Survivor From(S0)
- Survivor To(S1)
- 老年代
- 元空间(jdk8,之前叫永久代)
新生代与老年代在堆中的比例为1:2;其中,新生代 Eden:S0:S1=8:1:1
java8不再有永久代这一概念,取而代之的是元空间,元空间直接使用本地内存。
2.1.1 堆内存溢出OOM
OOM:java heap space
:OutOfMemoryError,堆内存溢出;当Full GC过后,老年代仍然没有剩余空间,就会报OOM。OOM的错误会有很多不同的类型。
出现OOM的原因?
- Java堆的空间太小,可通过参数-Xms(初始堆内存) -Xmx(最大堆内存)调整 (X表示常量,m表示memory,s表示start,x表示max)
- 代码中创建了大量的大对象,并且长时间无法被GC收集
出现OOM:java heap space怎么办?
- 调整JVM的参数,扩大堆内存 -Xms(初始堆内存) -Xmx(最大堆内存)
- 扩大了仍然不够,检查代码(是否出现死循环)
Object o = new Object(); // 创建一个Object 对象大约占用16kb的内存
2.1.2 方法区与堆的关系
实际而言,方法区(Method Area)和堆-样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有个别名叫做Non-Heap(非堆), 目的就是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)”,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接Dinterface)的一个实现。
永久代Perm( java7之前有)
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class.、Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
元空间Metaspace (java8)
区别于永久代,元空间在本地内存中分配。在JDK8里,Perm 区中的所有内容中字符串常量移至堆内存,其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间内。
2.2 GC
- 什么是GC?
- JVM中,如何判断一个对象是垃圾;是不是马上就回收
- 如何回收?有哪些条件和支撑前提?
- 说说你知道的GC算法
GC=发生在新生代的轻量级GC(Minor GC)+发生在老年代的重量级GC(Full GC)
2.2.1 复制算法
新生代发生的垃圾回收用的算法就是:复制算法。GC回收里面,一般说GC就是用复制算法进行垃圾回收。
新生代中,从GC的角度来看对象的诞生、运行、回收及整个声明周期如下:GC(复制)之后有交换,谁空谁是to
新生代中,Eden区和S0与S1区默认的比例是8:1:1。新生代=Eden+S0+S1
MinorGC的过程(复制->清空->互换):
- eden、SurvivorFrom复制到SurvivorTo, 年龄+1
首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则复制到老年代区,默认15),同时把这些对象的年龄+1。
- 清空eden、SurvivorFrom
然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to
- SurvivorTo和SurvivorFrom互换
最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成 为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
- 大对象特殊情况
如果分配的新对象比较大Eden区放不下但Old区可以放下时,对象会被直接分配到Old区(即没有晋升过程,直接到老年代了)
面试题
1. 新生代为什么需要Survivor区?
为什么不直接从Eden到老年代?为什么要这么复杂?
如果没有Survivor区,Eden区每进行一次MinorGC存活的对象就会送到老年代,老年代很快就会被填满。而很多对象虽然一次MinorGC没有消灭但其并不会存活很久,这时候移入老年代是一个很不明智的选择。所以Survivor存在的意义就是减少被送到老年代的对象,进而减少FullGC的发生。
2. 新生代为什么要两个Survivor,为什么是8:1:1?
设置两个Survivor的最大好处就是解决内存的碎片化。
如果Survivor只有一个,当需要进行垃圾回收的时候,无法区分Survivor中哪些是需要清除的。那么此时只能使用标记清除,而标记清除会产生内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。两个Survivor在MinorGC时相互交换的最大好处就是,整个过程中,永远有一个Survivorspace是空的,另一个非空的Survivorspace是无碎片的。
那么,Survivor为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果Survivor区 再细分下去,每一块的空间就会比较小,容易导致Survivor区满,两块Survivor区是经过权衡之后的最佳方案。
三、堆参数调优入门
-Xmn:新生代大小参数,X表示常量,m表示memory,n表示new。
在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间与永久代之间最大的区别在于:
永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。因此,默认情况下,元空间的大小仅受本地内存限制。
3.1 堆内存调优简介
-Xms | 设置堆初始分配大小,默认为物理内存的1/64 |
---|---|
-Xmx | 堆的最大分配内存,默认为物理内存的1/4 |
-XX:+PrintGCDetails | 输出详细的GC处理日志 |
基本不调ms跟mx,如果调整则调成一样,避免忽高忽低
public class JVMDemo {
public static void main(String[] args) {
long maxMemory = Runtime.getRuntime().maxMemory(); // 返回JVM试图使用的最大内存量
long totalMemory = Runtime.getRuntime().totalMemory(); // 返回JVM中的内存总量
System.out.println("TOTAL_MEMORY(-Xms) =" + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
System.out.println("MAX_MEMORY(-Xmx) =" + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
}
}
3.2 怎么调整JVM的堆大小
四、总结
五、GC专题
5.1 GC是什么
GC就是垃圾回收机制,遵守分代收集的算法思路:
- 引用计数(弊端比较明显,基本淘汰了)
- GCRoots根可达算法
5.2.1 引用计数
每个对象都有一个引用计数器,任何一个对象对A的引用,那么对象A的引用计数器+1,当引用失败时,对象A的引用计数器就-1,如果对象A的计数器的值为0,就说明对象A没有引用了,可以被回收。
优点:实时性高;区域性强,不需要扫描全部对象。5.2.2 GCRoots根可达算法
为了解决引用计数的循环引用问题,Java使用了可达性分析算法。
所谓”GC Roots”或者说tracing GC的”根集合”就是一组必须活跃的引用。
基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡。5.2.3 Java中可以作为GCRoots的对象
GCRoots是一个set,其中包含了如下对象:
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
5.3 GC三大算法
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)
Minor GC和Full GC的区别:
- 普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
- 全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上
5.3.1 复制算法
年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)。
因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法(复制算法在复制数量少且小的对象时效率很高),复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden是就变成空的了。
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,通过-XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代。
-XX:MaxTenuringThreshold — 设置对象在新生代中存活的次数
GC年龄采用4为bit存储,最大为15,例如MaxTenuringThreshold参数默认值就是15
优缺点:
- 优点:没有清除和标记的过程,效率高;没有内存碎片;不会暂停程序;
- 缺点:浪费了一半内存;如果对象存活率很高,则效率极低。
5.3.2 标记清除
老年代一般是由标记清除或者是标记清除与标记压缩(标记整理)的混合实现。当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停。标记清除法分为两个阶段,先标记出要回收的对象,然后统一进行回收。
- 标记:从引用根节点开始标记遍历所有的GC Roots, 先标记出可达对象(未标记的就是要清除的)。
- 清除:遍历整个堆,把未标记的对象清除。
优缺点:
- 优点:不会浪费内存空间;
缺点:此算法需要暂停整个应用,会产生内存碎片(内存空间是不连续的) ;效率低,需要两次遍历;
5.3.3 标记压缩
老年代一般是由标记清除或者是标记清除与标记压缩(标记整理)的混合实现。
在整理压缩阶段,不再对标记的对像做回收,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价优缺点
优点:解决了标记清理算法产生内存碎片的问题;
- 缺点:效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。
总结
- 内存效率:复制算法>标记清除>标记整理(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
- 内存整齐度:复制算法=标记整理>标记清除
- 内存利用率:标记清除=标记整理>复制算法
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程
六、JMM
6.1 谈谈你对JMM的理解
JMM(Java Memory Model,Java内存模型),本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范。通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,因此线程间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
6.1.1 可见性
可见性需要结合volatile来说明,volatile是java虚拟机提供的轻量级的同步机制;volatile主要有以下三大特性:保证可见性、不保证原子性、禁止指令重排序
写一个程序来验证可见性
class Number {
volatile int number = 10;
public void addTo1205() {
this.number = 1205;
}
}
/**
* @author mrlinxi
* @create 2022-04-07 13:09
*
* JMM:可见性(通知机制)
*/
public class JMMDemo {
public static void main(String[] args) {
Number number = new Number();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
number.addTo1205(); // 将10修改为1205,并写回了主内存
System.out.println(Thread.currentThread().getName() + "\t update number, number value = " + number.number);
}, "Thread A").start();
while (number.number == 10) {
// 如果number=10,那么main线程一直等待
// 需要有一种通知机制告诉main线程,number已经修改为1205,跳出while
}
System.out.println(Thread.currentThread().getName() + "\t mission over" + number.number);
}
}