【Java学习】JUC并发编程
目录
- 1.JUC是什么
- 1.1 JUC简介
- 1.2 线程和进程概念
- 1.2.1 进程和线程
- 1.3 线程的状态
- 1.3.1 线程状态枚举类
- 1.3.2 wait和sleep的区别
- 1.3.3 使用范围不同
- 1.4 并发和并行
- 1.4.1 串行模式
- 1.4.2 并行模式
- 1.4.3 并发模式
- 1.4.4 小结
- 1.5 管程
- 1.6 用户线程和守护线程
- 1.6.1 用户线程
- 1.6.2 守护线程
- 2.Lock接口
- 2.1 Synchronized关键字
- 2.2 Lock锁
- 2.3 synchronized和lock区别
- 2.4 锁是什么?如何判断锁?锁的是谁?
- 2.4.1.生产者和消费者问题
- 1.synchronized版本
- 2.JUC版本
- 3. 八锁问题
- 4. 集合类不安全
- 4.1 List不安全
- 4.2 Set不安全
- 4.2 Map不安全
- 5. Callable(简单)
- 6. 三大常用辅助类(必会)
- 6.1 CountDownLatch
- 6.2 CyclicBarrier
- 6.3 Semaphore
- 7.读写锁
- 8. 阻塞队列
- 9. SynchronousQueue同步队列
- 10.线程池(重点)
- 11.四大函数式接口(必须掌握)
- 11.1 Funcition接口
- 11.2 Predicate
- 11.3 Consumer
- 11.4 Supplier
- 12. 流式计算
- 13.ForkJoin
- 14.异步回调
- 15.JMM
- 16. Volatile
- 16.1 可见性
- 16.2 原子性
- 16.3. 指令重排
- 17.彻底玩转单例模式
- 17.1 饿汉式
- 17.2 DCL懒汉式
- 17.2 静态内部类构建
- 17.3 存在问题
- 17.4枚举式
- 18.深入理解CAS
- 18.1 unsafe类
- 18.2 ABA问题(狸猫换太子)
- 18.3 原子引用解决ABA问题
- 19.各种锁的理解
- 19.1 公平锁、非公平锁
- 19.2 可重入锁
- 19.3自旋锁
- 19.4死锁
1.JUC是什么
1.1 JUC简介
在Java中,线程部分是一个重点,本次笔记说的JUC也是关于线程的,JUC是java.util.concurrent工具包的简称。这是一个处理线程的工具包,JDK1.5开始出现的
juc是java.util工具包
业务: 可能无法通过普通的线程代码new Thread()实现
Runnable: 没有返回值,效率相比Callable相对较低
1.2 线程和进程概念
1.2.1 进程和线程
进程(Process)
- 一个程序,例如QQ.exe,Music.exe程序的集合;
- 一个进程往往包含多个线程,至少包含一个
- Java默认有2个线程,main线程和gc线程
- 资源分配的基本单位
线程(thread)
- 开一了一个进程Typora,写字,自动保存(线程负责)
- 对于Java而言开启线程的方式Thread、Runnable、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 */
}
}
}
//本地方法,底层的c++,Java无法直接操作硬件
private native void start0();
总体而言
进程: 指的是系统中正在运行的一个应用程序,程序一旦运行就是进程,进程是资源分配的最小单位
线程: 系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单元
并发: 多线程操作同一个资源
- CPU一核,模拟出来多条线程,天下武功,唯快不破,快速交替
并行: 多个人一起行走
- cpu多核,多个线程可以同时执行
并发编程的本质: 充分利用CPU的资源
1.3 线程的状态
1.3.1 线程状态枚举类
ThreadState
- NEW(新建态)
- RUNNABLE(运行)
- BLOCKED(阻塞)
- WATING(等待,一直等待)
- TIMED_WATING(超时等待)
- TERMINATED(终止)
1.3.2 wait和sleep的区别
- sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能够调用
sleep不会释放锁
,抱着锁睡觉,不会释放!wait会释放锁
,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。- 它们都可以被interrupted打断
1.3.3 使用范围不同
wait: wait必须在同一个代码块中
sleep: 可以在任何地方睡
1.4 并发和并行
1.4.1 串行模式
串行表示所有任务都——按先后顺序进行,串行意味着必须先装完一车柴才能运送这车柴,只有运行到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤
1.4.2 并行模式
并行意味着可以同时取得多个任务,并同时去执行所得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核CPU
1.4.3 并发模式
并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或多指令可以同时运行。
1.4.4 小结
并发: 同一时间段内多个线程在访问同一个资源,多个线程对一个点
例如: 春运抢票、电商秒杀
并行: 多项工作一起执行,之后再汇总,也就是同一时刻同时执行多项操作
例子: 一边泡方便面,一边电水壶烧水,一边撕调料导入桶中
1.5 管程
管程在操作系统中称Monitor监视器
,也就是Java中的所说锁
,是一种同步的机制,保证同一个时间,只有一个线程访问被保护数据或者代码(或者叫资源)
jvm同步基于进入和退出,使用管程对象实现的
1.6 用户线程和守护线程
1.6.1 用户线程
自定义线程
主线程已经结束了,用户线程还在运行,jvm还是存活的状态
package com.asule._9JUC;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: ${DATE} ${TIME}
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread aa = new Thread(() -> {
// isDaemon() true是守护线程,false是用户线程
while (true){
try {
Thread.sleep(1000);
System.out.println("我是用户线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
aa.start();
Thread.sleep(3000);
System.out.println("主线程执行完毕......");
}
}
1.6.2 守护线程
垃圾回收
没有用户线程了,都是守护线程,jvm结束
main 方法所在的主线程执行完毕之后,程序就退出了
2.Lock接口
2.1 Synchronized关键字
真正的多线程开发(公司中的开发,要求降低耦合性): 线程就是一个单独的资源类,没用任何负数的操作
包含:
- 属性、方法
此处的代码解耦了,多线程中示例的购票代码没有解耦
package com.asule;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/2 13:26
* @Version: 1.0
* @Description: 基本的卖票例子....
* <p>
* 真正的多线程开发,公司中的开发
* 线程就是一个单独的资源类,没用任何负数的操作
*/
public class SaleTicket {
public static void main(String[] args) {
//并发多线程操作同一个资源类,把资源类丢入线程
Ticket ticket = new Ticket();
//@FunctionalInterface 函数式接口
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 60; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 60; 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);
}
}
}
结果与我们所想的递减并不一样,剩余45后变成剩余46了
传统方式: 加synchronized,本质:队列,锁
输出正常
2.2 Lock锁
Lock接口
Lock实现类
- 可重用锁(常用)
- 读锁
- 写锁
公平锁: 十分公平:可以先来后到
非公平锁: 十分不公平:可以插队(默认)
假设: 当前有两个进程/线程,一个3s就可以执行完毕,另一个则需要3h,此时让3h先执行,那3s的进程/线程需要等待3小时,所以默认为非公平锁
package com.asule;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/2 13:26
* @Version: 1.0
* @Description: 基本的卖票例子....
* <p>
* 真正的多线程开发,公司中的开发
* 线程就是一个单独的资源类,没用任何负数的操作
*/
public class SaleTicketDemo02 {
public static void main(String[] args) {
//并发多线程操作同一个资源类,把资源类丢入线程
Ticket2 ticket = new Ticket2();
//@FunctionalInterface 函数式接口
new Thread(()->{for (int i = 0; i < 60; i++) ticket.sale();},"A").start();
new Thread(()->{for (int i = 0; i < 60; i++) ticket.sale();},"B").start();
new Thread(()->{for (int i = 0; i < 60; i++) ticket.sale();},"C").start();
}
}
//Lock三部曲
//1.new ReentrantLock();
//2.lock.lock();
//3.finally {lock.unlock(); //解锁}
class Ticket2 {
//属性、方法
private int number = 50;
private Lock lock = new ReentrantLock();
//卖票的方式
public void sale() {
//加锁
lock.lock();
try {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "张票,剩余:" + number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock(); //解锁
}
}
}
结果正常
2.3 synchronized和lock区别
- synchronized 是Java关键字,Lock 是Java接口
- synchronized 无法获取锁的状态,Lock 可以判断是否获取到了锁
- synchronized 会自动释放锁,Lock 必须要手动释放锁,如果不释放锁,会
死锁
- synchronized 线程1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去
- synchronized 可重入锁,不可以中断的,非公平;Lock,可重入锁,可以判断所,非公平(可以自己设置)
- synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码
2.4 锁是什么?如何判断锁?锁的是谁?
2.4.1.生产者和消费者问题
1.synchronized版本
package com.asule;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/2 14:55
* @Version: 1.0
* @Description: 生产者消费者....
* <p>
* 线程之间的通信问题 等待唤醒 通知唤醒
* 线程交替执行 A B 操作同一个变量 num = 0
* A num + 1
* B num - 1
*/
public class A {
public static void main(String[] args) {
Date date = new Date();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
date.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
date.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//等待,业务,通知
class Date { //数字 资源类
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
if (number != 0) {
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + " ->" + number);
//通知其他线程,我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
if (number == 0) {
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + " ->" + number);
//通知其他线程,我-1完毕了
this.notifyAll();
}
}
问题存在: 如果有A,B,C,D四个或更多线程
问题: 结果出现不只为0或1
原因:
- 生产者A进入increment()方法,此时没有产品,wait()不成立,生产产品后唤醒其他线程
- 生产者A的时间片未结束继续进入increment()方法,但是此时已有一个产品,条件满足,
进入阻塞队列并释放锁
- 消费者B进入decrement()方法,此时已有产品,wait()不成立,使用产品后唤醒其他线程
- 消费者B的时间片未结束继续进入decrement()方法,但是此时已经没有产品了,wait()成立,
进入阻塞队列并释放锁
- 由于步骤3已经唤醒了其他线程(注意:
步骤2生产者A停留在if代码块中
),此时生产者A直接跳出if代码块,并添加产品后唤醒其他线程 - 生产者A时间片未结束继续进入increment()方法,但是此时已有一个产品,条件满足,
进入阻塞队列并释放锁
- 生产者C进入increment()方法,但是此时已有一个产品,条件满足,
进入阻塞队列并释放锁
- 在步骤5中唤醒了阻塞队列中的消费者B线程,此时消费者B线程跳出if代码块消费产品
并唤醒了生产者A线程、生产者C线程
- 消费者B的时间片未结束继续进入decrement()方法,但是此时已经没有产品了,wait()成立,
进入阻塞队列并释放锁
(同步错误出现)
步骤8中消费者A唤醒了位于阻塞队列中的生产者A线程和生产者C线程,而这两个线程的代码此时都停留在if代码块
中,首先CPU时间片给到了生产者A,生产者A生产了一个产品,但时间片并未结束,继续进入increment()方法,此时已有产品,生产者A停留在wait()处,并进入阻塞队列后释放锁- 此时CPU时间片又给到了生产者C,生产者C跳出if判断条件添加商品
(此时产品变为2个)
并唤醒其他线程(生产者A线程又被唤醒了)
,同一CPU时间片未结束,会产生和步骤10
同样的操作 - 在步骤11中生产者A线程又被唤醒了,此时CPU又给到生产者A,生产者跳出if代码块并生产一个产品(此时产品变为3个)
- 如此一来,两个生产者就有可能一直往复生产下去,产品数量可能变得很大。同时,若两个消费者一直交替消费产品,那产品数量可能就会出现负数的情况。
解决办法: if改成while
2.JUC版本
通过Lock可以找到Condition
package com.asule.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/3 9:02
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class B {
public static void main(String[] args) {
Date2 date = new Date2();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
date.increment();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
date.decrement();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
date.increment();
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
date.decrement();
}
}, "D").start();
}
}
//等待,业务,通知
class Date2 { //数字 资源类
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
/**
* condition.await();//等待
* condition.signalAll();//唤醒全部
* @throws InterruptedException
*/
public void increment(){
lock.lock();
try {
while (number != 0) {
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public void decrement() {
lock.lock();
try {
while (number == 0){
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
问题: 目前是随机的状态,与synchronized一样,如何使用Condition使其变成A,B,C,D有序的状态?
package com.asule.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/3 9:17
* @Version: 1.0
* @Description: 文件作用详细描述....
*
* A执行完调用B,B执行完调用C,C执行完调用A
*/
public class C {
public static void main(String[] args) {
Date3 date = new Date3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
date.printA();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
date.printB();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
date.printC();
}
}, "C").start();
}
}
//等待,业务,通知
class Date3 { //数字 资源类
private int number = 1;// 1A 2B 3C
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 {
//业务:判断 => 执行 => 通知
while (number != 1){
//等待
condition1.await();
}
System.out.println(Thread.currentThread().getName() + "AAA");
//唤醒指定的人 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() + "BBB");
//唤醒指定的人 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() + "CCC");
//唤醒指定的人 A
number = 1;
condition1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
使用场景: 上产线:下单 ->支付->交易
3. 八锁问题
问题1: 标准情况下,两个线程先打印发短信还是打电话?
问题2: sendSms延时4秒,两个线程先打印发短信还是打电话?
回答1、2: 先发短信后打电话,不是因为先调用的问题,是因为有锁的存在
原因1、2: synchronized锁的对象是方法的调用者,两个方法用的是同一个锁,调用的是phone的锁,谁先拿到锁谁先执行
package com.asule.lock8;
import java.util.concurrent.TimeUnit;
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone{
//synchronized锁的对象是方法的调用者
//两个方法用的是同一个锁,调用的是phone的锁,谁先拿到锁谁先执行
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
问题3: 增加了一个普通方法后,是先执行发短信还是Hello
回答3: 先执行hello,再执行发短信
原因3: 普通方法没有锁,不受锁的影响
package com.asule.lock8;
import java.util.concurrent.TimeUnit;
public class Test2 {
public static void main(String[] args) {
Phone2 phone = new Phone2();
new Thread(()->{
phone.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.hello();
},"B").start();
}
}
class Phone2{
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");
}
}
问题4: 两个对象,分别去执行两个同步方法,会先执行发短信还是打电话?
回答4: 先打电话,发短信
原因4: 两个对象,两把锁,按时间来
public class Test2 {
public static void main(String[] args) {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(()->{
phone1.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone2{
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");
}
}
问题5: 将同步方法改为两个静态同步方法,只有一个对象,先执行打印 发短信 or 打电话?
回答5: 先发短信 后打电话
原因5: Phone3只有一个唯一的class对象,而static是静态方法,类一加载就有了,此时锁的是Class(一个类只能有一个class对象,全局唯一)
package com.asule.lock8;
import java.util.concurrent.TimeUnit;
public class Test3 {
public static void main(String[] args) {
Phone3 phone1 = new Phone3();
new Thread(()->{
phone1.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone1.call();
},"B").start();
}
}
class Phone3{
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("打电话");
}
}
问题6: 两个对象,将同步方法改为两个静态同步方法,先执行打印 发短信 or 打电话?
回答6: 先发短信,后打电话
原因6: 与问题5一样,两个对象的class类模板只有一个,static锁的是class
package com.asule.lock8;
import java.util.concurrent.TimeUnit;
public class Test3 {
public static void main(String[] args) {
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(()->{
phone1.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone3{
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("打电话");
}
}
问题7: 将发短信方法改成静态同步方法,打电话方法还是普通同步方法,一个对象,先发短信 or 打电话?
回答7: 先打电话,后发短信
原因7: 发短信锁的是class,打电话锁的是调用者(对象phone),此时打电话不需要去等待锁,所以按照时间
package com.asule.lock8;
import java.util.concurrent.TimeUnit;
public class Test4 {
public static void main(String[] args) {
Phone4 phone1 = new Phone4();
new Thread(()->{
phone1.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone1.call();
},"B").start();
}
}
class Phone4{
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("打电话");
}
}
问题8: 将发短信方法改成静态同步方法,打电话方法还是普通同步方法,两个个对象,先发短信 or 打电话?
回答8: 先打电话,后发短信
原因8: 两个对象,两把锁,此时打电话不需要去等待锁,所以按照时间
package com.asule.lock8;
import java.util.concurrent.TimeUnit;
public class Test4 {
public static void main(String[] args) {
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(()->{
phone1.sendSms();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone4{
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("打电话");
}
}
4. 集合类不安全
4.1 List不安全
public class ListTest {
public static void main(String[] args) {
//List<String> list = Arrays.asList("1", "2", "3");
//list.forEach(System.out::println);
//并发下ArrayList是不安全的
List<String> list = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
多线程执行报异常: java.util.ConcurrentModificationException 并发修改异常
问题: 并发下ArrayList是不安全的
解决办法1: Vector是安全的
解决办法2:
Collections.synchronizedList(new ArrayList<>());
解决办法3:
//解决办法3
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWrite为写入时复制简称COW 计算机程序涉及领域的一种优化策略
问题1: list在多个线程调用的时候
- 读取的时候是固定的
- 写入的时候可能会发生覆盖操作
解决1: 在写入的时候避免覆盖,造成数据问题,用CopyOnWrite先复制要改变的对象,对副本进行写操作,完成对副本的操作后,把原有对象的引用指向副本对象
CopyOnWrite 思想及其应用场景
问题2: 读写分离
Mycat中说明,基于数据库层级
为什么CopyOnWriteArrayList 比 Vetor更具优势?
Vetor的addElement()方法含有synchronized,但由于含有synchronized所以方法的效率较低
CopyOnWriteArrayList源码
用的lock锁
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
4.2 Set不安全
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 0; i < 50; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
解决办法1:
Set<String> set = Collections.synchronizedSet(new HashSet<>());
解决办法2:
Set<String> set = new CopyOnWriteArraySet<>();
问题1: hashSet底层是什么?
回答1: hashMap
问题2: set的本质是什么?
回答2: map的key,key是无法重复的
PRESENT是一个不变的值,常量
private static final Object PRESENT = new Object();
4.2 Map不安全
问题1: hashMap是这样用的嘛?
Map<String,String> map = new HashMap<>();
回答1: 不是,工作中不用HashMap
问题2: 默认等价于什么?
回答2: 等价于
Map<String,String> map = new HashMap<>(,16,0.75);
出现并发编程异常
package com.asule.unsafe;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/3 11:08
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class MapTest {
public static void main(String[] args) {
//map是这样用的吗?
//默认等价于什么?
Map<String,String> map = new HashMap<>(16,0.75f);
//加载因子,初始化容量
for (int i = 0; i < 50; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
}).start();
}
}
}
解决办法1:
Map<String,String> map = Collections.synchronizedMap(new HashMap<>());
解决办法2:
Map<String,String> map = new ConcurrentHashMap<>();
5. Callable(简单)
- 可以有返回值
- 可以抛出异常
- 方法不同,run()/call()
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//new Thread().start(); //怎么启动callable
//new Thread(new Runnable()).start();
//new Thread(new FutureTask<V>()).start();
//new Thread(new FutureTask<V>(Callable)).start();
MyThread thread = new MyThread();
//适配类
FutureTask futureTask = new FutureTask(thread);
new Thread(futureTask,"A").start();
//获取Callable的返回结果
Integer o = (Integer) futureTask.get();//这个get方法可能会产生阻塞,把他放到最后
//或者使用异步通信来处理
System.out.println(o);
}
}
class MyThread implements Callable<Integer> {
@Override
public Integer call() {
System.out.println("call");
return 1024;
}
}
问题: 两个线程去执行会打印几个call()?
回答: 1个
结论: 结果会被缓存,打印效率高
细节:
- 有缓存
- 结果可能需要等待,会阻塞
细节了解
6. 三大常用辅助类(必会)
6.1 CountDownLatch
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//总数是6,必须要执行任务的时候再使用
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + " go out ");
countDownLatch.countDown();//数量-1
},String.valueOf(i)).start();
}
System.out.println("关门");
}
}
加上await()方法
原理:
countDownLatch.countDown();
数量-1
countDownLatch.await();
//等待计数器归零,然后再向下执行
每次有线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await()方法就会被唤醒,继续执行
6.2 CyclicBarrier
加法计数器
Java_lambda中引用的局部变量为什么必须为final类型
CyclicBarrier使用详解
package com.asule.add;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/3 13:07
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class CyclicBarrierDemo {
/**
* 集齐七颗龙珠召唤神龙
* @param args
*/
public static void main(String[] args) {
//召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(8,()->{
System.out.println("召唤神龙成功");
});
for (int i = 1; i <= 7; i++) {
//lambda能操作到i吗
//lambda表达式本质是new了一个类,不可能拿到for循环的变量
final int temp = 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,但是只有7个线程,永远执行不完
线程计数器为7,线程集结成功,输出召唤神龙成功
6.3 Semaphore
Semaphore:信号量
Semaphore详解
public class SemaphoreDemo {
public static void main(String[] args) {
//线程数量,停车位! 限流
Semaphore semaphore = new Semaphore(3);
for (int i = 0; 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 {
//release() 释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
原理:
semaphore.acquire();获得,如果无可用许可则将一阻塞等待,直至许可被释放
semaphore.release();释放,会将当前的信号量释放+1,然后唤醒等待的线程
作用:多个共享资源互斥的使用!并发限流,控制最大线程数量
7.读写锁
初始代码
package com.asule.rw;
import java.util.HashMap;
import java.util.Map;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/3 13:54
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写入
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.put(temp + "", temp + "");
}, String.valueOf(i)).start();
}
//读取
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(() -> {
myCache.get(temp + "");
}, String.valueOf(i)).start();
}
}
}
/**
* 自定义缓存
*/
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
//存,写
public void put(String key, Object value) {
System.out.println(Thread.currentThread().getName() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入OK");
}
//取,读
public void get(String key) {
System.out.println(Thread.currentThread().getName() + "读取" + key);
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
}
}
结果:
结论: 写入操作被插入了,但是写入操作应该是原子性的不应该被其他操作打断
改进代码
class MyCacheLock {
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() + "写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "写入OK");
} 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);
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
结果:
ReadWriteLock
独占锁(写锁)一次只能被一个线程占有
共享锁(读锁)多个线程可以同时占有
- 读-读 可共存
- 读-写 不能共存
- 写-写 不能共存
8. 阻塞队列
阻塞:
- 写入: 如果队列满了,就必须阻塞
- 取出: 如果队列是空的,就必须阻塞等待生产
队列:
问题: 什么情况下我们会使用阻塞队列?
回答: 多线程并发处理,线程池
学会使用队列
添加、移除
四组API
-
抛出异常
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: Queue full抛出异常 //System.out.println(blockingQueue.add("d")); System.out.println("队首是" + blockingQueue.element()); //查看队首元素是谁 System.out.println("================================"); System.out.println(blockingQueue.remove()); System.out.println("队首是" +blockingQueue.element()); //查看队首元素是谁 System.out.println(blockingQueue.remove()); System.out.println(blockingQueue.remove()); //java.util.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.peek()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); //null 不抛出异常 }
-
阻塞等待
/** * 等待,阻塞(一直阻塞) */ public static void test3() throws InterruptedException { //队列的大小 ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(3); 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()); //没有这个元素,一直阻塞 }
-
超时等待
/** * 等待,阻塞(等待超时) */ public static void test4() throws InterruptedException { //队列的大小 ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(3); blockingQueue.offer("a"); blockingQueue.offer("b"); blockingQueue.offer("c"); blockingQueue.offer("d", 2,TimeUnit.SECONDS); //等待超过2秒就退出 System.out.println("========================="); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll()); System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS)); //等待超过2秒就退出 }
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(参数1,参数2,参数3) |
移除 | remove() | poll() | take() | poll(参数1,参数2) |
判断队首元素 | element() | peek() | - | - |
9. SynchronousQueue同步队列
没有容量,进入一个元素,必须等待取出来,才能再往同步队列里面放一个元素
就是说向队列里面put了一个元素,必须从里面先取出来,否则不能再put进去值
public class SynchronizedQueueDemo {
public static void main(String[] args) {
//同步队列
BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
new Thread(()->{
try {
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName() + " put 1 ");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName() + " put 2 ");
blockingQueue.put("3");
System.out.println(Thread.currentThread().getName() + " put 3 ");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " " + blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " " + blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " " + blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
}
10.线程池(重点)
**线程池:**三大方法、七大参数、四种拒绝策略
池化技术: 由于程序运行的本质是占用系统的资源,所以我们要优化资源的使用,衍生出来一种策略就叫池化技术
例如: 线程池、连接池、内存池、对象池
**池化技术策略:**因为创建和销毁十分浪费资源,所以池化技术事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我
线程池的优势:
- 降低资源消耗
- 提高响应速度
- 方便管理
总结:线程可以复用、可以控制最大并发数、管理线程
线程池三大方法:
- **单个线程:**Executors.newSingleThreadExecutor();
- 创建固定大小的线程池: Executors.newFixedThreadPool(你所需要的线程池大小);
- **动态伸缩的线程池:**Executors.newCachedThreadPool();
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 = 0; i < 100; i++) {
//使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
七大参数以及自定义线程池:
源码分析:
- 单个线程源码
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 固定线程池源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 动态伸缩线程池源码
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
本质: 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;
}
手动创建一个线程池
最大承载: maximumPoolSize + 阻塞队列的容量
四种拒绝策略:
-
AbortPolicy:超出最大承载,抛出异常
-
CallerRunsPolicy:哪来的去哪里,测试代码让main线程去执行
-
DiscardOldestPolicy:超出最大承载,尝试去和最早的竞争,竞争失败就丢弃任务,也不会抛出异常
-
DiscardPolicy:超出最大承载,丢掉任务,不会抛出异常
package com.asule.bq;
import java.util.concurrent.*;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/4 9:47
* @Version: 1.0
* @Description: 文件作用详细描述....
*
* Executors工具类,三大方法
* 使用线程池之后,使用线程池来创建线程
*/
public class Demo01 {
public static void main(String[] args) {
//自定义线程池!工作肯定使用ThreadPoolExecutor
//1.CPU密集型 CPU几核就定义为几,可以保证CPU效率最高
//2.IO密集型 > 判断你的程序中十分耗IO的线程
//程序 中有15个大型任务 io十分占用资源
//获取cpu核数
System.out.println(Runtime.getRuntime().availableProcessors());
ExecutorService threadPool = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
//银行满了,还有人进来,不处理这个人的需求并且抛出异常
//new ThreadPoolExecutor.AbortPolicy()
//哪来的去哪里,这里就是让main线程去执行
//new ThreadPoolExecutor.CallerRunsPolicy()
//队列满了,尝试去和最早的竞争,竞争失败就丢弃任务,也不会抛出异常
new ThreadPoolExecutor.DiscardOldestPolicy()
//队列满了不会抛出异常
//new ThreadPoolExecutor.DiscardPolicy()
);
try {
// 最大承载,Deque + max
//超出最大承载,抛出异常RejectedExecutionException
for (int i = 0; i < 9; i++) {
//使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
小结:
- CPU密集型 CPU几核就定义为几,可以保证CPU效率最高
- IO密集型 > 判断你的程序中十分耗IO的线程
11.四大函数式接口(必须掌握)
lambda表达式、链式编程、函数式接口、Stream流式计算
函数式接口: 只有一个方法的接口
例如:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Java中有非常多的FunctionalInterface
目的: 简化编程模型,在新版本底层框架中大量应用
list.forEach()是消费者类型的函数式接口
四大函数式接口:
- Consumer
- Function
- Predicate
- Supplier
11.1 Funcition接口
函数型接口
package com.asule.function;
import java.util.function.Function;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/4 11:06
* @Version: 1.0
* @Description: 函数型接口....
*/
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 -> str;
//如果大括号有且只有一个语句,无论是否有返回值,大括号、return关键字、分号可以省略。
Function function = (str)->{return str;};
System.out.println(function.apply("asd"));
}
}
11.2 Predicate
断定型接口: 有一个输入参数,返回值只能是 布尔值!
package com.asule.function;
import java.util.function.Predicate;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/4 11:15
* @Version: 1.0
* @Description: 断定型接口....
*
* 断定型接口:有一个输入参数,返回值只能是 布尔值!
*/
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 -> str.isEmpty();
Predicate<String> predicate = (str)->{return str.isEmpty();};
System.out.println(predicate.test("asd"));
}
}
11.3 Consumer
消费型接口: 消费者只有输入没有返回值
package com.asule.function;
import java.util.function.Consumer;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/4 11:21
* @Version: 1.0
* @Description: 消费型接口....
*
*
* 消费型接口:只有输入,没有返回值
*/
public class Demo03 {
public static void main(String[] args) {
//Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String str) {
// System.out.println("str");
// }
//};
//Consumer<String> consumer = (str) -> System.out.println("str");
Consumer<String> consumer = (str) -> {System.out.println("str");};
consumer.accept("asdasdasd");
}
}
11.4 Supplier
供给型接口: 没有参数只有返回值
package com.asule.function;
import java.util.function.Supplier;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/4 11:26
* @Version: 1.0
* @Description: 供给型接口....
*/
public class Demo04 {
public static void main(String[] args) {
//Supplier<Integer> supplier = new Supplier<Integer>() {
// @Override
// public Integer get() {
// return 1024;
// }
//};
//Supplier<Integer> supplier = () -> 1024;
Supplier<Integer> supplier = () -> {
return 1024;
};
System.out.println(supplier.get());
}
}
12. 流式计算
问题: 什么是Stream流式计算
大数据: 存储 + 计算
集合、Mysql本质就是存储东西,计算应该都交给流来操作
package com.asule.stream;
import java.util.Arrays;
import java.util.List;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/4 12:06
* @Version: 1.0
* @Description: 文件作用详细描述....
* 题目要求:一分钟内完成此题,只能用一行代码实现!
* 现在有5个用户!
* <p>
* 筛选:
* 1.ID必须是偶数
* 2.年龄必须大于23岁
* 3.用户名转为大写字母
* 4.用户名字倒着排序
* 5.只输出一个用户
*/
public class Test {
public static void main(String[] args) {
User u1 = new User(1, "a", 21);
User u2 = new User(2, "b", 22);
User u3 = new User(3, "c", 23);
User u4 = new User(4, "d", 24);
User u5 = new User(6, "e", 25);
//集合就是存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
//计算交给Stream流
list.stream()
.filter(u -> {
return u.getId() % 2 == 0;
})
.filter(u -> {
return u.getAge() > 23;
})
.map(u -> {
return u.getName().toUpperCase();
})
.sorted((uu1, uu2) -> {
return uu2.compareTo(uu1);
})
.forEach(System.out::println);
}
}
- 流式计算为数组和集合服务,不为Map服务,对于集合来说,所有集合的父类Collection中有一个stream()方法,所以所有的集合对象都可以用stream方法,而数组可以通过
Arrays.stream(数组名称)
来把一个数组变成流,然后就可以进行流式计算; - stream方法的返回值是一个流,流后面的泛型可以是集合中的数据类型,例如
List<User
>中的User
,根据default Stream<E> stream(){XXX}
可知;也可以是数组中的类型,例如Integer[]
中的Integer
,根据public static <T> Stream<T> stream(T[] array){XXX}
可知; - filter中用的是
断定型接口Predicate
,可以用来过滤,传入一个参数,但是返回一个布尔值,最终filter方法返回的是由Predicate断定型接口的输入值作为泛型的流
,根据Stream<T> filter(Predicate<? super T> predicate)
可知; - map中用的是
函数型接口Function
,可以用来映射,注意map()可不是集合,传入一个参数,可以返回一个任意类型的参数,最终map方法的返回值是一个由Function函数型接口的返回值作为泛型的流
,根据<R> Stream<R> map(Function<? super T, ? extends R> mapper)
可知; - sorted中用的是
比较型接口Comparator
,可以用来比较,需要传输两个相同类型的参数,通过比较得出返回值,例如o1.compareTo(o2)
就是按照正序排列,而o2.compareTo(o1)
就是按照倒序排列,返回值就是比较型接口输入值的类型作为泛型的流
,根据Stream<T> sorted(Comparator<? super T> comparator)
可知 - limit中用的是一个long型的数字,返回的还是调用limit()方法的流类型
- forEach中用的是
消费型接口Consumer
,可以用来输出,没有返回值,只需要传输一个参数就可以了,根据void forEach(Consumer<? super T> action)
可知 - collect(Collectors.toList())叫做收集,一般用在最后将流变成一个List集合
- toArray()也叫做收集,一般用在最后将流变成一个数组,但是只能是Object类型的
- 除此之外还有
count()
、distinct()
、empty()
、generate(Supplier<T> s)
、mapToInt(ToIntFunction<? super T> mapper)
,但是有可能使用的不是我们平常使用的方法,例如:
注意:看不懂去看Java新特性
13.ForkJoin
问题: 什么是ForkJoin
回答: ForkJoin在JDK1.7,并行执行任务,提高效率、大数据量
大数据: Map Reduce(大任务拆分成子任务)
特点: 工作窃取,A线程执行到某个位置时,B线程已经执行完了,不能让B线程一直等待,让B线程去把A线程的任务偷过来执行,就叫做工作窃取
如何使用ForkJoin
如何使用ForkJoin
- 通过ForkJoinPool来执行
- 计算任务ForkJoinPool.execute(ForkJoinTask<?> task)
- 计算类要继承ForkJoinTask
ForkJoinPool.execute(ForkJoinTask<?> task)
package com.asule.forkjoin;
import java.util.concurrent.RecursiveTask;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/4 12:32
* @Version: 1.0
* @Description: 求和计算的任务....
* <p>
* 解决办法1 ForkJoin
* 解决办法2 Stream
* <p>
* 如何使用forkjoin
* 1
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
//临界值
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
//计算方法
@Override
protected Long compute() {
if ((end - start) < temp) {
long sum = 0L;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
//分支合并计算 forkjoin 递归
long middle = (start + end) / 2;//中间值
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork(); // 拆分任务,把任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(middle+1, end);
task2.fork(); // 拆分任务,把任务压入线程队列
return task1.join() + task2.join();
}
}
}
package com.asule.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/4 13:12
* @Version: 1.0
* @Description: 文件作用详细描述....
*
* 同一个任务,效率高很多
*/
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//test01(); //673
//test02(); //207
test03(); //103
}
//普通程序员
public static void test01(){
long sum = 0;
long start = System.currentTimeMillis();
for (long i = 1; i <= 20_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + "时间:" + (end - start));
}
//会使用forkjoin的
public static void test02() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, 20_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);//提交任务
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + "时间:" + (end - start));
}
//Stream并行流
public static void test03() {
long start = System.currentTimeMillis();
//range是() rangeClosed是(]
long sum = LongStream.rangeClosed(0L, 20_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum=" + sum + "时间:" + (end - start));
}
}
14.异步回调
Future设计的初衷: 对将来的某个事件的结果进行建模
CompletableFuture类的两个静态方法
-
runAsync(Runnable runnable):没有返回值的runAsync异步回调
-
supplyAsync(Supplier supplier):有返回值的supplyAsync 异步回调
package com.asule.future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 8:29
* @Version: 1.0
* @Description: 异步调用 CompletableFuture Ajax....
* 成功回调
* 失败回调
*/
public class Demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//没有返回值的runAsync异步回调
//CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread().getName() + "runAsync=>Void");
//});
//
//System.out.println("1111");
//
//completableFuture.get(); // 获取阻塞执行结果
//有返回值的supplyAsync 异步回调
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "supplyAsync=>Integer");
int i = 10 / 0;
return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t=>" + t); //正常的返回结果
System.out.println("u=>" + u); //u=>java.util.concurrent.CompletionException: java.lang.ArithmeticException
}).exceptionally((e) -> {
System.out.println(e.getMessage());
e.printStackTrace();
return 233; //可以获取到错误的返回结果
}).get());
}
}
15.JMM
问题1: 请你谈谈对Volatile的理解
回答1: Volatile是Java虚拟机提供的轻量级同步机制
提供1:
- 保证可见性
- 不保证原子性
- 禁止指令重排
问题2: 什么是JMM
回答2: Java内存模型,不存在的东西,是一种约定,实际不存在
关于JMM的一些同步的约定:
- 线程解锁前,必须把共享变量立刻刷回主存
- 线程加锁前,必须读取主存中的最新值到工作内存中
- 加锁和解锁是同一把锁
线程 工作内存 主内存
8种操作:
主内存→read→load→工作内存→use→业务方法→assign→工作内存→store→write→主内存
- lock (锁定) - 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。【加琐时的操作】
- unlock (解锁) - 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read (读取) - 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
- write (写入) - 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
- load (载入) - 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use (使用) - 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作【运算】。
- assign (赋值) - 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作【赋值】。
- store (存储) - 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write 操作使用。
如果要把一个变量从主内存中复制到工作内存,就需要按序执行 read 和 load 操作
如果把变量从工作内存中同步回主内存中,就需要按序执行 store 和 write 操作。但 Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
问题3: 程序不知道主内存的值已经被修改过了
package com.asule.tvolatile;
import java.util.concurrent.TimeUnit;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 9:05
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
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);
}
}
执行结果: 程序一直在运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-biglb1qd-1675601372874)(G:\图片缓存\image-20230205092138160.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YRwjFsdV-1675601279745)(G:\图片缓存\image-20230205091340532.png)]
结论: 主线程将num设置为1时,线程A并没有写回内存也没有拿到num最新的值
**问题4:**如何实现主内存中的值一旦发生变化必须通知线程?即需要线程需要知道主内存的值发生了变化
16. Volatile
16.1 可见性
保证可见性:指当多个线程访问同一个变量(共享变量)时,如果在这期间有某个线程修改了该共享变量的值,那么其他线程能够立即看得到修改后的值。
结果:
结论: 当主线程中num=1,线程立即停止
- 不加volatile,程序会死循环
- 加volatile,可以保证可见性
16.2 原子性
**不保证原子性:**原子性就是一个操作是不可再分割的,就像原子一样,可以理解为操作只有一步,一步已经是最小步骤了,自然就不能再分了。
线程A在执行任务的时候,不能被打扰,也不能被分割,要么同时成功,要么同时失败
package com.asule.tvolatile;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 9:17
* @Version: 1.0
* @Description: 不保证原子性....
*/
public class VDemo02 {
private static int num = 0;
public static void add() {
num++; //不是一个原子性操作
}
//理论上num结果应该为2万
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 1000; i1++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) { //main gc
Thread.yield(); //礼让
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
结果:
结论: 该程序没有原子性
解决办法:
- synchronized
- volatile
说明: volatile不保证原子性
问题1: 如果不加lock和synchronized,怎么样保证原子性
查看1: 编译VDemo02.class
命令: javap -c VDemo02.class
回答: 使用原子类,解决原子性问题
package com.asule.tvolatile;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 9:17
* @Version: 1.0
* @Description: 不保证原子性....
*/
public class VDemo02 {
private volatile static AtomicInteger num = new AtomicInteger();
public static void add() {
num.getAndIncrement();// AtomicInteger + 1,CAS
}
//理论上num结果应该为2万
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 1000; i1++) {
add();
}
}).start();
}
while (Thread.activeCount() > 2) { //main gc
Thread.yield(); //礼让
}
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
这些类的底层都直接和操作系统挂钩,在内存中修改值Unsafe类是一个很特殊的存在,详情见CAS
16.3. 指令重排
问题1: 什么是指令重排
回答1: 你写的程序,计算机并不是按照你写的顺序那样去执行的
程序执行步骤:
- 源代码
- 编译器优化的重排
- 指令并行重排
- 内存系统重排
- 执行
int x = 1;//1
int y = 2;//2
x = x + 4;//3
y = x + x;//4
**我们所期望的执行顺序:**1234 | 2134 | 1324
但是不可能是4123!
处理器在进行指令重排的时候,需要考虑:数据之间的依赖性!
可能造成影响的结果:a b x y这四个值默认都是0
线程A | 线程B |
---|---|
x=a | y=b |
b=1 | a=2 |
正常的结果:x = 0 ; y = 0,但是可能由于指令重排
线程A | 线程B |
---|---|
b=1 | a=2 |
x=a | y=b |
指令重排导致的诡异结果:x = 2,y = 1
volatile可以避免指令重排: 内存屏障、CPU指令,作用:
-
保证特定操作的执行顺序
-
可以保证某些变量的内存可见性(利用这些特性volatile实现 了可见性)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jAsD0Cju-1675601279752)(G:\图片缓存\image-20230205095250709.png)]
volatile读写前后,都加屏障
结论: volatile是可以保证可见性,不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生
**问题:**内存屏障在哪使用的最多?
回答: 单例模式中使用的最多
17.彻底玩转单例模式
17.1 饿汉式
饿汉式单例在多线程情况下也可以保证单一实例,因为在类加载的时候就已经实例化对象了
package com.asule.single;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 10:17
* @Version: 1.0
* @Description: 饿汉式单例....
*/
public class Hungry {
private final static Hungry HUNGRY = new Hungry();
private Hungry(){
}
public static Hungry getInstance(){
return HUNGRY;
}
}
17.2 DCL懒汉式
懒汉式单例在多线程下不能保证获取单一实例,因为在一个线程获取实例的时候,可能会有另一个线程也在获取实例,导致产生两个以上的实例对象出现
package com.asule.single;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 10:18
* @Version: 1.0
* @Description: 懒汉式....
*/
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan(){
System.out.println(Thread.currentThread().getName() + " ok");
}
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();
}
}
}
结果:
结论: 单线程没有问题,但是多线程并发就会有问题,所以要加锁
- 如果把锁放在获取实例的静态方法上(锁的是class),也是可行的,但是效率低下,因为只有第一次创建对象的时候需要加锁,其他时候只是返回对象,不会创建对象所以不需要加锁,采用双重检查锁模式的懒汉式单例,DCL懒汉式
逻辑执行顺序:
- 分配内存空间
- 执行构造方法,初始化对象
- 把这个对象指向分配的内存空间
问题: 如果发生指令重排,原本的顺序为123,但是可能出现132的顺序,那会怎样呢?
回答: A线程如果执行了顺序132,A线程本身是不会有问题的此时B线程再去获取实例,此时lazyMan已经指向了内存空间,B线程会认为lazyMan不等于null并返回lazyMan, 而此时layMan所指的内存空间其实是为空的,因为lazyMan并没有完成实例的构造
17.2 静态内部类构建
package com.asule.single;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 10:40
* @Version: 1.0
* @Description: 静态内部类实现....
*/
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
17.3 存在问题
上述说的几种方式都是不安全的,都可以通过反射来创建新的实例,可以破坏这种单例
问题1: 可以通过反射破坏单例
//反射!
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
解决1: 构造方法加同步锁,并抛出异常
问题2: 创建两个对象,都通过反射的方式创建,发现单例又被破坏了
解决2: 设置标志位,用标志位来判断是否要创建对象
**问题3:**通过破坏标志位,发现单例又被破坏了
结论: 道高一尺魔高一丈啊!!!!
**问题4:**如何一次性解决上述问题呢?
回答4: 观察newInstance(),发现不能通过反射破坏枚举类型
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
17.4枚举式
根据EnumSingle的编译文件发现是无参构造
代码:
package com.asule.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 11:30
* @Version: 1.0
* @Description: enum单例....
*
* enum是什么? 本身也是一个Class类
*/
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle instance1 = EnumSingle.INSTANCE;
//观察class发现没有参数
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
运行结果: Exception in thread “main” java.lang.NoSuchMethodException: com.asule.single.EnumSingle.(),说明我们这个类里面没有空参构造器
但是正常报错应该是不能使用反射破坏枚举
说明idea欺骗了我们,没有空参构造,去源代码,通过反编译来查看
命令: javap -p EnumSingle.class
发现还是有个空参构造,说明它也欺骗了我们,通过jad反编译器反编译发现不是无参构造器了
此时抛出不能使用反射破坏枚举异常,与预期结果一致
18.深入理解CAS
在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁机制存在以下问题:
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 一个线程持有锁会导致其它所有需要此锁的线程挂起。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
虽然volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。
问题1: 什么是CAS
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
回答1: CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。
**功能1:**它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS通俗的解释就是:
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止.
**应用1:**CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
多线程它是怎么保证数据的原子性的呢?
举个例子:
假设线程A和线程B两个线程同时执行getAndAddlInt操作(分别跑在不同CPU上) :
- AtomicInteger里面的value原始值为3,即主内存中Atomiclnteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
- 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
- 线程B也通过getlntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
- 这时线程A恢复,执行compareAndSwaplnt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值己经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
- 线程A重新获取value值,因为变量value被volatle修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwaplnt进行比较替换,直到成功。
18.1 unsafe类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hl8eKwuA-1675601279758)(G:\图片缓存\image-20230205122259369.png)]
- Java无法操作内存
- Java可以通过native调用c++
- c++可以操作内存
- Java的后门,可以通过这个类操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KYGnozno-1675601279758)(G:\图片缓存\image-20230205122704456.png)]
- var1 Atomiclnteger对象本身。
- var2该对象值得引用地址。
- var4需要变动的数量。
- var5是用过var1 var2找出的主内存中真实的值。
- 用该对象当前的值与var5比较:
- 如果相同,更新var5+var4并且返回true,如果不同,继续取值然后再比较,直到更新完成。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SzrBPlR3-1675601279758)(G:\图片缓存\image-20230205123202141.png)]
CAS: 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环
缺点:
- 循环会耗时
- 一次性只能保证一个共享变量的原子性
- 存在ABA问题
package com.asule.cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 12:08
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class CasDemo {
/**
* compareAndSet():比较并交换
* 期望、更新
* public final boolean compareAndSet(int expect, int update)
* @param args
*/
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//如果我期望的值达到了,那么就更新,否则就不更新,CAS是CPU的并发原语
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
atomicInteger.getAndIncrement();
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
18.2 ABA问题(狸猫换太子)
**问题1:**什么是悲观锁?
**回答1:**悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会一直阻塞,知道锁被上一个持有者释放
简介描述1: 共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程
例如1: Java中的ReentrantLock和synchroniezd等独占锁都是悲观锁的思想
应用场景1: 悲观锁通常多用于写比较多的情况下(多写场景),避免频繁失败和重试影响性能
**问题2:**什么是乐观锁?
回答1: 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停的执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其他线程修改了(具体方法可以使用版本号机制或CAS算法)
例如2: Java中的java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的
应用场景2: 乐观锁通常用于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量
**问题3:**如何实现乐观锁?
**回答3:**乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。
1.版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
**举一个简单的例子:**假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
- 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
- 操作员 A 完成了修改工作,将数据版本号( version=1 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,也将版本号( version=1 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。
2.CAS算法
CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。
原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。
CAS涉及到的三个操作数:
- V: 要更新的变量值(var)
- E: 预期值(Expected)
- N: 拟写入的新值(New)
==当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。==如果不等,说明已经有其它线程更新了V,则当前线程放弃更新。
**举一个简单的例子:**线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。
- i 与1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
- i 与1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及CPU都有关系。
**ABA问题:**如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
ABA 问题的解决思路: 是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 ==compareAndSet()==方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LVBs7i1P-1675601279759)(G:\图片缓存\image-20230205174638275.png)]
package com.asule.cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 12:08
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class CasDemo {
/**
* compareAndSet():比较并交换
* 期望、更新
* public final boolean compareAndSet(int expect, int update)
* @param args
*/
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//对于我们平时写的SQL:乐观锁!
//如果我期望的值达到了,那么就更新,否则就不更新,CAS是CPU的并发原语
//=====================捣乱的线程===================
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
//=====================期望的线程===================
System.out.println(atomicInteger.compareAndSet(2020, 6666));
System.out.println(atomicInteger.get());
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AEUafVD9-1675601279759)(G:\图片缓存\image-20230205174857663.png)]
**问题1:**循环时间长开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:
- 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
- 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。
问题2: 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。AtomicReference类把多个共享变量合并成一个共享变量来操作。
18.3 原子引用解决ABA问题
带版本号的原子操作
大坑:所有相同类型的包装类对象之间值的比较,全部使用equals方法比较
说明: 对于Integer var = ? 在==-128~127之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以使用等于符号()进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断
package com.asule.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 12:08
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class CasDemo {
/**
* compareAndSet():比较并交换
* 期望、更新
* public final boolean compareAndSet(int expect, int update)
*
* @param args
*/
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(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 2,
atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
System.out.println("a2=>" + atomicStampedReference.getStamp());
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=>" + atomicStampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicStampedReference.compareAndSet(1, 6, stamp, stamp + 1));
System.out.println("b1=>" + atomicStampedReference.getStamp());
}).start();
}
}
执行结果:
19.各种锁的理解
19.1 公平锁、非公平锁
- 公平锁:非常公平,不能插队,必须先来后到。
- 非公平锁:非常不公平,可以插队(synchronized和lock默认都是非公平锁)
//默认为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
19.2 可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
synchronized版
package com.asule.lock;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 20:09
* @Version: 1.0
* @Description:
*
* 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");
}
}
Lock版
package com.asule.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 20:12
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
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 synchronized void sms(){
//细节问题:有两把锁,第一把锁sms,第二把锁在call
//lock锁必须配对,否则就会死锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " sms");
call(); // 这里也有锁
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public synchronized void call(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " call");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
19.3自旋锁
package com.asule.lock;
import java.util.concurrent.atomic.AtomicReference;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 20:19
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
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)){
}
}
//解锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "====> myUnLock");
atomicReference.compareAndSet(thread,null);
}
}
package com.asule.lock;
import java.util.concurrent.TimeUnit;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 20:23
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
//ReentrantLock reentrantLock = new ReentrantLock();
//reentrantLock.lock();
//reentrantLock.unlock();
//底层使用CAS实现
SpinLockDemo lockDemo = new SpinLockDemo();
new Thread(()->{
lockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
lockDemo.myUnLock();
}
},"T1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
lockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
lockDemo.myUnLock();
}
},"T2").start();
}
}
执行结果:
结论: T2线程需要等待T1线程释放锁才能去获取锁,如果因为冲突失败就重试,直到成功为止
19.4死锁
**问题:**死锁是什么
死锁测试
package com.asule.lock;
import java.util.concurrent.TimeUnit;
/**
* 简要描述
*
* @Author: ASuLe
* @Date: 2023/2/5 20:36
* @Version: 1.0
* @Description: 文件作用详细描述....
*/
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new MyThread(lockA,lockB),"T1").start();
new Thread(new MyThread(lockB,lockA),"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);
}
}
}
}
执行结果:
解决问题:
- 使用jps-l定位进程号
2.使用jstack 进程号找到死锁问题
面试或者工作中排查问题
- 日志
- 堆栈信息