JUC并发编程【java提高】
前言
本文依照狂神视频学习,
并参考,CSDN优秀博主的博文
掺杂自己的拙见
仅作学习交流使用
推荐
【【狂神说Java】JUC并发编程最新版通俗易懂-哔哩哔哩】
JUC并发编程
准备工作
1.新建maven项目juc
导入lombok
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
2.确定jdk8工作环境
1、什么是JUC
源码+官方文档
JUC就是java.util.concurrent下面的类包,专门用于多线程的开发。
java.util 工具包、包、分类
业务:普通的线程代码 Thread
Runnable 没有返回值、效率相比于Callable相对较低!
2、线程和进程
线程、进程,如果不能使用一句话说出来的,技术不扎实
进程是操作系统中的应用程序、是资源分配的基本单位,线程是用来执行具体的任务和功能,是CPU调度和分派的最小单位一个进程往往可以包含多个线程,至少包含一个
1)进程
一个程序,QQ.exe Music.exe 程序的集合;
一个进程往往可以包含多个线程,至少包含一个!
Java默认有几个线程? 2 个 mian、GC
2)线程
开了一个进程 Typora,写字,自动保存(线程负责的)
对于Java而言:Thread、Runable、Callable进行开启线程的。
JAVA真的可以开启线程吗? 开不了的!
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
Java是没有权限去开启线程、操作硬件的,这是一个native的一个本地方法,它调用的底层的C++代码。
并发、并行
并发编程:并发(多线程操作同一资源)
- CPU 一核,模拟出来多条线程,天下武功,唯快不破,快速交替
- 并发编程的本质:充分利用CPU的资源!
并行(多个人一起行走)
- CPU多核,多个线程可以同时执行。 我们可以使用线程池!
获取cpu的核数
package com.kuang.demo01;
public class Test1 {
public static void main(String[] args) {
//获取cpu的核数
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
线程有几个状态
public enum State {
//新生
NEW,
//运行
RUNNABLE,
//阻塞
BLOCKED,
//等待
WAITING,
//超时等待
TIMED_WAITING,
//终止
TERMINATED;
}
wait/sleep区别
1、来自不同的类
wait => Object
sleep => Thread
一般情况企业中使用休眠是:
TimeUnit.DAYS.sleep(1); //休眠1天
TimeUnit.SECONDS.sleep(1); //休眠1s
2、关于锁的释放
wait 会释放锁;
sleep睡觉了,不会释放锁;
3、使用的范围是不同的
wait 必须在同步代码块中;
sleep 可以在任何地方睡;
4、是否需要捕获异常
wait是不需要捕获异常;
sleep必须要捕获异常;
3、Lock锁(重点)
传统 Synchronized
package com.kuang.demo01;
//基本的卖票例子
/**
* 真正的多线程开发,公司中的开发
* 线程就是一个资源类,没有任何附属的操作
*/
public class SaleTicketDemo01 {
public static void main(String[] args) {
//并发,多线程操作同一个资源类,把资源类丢入线程
Ticket ticket=new Ticket();
//@FunctionalInterface 函数式接口,jdk1.8 Lambdab表达式(参数)->{代码}
new Thread(()->{
for(int i=1;i<40;i++){
ticket.sale();
}
},"A").start();
new Thread(()->{
for(int i=1;i<40;i++){
ticket.sale();
}
},"B").start();
new Thread(()->{
for(int i=1;i<40;i++){
ticket.sale();
}
},"C").start();
}
}
//资源类 OOP
class Ticket{
//属性、方法
private int number=50;
//卖票的方式
public void sale() {
if (number>0) {
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
}
}
修改
//卖票的方式
public synchronized void sale() {
if (number>0) {
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
}
lock
公平锁:十分公平,先来后到,排队.
非公平锁:不公平,可以插队(默认)
默认是非公平锁,是为了公平,比如一个线程要3s,另一个线程要3h,难道一定要让3h的锁先来就先执行吗?!
package com.kuang.demo01;
//基本的卖票例子
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 真正的多线程开发,公司中的开发
* 线程就是一个资源类,没有任何附属的操作
*/
public class SaleTicketDemo02 {
public static void main(String[] args) {
//并发,多线程操作同一个资源类,把资源类丢入线程
Ticket2 ticket=new Ticket2();
//@FunctionalInterface 函数式接口,jdk1.8 Lambdab表达式(参数)->{代码}
new Thread(()->{ for(int i=1;i<40;i++) ticket.sale(); },"A").start();
new Thread(()->{ for(int i=1;i<40;i++) ticket.sale(); },"B").start();
new Thread(()->{ for(int i=1;i<40;i++) ticket.sale(); },"C").start();
}
}
// Lock
class Ticket2{
//属性、方法
private int number=50;
Lock lock=new ReentrantLock();
//卖票的方式
public synchronized void sale() {
lock.lock();//加锁
try {
//业务代码
if (number>0) {
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
}
}
Synchronized 与Lock 的区别
1、Synchronized 内置的Java关键字,Lock是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock可以判断
3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁
4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。
5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;
6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;
锁是什么,如何判断锁的是谁
4、生产者和消费者问题
面试高频: 单例模式, 八大排序,生产者消费者,死锁
生产者和消费者问题 Synchronzied 版
package com.kuang.pc;
/**
* 线程之间的通信问题:生产者和消费者问题! 等待唤醒 通知唤醒
* 线程交替执行 A B 操作同一个变量 num=0
* A num+1
* B num-1
*/
public class A {
public static void main(String[] args) {
Data data=new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//等待 业务 通知
class Data{//数字 资源类
private int number=0;
//+1
public synchronized void increment() throws InterruptedException {
if(number!=0){//0
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
this.notify();
}
//-1
public synchronized void decrement() throws InterruptedException {
if(number==0){//1
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
this.notify();
}
}
问题存在,A B C D 4个线程 虚假唤醒
解决方式 ,if 改为while即可,防止虚假唤醒
结论:就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
这也就是为什么用while而不用if的原因了,因为线程被唤醒后,执行开始的地方是wait之后
自己的见解
比如超市要抓小偷,
小偷偷了100个东西,
if就是你让小偷只走一次防盗检测门,它报警了,小偷成功的偷走了99个东西
while就是让小偷再走一次,直到不报警
package com.kuang.pc;
/**
* 线程之间的通信问题:生产者和消费者问题! 等待唤醒 通知唤醒
* 线程交替执行 A B 操作同一个变量 num=0
* A num+1
* B num-1
*/
public class A {
public static void main(String[] args) {
Data data=new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//等待 业务 通知
class Data{//数字 资源类
private int number=0;
//+1
public synchronized void increment() throws InterruptedException {
while(number!=0){//0
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
this.notify();
}
//-1
public synchronized void decrement() throws InterruptedException {
while(number==0){//1
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
this.notify();
}
}
JUC版的生产者消费者:Clock 版
代码实现
package com.kuang.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 线程之间的通信问题:生产者和消费者问题! 等待唤醒 通知唤醒
* 线程交替执行 A B 操作同一个变量 num=0
* A num+1
* B num-1
*/
public class B {
public static void main(String[] args) {
Data2 data=new Data2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
//等待 业务 通知
class Data2{//数字 资源类
private int number=0;
Lock lock=new ReentrantLock();
Condition condition = lock.newCondition();
// condition.await();//等待
// condition.signalAll();//唤醒全部
//+1
public void increment() throws InterruptedException {
lock.lock();
try {
//业务代码
while(number!=0){//0
//等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public void decrement() throws InterruptedException {
lock.lock();
try {
//业务代码
while(number==0){//1
//等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
//通知其他线程,我+1完毕了
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
任何一个新技术,绝对不仅仅只是覆盖了原来的技术,优势和补充!
Condition 精准的通知和唤醒的线程!
如果我们要指定通知的下一个进行顺序怎么办呢? 我们可以使用Condition来指定通知进程~
代码测试:
package com.kuang.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Description:
* A 执行完 调用B
* B 执行完 调用C
* C 执行完 调用A
*
* @author CSDN@日星月云
* @date 11:31:18 2022年7月13日
**/
public class C {
public static void main(String[] args) {
Data3 data=new Data3();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.printC();
}
},"C").start();
}
}
class Data3{// 资源类 lock
private Lock lock=new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number=1;// 1A 2B 3C
public void printA() {
lock.lock();
try {
//业务,判断->执行->通知
while (number!=1){
//等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAA");
//唤醒,唤醒指定的人,B
number=2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
//业务,判断->执行->通知
while (number!=2){
//等待
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBB");
//唤醒,唤醒指定的人,C
number=3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
//业务,判断->执行->通知
while (number!=3){
//等待
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>CCCCCC");
//唤醒,唤醒指定的人,A
number=1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
信号量机制 Semaphore 实现
经典 生产者-消费者线程【操作系统】
5、8锁现象
如何判断锁的是谁!锁到底锁的是谁?
锁会锁住:对象、Class
深刻理解我们的锁
1、标准情况下,两个线程先打印 发短信 还是 打电话 ?
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁就,就是关于锁的8个问题
* 1、标准情况下,两个线程先打印 发短信 还是 打电话 ? 1/发短信 2/打电话
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> { phone.sendSMs(); }).start();
//捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone.call(); }).start();
}
}
class Phone {
public synchronized void sendSMs() {
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
结果:
1/发短信 2/打电话
为什么? 如果你认为是顺序在前? 这个答案是错误的!
2、sendMs延迟4秒,两个线程先打印 发短信 还是 打电话 ?
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁就,就是关于锁的8个问题
* 1、标准情况下,两个线程先打印 发短信 还是 打电话 ? 1/发短信 2/打电话
* 2、sendSMs延迟4秒,两个线程先打印 发短信 还是 打电话 ? (4s) 1/发短信 2/打电话
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
//锁的存在
new Thread(() -> { phone.sendSMs(); }).start();
//捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone.call(); }).start();
}
}
class Phone {
//synchronized 锁的对象是方法的调用者!
//两个方法用的是同一个锁,谁先拿到谁先执行!
public synchronized void sendSMs() {
//捕获
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
}
结果:
(4s) 1/发短信 2/打电话
原因:并不是顺序执行,而是synchronized 锁住的对象是方法的调用!对于两个方法用的是同一个锁,谁先拿到谁先执行,另外一个等待
3、增加一个普通方法后!两个线程先打印 发短信 还是 hello ?
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 3、增加一个普通方法后!两个线程先打印 发短信 还是 hello ? (1s) 1/hello (4s) 2/发短信
*/
public class Test2 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
//锁的存在
new Thread(() -> { phone.sendSMs(); }).start();
//捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone.hello(); }).start();
}
}
class Phone2 {
//synchronized 锁的对象是方法的调用者!
public synchronized void sendSMs() {
//捕获
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
//这里没有锁!不是同步方法,不受锁的影响
public void hello(){
System.out.println("hello");
}
}
结果:
(1s) 1/hello (4s) 2/发短信
4、两个对象,两个同步方法,先打印 发短信 还是 打电话 ?
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 3、增加一个普通方法后!两个线程先打印 发短信 还是 hello ? (1s) 1/hello (4s) 2/发短信
* 4、两个对象,两个同步方法,先打印 发短信 还是 打电话 ? (4s) 1/发短信 2/打电话
*/
public class Test2 {
public static void main(String[] args) {
//两个对象,两个调用者,两个锁
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
//锁的存在
new Thread(() -> { phone1.sendSMs(); }).start();
//捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone2.call(); }).start();
}
}
class Phone2 {
//synchronized 锁的对象是方法的调用者!
public synchronized void sendSMs() {
//捕获
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call() {
System.out.println("打电话");
}
//这里没有锁!不是同步方法,不受锁的影响
public void hello(){
System.out.println("hello");
}
}
结果:
(4s) 1/发短信 2/打电话
5、增加两个静态的同步方法,只有一个对象,先打印 发短信 还是 打电话 ?
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 5、增加两个静态的同步方法,只有一个对象,先打印 发短信 还是 打电话 ? (4s) 1/发短信 2/打电话
*/
public class Test3 {
public static void main(String[] args) {
Phone3 phone = new Phone3();
//锁的存在
new Thread(() -> { phone.sendSMs(); }).start();
//捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone.call(); }).start();
}
}
//Phone唯一放入一个class对象
class Phone3 {
//synchronized 锁的对象是方法的调用者!
//static 静态方法
//类一加载就有了!锁的是 Class 模板
public static synchronized void sendSMs() {
//捕获
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
结果:
(4s) 1/发短信 2/打电话
6、两个对象!增加两个静态的同步方法,只有一个对象,先打印 发短信 还是 打电话 ?
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 5、增加两个静态的同步方法,只有一个对象,先打印 发短信 还是 打电话 ? (4s) 1/发短信 2/打电话
* 6、两个对象!增加两个静态的同步方法,只有一个对象,先打印 发短信 还是 打电话 ? (4s) 1/发短信 2/打电话
*/
public class Test3 {
public static void main(String[] args) {
//两个对象的Class类模板只有一个,static,锁的是Class
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
//锁的存在
new Thread(() -> { phone1.sendSMs(); }).start();
//捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone2.call(); }).start();
}
}
//Phone唯一放入一个class对象
class Phone3 {
//synchronized 锁的对象是方法的调用者!
//static 静态方法
//类一加载就有了!锁的是 Class 模板
public static synchronized void sendSMs() {
//捕获
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call() {
System.out.println("打电话");
}
}
结果:
(4s) 1/发短信 2/打电话
7、1个静态同步方法,1个普通同步方法,一个对象,先打印 发短信 还是 打电话 ?
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 7、1个静态同步方法,1个普通同步方法,一个对象,先打印 发短信 还是 打电话 ? (1s)1/打电话(4s) 2/发短信
*/
public class Test4 {
public static void main(String[] args) {
Phone4 phone = new Phone4();
//锁的存在
new Thread(() -> { phone.sendSMs(); }).start();
//捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone.call(); }).start();
}
}
//Phone唯一放入一个class对象
class Phone4 {
//静态同步方法 锁的是Class 类模板
public static synchronized void sendSMs() {
//捕获
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
//普通同步方法 锁的是调用者
public synchronized void call() {
System.out.println("打电话");
}
}
结果:
(1s)1/打电话(4s) 2/发短信
8、1个静态同步方法,1个普通同步方法,两个对象,先打印 发短信 还是 打电话 ?
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 7、1个静态同步方法,1个普通同步方法,一个对象,先打印 发短信 还是 打电话 ? (1s)1/打电话(4s) 2/发短信
* 8、1个静态同步方法,1个普通同步方法,两个对象,先打印 发短信 还是 打电话 ? (1s)1/打电话(4s) 2/发短信
*/
public class Test4 {
public static void main(String[] args) {
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
//锁的存在
new Thread(() -> { phone1.sendSMs(); }).start();
//捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> { phone2.call(); }).start();
}
}
//Phone唯一放入一个class对象
class Phone4 {
//静态同步方法 锁的是Class 类模板
public static synchronized void sendSMs() {
//捕获
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
//普通同步方法 锁的是调用者
public synchronized void call() {
System.out.println("打电话");
}
}
结果:
(1s)1/打电话(4s) 2/发短信
小结
new 出来的 this 是具体的一个对象
static Class 是唯一的一个模板
6、集合类不安全
list 不安全
package com.kuang.unsafe;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
//java.util.ConcurrentModificationException 并发修改异常
public class ListTest {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
解决方案:
package com.kuang.unsafe;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
//java.util.ConcurrentModificationException 并发修改异常
public class ListTest {
public static void main(String[] args) {
//并发下 ArrayList 不安全的
/**
* 解决方案:
* 1、 List<String> list=new Vector<>();
* 2、 List<String> list= Collections.synchronizedList(new ArrayList<>());
* 3、 List<String> list= new CopyOnWriteArrayList<>();
*/
//CopyOnWrite写入是复制 COW 计算机程序设计领域的一种优化策略;
//多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
//在写入的时候,避免覆盖,造成数据问题
//读写分离
//CopyOnWriteArrayList 比 Vector 牛逼在哪里
List<String> list= new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
CopyOnWriteArrayList比Vector厉害在哪里?
Vector底层是使用synchronized关键字来实现的:效率特别低下。
CopyOnWriteArrayList使用的是Lock锁,效率会更加高效!
Set 不安全
package com.kuang.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 同理可证:ConcurrentModificationException
* 1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
* 2、Set<String> set = new CopyOnWriteArraySet<>();
*/
public class SetTest {
public static void main(String[] args) {
// Set<String> set = new HashSet<>();
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <= 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
HashSet底层是什么?
hashSet底层就是一个HashMap;
HashMap 不安全
package com.kuang.unsafe;
import java.util.HashMap;
import java.util.Map;
public class MapTest {
public static void main(String[] args) {
//Map是这样用的吗? 不是,工作中不用HashMap
// 默认等价于什么? new HashMap<>(16,0.75)
Map<String,String> map = new HashMap<>();
//加载因子、初始化容量
}
}
同样的HashMap基础类也存在并发修改异常!
package com.kuang.unsafe;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
//ConcurrentModificationException
public class MapTest {
public static void main(String[] args) {
//Map是这样用的吗? 不是,工作中不用HashMap
// 默认等价于什么? new HashMap<>(16,0.75)
// Map<String,String> map = new HashMap<>();
//唯一的一个家庭作业,探究ConcurrentHashMap的源码
Map<String,String> map = new ConcurrentHashMap<>();
for (int i = 1; i <= 30; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
7、Callable(简单)
1、可以有返回值;
2、可以抛出异常;
3、方法不同,run()/call()
package com.kuang.callable;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) {
// new Thread(new Runnable()).start();
// new Thread(new FutureTask<V>()).start();
// new Thread(new FutureTask<V>(callable)).start();
// new Thread().start();//怎么启动callable
MyThread thread = new MyThread();
//适配类
FutureTask futureTask = new FutureTask(thread); //适配类
new Thread(futureTask,"A").start();
new Thread(futureTask,"B").start();//结果会被缓存,效率高
try {
String s = (String) futureTask.get();//callable的返回值,这个get方法可能会产生阻塞,把他放在最后
//或者使用异步通信来处理
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("call");//会打印1个call
return "1234";
}
}
细节:
1、有缓存
2、结果可能需要等待,会阻塞
8、常用的辅助类(必会)
8.1、CountDownLatch
package com.kuang.add;
import java.util.concurrent.CountDownLatch;
//计数器
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//总数是6,必须要执行任务的时候,再使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"Go out");
countDownLatch.countDown();//-1
},String.valueOf(i)).start();
}
countDownLatch.await();// 等待计数器归零 然后向下执行
System.out.println("Close Door");
}
}
主要方法:
countDownLatch.countDown(); //减一操作;
countDownLatch.await();// 等待计数器归零
await 等待计数器归零,就唤醒,再继续向下运行
8.2、CyclicBarrier
加法计数器
package com.kuang.add;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
//集齐7颗龙珠召唤神龙
//召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功");
});
for (int i = 1; i <= 7; i++) {
final int temp=i;
//lambda表达式操作到i吗?
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
8.3、Semaphore
抢车位
6车–3个停车位置
package com.kuang.add;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
//线程数量:停车位!限流
Semaphore semaphore = new Semaphore(3);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
//acquire()
try {
semaphore.acquire();//获得,如果满了,会等待被释放为止
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();//释放
}
},String.valueOf(i)).start();
}
}
}
原理:
semaphore.acquire();//获得资源,如果资源已经使用完了,就等待资源释放后再进行使用!
semaphore.release();//释放,会将当前的信号量释放+1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用! 并发限流,控制最大的线程数!
9、读写锁
package com.kuang.rw;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//ReadWriteLock
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCatch = new MyCache();
//写入
for (int i = 0; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCatch.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for (int i = 0; i <=5; i++) {
final int temp = i;
new Thread(()->{
myCatch.get(temp+"");
},String.valueOf(i)).start();
}
}
}
// 自定义缓存
class MyCache {
private volatile Map<String,Object> map = new HashMap<>(0);
//存
public void put(String key,Object value) {
System.out.println(Thread.currentThread().getName()+"写入"+value);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入成功");
}
//取
public void get(String key) {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取成功");
}
}
结果:
同时写的问题
所以如果我们不加锁的情况,多线程的读写会造成数据不可靠的问题。
我们也可以采用synchronized这种重量锁和轻量锁 lock去保证数据的可靠。
但是这次我们采用更细粒度的锁:ReadWriteLock 读写锁来保证
package com.kuang.rw;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 可以同时被多个线程占有
* ReadWriteLock
* 读--读 可以共存
* 读--写 不能共存
* 写--写 不能共存
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
// MyCache myCatch = new MyCache();
MyCacheLock myCatch = new MyCacheLock();
//写入
for (int i = 0; i <= 5; i++) {
final int temp = i;
new Thread(()->{
myCatch.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
//读取
for (int i = 0; i <=5; i++) {
final int temp = i;
new Thread(()->{
myCatch.get(temp+"");
},String.valueOf(i)).start();
}
}
}
// 自定义缓存
class MyCache {
private volatile Map<String,Object> map = new HashMap<>(0);
//存
public void put(String key,Object value) {
System.out.println(Thread.currentThread().getName()+"写入"+value);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入成功");
}
//取
public void get(String key) {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取成功");
}
}
//加锁的
class MyCacheLock {
private volatile Map<String,Object> map = new HashMap<>(0);
//读写锁:更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// private Lock lock=new ReentrantLock();
//存,写的时候,只希望同时有一个线程写
public void put(String key,Object value) {
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
//取,读,所有的人都可以读
public void get(String key) {
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
结果:
只有一个写
独占锁(写锁) 一次只能被一个线程占有
共享锁(读锁) 可以同时被多个线程占有
10、阻塞队列
BlockingQueue 是Collection的一个子类
什么情况下我们会使用阻塞队列
多线程并发处理、线程池
学会使用队列
添加、移除
四组API
1.抛出异常
2.不会抛出异常
3.阻塞等待
4.超时等待
抛出异常
package com.kuang.bq;
import java.util.concurrent.ArrayBlockingQueue;
public class Test {
public static void main(String[] args) {
test1();
}
/**
* 抛出异常
*/
public static void test1(){
//队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
//抛出异常 IllegalStateException
// System.out.println(blockingQueue.add("c"));
System.out.println(blockingQueue.element());//查看队首元素是谁
System.out.println("======================");
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
//抛出异常 NoSuchElementException
System.out.println(blockingQueue.remove());
}
}
不抛出异常
/**
*有返回值,没有异常
*/
public static void test2(){
//队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("d"));//false 不抛出异常
System.out.println(blockingQueue.peek());//查看队首元素是谁
System.out.println("======================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());//null 不抛出异常
}
等待,阻塞(一直阻塞)
/**
* 等待,阻塞(一直阻塞)
*/
public static void test3() throws InterruptedException {
//队列的大小
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
arrayBlockingQueue.put("a");
arrayBlockingQueue.put("b");
arrayBlockingQueue.put("c");
// arrayBlockingQueue.put("d");队列没有位置,一直阻塞
System.out.println("=============");
//队列移除顺序
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());//没有这个元素,一直阻塞
}
等待,阻塞(等待超时)
/**
* 等待,阻塞(等待超时)
*/
public static void test4() throws InterruptedException {
//队列的大小
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(arrayBlockingQueue.offer("a"));
System.out.println(arrayBlockingQueue.offer("b"));
System.out.println(arrayBlockingQueue.offer("c"));
//等待超过两秒退出
arrayBlockingQueue.offer("d", 2, TimeUnit.SECONDS);
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
//等待超过两秒就退出
System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
}
SynchronousQueue 同步队列
同步队列 没有容量,也可以视为容量为1的队列;
put方法 和 take方法;
package com.kuang.bq;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 同步队列 没有容量,也可以视为容量为1的队列;
* Synchronized 和 其他的BlockingQueue 不一样 它不存储元素;
*
* put了一个元素,就必须从里面先take出来,否则不能再put进去值!
*/
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> synchronousQueue = new java.util.concurrent.SynchronousQueue<>();
// 往queue中添加元素
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " put 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName() + " put 2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName() + " put 3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
// 取出元素
new Thread(()-> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " take " + synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " take " + synchronousQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " take " + synchronousQueue.take());
}catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
结果
T1 put 1
T2 take 1
T1 put 2
T2 take 2
T1 put 3
T2 take 3
11、线程池(重点)
线程池:三大方式、七大参数、四种拒绝策略
池化技术
程序的运行,本质:占用系统的资源!我们需要去优化资源的使用 ===> 池化技术
线程池、JDBC的连接池、内存池、对象池 等等。。。。
池化技术:事先准备好一些资源,如果有人要用,就来我这里拿,用完之后还给我,以此来提高效率。
线程池的好处
1.降低资源的消耗
2.提高响应速度
3.方便管理
线程可以复用,可以控制最大并发量,管理线程
线程池:三大方法
- ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
- ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
- ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的
package com.kuang.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
//工具类 Executors 三大方法;
//使用了线程池之后,使用线程池创建线程
public class Demo01 {
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
// ExecutorService threadPool = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
ExecutorService threadPool = Executors.newCachedThreadPool(); //可伸缩的
//线程池用完必须要关闭线程池
try {
for (int i = 1; i <=10 ; i++) {
//通过线程池创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+ " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
// ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
// ExecutorService threadPool = Executors.newFixedThreadPool(5);//创建一个固定大小得线程池
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-3 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-5 ok
ExecutorService threadPool = Executors.newCachedThreadPool();//可伸缩,线程数可变
pool-1-thread-3 ok
pool-1-thread-4 ok
pool-1-thread-1 ok
pool-1-thread-5 ok
pool-1-thread-2 ok
pool-1-thread-6 ok
pool-1-thread-8 ok
pool-1-thread-9 ok
pool-1-thread-10 ok
pool-1-thread-7 ok
7大参数
newSingleThreadExecutor()源码分析
/**
* Creates an Executor that uses a single worker thread operating
* off an unbounded queue. (Note however that if this single
* thread terminates due to a failure during execution prior to
* shutdown, a new one will take its place if needed to execute
* subsequent tasks.) Tasks are guaranteed to execute
* sequentially, and no more than one task will be active at any
* given time. Unlike the otherwise equivalent
* {@code newFixedThreadPool(1)} the returned executor is
* guaranteed not to be reconfigurable to use additional threads.
*
* @return the newly created single-threaded Executor
*/
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
newFixedThreadPool()源码分析
/**
* Creates a thread pool that reuses a fixed number of threads
* operating off a shared unbounded queue. At any point, at most
* {@code nThreads} threads will be active processing tasks.
* If additional tasks are submitted when all threads are active,
* they will wait in the queue until a thread is available.
* If any thread terminates due to a failure during execution
* prior to shutdown, a new one will take its place if needed to
* execute subsequent tasks. The threads in the pool will exist
* until it is explicitly {@link ExecutorService#shutdown shutdown}.
*
* @param nThreads the number of threads in the pool
* @return the newly created thread pool
* @throws IllegalArgumentException if {@code nThreads <= 0}
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newCachedThreadPool()源码分析
/**
* Creates a thread pool that creates new threads as needed, but
* will reuse previously constructed threads when they are
* available. These pools will typically improve the performance
* of programs that execute many short-lived asynchronous tasks.
* Calls to {@code execute} will reuse previously constructed
* threads if available. If no existing thread is available, a new
* thread will be created and added to the pool. Threads that have
* not been used for sixty seconds are terminated and removed from
* the cache. Thus, a pool that remains idle for long enough will
* not consume any resources. Note that pools with similar
* properties but different details (for example, timeout parameters)
* may be created using {@link ThreadPoolExecutor} constructors.
*
* @return the newly created thread pool
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到,三个方法的底层都是new ThreadPoolExecutor
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;
}
阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。
手动创建一个线程池
模拟上面的银行业务
核心线程大小设为2:就是一直工作的窗口
最大线程设为5:就是银行最多的工作窗口
keepAliveTime设置为1小时:如果1小时都没有业务,就关闭窗口
候客区:new LinkedBlockingQueue(3),假设候客区最多3个人
线程工厂:就用默认的,Executors.defaultThreaFactory()
拒绝策略: 可以发现有4种拒绝策略,用默认的AbortPolicy()//银行满了,但是还有人进来,就不处理这个人,并抛出异常
package com.kuang.pool;
import java.util.concurrent.*;
public class PollDemo {
public static void main(String[] args) {
//自定义线程池!工作 ThreadPoolExecutor
ExecutorService threadPool =new ThreadPoolExecutor(
2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()//银行满了,还有人精力,不处理这个人,抛出异常
);
try {
for (int i = 1; i <= 8; i++) { // 1 5 6 7 8 9(RejectedExecutionException)
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "ok");
});
}
}catch (Exception e) {
e.printStackTrace();
}
finally {
threadPool.shutdown();
}
}
}
4种拒绝策略
- new ThreadPoolExecutor.AbortPolicy(): //该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常
超出最大承载,就会抛出异常:队列容量大小+maxPoolSize
pool-1-thread-1ok
pool-1-thread-3ok
pool-1-thread-3ok
pool-1-thread-3ok
pool-1-thread-2ok
pool-1-thread-4ok
pool-1-thread-5ok
pool-1-thread-1ok
java.util.concurrent.RejectedExecutionException: Task com.kuang.pool.PollDemo$$Lambda$1/1096979270@7ba4f24f rejected from java.util.concurrent.ThreadPoolExecutor@3b9a45b3[Running, pool size = 5, active threads = 0, queued tasks = 0, completed tasks = 8]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.kuang.pool.PollDemo.main(PollDemo.java:19)
Process finished with exit code 0
- new ThreadPoolExecutor.CallerRunsPolicy(): //该拒绝策略为:哪来的去哪里 main线程进行处理
pool-1-thread-1ok
pool-1-thread-4ok
mainok
pool-1-thread-3ok
pool-1-thread-4ok
pool-1-thread-2ok
pool-1-thread-3ok
pool-1-thread-1ok
pool-1-thread-5ok
Process finished with exit code 0
- new ThreadPoolExecutor.DiscardPolicy(): //该拒绝策略为:队列满了,丢掉任务,不会抛出异常。
pool-1-thread-1ok
pool-1-thread-4ok
pool-1-thread-3ok
pool-1-thread-2ok
pool-1-thread-4ok
pool-1-thread-3ok
pool-1-thread-5ok
pool-1-thread-1ok
Process finished with exit code 0
- new ThreadPoolExecutor.DiscardOldestPolicy(): //该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常
pool-1-thread-2ok
pool-1-thread-4ok
pool-1-thread-3ok
pool-1-thread-4ok
pool-1-thread-2ok
pool-1-thread-1ok
pool-1-thread-3ok
pool-1-thread-5ok
Process finished with exit code 0
小结和扩展
如何设置线程池的最大大小maximumPoolSize
了解CPU密集型
和I/O密集型
1、CPU密集型:电脑的核数是几核就选择几;选择maximunPoolSize的大小
// 获取cpu 的核数
int max = Runtime.getRuntime().availableProcessors();
ExecutorService service =new ThreadPoolExecutor(
2,
max,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
2、I/O密集型:
在程序中有15个大型任务,io十分占用资源;I/O密集型就是判断我们程序中十分耗I/O的线程数量,大约是最大I/O数的一倍到两倍之间。
12、四大函数式接口(必须掌握)
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口:只有一个方法的接口
简化编程模型,在新版本的框架底层大量应用
foreach(消费者类型的函数式接口)
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Function 函数型接口
代码测试:
package com.kuang.function;
import java.util.function.Function;
/**
* Function 函数型接口
* 只要是函数式接口 可以用Lambda表达式简化
*/
public class Demo01 {
public static void main(String[] args) {
//工具类:输出输入的值
// Function<String,String> function=new Function<String, String>() {
// @Override
// public String apply(String str) {
// return str;
// }
// };
// Function function=(str)->{return str;};
Function function=str->str;;
System.out.println(function.apply("asd"));
}
}
Predicate 断定型接口
package com.kuang.function;
import java.util.function.Predicate;
/**
* 断定型接口,有一个输入参数,返回值只能是Boolean值
*/
public class Demo02 {
public static void main(String[] args) {
//判断字符串是否为空
// Predicate<String> predicate=new Predicate<String>(){
// @Override
// public boolean test(String str) {
// return str.isEmpty();
// }
// };
Predicate<String> predicate=(str)->{
return str.isEmpty();
};
System.out.println(predicate.test("asd"));
}
}
Consummer 消费型接口
package com.kuang.function;
import java.util.function.Consumer;
/**
* Consumer: 消费型接口:只有输入,没有返回值
*/
public class Demo03 {
public static void main(String[] args) {
// Consumer<String> consumer=new Consumer<String>() {
// @Override
// public void accept(String s) {
// System.out.println(s);
// }
// };
Consumer<String> consumer=(str)->{
System.out.println(str);
};
consumer.accept("asd");
}
}
Suppier 供给型接口
package com.kuang.function;
import java.util.function.Supplier;
/**
* Supplier: 供给型接口:没有参数,只有返回值
*/
public class Demo04 {
public static void main(String[] args) {
// Supplier<Integer> supplier=new Supplier<Integer>() {
// @Override
// public Integer get() {
// return 1024;
// }
// };
Supplier<Integer> supplier=()->{return 1024;};
System.out.println(supplier.get());
}
}
13、Stream 流式计算
什么是Stream流式计算
大数据:存储、计算
集合、Mysql本质计算存储东西的
计算都应该交给流来操作
package com.kuang.stream;
import java.util.Arrays;
import java.util.List;
/**
* Description:
* 题目要求: 用一行代码实现
* 1. Id 必须是偶数
* 2.年龄必须大于23
* 3. 用户名转为大写
* 4. 用户名倒序
* 5. 只能输出一个用户
*
* @author CSDN@日星月云
**/
public class Test {
public static void main(String[] args) {
User u1 = new User(1, "a", 23);
User u2 = new User(2, "b", 23);
User u3 = new User(3, "c", 23);
User u4 = new User(6, "d", 24);
User u5 = new User(4, "e", 25);
//集合存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
//计算交给流
//lambda、链式编程、函数式接口、流式计算
list.stream()
.filter(user -> {return user.getId()%2 == 0;})
.filter(user -> {return user.getAge() > 23;})
.map(user -> {return user.getName().toUpperCase();})
.sorted((user1, user2) -> {return user2.compareTo(user1);})
.limit(1)
.forEach(System.out::println);
}
}
14、ForkJoin(分支合并)
什么是 ForkJoin
ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!
大数据中:MapReduce 核心思想->把大任务拆分为小任务!
1)ForkJoin 特点: 工作窃取!
实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!
2)如何使用ForkJoin?
1、通过ForkJoinPool来执行
2、计算任务 execute(ForkJoinTask<?> task)
3、计算类要去继承ForkJoinTask;
ForkJoin 的计算类
package com.kuang.forkjoin;
import java.util.concurrent.RecursiveTask;
/**
* 求和计算的任务!
*
* 如何使用ForkJoin?
* 1、通过ForkJoinPool来执行
* 2、计算任务 execute(ForkJoinTask<?> task)
* 3、计算类要去继承ForkJoinTask;
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private long star;
private long end;
/** 临界值 */
private long temp = 1000000L;
public ForkJoinDemo(long star, long end) {
this.star = star;
this.end = end;
}
/**
* 计算方法
* @return
*/
@Override
protected Long compute() {
if ((end - star) < temp) {
Long sum = 0L;
for (Long i = star; i < end; i++) {
sum += i;
}
return sum;
}else {
// 使用ForkJoin 分而治之 计算
long middle = (star + end) / 2;//中间值
// 拆分任务,把任务线程压入线程队列
ForkJoinDemo task1 = new ForkJoinDemo(star, middle);
task1.fork();
ForkJoinDemo task2 = new ForkJoinDemo(middle+1,end);
task2.fork();
long taskSum = task1.join() + task2.join();
return taskSum;
}
}
}
测试类
package com.kuang.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
public class ForkJoinTest {
private static final long END = 20_0000_0000L;
public static void main(String[] args) throws ExecutionException, InterruptedException {
test1();
test2();
test3();
}
/**
* 使用普通方法
*/
public static void test1() {
long star = System.currentTimeMillis();
long sum = 0L;
for (long i = 0; i < END; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println(sum);
System.out.println("时间:" + (end - star));
System.out.println("----------------------");
}
/**
* 使用ForkJoin 方法
*/
public static void test2() throws ExecutionException, InterruptedException {
long star = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, END);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);//execute执行任务,无结果;submit提交任务,有结果
Long sum = submit.get();
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("时间:" + (end - star));
System.out.println("-----------");
}
/**
* 使用 Stream 流计算
*/
public static void test3() {
long star = System.currentTimeMillis();
long sum = LongStream.range(0L, END).parallel().reduce(0, Long::sum);
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("时间:" + (end - star));
System.out.println("-----------");
}
}
补充:
.parallel().reduce(0, Long::sum)使用一个并行流去计算整个计算,提高效率。
15、异步回调
Future 设计的初衷:对将来的某个事件结果进行建模!
其实就是前端 --> 发送ajax异步请求给后端
但是我们平时都使用CompletableFuture
(1)没有返回值的runAsync异步回调
package com.kuang.future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 异步调用:Ajax
* //异步执行
* //成功回调
* //失败回调
*/
public class Demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//发起一个请求,没有返回值得异步回调
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"run");
});
System.out.println("1111");
//获取阻塞执行结果
completableFuture.get();
//1111
//ForkJoinPool.commonPool-worker-1run
}
(2)有返回值的异步回调supplyAsync
public static void main(String[] args) throws ExecutionException, InterruptedException {
//有返回值的异步回调
//ajax,成功和失败回调
//返回的是错误信息
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println("completableFuture"+Thread.currentThread().getName());
int i = 10/0;
return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println(t);//正常的返回结果
System.out.println(u);//错误信息java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
}).exceptionally((e) -> {
System.out.println(e.getMessage());//java.lang.ArithmeticException: / by zero
return 233;
}).get());
}
补充:
whenComplete: 有两个参数,一个是t 一个是u
T:是代表的 正常返回的结果;
U:是代表的 抛出异常的错误信息;
如果发生了异常,get可以获取到exceptionally返回的值;
16、JMM
对Volatile 的理解
Volatile 是 Java 虚拟机提供 轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
如何实现可见性
volatile变量修饰的共享变量在进行写操作的时候回多出一行汇编:
0x01a3de1d:movb $0×0,0×1104800(%esi);0x01a3de24**:lock** addl $0×0,(%esp);
Lock前缀的指令在多核处理器下会引发两件事情。
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使其他cpu里缓存了该内存地址的数据无效。
多处理器总线嗅探:
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址呗修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据库读到处理器缓存中。
什么是JMM
JMM:JAVA内存模型,不存在的东西,是一个概念,也是一个约定!
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存;
2、线程加锁前,必须读取主存中的最新值到工作内存中;
3、加锁和解锁是同一把锁;
线程中分为 工作内存、主内存
8种操作:
- read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
- load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
- use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
- assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
- store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
- write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
- lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
出现问题:
有可能会出现线程B修改了值刷新回主内存,但是线程A没有取到这个最新的值
JMM对这8种操作给了相应的规定:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
问题:程序不知道主内存的值已被修改
17、volatile
1)保证可见性
package com.kuang.tvolatile;
import java.util.concurrent.TimeUnit;
public class JMMDemo {
private static int num=0;
public static void main(String[] args) { // main
new Thread(()->{ // 线程 1 对主内存的变化不知道的
while (num==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
验证可见性
package com.kuang.tvolatile;
import java.util.concurrent.TimeUnit;
public class JMMDemo {
// 不加 volatile 程序就会死循环!
// private static int num=0;
// 加 volatile 可以保证可见性
private volatile static int num=0;
public static void main(String[] args) { // main
new Thread(()->{ // 线程 1 对主内存的变化不知道的
while (num==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
2)不保证原子性
原子性: 不可分割(中断)
线程A在执行任务的时候,是不能被打扰的,要么同时成功,要么同时失败
package com.kuang.tvolatile;
//不保证原子性
public class VDemo {
private static int num=0;
private static void add(){
num++;
}
public static void main(String[] args) {
//理论上num结果是20000
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){//因为程序里面始终是有2个线程的,main线程和gc线程
Thread.yield();
//Thread.yield()是在主线程中执行的,
// 意思是还有出路GC和main之外的其他线程在跑,主线程就让出cpu不往下执行,让出然后重新竞争cpu的执行权,有可能还是main抢到,不过这里是循环,抢到继续让出,直到只有2个线程
}
//理论上打印出来应该是20000
System.out.println(Thread.currentThread().getName()+" "+num);//肯定不能
//main 18807
}
}
解决方法
我们可以加synchronized,是add方法变成原子操作
private synchronized static void add(){
num++;
}
如果只是给变量加上volatile
private volatile static int num=0;
结果不是20000
说明volatile
不保证原子性
如果不加lock和synchronized,怎么保证原子性?
num++;//不是原子性操作
我们来看看对于操作++,
jvm底层是怎么操作的
打开IDEA/Terminal
E:\IdeaProjects\juc>cd ./src/main/java/com/kuang/tvolatile
E:\IdeaProjects\juc\src\main\java\com\kuang\tvolatile>javac -encoding UTF-8 VDemo.java
E:\IdeaProjects\juc\src\main\java\com\kuang\tvolatile>javap -c VDemo.class
Compiled from "VDemo.java"
public class com.kuang.tvolatile.VDemo {
public com.kuang.tvolatile.VDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iload_1
3: bipush 20
5: if_icmpgt 29
8: new #13 // class java/lang/Thread
11: dup
12: invokedynamic #15, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
17: invokespecial #19 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
20: invokevirtual #22 // Method java/lang/Thread.start:()V
23: iinc 1, 1
26: goto 2
29: invokestatic #25 // Method java/lang/Thread.activeCount:()I
32: iconst_2
33: if_icmple 42
36: invokestatic #29 // Method java/lang/Thread.yield:()V
39: goto 29
42: getstatic #32 // Field java/lang/System.out:Ljava/io/PrintStream;
45: invokestatic #38 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
48: invokevirtual #42 // Method java/lang/Thread.getName:()Ljava/lang/String;
51: getstatic #7 // Field num:I
54: invokedynamic #46, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
59: invokevirtual #50 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
62: return
static {};
Code:
0: iconst_0
1: putstatic #7 // Field num:I
4: return
}
回到刚刚的问题,不加lock和synchronized,怎么保证原子性?
使用原子类解决原子性问题
private static AtomicInteger num=new AtomicInteger();
public static void add()
{
num.getAndIncrement(); //cas
//num=num+1;
}
因为原子类底层初始化的时候,将赋值给了一个volatiel属性
原子类的底层都直接和操作系统挂钩,在内存中修改值,Unsafe类是很特殊的存在
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
结果:
联系操作系统实验
实验 线程编程-加1操作为什么会出错?【操作系统】
3)禁止指令重排
什么是指令重排?
我们写的程序,计算机并不是按照我们自己写的那样去执行的
源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性!
int x = 1; //1
int y = 2; //2
x = x + 5; //3
y = x * x; //4
//我们期望的执行顺序是 1_2_3_4 //可能执行的顺序会变成2134 1324
//可不可能是 4123? 不可能的 依赖性
可能造成的影响结果:前提:a b x y这四个值 默认都是0
volatile可以避免指令重排:
volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。
内存屏障:CPU指令。作用:
1、保证特定的操作的执行顺序;
2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)
volatile标注后,编译器生成代码会不优化
volatile写前加SS屏障写后加SL屏障,读前加LL屏障,读后加LS屏障
4)总结
- volatile可以保证可见性;
- 不能保证原子性
- 由于内存屏障,可以保证避免指令重排的现象产生
面试官:那么你知道在哪里用这个内存屏障用得最多呢?单例模式
18、玩转单例模式
饿汉式、DCL懒汉式
说到volatile的防止指令重排,那么volatile的内存屏障在哪里使用的最多,就是单例模式了。
1)饿汉式
饿汉式的问题:可能会浪费内存
饿汉式一上来就会把所有的东西加载到内存,对象就已经存在了,对象没有使用的话,可能会浪费内存
主要特点有:
构造函数私有,避免在外部被创建对象
提前创建好对象
提供公有的接口,返回在类内部提前创建好的对象
静态变量随着类的加载就已经实例化了,跟是否调用静态方法没有关系
饿汉式加载时就会初始化,懒汉式只有在获取单例的时候才会初始化
类加载时,成员变量会被初始化,局部变量不会
package com.kuang.single;
//饿汉式单例
public class Hungry {
//可能会浪费空间
private byte[] data1=new byte[1024*1024];
private byte[] data2=new byte[1024*1024];
private byte[] data3=new byte[1024*1024];
private byte[] data4=new byte[1024*1024];
private Hungry(){
}
private final static Hungry HUNGRY=new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
2)DCL懒汉式
针对饿汉式单例的浪费内存的问题,提出了懒汉式单例,要用的时候再创建对象
package com.kuang.single;
//懒汉式单例
public class LazyMan {
private LazyMan(){
}
private static LazyMan lazyman;//还没有创建对象,只是声明,没有new
public static LazyMan getInstance(){
if(lazyman==null)
{
lazyman=new LazyMan();//如果这个对象为空,就实例化这个对象
}
return lazyman;
}
}
在多个线程的情况下,懒汉式单例可能会出现安全问题,就是线程1进入了if判断,并开始构造对象
package com.kuang.single;
//懒汉式单例
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
//多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
可以看到,有2个线程调用了构造函数,这说明程序中现在有2个Lazyman对象,就不是单例了,所以不安全
双重锁机制
package com.kuang.single;
//懒汉式单例
public class LazyMan {
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"ok");
}
private static LazyMan lazyMan;
//双重锁机制 DCL懒汉式
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan=new LazyMan();
}
}
}
return lazyMan;
}
//多线程并发
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
可以看到只创建了一个对象
但是还是有可能出现问题
创建对象的过程在极端情况下肯定是会出现问题的,因为不是原子性操作,会经历
1、分配内存空间,
2、执行构造方法(初始化对象)
3、把对象指向分配的空间
但是可能会发生指令重排,可能会按132的顺序执行,就是先分配内存空间,然后用空对象先占用内存空间,占用之后再执行构造方法
如下图,很有可能A执行了13还没执行2,但是现在lazyman已经不是null了,如果现在进来一个B线程,外层判断不为空,那么B线程会直接返回lazyman,但lazyman实际上还没有完成构造,所以不安全(new只是把应用加上了,但是堆还没有创建完,return就会有问题)
所以要用volatile修饰防止指令重排(防止第二个线程抢先执行,抢先返回一个尚未初始化完成的引用)
所以这里是同步代码块保证了操作的原子性,volatile禁止了指令重排
指令重排的原理是为了提升CPU多段流水的效率,但并不是指令任意重排,处理器必须能正确处理指令依赖关系保障程序得出正确的执行结果。
总结:synchronized保证的是if判断和new一个对象能同时成功或同时失败,但是new一个对象不是原子操作,执行13后,第二个线程认为已经new对象成功了,最上面的if判断不等于null
3)静态内部类
在一个类里面写一个静态的类
首先只要单例一定要先构造器私有
加载外部类时,不会加载静态内部类
线程安全且懒加载
但是静态内部类单例也是不安全的,因为反射可以破坏单例
//静态内部类
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.holder;
}
public static class InnerClass{
private static final Holder holder = new Holder();
}
}
测试:
package com.kuang.single;
//静态内部类
public class Holder {
private Holder(){
System.out.println(Thread.currentThread().getName()+"OK");
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER=new Holder();
}
public static void main(String[] args) {
for(int i=0;i<10;i++)
{
new Thread(()->{
Holder.getInstance();
}).start();
}
}
}
可以看到,内存中只有一个实例,就是只有一个线程进入了构造函数,因为静态类只加载一次
单例不安全,因为反射
但是只要有反射,任何私有的都是纸老虎,我们以DCL的单例为例,来试试反射
//反射!
public static void main(String[] args) throws Exception {
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
结果:
可以破解:
private LazyMan(){
synchronized (LazyMan.class){
if (lazyMan!=null){
throw new RuntimeException("不用试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName()+"ok");
}
结果:
相当于在DCL的基础上又在构造函数里面加了一重检测
完整代码如下:
package com.kuang.single;
import java.lang.reflect.Constructor;
//懒汉式单例
public class LazyMan {
private LazyMan(){
synchronized (LazyMan.class){
if (lazyMan!=null){
throw new RuntimeException("不用试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName()+"ok");
}
private volatile static LazyMan lazyMan;
//双重锁机制 DCL懒汉式
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan=new LazyMan();
/**
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 123
* 132 A
* B
*/
}
}
}
return lazyMan;
}
// //多线程并发
// public static void main(String[] args) {
// for (int i = 0; i < 10; i++) {
// new Thread(()->{
// LazyMan.getInstance();
// }).start();
// }
// }
//反射!
public static void main(String[] args) throws Exception {
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
}
现在我们不用getInstance()去获取对象,而是直接通过反射创建两个对象
//反射!
public static void main(String[] args) throws Exception {
// LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
可以发现,单例又被破坏了,
因为构造函数里面判断的是
但是注意,我们用反射new 的对象跟类里面的lazyman对象肯定是不一样的啊,没有调用getInstance(),类里面的lazyman就一直为空,所以单例又被破坏了
解决方法,用个标志位
private static boolean flag=false;
private LazyMan(){
synchronized (LazyMan.class){
if (flag==false){
flag=true;
}else {
throw new RuntimeException("不用试图使用反射破坏异常");
}
// if (lazyMan!=null){
// throw new RuntimeException("不用试图使用反射破坏异常");
// }
}
System.out.println(Thread.currentThread().getName()+"ok");
}
但是,再牛逼的加密也会解密
来我们继续破坏单例,我们把这个flag字段给它破坏了
//反射!
public static void main(String[] args) throws Exception {
// LazyMan instance = LazyMan.getInstance();
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance = declaredConstructor.newInstance();
flag.set(instance,false);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
结果:可以发现单例又被破坏了
完整代码:
package com.kuang.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
//懒汉式单例
public class LazyMan {
private static boolean flag=false;
private LazyMan(){
synchronized (LazyMan.class){
if (flag==false){
flag=true;
}else {
throw new RuntimeException("不用试图使用反射破坏异常");
}
// if (lazyMan!=null){
// throw new RuntimeException("不用试图使用反射破坏异常");
// }
}
System.out.println(Thread.currentThread().getName()+"ok");
}
private volatile static LazyMan lazyMan;
//双重锁机制 DCL懒汉式
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan=new LazyMan();
/**
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 123
* 132 A
* B
*/
}
}
}
return lazyMan;
}
// //多线程并发
// public static void main(String[] args) {
// for (int i = 0; i < 10; i++) {
// new Thread(()->{
// LazyMan.getInstance();
// }).start();
// }
// }
//反射!
public static void main(String[] args) throws Exception {
// LazyMan instance = LazyMan.getInstance();
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance = declaredConstructor.newInstance();
flag.set(instance,false);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
}
}
那怎么解决呢?我们点进去反射的newInstance()看看呢
我们可以看到,如果类是一个枚举类型的话,就会告诉你不能使用反射破坏枚举,枚举是jdk 1.5 开始出现的,自带单例模式
4)枚举
枚举本身也是一个类
package com.kuang.single;
//enum 是一个什么? 本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance()
{
return INSTANCE;
}
}
class Test{
public static void main(String[] args) {
EnumSingle instance1=EnumSingle.INSTANCE;
EnumSingle instance2=EnumSingle.INSTANCE;
EnumSingle instance3=EnumSingle.getInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance3);
}
}
我们来试试用反射破坏枚举单例
package com.kuang.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
//enum 是一个什么? 本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance()
{
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle instance1=EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
下面的错误提示是枚举类没有空参的构造方法
也就是下面这句话出错了idea骗了我们
//NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
正常破坏单例是应该报错不能使用反射破坏枚举
通过反编译我们可以看到,这个枚举本身也是一个class,它继承了一个枚举类
然而构造器还是空参的啊,说明我们还是被骗了
现在我们用jad.exe反编译试试
下载地址
Java反编译工具Jad的使用
jad.exe复制到.class文件夹下
我们把class字节码生成java文件看看
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingle.java
package com.kuang.single;
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(com/kuang/single/EnumSingle, name);
}
private EnumSingle(String s, int i)
{
super(s, i);
}
public static EnumSingle getInstance()
{
return INSTANCE;
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static
{
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[] {
INSTANCE
});
}
}
可以看到,不是无参构造器哦,而是有参构造器,有一个String,一个Int
现在我们修改反射代码
// Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
完整代码:
package com.kuang.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
//enum 是一个什么? 本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance()
{
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle instance1=EnumSingle.INSTANCE;
//NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
// Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
19、深入理解CAS
1)什么是CAS
大厂必须深入研究底层!!!!修内功!操作系统、计算机网络原理、组成原理、数据结构
package com.kuang.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
//CAS
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(2020);
// 期望值、更新值
// public final boolean compareAndSet(int expect, int update)
// 如果实际值 和 期望值相同,那么就更新
// 如果实际值 和 期望值不同,那么就不更新
System.out.println(atomicInteger.compareAndSet(2020, 2021));//true
System.out.println(atomicInteger.get());//2021
//因为期望值是2020 实际值却变成了2021 所以会修改失败
//CAS 是CPU的并发原语
atomicInteger.getAndIncrement(); //++操作
System.out.println(atomicInteger.compareAndSet(2020, 2021));//false
System.out.println(atomicInteger.get());//2021
}
}
Java无法操作内存,但是C++可以操作内存,Java可以通过native方法调用c++从而操作内存
Unsafe 类
总结:
CAS:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作,如果不是,就一直循环
好处:是不用切换线程状态,因为切换线程状态性能消耗比较大
缺点:
1:由于底层是自旋锁,循环会浪费时间
2:因为是底层的cpu操作,一次只能保证一个共享变量的原子性
3:ABA问题
CAS: ABA问题(狸猫换太子)
线程1:期望值是1,要变成2;
线程2:两个操作:
1、期望值是1,变成3
2、期望是3,变成1
所以对于线程1来说,A的值还是1,所以就出现了问题,骗过了线程1;
测试ABA问题:
package com.kuang.cas;
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
//CAS
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger(2020);
// 期望值、更新值
// public final boolean compareAndSet(int expect, int update)
// 如果实际值 和 期望值相同,那么就更新
// 如果实际值 和 期望值不同,那么就不更新
//============捣乱的线程===========
System.out.println(atomicInteger.compareAndSet(2020, 2021));//true
System.out.println(atomicInteger.get());//2021
System.out.println(atomicInteger.compareAndSet(2021, 2020));//true
System.out.println(atomicInteger.get());//2021
// //因为期望值是2020 实际值却变成了2021 所以会修改失败
// //CAS 是CPU的并发原语
// atomicInteger.getAndIncrement(); //++操作
//============期望的线程===========
System.out.println(atomicInteger.compareAndSet(2020, 6666));//false
System.out.println(atomicInteger.get());//2021
}
}
结果:
如何解决ABA问题
原子引用
20、原子引用
解决ABA问题,引入原子引用;对应思想:乐观锁
带版本号的原子引用!
因为integer对象的问题,导致下面结果没达到预期。
compareAndSet()//返回false
//Integer(2020)不会==Integer(2020)
当前值不会==期望值
expectedReference == current.reference //false
//源码:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(2020,1);
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//获得版本号
System.out.println("a1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(2020, 2022, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a2=>"+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2022, 2020, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a3=>"+atomicStampedReference.getStamp());
},"a").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//获得版本号
System.out.println("b1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(2020, 6666, stamp, stamp + 1));
System.out.println("b2=>"+atomicStampedReference.getStamp());
},"b").start();
}
把2020->1,2022->2,6666->6
package com.kuang.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);
//乐观锁的原理相同
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//获得版本号
System.out.println("a1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a2=>"+atomicStampedReference.getStamp());
System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a3=>"+atomicStampedReference.getStamp());
},"a").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();//获得版本号
System.out.println("b1=>"+stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6, stamp, stamp + 1));
System.out.println("b2=>"+atomicStampedReference.getStamp());
},"b").start();
}
}
测试结果:
21、各种锁的理解
1、公平锁、非公平锁
公平锁:非常公平,不能插队,必须先来后到
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
非公平锁:非常不公平,允许插队,可以改变顺序,synchronized和lock(默认都是非公平锁)
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2、可重入锁
所有的锁都是可重入锁,有些地方叫做递归锁
你进入你家,拿到了大门的锁,也就自动拿到了里面小门的锁
1、Synchonized 锁
package com.kuang.lock;
//Synchronized
public class Demo01 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone{
public synchronized void sms(){
System.out.println(Thread.currentThread().getName()+"=> sms");
call();//这里也有一把锁
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"=> call");
}
}
2、Lock 锁
package com.kuang.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//lock
public class Demo02 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sms();
},"A").start();
new Thread(()->{
phone.sms();
},"B").start();
}
}
class Phone2{
Lock lock=new ReentrantLock();
public void sms(){
lock.lock(); //细节:这个是两把锁,两个钥匙
//lock锁必须配对,否则就会死锁在里面
try {
System.out.println(Thread.currentThread().getName()+"=> sms");
call();//这里也有一把锁
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void call(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "=> call");
}catch (Exception e){
e.printStackTrace();
}
finally {
lock.unlock();
}
}
}
注意细节:
- lock锁必须配对,相当于lock和 unlock 必须数量相同;
- 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁;
3、自旋锁
spinlock
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
自己设计自旋锁
package com.kuang.lock;
import java.util.concurrent.atomic.AtomicReference;
/**
* 自旋锁
*/
public class SpinlockDemo {
// 默认
// int 0
//Thread null
AtomicReference<Thread> atomicReference=new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(thread.currentThread().getName()+"===> mylock");
//加自旋锁
while (!atomicReference.compareAndSet(null,thread)){
System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");
}
}
//解锁
public void myUnlock(){
Thread thread=Thread.currentThread();
System.out.println(thread.currentThread().getName()+"===> myUnlock");
//解锁操作
atomicReference.compareAndSet(thread,null);
}
}
测试:
package com.kuang.lock;
import java.util.concurrent.TimeUnit;
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
//ReentrantLock reentrantLock = new ReentrantLock();
//reentrantLock.lock();
//reentrantLock.unlock();
//使用CAS实现自旋锁
SpinlockDemo spinlockDemo=new SpinlockDemo();
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
spinlockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
spinlockDemo.myUnlock();
}
},"t2").start();
}
}
测试结果:
t1===> myLock
t2===> myLock
t2 ==> 自旋中~
t2 ==> 自旋中~
t2 ==> 自旋中~
t2 ==> 自旋中~
...
...
t2 ==> 自旋中~
t2 ==> 自旋中~
t2 ==> 自旋中~
t2 ==> 自旋中~
t1===> myUnlock
t2 ==> 自旋中~
===========t2进程必须等待t1进程Unlock后,才能Unlock,在这之前进行自旋等待======
t2===> myUnlock
4、死锁
联系操作系统
第三章 处理机调度和死锁【操作系统】
死锁测试,怎么排除死锁
package com.kuang.lock;
import java.util.concurrent.TimeUnit;
public class DeadLock {
public static void main(String[] args) {
String lockX= "lockX";
String lockY= "lockY";
new Thread(new MyThread(lockX,lockY),"t1").start();
new Thread(new MyThread(lockY,lockX),"t2").start();
}
}
class MyThread implements Runnable{
private String lockA;
private String lockB;
public MyThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);
}
}
}
}
结果:死锁
解决问题
1、使用jps -l
定位进程号,
查看哪一个进程出了问题
2、使用jstack [进程号]
查看进程的信息,找到死锁问题
面试,工作中!排查问题:
1、日志信息
2、堆栈信息
最后
关于本文
Markdown 74129 字数 4200 行数
HTML 66309 字数 2973 段落
本文依照狂神视频学习,
并参考,CSDN优秀博主的博文
掺杂自己的拙见
仅作学习交流使用
安利狂神说java
视频下狂神的评论:
[置顶]白漂有罪,拒绝白嫖,从点赞转发关注做起!
文章同步在公众号:狂神说 (公众号日更,记得关注)
视频文档地址:https://gitee.com/kuangstudy/openclass 记得三连转发