22275 字
60 分钟
JVM与JUC并发编程完全指南

1、JUC是什么#

java.util.concurrent在并发编程中使用的工具类#

  • concurrent 并发类
  • consurrent.atomic 并发原子类
  • concurrent.locks 并发锁类

image-20210616103514010

进程/线程回顾#

1进程/线程是什么?#

进程:进程(Process)是计算机中运行中的程序的实例。它是作为分配资源的基本单位。进程是线程的容器。

线程:线程是进程中的执行单元,是程序执行流的最小单元。通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。

2进程/线程例子?#

使用QQ,查看进程一定有一个QQ.exe的进程,我可以用qq和A文字聊天,和B视频聊天,给C传文件,给D发一段语言,QQ支持录入信息的搜索。

大四的时候写论文,用word写论文,同时用QQ音乐放音乐,同时用QQ聊天,多个进程。

word如没有保存,停电关机,再通电后打开word可以恢复之前未保存的文档,word也会检查你的拼写,两个线程:容灾备份,语法检查

3线程状态?#

  • 一般定义:新建-就绪-运行-阻塞-销毁
  • Java定义:新建-就绪-运行-等待-超时等待-销毁
Thread.State
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,(新建)
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,(准备就绪
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,(阻塞
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,(不见不散
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,(过时不候
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;(终结)
}

4 wait/sleep的区别?#

wait/sleep 功能都是当前线程暂停,有什么区别? wait放开手去睡,放开手里的锁 sleep握紧手去睡,醒了手里还有锁

记忆:

wait是重新排队,是要释放资源的锁。

sleep是睡觉,抱着锁睡。

5什么是并发?什么是并行?#

并发:同一时刻多个线程在访问同一个资源,多个线程对一个点 例子:小米9今天上午10点,限量抢购 春运抢票 电商秒杀… 并行:多项工作一起执行,之后再汇总 例子:泡方便面,电水壶烧水,一边撕调料倒入桶中

2、Lock接口#

复习Synchronized#

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象

下面我将使用代码示例来说明这三种情况:

  1. 普通同步方法,锁是当前实例对象:
public class Example {
public synchronized void synchronizedMethod() {
// 这里的锁是当前实例对象
// 执行操作
}
}
  1. 静态同步方法,锁是当前类的 Class 对象:
public class Example {
public static synchronized void synchronizedStaticMethod() {
// 这里的锁是当前类的 Class 对象
// 执行操作
}
}
  1. 同步方法块,锁是 Synchronized 括号里配置的对象:
public class Example {
private final Object lockObject = new Object();
public void synchronizedBlock() {
synchronized (lockObject) {
// 这里的锁是 Synchronized 括号里配置的对象,即 lockObject
// 执行操作
}
}
}

这三个例子分别展示了普通同步方法、静态同步方法和同步方法块的情况。在普通同步方法中,锁是当前实例对象;在静态同步方法中,锁是当前类的 Class 对象;在同步方法块中,锁是 Synchronized 括号里配置的对象。

这样的锁机制确保了在多线程环境中对共享资源的访问是同步的,避免了竞争条件和数据不一致的问题。

多线程编程模板上#

1、线程 操作 资源类

相信Thread对我们来说并不陌生,学java的时候都是入门的东西,我们一般使用thread都是new一个线程,然后调用start方法启动,使用start方法才真正实现了多线程运行,因为这个时候不用等待我们的run方法执行完成就可以继续执行下面的代码,这才叫多线程嘛!因为thread线程有5种状态,

创建-就绪-运行-阻塞-死亡这五种,

那么我们的start方法呢就是就绪这一步,因为这个时候我们的线程并没有立即的执行,而是得等待,等到我们的cpu有空闲的时候,才会执行线程里面的run方法,等run方法执行完了,线程就结束了。

2、高内聚低耦合

  • 耦合: 模块与模块之间的联系。

  • 内聚:一般指(东西聚集在一起)形成一个模块,例如方法,变量,对象,或者是功能模块。

  • 高内聚:尽可能的让一个模块内部的代码相关程度高,相互联系的紧密。模块内部的代码,相互之间的联系越强,内聚就越高, 模块的独立性就越好。 一个模块应该尽量去独立的完成一个功能!如果必须写另外的功能,建议拆分成多个模块,低内聚的代码,不好维护,代码也不够健壮。

  • 低耦合:尽可能的将每一个功能通过模块单独写出去 ,然后通过指定的接口来相互联系,模块与模块之间的关系越是紧密,独立性就越不好,改变一个模块可能会影响其他的模块。

实现步骤#

1、创建资源类

2、资源类里创建同步方法、同步代码块

例子卖票程序#

//资源类
@Slf4j
class Ticket{
private int number = 30;
public synchronized void saleTicket(){
if(number > 0){
log.info(Thread.currentThread().getName()+"卖出第:{}",number--,"还剩下:{}",number);
}
}
}
/**
* 题目: 三个售票员 卖出 30张票
*
*多线程编程的企业套路+模板
* 1、在高内聚低耦合的前提下: 线程 操作(对外暴露的调用方法) 资源类
*/
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.saleTicket();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.saleTicket();
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.saleTicket();
}
}
},"C").start();
}
}

Lock#

是什么#

image-20210616124754276

Lock接口的实现#

ReentrantLock可重入锁

image-20210616124848114

如何使用#

class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
synchronized与Lock的区别
两者区别:
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),
Lock需在finally中手工释放锁(unlock()方法释放锁),s否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。
如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

这句话描述了 Java 中的两种不同的锁机制:synchronized 和 Lock(java.util.concurrent 包中的锁),并总结了它们的特性。让我们逐一解释这些特性的含义:

  1. synchronized 的锁可重入、不可中断、非公平:

    • 可重入性(Reentrancy): 当一个线程获得了某个对象的锁后,它可以再次请求该对象的锁,而无需释放之前的锁。这是允许的,因为 Java 中的 synchronized 锁是可重入的,也就是说,同一个线程可以多次获取同一个锁,而不会出现死锁。
    • 不可中断性: synchronized 锁是不可中断的,意味着在等待获取锁的过程中,其他线程无法中断正在等待的线程。只有当等待获取锁的线程成功获得锁之后,或者它自己放弃等待,其他线程才有机会获得锁。
    • 非公平性: 默认情况下,synchronized 锁是非公平的,这意味着当多个线程竞争锁时,**不会按照线程等待的时间顺序来分配锁。**这可能导致某些线程一直无法获得锁,而其他线程则一直占用锁。
  2. Lock 的锁可重入、可判断、可公平:

    • 可重入性(Reentrancy): 类似于 synchronized,Lock 也支持可重入性,允许同一个线程多次获取同一个锁。
    • **可中断:**Lock接口的实现类ReentrantLock提供了一种可中断的锁特性,它允许线程在等待锁的过程中被中断,而不是一直等待下去。
    • 可公平性: Lock 可以被配置为公平锁或非公平锁。在公平锁模式下,线程将按照请求锁的顺序获取锁。在非公平锁模式下,与 synchronized 类似,锁将根据某些策略分配给等待的线程,不一定按照请求的顺序。

总之,这句话总结了 synchronized 和 Lock 锁的几个关键特性,包括可重入性、中断行为、公平性和不同线程竞争锁的策略。理解这些特性有助于你在编写多线程代码时选择适当的锁机制。

lock示例:#

//资源类
@Slf4j
class Ticket{
private int number = 30;
private final Lock lock = new ReentrantLock();
public void saleTicket(){
lock.lock();
try {
if(number > 0){
log.info(Thread.currentThread().getName()+"卖出第:{}",number--,"还剩下:{}",number);
}
}finally {
lock.unlock();
}
}
}
/**
* 题目: 三个售票员 卖出 30张票
*
*多线程编程的企业套路+模板
* 1、在高内聚低耦合的前提下: 线程 操作(对外暴露的调用方法) 资源类
*/
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.saleTicket();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.saleTicket();
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 40; i++) {
ticket.saleTicket();
}
}
},"C").start();
}
}

创建线程方式#

  • 继承Thread类
  • 实现Runnable接口
  • 使用Callable和Future
  • 使用线程池

继承Thread (不能这样写)#

public class SaleTicket extends Thread
java是单继承,资源宝贵,要用接口方式

new Thread() (不能这样写)#

Thread t1 = new Thread();
t1.start();

第三种#

Thread(Runnable target, String name)

image-20210616130626423

实现runnable方法#

新建类实现runnable接口#

class MyThread implements Runnable//新建类实现runnable接口
new Thread(new MyThread,...)
这种方法会新增类,有更好的方法

匿名内部类#

new Thread(new Runnable() {
@Override
public void run() {
}
}, "your thread name").start();
这种方法不需要创建新的类,可以new接口

lambda表达式#

new Thread(() -> {
}, "your thread name").start();
这种方法代码更简洁精炼
//资源类
@Slf4j
class Ticket{
private int number = 30;
private final Lock lock = new ReentrantLock();
public void saleTicket(){
lock.lock();
try {
if(number > 0){
log.info(Thread.currentThread().getName()+"卖出第:{}",number--,"还剩下:{}",number);
}
}finally {
lock.unlock();
}
}
}
/**
* 题目: 三个售票员 卖出 30张票
*
*多线程编程的企业套路+模板
* 1、在高内聚低耦合的前提下: 线程 操作(对外暴露的调用方法) 资源类
*/
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() ->{
for (int i = 0; i <40; i++) {
ticket.saleTicket();
}
},"A").start();
new Thread(() ->{
for (int i = 0; i <40; i++) {
ticket.saleTicket();
}
},"B").start();
}
}

3、Java8之lambda表达式复习#

lambda表达式#

什么是Lambda#

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码 像数据一样进行传递)。可以写出更简洁、更 灵活的代码。作为一种更紧凑的代码风格,使 Java的语言表达能力得到了提升。

Lambda 表达式在Java 语言中引入了一个新的语法元 素和操作符。这个操作符为 “->” , 该操作符被称 为 Lambda 操作符或剪头操作符。它将 Lambda 分为 两个部分: 左侧:指定了 Lambda 表达式需要的所有参数 右侧:指定了 Lambda 体,即 Lambda 表达式要执行 的功能

要求#

接口只有一个方法

lambda表达式,如果一个接口只有一个方法,我可以把方法名省略 Foo foo = () -> {System.out.println(“****hello lambda”);};

写法#

拷贝小括号(),写死右箭头->,落地大括号{…}

函数式接口#

lambda表达式,必须是函数式接口,必须只有一个方法 如果接口只有一个方法java默认它为函数式接口。 为了正确使用Lambda表达式,需要给接口加个注解:@FunctionalInterface 如有两个方法,立刻报错

Runnable接口为什么可以用lambda表达式?

接口里是否能有实现方法?#

default方法#

接口里在java8后允许有接口的实现,default方法默认实现 default int div(int x,int y) { return x/y; } 接口里default方法可以有几个? 多个

静态方法实现#

静态方法实现:接口新增 public static int sub(int x,int y){ return x-y; } 可以有几个? 多个 注意静态的叫类方法,能用foo去调吗?要改成Foo

代码#

package com.atguigu.thread;
@FunctionalInterface
interface Foo{
// public void sayHello() ;
// public void say886() ;
public int add(int x,int y);
default int div(int x,int y) {
return x/y;
}
public static int sub(int x,int y) {
return x-y;
}
}
/**
*
* @Description: Lambda Express-----> 函数式编程
* 1 拷贝小括号(形参列表),写死右箭头 ->,落地大括号 {方法实现}
* 2 有且只有一个public方法@FunctionalInterface注解增强定义
* 3 default方法默认实现
* 4 静态方法实现
*/
public class LambdaDemo
{
public static void main(String[] args)
{
// Foo foo = new Foo() {
// @Override
// public void sayHello() {
// System.out.println("Hello!!");
// }
//
// @Override
// public void say886() {
// // TODO Auto-generated method stub
//
// }
// };
// foo.sayHello();
// System.out.println("**=****=**==");
// foo = ()->{System.out.println("Hello!! lambda !!");};
// foo.sayHello();
Foo foo = (x,y)->{
System.out.println("Hello!! lambda !!");
return x+y;
};
int result = foo.add(3,5);
System.out.println("******result="+result);
System.out.println("******result div="+foo.div(10, 2));
System.out.println("******result sub="+Foo.sub(10, 2));
}
}

4、线程间通信#

面试题:两个线程打印#

两个线程,一个线程打印1-52,另一个打印字母A-Z 打印顺序为12A34B…5152Z, 要求用线程间通信

例子:NotifyWaitDemo#

线程间通信:

1、生产者+消费者

2、通知等待唤醒机制

多线程编程模板中#

1、判断

2、干活

3、通知

synchronized实现#

代码#

package com.atguigu.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareDataOne//资源类
{
private int number = 0;//初始值为零的一个变量
//为0时加1,不为0时进入等待
public synchronized void increment() throws InterruptedException
{
//1判断
if(number !=0 ) {
this.wait();
}
//2干活
++number;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3通知
this.notifyAll();
}
//为0时进入等待,不为0时减1
public synchronized void decrement() throws InterruptedException
{
// 1判断
if (number == 0) {
this.wait();
}
// 2干活
--number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
// 3通知
this.notifyAll();
}
}
/**
*
* @Description:
*现在两个线程,
* 可以操作初始值为零的一个变量,
* 实现一个线程对该变量加1,一个线程对该变量减1,
* 交替,来10轮。
* @author xialei
*
* * 笔记:Java里面如何进行工程级别的多线程编写
* 1 多线程变成模板(套路)-----上
* 1.1 线程 操作 资源类
* 1.2 高内聚 低耦合
* 2 多线程变成模板(套路)-----下
* 2.1 判断
* 2.2 干活
* 2.3 通知
*/
public class NotifyWaitDemoOne
{
public static void main(String[] args)
{
ShareDataOne sd = new ShareDataOne();
new Thread(() -> {
for (int i = 1; i < 10; i++) {
try {
sd.increment();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i < 10; i++) {
try {
sd.decrement();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, "B").start();
}
}
/*
* *
* 2 多线程变成模板(套路)-----下
* 2.1 判断
* 2.2 干活
* 2.3 通知
* 3 防止虚假唤醒用while
* 虚假唤醒(Spurious Wakeup)是指在多线程编程中,有时候线程可能会在没有显式调用 notify 或 notifyAll 的情况下被唤醒,
即使条件并未满足。
为了防止虚假唤醒,应该使用 while 循环来进行条件的检查,而不是使用 if 语句。
*
* */
class ShareDataOne {
private int number = 0;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void increment() throws InterruptedException {
lock.lock();
try {
while (number != 0) { // 使用while循环判断条件
condition.await();
}
++number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (number == 0) { // 使用while循环判断条件
condition.await();
}
--number;
System.out.println(Thread.currentThread().getName() + "\t" + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
}

Q&A#

这段代码中的this.wait();怎么解释,我理解的wait是释放锁,线程结束

你对 wait() 的理解有些不准确,我会为你解释一下。

在 Java 中,wait()Object 类的一个方法用于让线程等待,同时释放对象的监视器(锁)。当一个线程调用某个对象的 wait() 方法时,它会进入对象的等待集(Waiting Set)中,暂时放弃锁,允许其他线程获得该锁并继续执行。被放入等待集中的线程只有在其他线程调用相同对象的 notify()notifyAll() 方法时,才有机会被唤醒。

在你的代码中,this.wait() 的作用是使当前线程进入等待状态,同时释放了对象 this 的监视器。这样做的目的是为了在某些条件未满足的情况下,让线程等待,并允许其他线程执行。

具体解释你代码中的 this.wait()

  1. increment() 方法中的 number 不等于 0 时,线程进入等待状态并释放锁,允许其他线程执行。
  2. decrement() 方法中的 number 等于 0 时,线程进入等待状态并释放锁,允许其他线程执行。

wait() 并不是线程的结束,而是使线程暂时停止执行,直到被其他线程唤醒。线程会在某些条件满足时被唤醒,然后再次竞争锁,继续执行。通常在使用 wait() 时,要结合条件判断来使用,以避免虚假唤醒。

为什么会出现:操作系统或虚拟机可能会在没有通知的情况下唤醒其中一个线程。

在多线程环境中,操作系统和虚拟机可能会选择在没有明确的 notify() 调用的情况下唤醒线程,这是为了提高整体系统的性能和响应性。

这种行为涉及到操作系统和虚拟机内部的实现细节,具体原因如下:

  1. 系统调度策略: 操作系统负责调度线程以便它们在多核处理器上运行。系统可能会周期性地重新调度线程,以平衡负载或执行其他管理任务。在这个过程中,等待状态的线程可能被重新调度并唤醒。

  2. 虚拟机内部机制: 虚拟机为了提高性能,可能会实现一些内部机制来更好地响应线程的状态变化。这可能导致在没有明确的 notify() 调用的情况下唤醒线程。

为了应对这种情况,Java 中引入了等待状态的线程的经典解决方案,即在等待的时候使用 while 循环而不是 if 语句。这是为了防止虚假唤醒,即使在没有明确通知的情况下,线程也会在等待状态中醒来。使用 while 循环会在每次唤醒后重新检查条件,确保在真正满足条件之前线程不会误以为条件已经满足。

synchronized (lock) {
while (!condition) {
lock.wait();
}
// 执行操作
}

这样的做法可以防止因虚假唤醒而导致的条件错误判断。

换成4个线程#

换成4个线程会导致错误,虚假唤醒

原因:在java多线程判断时,不能用if,程序出事出在了判断上面, 突然有一添加的线程进到if了,突然中断了交出控制权, 没有进行验证,而是直接走下去了,加了两次,甚至多次

解决办法#

解决虚假唤醒:查看API,java.lang.Object

image-20210616154210661

中断和虚假唤醒是可能产生的,所以要用loop循环,if只判断一次,while是只要唤醒就要拉回来再判断一次。if换成while

代码#

package com.atguigu.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.omg.IOP.Codec;
class ShareData//资源类
{
private int number = 0;//初始值为零的一个变量
public synchronized void increment() throws InterruptedException
{
//判断
while(number!=0) {
this.wait();
}
//干活
++number;
System.out.println(Thread.currentThread().getName()+" \t "+number);
//通知
this.notifyAll();;
}
public synchronized void decrement() throws InterruptedException
{
//判断
while(number!=1) {
this.wait();
}
//干活
--number;
System.out.println(Thread.currentThread().getName()+" \t "+number);
//通知
this.notifyAll();
}
}
/**
*
* @Description:
*现在两个线程,
* 可以操作初始值为零的一个变量,
* 实现一个线程对该变量加1,一个线程对该变量减1,
* 交替,来10轮。
* @author xialei
*
* * 笔记:Java里面如何进行工程级别的多线程编写
* 1 多线程变成模板(套路)-----上
* 1.1 线程 操作 资源类
* 1.2 高内聚 低耦合
* 2 多线程变成模板(套路)-----下
* 2.1 判断
* 2.2 干活
* 2.3 通知
*/
public class NotifyWaitDemo
{
public static void main(String[] args)
{
ShareData sd = new ShareData();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
sd.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
sd.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
sd.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
sd.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}

原理图#

image-20210616154401776

多线程编程模板下#

1、注意多线程之间的虚假唤醒#

  • 虚假唤醒(Spurious Wakeup)是指在多线程编程中,有时候线程可能会在没有显式调用 notify 或 notifyAll 的情况下被唤醒,即使条件并未满足。 为了防止虚假唤醒,应该使用 while 循环来进行条件的检查,而不是使用 if 语句。

java8新版实现#

synchronized和locks对标实现#

image-20210616155048561

image-20210616155057983

Condition#

Condition:查看API,java.util.concurrent

image-20210616155145454

class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}

代码#

package com.atguigu.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.omg.IOP.Codec;
class ShareData//资源类
{
private int number = 0;//初始值为零的一个变量
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws InterruptedException
{
lock.lock();
try {
//判断
while(number!=0) {
condition.await();
}
//干活
++number;
System.out.println(Thread.currentThread().getName()+" \t "+number);
//通知
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException
{
lock.lock();
try {
//判断
while(number!=1) {
condition.await();
}
//干活
--number;
System.out.println(Thread.currentThread().getName()+" \t "+number);
//通知
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/*public synchronized void increment() throws InterruptedException
{
//判断
while(number!=0) {
this.wait();
}
//干活
++number;
System.out.println(Thread.currentThread().getName()+" \t "+number);
//通知
this.notifyAll();;
}
public synchronized void decrement() throws InterruptedException
{
//判断
while(number!=1) {
this.wait();
}
//干活
--number;
System.out.println(Thread.currentThread().getName()+" \t "+number);
//通知
this.notifyAll();
}*/
}
/**
*
* @Description:
*现在两个线程,
* 可以操作初始值为零的一个变量,
* 实现一个线程对该变量加1,一个线程对该变量减1,
* 交替,来10轮。
*
* * 笔记:Java里面如何进行工程级别的多线程编写
* 1 多线程变成模板(套路)-----上
* 1.1 线程 操作 资源类
* 1.2 高内聚 低耦合
* 2 多线程变成模板(套路)-----下
* 2.1 判断
* 2.2 干活
* 2.3 通知
*/
public class NotifyWaitDemo
{
public static void main(String[] args)
{
ShareData sd = new ShareData();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
sd.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
sd.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
sd.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
sd.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}

5、线程间定制化调用通信Condition#

例子:ThreadOrderAccess#

线程-调用-资源类#

判断-干活-通知#

1、有顺序通知,需要有标识位

2、有一个锁Lock,3把钥匙Condition

3、判断标志位

4、输出线程名+第几次+第几轮

5、修改标志位,通知下一个

代码#

package com.atguigu.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ShareResource
{
private int number = 1;//1:A 2:B 3:C
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5(int totalLoopNumber)
{
lock.lock();
try
{
//1 判断
while(number != 1)
{
//A 就要停止
c1.await();
}
//2 干活
for (int i = 1; i <=5; i++)
{
System.out.println(Thread.currentThread().getName()+"\t"+i+"\t totalLoopNumber: "+totalLoopNumber);
}
//3 通知
number = 2;
c2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10(int totalLoopNumber)
{
lock.lock();
try
{
//1 判断
while(number != 2)
{
//A 就要停止
c2.await();
}
//2 干活
for (int i = 1; i <=10; i++)
{
System.out.println(Thread.currentThread().getName()+"\t"+i+"\t totalLoopNumber: "+totalLoopNumber);
}
//3 通知
number = 3;
c3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15(int totalLoopNumber)
{
lock.lock();
try
{
//1 判断
while(number != 3)
{
//A 就要停止
c3.await();
}
//2 干活
for (int i = 1; i <=15; i++)
{
System.out.println(Thread.currentThread().getName()+"\t"+i+"\t totalLoopNumber: "+totalLoopNumber);
}
//3 通知
number = 1;
c1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
*
* @Description:
* 多线程之间按顺序调用,实现A->B->C
* 三个线程启动,要求如下:
*
* AA打印5次,BB打印10次,CC打印15次
* 接着
* AA打印5次,BB打印10次,CC打印15次
* ......来10轮
*
*/
public class ThreadOrderAccess
{
public static void main(String[] args)
{
ShareResource sr = new ShareResource();
new Thread(() -> {
for (int i = 1; i <=10; i++)
{
sr.print5(i);
}
}, "AA").start();
new Thread(() -> {
for (int i = 1; i <=10; i++)
{
sr.print10(i);
}
}, "BB").start();
new Thread(() -> {
for (int i = 1; i <=10; i++)
{
sr.print15(i);
}
}, "CC").start();
}
}

6、多线程锁#

例子:Lock_8

锁的8个问题#

8锁分析#

锁一、锁二

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了, 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

锁三、锁四

加个普通方法后发现和同步锁无关 换成两个对象后,不是同一把锁了,情况立刻变化。

锁五、锁六

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。 具体表现为以下3种形式。 对于普通同步方法,锁是当前实例对象。 对于静态同步方法,锁是当前类的Class对象。 对于同步方法块,锁是Synchonized括号里配置的对象

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。

也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁, 可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁, 所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

所有的静态同步方法用的也是同一把锁——类对象本身, 这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁, 而不管是同一个实例对象的静态同步方法之间, 还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

代码#

/**
* @author Guanghao Wei
* @create 2023-04-10 14:57
*/
class Phone {
public synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------sendEmail");
}
public synchronized void sendSMS() {
System.out.println("------sendSMS");
}
public void hello() {
System.out.println("------hello");
}
}
/**
* 现象描述:
* 1 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信 共用一个对象锁
* 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?---------先邮件,后短信 共用一个对象锁
* 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件
* 4. 有两部手机,请问先打印邮件还是短信? ----先短信后邮件 资源没有争抢,不是同一个对象锁
* 5. 有两个静态同步方法,一步手机, 请问先打印邮件还是短信?---------先邮件后短信 共用一个类锁
* 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? ----------先邮件后短信 共用一个类锁
* 7. 有一个静态同步方法 一个普通同步方法,请问先打印邮件还是短信? ---------先短信后邮件 一个用类锁一个用对象锁
* 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? -------先短信后邮件 一个类锁一个对象锁
*/
public class Lock8Demo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(() -> {
phone.sendEmail();
}, "a").start();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
phone.sendSMS();
}, "b").start();
}
}

结论:

  • 对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁--->实例对象本身
  • 对于静态同步方法,锁的时当前类的Class对象
  • 对于同步方法块,锁的时synchronized括号内的对象

7、NotSafeDemo#

需求#

请举例说明集合类是不安全的

证明集合不安全#

例子:NotSafeDemo#

public class NotSafeDemo {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i <30; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}

线程不安全错误#

java.util.ConcurrentModificationException

ArrayList在迭代的时候如果同时对其进行修改就会 抛出java.util.ConcurrentModificationException异常 并发修改异常

Q&A#

正在遍历的线程和添加元素的线程会同时操作同一个集合为什么就会报错?

当一个线程正在迭代(遍历)一个 ArrayList,而另一个线程同时修改这个 ArrayList 时,可能会出现以下情况:

  1. 迭代器失效: Java 的 ArrayList 使用迭代器来遍历集合,当集合发生结构性改变(元素增加或删除)时,**迭代器的状态会失效,因为迭代器无法预知集合的结构变化。**这就是为什么会抛出 ConcurrentModificationException 异常。

  2. 数据不一致: 当一个线程正在迭代 ArrayList 的同时,另一个线程进行元素的增加、删除等操作,可能会导致数据不一致性,从而影响迭代过程中获取的元素或迭代结束的判定条件。

  3. image-20230822115228132

    修改的数量和预期修改的数量不一致,直接抛出异常。

在arraylist源码中为什么会导致expectedModCount 和 modCount数量不一致,最后抛出并发修改异常?

ArrayList 的源码中,modCount 是一个用于记录对集合结构(元素的增加、删除等)进行修改的计数器。而 expectedModCount 是用于迭代器的快速失败机制,用来检测在迭代过程中是否有其他线程对集合进行了修改。

当你创建一个迭代器并开始遍历 ArrayList 时,expectedModCount 会被初始化为当前的 modCount 值。在每次调用迭代器的 next() 方法时,都会检查当前的 modCount 是否与初始化时的 expectedModCount 值相等,如果不相等,就会抛出 ConcurrentModificationException 异常。

这个机制旨在在迭代过程中,如果有其他线程对集合进行了结构性修改,就可以快速失败,避免不确定的结果和数据一致性问题。

导致 expectedModCountmodCount 数量不一致并抛出并发修改异常的情况可能是因为:

  1. 并发修改: 在你创建迭代器后,其他线程对 ArrayList 进行了结构性修改,比如添加或删除元素,从而增加了 modCount 的值,而 expectedModCount 仍保持初始值。

  2. 只读迭代器: 如果使用 Collections.unmodifiableList() 方法创建只读的迭代器,这时对 ArrayList 的修改会导致 modCount 值增加,而只读迭代器的 expectedModCount 保持不变,从而引发异常。

  3. 多线程修改: 在多线程环境下,一个线程进行迭代,另一个线程同时修改 ArrayList 的结构,导致 modCount 值发生变化。

总之,ArrayList 在迭代时使用 modCountexpectedModCount 机制来检测并发修改,以保证迭代过程中数据的一致性。当 expectedModCountmodCount 数量不一致时,就会抛出 ConcurrentModificationException 异常。这是一种快速失败机制,帮助发现并发修改问题。

原理#

List<String> list = new ArrayList<>();
for (int i = 0; i <30 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}

看ArrayList的源码

public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

checkForComodification

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

没有synchronized线程不安全

解决方案#

1、Vector#

List<String> list = new Vector<>();
看Vector的源码
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
有synchronized线程安全

2、Collections#

List list = Collections.synchronizedList(new ArrayList<>()); Collections提供了方法synchronizedList保证list是同步线程安全的

那HashMap,HashSet是线程安全的吗?也不是 所以有同样的线程安全方法

image-20210617101336152

3、写时复制#

image-20210617101409018

List<String> list = new CopyOnWriteArrayList<>();

写时复制#

不加锁性能提升出错误,加锁数据一致性能下降#

CopyOnWriteArrayList定义#

A thread-safe variant of ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array. CopyOnWriteArrayList是arraylist的一种线程安全变体, 其中所有可变操作(add、set等)都是通过生成底层数组的新副本来实现的。

举例:名单签到#

image-20210617103516026

CopyOnWrite理论#

/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();
}
}
CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,
而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容器Object[] newElements里添加元素。
添加元素后,再将原容器的引用指向新的容器setArray(newElements)。
这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,****读和写不同的容器。****

扩展类比#

HashSet#

Set set = new HashSet<>();//线程不安全

Set set = new CopyOnWriteArraySet<>();//线程安全

HashSet底层数据结构是什么? HashMap ?

但HashSet的add是放一个键值,value永远是一个常量值,而HashMap是放K、V键值对

public HashSet() { map = new HashMap<>(); }

private static final Object PRESENT = new Object();

public boolean add(E e) { return map.put(e, PRESENT)==null; }

HashMap#

Map<String,String> map = new HashMap<>();//线程不安全

Map<String,String> map = new ConcurrentHashMap<>();//线程安全

默认容量16,负载因子0.75,阈值12

hashmap原理:

底层是node节点的单向链表,node节点中包含有k v,当容量达到8会变成红黑树

集合比较:#

口诀:要有统统有,要无统统无

Vector 有序 有重复

arrylist 有序 有重复

LinkedList 有序 有重复

hashset 无序 无重复

hashMap 无序 无重复

TreeSet 有序 无重复

TreeMap 有序 无重复

扩容工具类:#

new HashMap 等价于 new HashMap(16,0.75)

底层数组长度 16*0.75=12,当超过12时,会自动扩容,hashMap扩容是扩一倍,arrayList扩容为原来的一半

Arrays.copyOf()

代码#

package com.atguigu.gmall.jucdemo;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 请举例说明集合类是不安全的
*/
public class NotSafeDemo {
public static void main(String[] args) {
Map<String,String> map = new ConcurrentHashMap<>();
for (int i = 0; i <30 ; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,8));
System.out.println(map);
},String.valueOf(i)).start();
}
}
private static void setNoSafe() {
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i <30 ; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(set);
},String.valueOf(i)).start();
}
}
private static void listNoSafe() {
// List<String> list = Arrays.asList("a","b","c");
// list.forEach(System.out::println);
//写时复制
List<String> list = new CopyOnWriteArrayList<>();
// new CopyOnWriteArrayList<>();
//Collections.synchronizedList(new ArrayList<>());
//new Vector<>();//new ArrayList<>();
for (int i = 0; i <30 ; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
/**
* 写时复制
CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,
而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后向新的容器Object[] newElements里添加元素。
添加元素后,再将原容器的引用指向新的容器setArray(newElements)。
这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
*
*
*
*
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();
}
}
*/

8、Callable接口#

是什么#

面试题:获得多线程的方法几种?

(1)继承thread类(2)runnable接口 如果只回答这两个你连被问到juc的机会都没有

正确答案如下: 传统的是继承thread类和实现runnable接口, java5以后又有实现callable接口和java的线程池获得

函数式接口

image-20210617113539751

这是一个函数式接口,因此可以用作lambda表达式或方法引用的赋值对象。

与runnable对比#

实现方法对比

// 创建新类MyThread实现runnable接口
class MyThread implements Runnable{
@Override
public void run() {
}
}
//新类MyThread2实现callable接口
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 200;
}
}
面试题:callable接口与runnable接口的区别?
答: (1)是否有返回值
2)是否抛异常
3)落地方法不一样,一个是run,一个是call
由于callable是新增的方法,所以有返回值,可以抛出异常

怎么用#

直接替换runnable是否可行?

image-20210617114251147

不可行,因为:thread类的构造方法根本没有Callable

image-20210617114305853

认识不同的人找中间人

这像认识一个不认识的同学,我可以找中间人介绍。 中间人是什么?java多态,一个类可以实现多个接口!!

image-20210617114329332

FutureTask ft = new FutureTask(new MyThread()); new Thread(ft, “AA”).start();

运行成功后如何获得返回值?

image-20210617114356561

​ ft.get();

FutureTask#

是什么#

未来的任务,用它就干一件事,异步调用 main方法就像一个冰糖葫芦,一个个方法由main串起来。 但解决不了一个问题:正常调用挂起堵塞问题

image-20210617115426817

例子: (1)老师上着课,口渴了,去买水不合适,讲课线程继续,我可以单起个线程找班长帮忙买水, 水买回来了放桌上,我需要的时候再去get。 (2)4个同学,A算1+20,B算21+30,C算31*到40,D算41+50,是不是C的计算量有点大啊, FutureTask单起个线程给C计算,我先汇总ABD,最后等C计算完了再汇总C,拿到最终结果 (3)高考:会做的先做,不会的放在后面做

原理#

在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成, 当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。

一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。

仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成, 就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态, 然后会返回结果或者抛出异常。

同一个FutureTask只计算一次 get方法放到最后

class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"买水");
TimeUnit.SECONDS.sleep(4);
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(Thread.currentThread().getName()+"准备上课");
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
FutureTask<Integer> futureTaskB = new FutureTask<>(new MyThread());
new Thread(futureTask,"A线程").start();
System.out.println(Thread.currentThread().getName()+"直接上课");
System.out.println(futureTask.get());
}
}

代码#

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
class MyThread implements Runnable{
@Override
public void run() {
}
}
class MyThread2 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+"come in callable");
return 200;
}
}
public class CallableDemo {
public static void main(String[] args) throws Exception {
//FutureTask<Integer> futureTask = new FutureTask(new MyThread2());
FutureTask<Integer> futureTask = new FutureTask(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 1024;
});
FutureTask<Integer> futureTask2 = new FutureTask(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
TimeUnit.SECONDS.sleep(4);
return 2048;
});
new Thread(futureTask,"zhang3").start();
new Thread(futureTask2,"li4").start();
//System.out.println(futureTask.get());
//System.out.println(futureTask2.get());
//1、一般放在程序后面,直接获取结果
//2、只会计算结果一次
while(!futureTask.isDone()){
System.out.println("***wait");
}
System.out.println(futureTask.get());
System.out.println(Thread.currentThread().getName()+" come over");
}
}
/**
*
*
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,
当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,
就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,
然后会返回结果或者抛出异常。
**只计算一次**
get方法放到最后
*/

9、JUC强大的辅助类讲解#

CountDownLatch(减法)#

减少计数 控制线程顺序

例子:CountDownLatchDemo#

原理#

  • CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
  • 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
  • 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。

代码#

package com.atguigu.thread;
import java.util.concurrent.CountDownLatch;
/**
*
* @Description:
* *让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒。
*
* CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
* 其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
* 当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。
*
* 解释:6个同学陆续离开教室后值班同学才可以关门。
*
* main主线程必须要等前面6个线程完成全部工作后,自己才能开干
*/
public class CountDownLatchDemo
{
public static void main(String[] args) throws InterruptedException
{
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6; i++) //6个上自习的同学,各自离开教室的时间不一致
{
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t 号同学离开教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t****** 班长关门走人,main线程是班长");
}
}

如果不使用countdownLatch的传统写法

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
AtomicInteger num = new AtomicInteger(6);
for (int i = 1; i <= num.get(); i++) {
new Thread(() -> {
lock.lock();
try {
// 1 判断 这里判断在这代码中可以省略
while (num.get() == 0) {
// A 就要停止
condition.await();
}
System.out.println(Thread.currentThread().getName() + "\t 号同学离开教室");
num.getAndDecrement();
condition.signal();//一定需要通知
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}, String.valueOf(i)).start();
}
lock.lock();
try {
// 1 判断
while (num.get() != 0) {
// A 就要停止
condition.await();
}
// 2 干活
System.out.println(Thread.currentThread().getName() + "\t****** 班长关门走人,main线程是班长");
// 3 通知
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

CyclicBarrier(加法)#

循环栅栏 做加法,集齐七颗龙珠召唤神龙

例子:CyclicBarrierDemo#

原理#

  • CyclicBarrier
  • 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,
  • 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
  • 直到最后一个线程到达屏障时,屏障才会开门,所有
  • 被屏障拦截的线程才会继续干活。
  • 线程进入屏障通过CyclicBarrier的await()方法。

代码#

package com.atguigu.thread;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
*
*
* CyclicBarrier
* 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,
* 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,
* 直到最后一个线程到达屏障时,屏障才会开门,所有
* 被屏障拦截的线程才会继续干活。
* 线程进入屏障通过CyclicBarrier的await()方法。
*
* 集齐7颗龙珠就可以召唤神龙
*/
public class CyclicBarrierDemo
{
private static final int NUMBER = 7;
public static void main(String[] args)
{
//CyclicBarrier(int parties, Runnable barrierAction)
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, ()->{System.out.println("*****集齐7颗龙珠就可以召唤神龙");}) ;
for (int i = 1; i <= 7; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+"\t 星龙珠被收集 ");
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}

Semaphore(控制多线程的并发数)#

信号灯 争车位(控制多线程的并发数)

例子:SemaphoreDemo#

原理#

在信号量上我们定义两种操作:

  • acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),
  • 要么一直等下去,直到有线程释放信号量,或超时。
  • release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
  • 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

代码#

package com.atguigu.thread;
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
*
* @Description: TODO(这里用一句话描述这个类的作用)
*
* 在信号量上我们定义两种操作:
* acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),
* 要么一直等下去,直到有线程释放信号量,或超时。
* release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
*
* 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
*/
public class SemaphoreDemo
{
public static void main(String[] args)
{
Semaphore semaphore = new Semaphore(3);//模拟3个停车位
for (int i = 1; i <=6; i++) //模拟6部汽车
{
new Thread(() -> {
try
{
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t 抢到了车位");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+"\t------- 离开");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
}

10、ReentrantReadWriteLock#

读写锁

口诀:读读可共享,写读写写要独占

例子:ReadWriteLockDemo#

类似案例#

红蜘蛛#

缓存#

问题例子#

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/*
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行
但是
如果有一个线程想去写共享资源来,就不应该再有其他线程可以对该资源镜像读或写
小总结:
读-读 能共存
读-写 不能共存
写-写 不能共存
*/
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+"\t 正在写"+key);
//暂停一会儿线程
try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace(); }
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t 写完了"+key);
}
public Object get(String key){
Object result = null;
System.out.println(Thread.currentThread().getName()+"\t 正在读"+key);
try {TimeUnit.MILLISECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace(); }
result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t 读完了"+result);
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}

代码#

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在写" + key);
//暂停一会儿线程
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写完了" + key);
System.out.println();
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
public Object get(String key) {
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读" + key);
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 读完了" + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
myCache.put(num + "", num + "");
}, String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
myCache.get(num + "");
}, String.valueOf(i)).start();
}
}
}

11、BlockingQueueDemo#

阻塞队列

例子:BlockingQueueDemo#

栈与队列#

栈:先进后出,后进先出

队列:先进先出

阻塞队列#

阻塞:必须要阻塞/不得不阻塞 阻塞队列是一个队列,在数据结构中起的作用如下图:

image-20210617150833920

线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素

当队列是空的,从队列中获取元素的操作将会被阻塞 当队列是满的,从队列中添加元素的操作将会被阻塞

试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素

阻塞队列的用处#

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起

为什么需要BlockingQueue 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了

在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

架构梳理、种类分析#

架构介绍#

image-20210617150953505

种类分析#

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。(重要)

  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。(重要)

  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。

  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列。

  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。(重要)

  • LinkedTransferQueue:由链表组成的无界阻塞队列。

  • LinkedBlockingDeque:由链表组成的双向阻塞队列。

BlockingQueue核心方法#

image-20210617151713123

  • 抛出异常

    当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException full 当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException

  • 特殊值

    插入方法,成功ture失败false

    移除方法,成功返回出队列的元素,队列里没有就返回null

  • 一直阻塞

    当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用

  • 超时退出

    当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出

代码#

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 阻塞队列
*/
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// List list = new ArrayList();
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
//第一组
// System.out.println(blockingQueue.add("a"));
// System.out.println(blockingQueue.add("b"));
// System.out.println(blockingQueue.add("c"));
// System.out.println(blockingQueue.element());
//System.out.println(blockingQueue.add("x"));
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// 第二组
// System.out.println(blockingQueue.offer("a"));
// System.out.println(blockingQueue.offer("b"));
// System.out.println(blockingQueue.offer("c"));
// System.out.println(blockingQueue.offer("x"));
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// 第三组
// blockingQueue.put("a");
// blockingQueue.put("b");
// blockingQueue.put("c");
// //blockingQueue.put("x");
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// System.out.println(blockingQueue.take());
// 第四组
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.offer("a",3L, TimeUnit.SECONDS));
}
}

12、ThreadPool线程池#

例子:MyThreadPoolDemo#

为什么用线程池#

例子: 10年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球,CPU需要来回切换。 现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。

线程池的优势: 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用;控制最大并发数;管理线程。

第一资源:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。 第二速度:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。 第三管理:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池如何使用#

架构说明#

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类

image-20210617162548001

编码实现#

Executors.newFixedThreadPool(int)

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue

Executors.newSingleThreadExecutor()

public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue

Executors.newCachedThreadPool()

执行很多短期异步任务,线程池根据需要创建新线程, 但在先前构建的线程可用时将重用它们。可扩容,遇强则强

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//newCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQueue,
//也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

ThreadPoolExecutor底层原理#

image-20210617162712752

线程池几个重要参数#

7大参数#

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue ** null || threadFactory ** null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
  • 1、corePoolSize:线程池中的常驻核心线程数
  • 2、maximumPoolSize:线程池中能够容纳同时 执行的最大线程数,此值必须大于等于1
  • 3、keepAliveTime:多余的空闲线程的存活时间 当前池中线程数量超过corePoolSize时,当空闲时间 达到keepAliveTime时,多余线程会被销毁直到 只剩下corePoolSize个线程为止
  • 4、unit:keepAliveTime的单位

​ 3、4连在一起的

  • 5、workQueue:任务队列,被提交但尚未被执行的任务
  • 6、threadFactory:表示生成线程池中工作线程的线程工厂, 用于创建线程,一般默认的即可
  • 7、handler:拒绝策略,表示当队列满了,并且工作线程大于 等于线程池的最大线程数(maximumPoolSize)时如何来拒绝 请求执行的runnable的策略

线程池底层工作原理#

image-20210617162439186

以下重要:以下重要:以下重要:以下重要:以下重要:以下重要:
* 1、在创建了线程池后,开始等待请求。
* 2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
* 3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
* 4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

任务队列和最大线程数之间存在密切的关系,它们共同决定了线程池的行为和性能。任务队列是用来存放待执行的任务的缓冲区,最大线程数则决定了线程池能够容纳的最大线程数量。下面详细说明它们之间的前后关系:

  1. 任务队列与核心线程数: 首先,任务队列对核心线程数的影响。当有新任务提交给线程池时,如果线程池中的线程数量还未达到核心线程数(corePoolSize),新任务会直接创建一个新的核心线程来执行任务,不会放入任务队列。这有助于快速响应任务,避免任务等待。

  2. 任务队列与最大线程数: 当线程池中的线程数量已达到核心线程数,而新任务继续提交时,这时任务就会被放入任务队列。当任务队列已满时,根据最大线程数(maximumPoolSize)的设置,线程池的行为会有所不同:

    • 如果线程池中的线程数还未达到最大线程数,那么会创建新的非核心线程来执行任务,直到达到最大线程数。
    • 如果线程池中的线程数已经达到最大线程数,这时新提交的任务就会被拒绝执行,根据所设置的拒绝策略来处理。

任务队列和最大线程数的关系影响了线程池的并发度和资源利用率:

  • 如果任务队列容量足够大,即使最大线程数设置较小,线程池也可以在有限的线程数下缓冲一定数量的任务,避免过多的线程创建和销毁,提高资源利用率。
  • 如果任务队列容量较小,且最大线程数设置较大,线程池可能在任务队列满了之后继续创建新的线程,以处理更多的任务,但同时可能会增加上下文切换开销。

在实际应用中,合理配置任务队列和最大线程数非常重要,需要根据应用的性质、系统资源和负载情况来调整,以达到最佳的性能和资源利用率。

线程池用哪个?生产中如设置合理参数#

线程池的拒绝策略#

是什么#

  • 条件一:等待队列已经排满了,再也塞不下新任务了 同时,
  • 条件二:线程池中的max线程也达到了,无法继续为新任务服务。

这个是时候我们就需要拒绝策略机制合理的处理这个问题。

JDK内置的拒绝策略#

  • AbortPolicy(默认)(中止策略):直接抛出RejectedExecutionException异常阻止系统正常运行

在拒绝策略中,AbortPolicy 是默认的策略。这种策略的实际效果是,如果任务队列已满,而线程池的线程数量也达到了最大线程数限制,那么任何新提交的任务都会被拒绝,不会执行。同时,线程池会抛出一个 RejectedExecutionException 异常,以通知调用者有任务被拒绝执行。

  • CallerRunsPolicy**(回退调用者策略)**:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不 会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。(回退)
  • DiscardOldestPolicy**(丢弃最旧策略)**:抛弃队列中等待最久的任务,然后把当前任务加人队列中 尝试再次提交当前任务。
  • DiscardPolicy**(丢弃策略)**:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。 如果允许任务丢失,这是最好的一种策略。

以上内置拒绝策略均实现了RejectedExecutionHandle接口#

在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?超级大坑#

答案是一个都不用,我们工作中只能使用自定义的

Executors中JDK已经给你提供了,为什么不用?

image-20210617171050341

在工作中如何使用线程池,是否自定义过线程池#

cpu密集型的计算工作

cup核数+1;设置到maximumPoolSize上

Runtime.getRuntime().availableProcessors(); //Java获取系统cpu核数

io密集型的计算工作

麻烦

代码

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
/**
* 线程池
* Arrays
* Collections
* Executors
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
//new ThreadPoolExecutor.AbortPolicy()
//new ThreadPoolExecutor.CallerRunsPolicy()
//new ThreadPoolExecutor.DiscardOldestPolicy()
new ThreadPoolExecutor.DiscardOldestPolicy()
);
//10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
private static void threadPool() {
//List list = new ArrayList();
//List list = Arrays.asList("a","b");
//固定数的线程池,一池五线程
// ExecutorService threadPool = Executors.newFixedThreadPool(5); //一个银行网点,5个受理业务的窗口
// ExecutorService threadPool = Executors.newSingleThreadExecutor(); //一个银行网点,1个受理业务的窗口
ExecutorService threadPool = Executors.newCachedThreadPool(); //一个银行网点,可扩展受理业务的窗口
//10个顾客请求
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}

线程池实际使用#

1、当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。 2、当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 3、当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务 4、当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理 5、当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程 6、当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭

可以看出,allowCoreThreadTimeOut这个方法就像其字面的意思一样,允许Core Thread超时后可以关闭。

书上说了,要想使线程池没有任务时销毁所有的进程,需要启用allowCoreThreadTimeOut(true)同时将core size设置为0,而实际上,core size设置成任意一个正数值就可以,设置成0时,加不加allowCoreThreadTimeOut(true)都没有影响,因为这个方法是对core thread产生影响,但此时core thread为0,而且当新任务进来时,必须等到workQueue满时才会创建新线程,这也不是我们想要的结果。

结论:allowCoreThreadTimeOut 保持默认设置

  • 创建线程池对象
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Bean
public ThreadPoolTaskExecutor getThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//必须根据线上核心数来设置,例如2核,必须写2,否则报错
//executor.setCorePoolSize(4); // 不推荐写法
/**
以下为推荐写法
*Runtime.getRuntime().availableProcessors()返回的是可用的计算资源,而不是CPU物理核心数,
*对于支持超线程的CPU来说,单个物理处理器相当于拥有两个逻辑处理器,能够同时执行两个线程。
*/
int processors = Runtime.getRuntime().availableProcessors();
executor.setCorePoolSize(processors);
//cup核数+1;设置到maximumPoolSize上,最大核心线程池的线程数必须大于核心线程池线程数,否则非法参数异常
executor.setMaxPoolSize(processors+1);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("Pool-A");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
  • 使用线程池
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Autowired
private ThreadPoolTaskExecutor executorService;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
SendResult sendResult = sendMessage(topic, tag, finalTradePay.getPayId(), JSON.toJSONString(finalTradePay));
log.info(JSON.toJSONString(sendResult));
if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
mqProducerTempMapper.deleteByPrimaryKey(mqProducerTemp.getId());
System.out.println("删除消息表成功");
}
} catch (Exception e) {
e.printStackTrace();
}
}
});

13、Java8之流式计算复习#

例子:StreamDemo#

函数式接口#

java.util.function#

image-20210617155932383

java内置核心四大函数式接口#

image-20210617155941135

实例#

//R apply(T t);函数型接口,一个参数,一个返回值
Function<String,Integer> function = t ->{return t.length();};
System.out.println(function.apply("abcd"));
//boolean test(T t);断定型接口,一个参数,返回boolean
Predicate<String> predicate = t->{return t.startsWith("a");};
System.out.println(predicate.test("a"));
// void accept(T t);消费型接口,一个参数,没有返回值
Consumer<String> consumer = t->{
System.out.println(t);
};
consumer.accept("javaXXXX");
//T get(); 供给型接口,无参数,有返回值
Supplier<String> supplier =()->{return UUID.randomUUID().toString();};
System.out.println(supplier.get());

Stream流#

What#

流(Stream) 到底是什么呢? 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。 “集合讲的是数据,流讲的是计算!”

Why#

特点

Stream 自己不会存储元素

Stream 不会改变源对象。相反, 他们会返回一个持有结果的新Stream。

Stream 操作是延迟执行的。这意味着 他们会等到需要结果的时候才执行。

How#

阶段

  • 创建一个Stream:一个数据源(数组、集合)

  • 中间操作:一个中间操作,处理数据源数据

  • 终止操作:一个终止操作,执行中间操作链,产生结果

源头=>中间流水线=>结果

package com.atguigu.juc.study;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
class User
{
private Integer id;
private String userName;
private int age;
}
/**
* @create 2019-02-26 22:24
*
* 题目:请按照给出数据,找出同时满足
* 偶数ID且年龄大于24且用户名转为大写且用户名字母倒排序
* 最后只输出一个用户名字
*/
public class StreamDemo
{
public static void main(String[] args)
{
User u1 = new User(11,"a",23);
User u2 = new User(12,"b",24);
User u3 = new User(13,"c",22);
User u4 = new User(14,"d",28);
User u5 = new User(16,"e",26);
List<User> list = Arrays.asList(u1,u2,u3,u4,u5);
list.stream().filter(p -> {
return p.getId() % 2 == 0;
}).filter(p -> {
return p.getAge() > 24;
}).map(f -> {
return f.getUserName().toUpperCase();
}).sorted((o1, o2) -> {
return o2.compareTo(o1);
}).limit(1).forEach(System.out::println);
// R apply(T t);
Function<String,Integer> function = t -> {return t.length();};
System.out.println(function.apply("abc"));
// boolean test(T t);
Predicate<String> predicate = t -> {return t.startsWith("a");};
System.out.println(predicate.test("a"));
//void accept(T t);
Consumer<String> consumer = t -> {System.out.println(t);};
consumer.accept("java1018");
// T get();
Supplier<String> supplier = () -> {return UUID.randomUUID().toString();};
System.out.println(supplier.get());;
}
}

=================================================================================****=====

变量理解#

1、类中的声明位置分:

  • 局部变量 ---必须人工赋初始化值

  • 成员变量(或属性)

    类变量(静态的)---会有默认值

    实例变量(非静态的)---会有默认值

2、数据类型分:基本数据变量(8种) vs 引用类型变量(类、数组、接口、字符串)

成员变量和局部变量都可以是基本数据类型或引用类型,它们的存储位置和生命周期不同。

JVM#

常见面试:

  • 请谈谈你对JVM的理解?java8的虚拟机有什么更新?
  • 什么是OOM?什么是StackOverflowError?有哪些方法分析?
  • JVM的常用参数调优你知道哪些?
  • 谈谈JVM中,对类加载器你的认识?

1、JVM体系结构概述#

1.1、JVM位置#

JVM是运行在操作系统之上的,它与硬件没有直接的交互

image-20210618091536765

1.2、jvm体系结构概览#

亮色:线程共享、存在垃圾回收

灰色:与之相反

image-20210618091611260

1.3、类装载器ClassLoader#

​ 负责加载class文件,class文件在文件开头有特定的文件标示(cafe.babe),将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定 。

echo %JAVA_HOME%

echo %PATH%

echo %CLASSPATH%

image-20210618091749202

1.4、类装载器ClassLoader2#

虚拟机自带的加载器

  • 启动类加载器(Bootstrap)C++,(通常是由C/C++编写的,初始化JVM运行时环境所需的类。)

  • 扩展类加载器(Extension)Java (加载javax的扩展包)

  • 应用程序类加载器(AppClassLoader)Java也叫(系统类加载器),加载(当前应用的classpath的所有类)

  • 用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式

启动-》扩展-》系统-》自定义

public class MyObj {
public static void main(String[] args) {
Object o = new Object();//是系统自带(根加载器一开始就会加载jdk自带的类)
//o.getClass() 获取到模板,o.getClass().getClassLoader() 获取到根加载器
System.out.println(o.getClass().getClassLoader());//应该是根加载器(c++写的),打印null
MyObj myObj = new MyObj();
System.out.println(myObj.getClass().getClassLoader());//(应用程序类加载器)sun.misc.Launcher$AppClassLoader@14dad5dc
System.out.println(myObj.getClass().getClassLoader().getParent().getParent());
//null 根加载器
System.out.println(myObj.getClass().getClassLoader().getParent());
//sun.misc.Launcher$ExtClassLoader@3a71f4dd 扩展加载器
System.out.println(myObj.getClass().getClassLoader());
//sun.misc.Launcher$AppClassLoader@14dad5dc 应用程序加载器
}
}

1.5、类装载器ClassLoader3#

image-20210618095259389

  • Code案例

  • sun.misc.Launcher 它是一个java虚拟机的入口应用

1.6、双亲委派#

解释:当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

好处:采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,**不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。 **

理由:沙箱安全机制

1.7、Execution Engine执行引擎负责解释命令,提交操作系统执行。#

1.8、Native Interface本地接口#

​ 本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。 ​ 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等,不多做介绍。

  • 本地操作系统
  • 第三方库

1.9、Native Method Stack本地方法栈#

它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。

  • 与 Java 虚拟机栈类似,但用于执行 Native 方法(即使用本地语言如 C/C++ 编写的方法)。
  • Native 方法可能涉及操作系统和硬件资源,因此需要单独的栈空间。

1.10 程序计数器(Program Counter Register)#

一句话:就是一个指针,这个指令执行完成后,接下来执行什么

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

如果执行的是一个Native方法,那这个计数器是空的。

用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误

  • 存放当前线程执行的字节码指令的地址。
  • 用于线程切换、分支、循环、异常处理等。
  • 不会出现内存溢出问题。
  • 每个线程独有,不会共享。

1.11 Method Area 方法区#

  • 各线程共享的运行时内存区域。

  • 它存储了每一个类的结构信息(模板,大Class),例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。

  • 上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。

    eg:

    方法区 f = new 永久代;(Java1.7)

    方法区 f = new 元空间; 规范和具体实现(Java1.8后)

    But

  • 实例变量存在堆内存中,和方法区无关

  • 存放类的元数据(是描述类的数据结构和信息的数据)、静态变量、常量、方法信息等。

  • 也称为永久代(Permanent Generation),但在 Java 8 后逐步被元空间(Metaspace)取代。

  • 可以通过 -XX:MaxMetaspaceSize 参数来调整元空间的大小。

  • 会出现永久代溢出或元空间溢出的问题。

  • 方法区是 Java 虚拟机规范中的一个概念,就像是一个接口吧;
  • 永久代是 HotSpot 虚拟机中对方法的一个实现,就像是接口的实现类;
  • Java 8 的时候,移除了永久代,取而代之的是元空间,是方法区的另外一个实现。
  • 在 Java 8 之前,字符串常量池在永久代中。

    image-20240322140639098

    • Java 8 之后,移除了永久代,字符串常量池就移到了堆中。

      image-20240322140656899

1.12 Stack栈#

栈管运行,堆管存储

程序 = 算法 + 数据结构(学术)

程序 = 框架 + 业务逻辑 (工作)

栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。

java 方法 = 栈帧

栈保存什么东西?

  • 存放方法的局部变量(8种基本数据类型+引用数据类型)、操作数栈、动态链接、方法出口等。
  • 每个方法在执行时都会创建一个栈帧,方法的调用和返回就是通过栈帧来管理的。
  • 有栈帧大小的限制,如果超出会抛出 StackOverflowError 异常。
  • 线程私有,生命周期与线程相同。

image-20230822165844791

局部变量表

在局部变量表里,

32位以内的类型只占用一个slot(包括reference、returnAddress类型),

64位的类型(long和double)占用两个slot。 byte 、 short 、 char在存储前被转换为int,boolean也被转换为int,0表示false ;非0 表示true。 long和double 则占据两个slot。

image-20230822170012474

image-20230822171012463

操作数栈的压栈操作

image-20230822170130781

操作数栈的存入局部变量表

如果是引用类型就存入对象的地址

image-20230822170230073

栈运行原理

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中, A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈, B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈, …… 执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……

遵循“先进后出”/“后进先出”原则。

每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间,与等于1Mb左右。

栈加载顺序

image-20210618111902399

  • 图示在一个栈中有两个栈帧:

    栈帧 2是最先被调用的方法,先入栈,

    然后方法 2 又调用了方法1,栈帧 1处于栈顶的位置,

    栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1和栈帧 2,

    线程结束,栈释放。

  • 每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕 后会自动将此栈帧出栈。

Exception in thread “main” java.lang.StackOverflowError (不是异常,是错误)

public class JvmNote {
public static void m1(){
m1();
}
public static void main(String[] args) {
System.out.println("111");
m1();
System.out.println("444");
}
}

image-20210618112720388

image-20210618112222027

1.14 堆#

  • 存放对象实例及数组,由垃圾回收器进行管理和回收。
  • 分为新生代(Young Generation)和老年代(Old Generation)。
  • 可以通过 -Xmx-Xms 参数来调整堆的大小。
  • 堆是 JVM 中最大的一块内存区域,也是垃圾回收主要关注的区域。

1.13 栈+堆+方法区的交互关系#

image-20210618112827116

HotSpot(jdk名字)是使用指针的方式来访问对象: Java堆中会存放访问类元数据的地址, reference存储的就直接是对象的地址

Q&A#

交互关系中能不能将所有的细节都讲出来,包含所有静态和非静态方法、常量,字符串,对象,对象的引用,数组,基本数据类型,静态和非静态方法等等所有,分别存在哪里如何相互关联。

当 Java 程序运行时,栈、堆和方法区之间相互关联,支持方法调用、内存分配和数据存储。以下是详细的交互关系,涵盖了静态和非静态方法、常量、字符串、对象、对象引用、数组、基本数据类型等各个方面:

  1. 常量池中的字符串:
    • 字符串常量(例如:“Hello”)存储在方法区的运行时常量池中,这是方法区的一部分。
    • 运行时常量池是方法区的一部分,包含了类的元数据、符号引用等信息。
  2. 类的加载和方法调用:
    • 类的元数据存储在方法区,包括类的结构、字段、方法信息等。
    • 当类被加载和解析时,相关信息存储在方法区,方法的字节码也存储在方法区。
    • 调用静态方法时,方法字节码会被加载到方法区并执行,调用非静态方法时,对象引用和方法调用参数存储在栈上。
  3. 方法调用和栈帧:
    • 当方法被调用时,栈帧会被创建在栈上,包含方法的局部变量表、操作数栈、动态链接等。
    • 方法的参数、局部变量和临时数据存储在栈帧的局部变量表和操作数栈中。
  4. 局部变量和基本数据类型:
    • 方法的局部变量,包括基本数据类型(如 int、double)和对象引用,存储在栈帧的局部变量表中。
  5. 对象实例和引用:
    • 对象实例存储在堆中,堆中包含了 Java 对象的数据。
    • 对象引用,即对象的引用变量,可以存储在栈帧的局部变量表中。
  6. 数组:
    • 数组是对象的一种形式,数组对象存储在堆中。
    • 数组引用可以存储在栈帧的局部变量表中。
  7. 实例方法和静态方法:
    • 实例方法的调用需要实例对象引用,引用存储在栈帧的局部变量表中。
    • 静态方法的调用直接使用类名调用,方法字节码在方法区加载并执行。

总结起来,栈用于方法调用的数据管理,包括方法参数、局部变量和方法调用过程中的数据。堆用于存放对象实例,包括类的实例和数组。方法区用于存放类的元数据、静态变量、方法字节码等信息。这些内存区域的相互关联和协作支持了 Java 程序的执行和内存管理。

我有一个对象person具有age=12和name=‘tom’,这个数据如何存储在jvm中?

在 JVM 中,一个对象的数据(即对象的实例变量)会存储在堆内存中。对于一个名为 Person 的类的对象,具有 agename 两个实例变量,假设 age 是整数类型,name 是字符串类型。下面是该对象在 JVM 中的存储示意:

  1. 堆内存中的对象实例:
    • 对象的实例数据存储在堆内存中。Person 类的对象实例会占用一块堆内存空间,其中包含了 agename 两个实例变量。
    • age 基本数据类型 int 的值 12 存储在栈内存中,因为它是一个原始数据类型,它的值直接存储在变量的栈帧中。
    • name 变量会存储指向字符串 “tom” 的引用。
    • 这些实例变量的数据存储在堆内存中的对象头和数据区域。
  2. 堆内存中的字符串对象:
    • 由于字符串是不可变的,“tom” 这个字符串常量会存储在方法区的字符串常量池中。
    • name 变量会持有指向字符串常量池中 “tom” 字符串的引用。
  3. 对象引用:
    • 在 Java 中,操作对象通常使用对象的引用。如果有一个名为 person 的对象引用,它会指向堆内存中的 Person 对象实例。
    • 这个对象引用可以存储在栈帧的局部变量表中,或者作为成员变量存储在其他对象中。

综合来说,Person 类的对象实例在堆内存中存储了 agename 两个实例变量的数据。age 存储了整数值 12,而 name 存储了指向字符串常量池中的 “tom” 字符串的引用。对象引用可以在栈帧的局部变量表中或其他对象的成员变量中存储,指向堆内存中的 Person 对象实例。

字符串常量池面试题#

字符串常量池的设计意图是什么?

字符串常量池在哪里?

如何操作字符串常量池?

  1. 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能

  2. JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化,为字符串开辟一个字符串常量池,类似于缓存区创建字符串常量时,首先坚持字符串常量池是否存在该字符串存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

  3. 实现的基础:实现该优化的基础是因为 字符串是不可变的,可以不用担心数据冲突进行共享,运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收代码:从字符串常量池中获取相应的字符串

String str1 = “hello”;
String str2 = “hello”;
System.out.printl("str1 ** str2" : str1 ** str2 ) //true

字符串常量池在哪里

在分析字符串常量池的位置时,首先了解一下堆、栈、方法区:

image-20240322141612147

存储的是对象,每个对象都包含一个与之对应的class

JVM只有一个堆区(heap)被所有 线程共享,堆中不存放基本类型和对象引用,只存放对象本身

对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定

每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)

每个栈中的数据(原始类型和对象引用)都是私有的

栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)

数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失

  • 方法区

静态区,跟堆一样,被所有的 线程共享

方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量;字符串常量池则存在于方法区

重点图#

代码:堆栈方法区存储字符串

String str1 = “abc”;
String str2 = “abc”;
String str3 = “abc”;
String str4 = new String(“abc”);
String str5 = new String(“abc”);

image-20240322141757742

面试题:

  • String str4 = new String(“abc”) 创建多少个对象?**

  1. 在常量池中查找是否有“abc”对象

    有则返回对应的引用实例

​ 没有则创建对应的实例对象

  1. 在堆中 new 一个 String(“abc”) 对象

  2. 将对象地址赋值给str4,创建一个引用

​ 所以,常量池中没有“abc”字面量则创建两个对象,否则创建一个对象,以及创建一个引用

​ 根据字面量,往往会提出这样的变式题:

  • String str1 = new String(“A”+“B”) ; 会创建多少个对象?

str1:

字符串常量池:“A”,“B”,“AB” : 3个

堆:new String(“AB”) :1个

引用: str1 :1个

总共 : 5个

  • String str2 = new String(“ABC”) + “ABC” ; 会创建多少个对象?

str2 :

字符串常量池:“ABC” : 1个

堆:new String(“ABC”) :1个

引用: str2 :1个

总共 : 3个

基础类型的变量和常量,变量和引用存储在栈中,常量存储在常量池中

int a1 = 1;
int a2 = 1;
int a3 = 1;
public static int INT1 =1 ;
public static int INT2 =1 ;
public static int INT3 =1 ;

image-20240322142230735

操作字符串常量池的方式#

JVM实例化字符串常量池时

String str1 = “hello”;
String str2 = “hello”;
System.out.printl("str1 ** str2" : str1 ** str2 ) //true
String.intern()

通过new操作符创建的字符串对象不指向字符串池中的任何对象,但是可以通过使用字符串的intern()方法来指向其中的某一个。

java.lang.String.intern()返回一个保留池字符串,就是一个在全局字符串池中有了一个入口。如果以前没有在全局字符串池中,那么它就会被添加到里面

// Create three strings in three different ways.
String s1 = "Hello";
String s2 = new StringBuffer("He").append("llo").toString();
String s3 = s2.intern();
// Determine which strings are equivalent using the ==
// operator
System.out.println("s1 ** s2? " + (s1 ** s2)); // false
System.out.println("s1 ** s3? " + (s1 ** s3)); // true

字面量和常量池初探

字符串对象内部是用字符数组存储的,那么看下面的例子:

String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");

1.会分配一个11长度的char数组,并在常量池分配一个由这个char数组组成的字符串,然后由m去引用这个字符串

2.用n去引用常量池里边的 字符串,所以和n引用的是同一个对象

3.生成一个新的字符串,但内部的字符数组引用着m内部的字符数组

4.同样会生成一个新的字符串,但内部的字符数组引用常量池里边的字符串内部的字符数组,意思是和u是同样的字符数组

使用图来表示的话,情况就大概是这样的(使用虚线只是表示两者其实没什么特别的关系):

image-20240322142441786

测试demo:

String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
System.out.println(m == n); //true
System.out.println(m == u); //false
System.out.println(m == v); //false
System.out.println(u == v); //false

结论:#

m和n是同一个对象

m,u,v都是不同的对象

m,u,v,n但都使用了同样的字符数组,并且用equal判断的话也会返回true

2、堆体系结构概述#

Heap 堆#

​ 一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:

  • Young Generation Space 新生区 Young/New

  • Tenure generation space 养老区 Old/ Tenure

  • Permanent Space 永久区 Perm 元空间(java8)

Heap堆(Java7之前) 一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。

  • 堆内存逻辑上分为三部分:新生+养老+永久

image-20210618113615439

  • 堆内存物理上分为两部分:新生+养老

新生区(如下是首次讲解,简单版,先入门大致理解,下一页ppt详细) 新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。

新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二: (1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。 (2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

MinorGC的过程(复制->清空->互换)#

image-20210618133853733

记:幸存者0区 = from 区、 幸存者1区=to区

from区和to区,他们的位置和名次,不是固定的,每次GC后会交换Gc之后有交换,谁空谁是to区

1:eden、SurvivorFrom 复制到 SurvivorTo,年龄+1 首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1

2:清空 eden、SurvivorFrom 然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to

3:SurvivorTo和 SurvivorFrom 互换 最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代

内存管理#

image-20210618133933461

实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆)目的就是要和堆分开

对于HotSpot(JDK)虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)” ,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。

image-20210618133948997

永久区(java7之前有)#

​ 永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

3、堆参数调优入门#

  • JVM垃圾收集(Java Garbage Collection )

  • 上集,本次均以JDK1.8+HotSpot为例

Java7

image-20210618140604885

Java8

JDK 1.8之后将最初的永久代取消了,由元空间取代。

image-20210618140826698

在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。

元空间与永久代之间最大的区别在于: 永久带使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。

因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。

堆调优#

(堆内存调优简介01)#

image-20210618141346521

public static void main(String[] args){
long maxMemory = Runtime.getRuntime().maxMemory() ;//返回 Java 虚拟机试图使用的最大内存量。
long totalMemory = Runtime.getRuntime().totalMemory() ;//返回 Java 虚拟机中的内存总量。
System.out.println("-Xmx:MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("-Xms:TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
}

实际上调优: 1、Xmx、Xms设置成一样大 , 理由 :防止忽大忽小。

(堆内存调优简介02)#

发现默认的情况下分配的内存是总内存的“1 / 4”、而初始化的内存为“1 / 64”

image-20210618142617839

VM参数: -Xms1024m -Xmx1024m -XX<+printgcdetails>

image-20210618142631859

(堆内存调优简介03)此图为java7,演示为8#

image-20210618143334294

实际案例:

public class T2 {
public static void main(String[] args){
long maxMemory = Runtime.getRuntime().maxMemory() ;//返回 Java 虚拟机试图使用的最大内存量。
long totalMemory = Runtime.getRuntime().totalMemory() ;//返回 Java 虚拟机中的内存总量。
System.out.println("-Xmx:MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("-Xms:TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
}
}
D:\java1.8_64\jdk1.8.0_71\bin\java.exe -Xms1024m -Xmx1024m -XX:+PrintGCDetails "-javaagent:F:\idea\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar=23464:F:\idea\IntelliJ IDEA 2020.3.2\bin" -Dfile.encoding=UTF-8 -classpath box\org\springframework\spring-expression\5.2.1.RELEASE\spring-expression-5.2.1.RELEASE.jar com.eim.jvm.T2
-Xmx:MAX_MEMORY = 1029177344(字节)、981.5MB
-Xms:TOTAL_MEMORY = 1029177344(字节)、981.5MB
Heap
PSYoungGen total 305664K, used 26214K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 10% used [0x00000000eab00000,0x00000000ec499be8,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3335K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 365K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0

(堆内存调优简介04)#

String str = "www.atguigu.com" ;
while(true)
{
str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999) ;
}

VM参数:-Xms8m -Xmx8m -XX<+printgcdetails>

image-20210618143712793

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:647)
at java.lang.StringBuilder.append(StringBuilder.java:208)
at com.eim.jvm.T2.main(T2.java:22)

4、总 结#

5个异常#

java.lang.OutOfMemoryError 内存溢出异常
java.util.ConcurrentModificationException java并发修改异常

Gc#

Gc收集日志信息#

-XX<+printgcdetails>

GC是什么(分代收集算法)#

次数上频繁收集Young区

次数上较少收集Old区

基本不动元空间

image-20220129153205646

GC4大算法#

Gc算法总体概述#

image-20210618150317813

JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。 因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC)

Minor GC和Full GC的区别   普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。   全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上

4算法#

1、引用计数法(了解)#

image-20210618150615945

package com.atguigu.jvm;
/**@Description:-verbose:gc*/
public class RefCountGC
{
private byte[] bigSize = new byte[2 * 1024 * 1024];//这个成员属性唯一的作用就是占用一点内存
Object instance = null;
public static void main(String[] args)
{
RefCountGC objectA = new RefCountGC();
RefCountGC objectB = new RefCountGC();
objectA.instance = objectB;
objectB.instance = objectA;
objectA = null;
objectB = null;
System.gc();
}
}

2、复制算法(Copying)#

年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)

what

原理

image-20210618151039287

Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden是就变成空的了。 当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,通过-XX 来设定参数),这些对象就会成为老年代。

-XX — 设置对象在新生代中存活的次数

动态演示

解释

年轻代中的GC,主要是复制算法(Copying)

HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片(优点)。

image-20210618151151593

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

image-20210618151210517

因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。

动图

gc_copying

劣势

复制算法它的缺点也是相当明显的。   1、它浪费了一半的内存,这太要命了。   2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

3、标记清除(Mark-Sweep)#

老年代一般是由(标记清除 或者是 标记清除)与 标记整理的混合实现

what

原理

image-20210618151444154

image-20210618151450329

用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作接下来便让应用程序恢复运行。

主要进行两项工作,第一项则是标记,第二项则是清除。
标记:从引用根节点开始标记遍历所有的GC Roots, 先标记出要回收的对象。 清除:遍历整个堆,把标记的对象清除。 缺点:此算法需要暂停整个应用,会产生内存碎片

动态演示

mark_sweep

劣势

1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲 2、其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

4、标记压缩(Mark-Compact)#

老年代一般是由标记清除或者是标记清除与标记整理的混合实现

what

原理

image-20210618151625353

在整理压缩阶段,不再对标记的对像做回收,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

  标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价

劣势

标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。 从效率上来说,标记/整理算法要低于复制算法。

标记清除压缩(Mark-Sweep-Compact)

image-20210618151658052

动态演示

mark_compact

小总结#

内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。 内存整齐度:复制算法=标记整理算法>标记清除算法。 内存利用率:标记整理算法=标记清除算法>复制算法。

可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程

  难道就没有一种最优算法吗? 猜猜看,下面还有

回答:无,没有最好的算法,只有最合适的算法。=****=>分代收集算法。

年轻代(Young Gen)

年轻代特点是区域相对老年代较小,对像存活率低。

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

老年代(Tenure Gen)

老年代的特点是区域较大,对像存活率高。

这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。

Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式提标记效率。

Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对像的移动。使其相对其它有对像移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题。

Compact阶段的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,做为老年代的第一选择并不合适。

基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

面试题#

  • JVM内存模型以及分区,需要详细到每个区放什么

  • 堆里面的分区:Eden,survival from to,老年代,各自的特点。

  • GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方

  • Minor GC与Full GC分别在什么时候发生

JMM#

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式. JMM关于同步规定: 1.线程解锁前,必须把共享变量的值刷新回主内存 2.线程加锁前,必须读取主内存的最新值到自己的工作内存 3.加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:

image-20210618165232309

特征:可见、原子、有序

1、可见性

2、原子性

3、演示可见性+原子性

4、有序性

volatile是Java虚拟机提供的轻量级的同步机制#

1.1保证可见性

1.2不保证原子性

1.3禁止指令重排

aqs和cas#

在面试中,关于 AQS(AbstractQueuedSynchronizer)和 CAS(Compare and Swap)的问题常常涉及到并发编程和锁机制。以下是一些可能的面试题及其答案:

  1. 什么是AQS?它在Java中的作用是什么?

    • 答案: AQS 是 Java 中用于构建锁和同步器的框架。它提供了一种通用的同步机制,通过内部维护一个同步队列(队列中的节点通常是等待获取锁的线程),实现了基于FIFO等待队列的锁和同步器的抽象。ReentrantLock 和 CountDownLatch 等 Java 中的锁和同步器都是基于 AQS 构建的。
  2. CAS 是什么?它与AQS有什么关系?

    • 答案: CAS(Compare and Swap)是一种无锁原子操作,用于实现多线程环境下的并发控制。在Java中,java.util.concurrent.atomic 包中的一些类(如 AtomicIntegerAtomicReference)使用 CAS 操作来实现线程安全的原子操作。AQS 使用 CAS 操作来实现对状态的原子更新,这是实现锁和同步器的关键机制之一。
  3. 简要解释一下AQS的工作原理。

    • 答案: AQS 的核心思想是基于一个整型的状态(state)来控制同步资源的访问。当线程尝试获取锁时,AQS 会通过 CAS 操作更新状态,如果成功则获取锁,否则将线程加入同步队列。在释放锁的时候,AQS 同样通过 CAS 操作来更新状态,并唤醒同步队列中等待的线程。
  4. 什么是可重入锁?ReentrantLock是如何实现可重入性的?

    • 答案: 可重入锁是指同一个线程可以多次获取同一个锁,而不会发生死锁。ReentrantLock 实现了可重入性,通过维护一个持有锁的线程和一个计数器。当线程第一次获取锁时,计数器值为1,每次重入锁,计数器递增。当线程释放锁时,计数器递减,只有当计数器为0时,锁才被完全释放。
  5. 什么是ABA问题?CAS 如何解决ABA问题?

    • 答案: ABA 问题指的是在并发环境下,一个值从A变成B再变成A,而某个线程在这个过程中可能发生误判。CAS 通过引入版本号或者时间戳等机制来解决 ABA 问题。在 CAS 操作中,除了比较当前值是否等于预期值外,还需要比较版本号等辅助信息。如果版本号发生变化,则 CAS 操作失败,即使值从 A 变成 B 再变成 A,版本号也会相应变化,从而保证了正确性。
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

JVM与JUC并发编程完全指南
https://onecodemaker.cn/posts/java核心-06-jvm与juc/
作者
糖糖IT
发布于
2026-01-21
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录