并发编程笔记

一、JUC概述

简介

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tc7Y6Jlv-1646561428370)(并发编程.assets/1645928755802.png)]

并发编程三要素?

1、原子性

原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操

作打断,要么就全部都不执行。

2、可见性

可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他

线程可以立即看到修改的结果。

3、有序性

有序性,即程序的执行顺序按照代码的先后顺序来执行

进程线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MB5wlHTd-1646561428371)(并发编程.assets/1645928823016.png)]

线程状态

新建

就绪

运行

阻塞

等待

超时等待

销毁

wait/sleep 的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ASPrE2U5-1646561428371)(并发编程.assets/1645928981240.png)]

并发 并行 串行

串行:是指同一时刻一个CPU只能处理一件事,类似于单车道
并行:相对来说资源比较充足,多个CPU可以同时处理不同的多件事,类似于多车道
并发:相对来说资源比较紧缺,多个进程同时抢占公共资源,比如多个进程抢占一个CPU

管程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttxYKkWV-1646561428372)(并发编程.assets/1645929489889.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HSiF0xNG-1646561428373)(并发编程.assets/1645955323690.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C44DHles-1646561428373)(并发编程.assets/1645955373173.png)]

二、Lock接口

1、synchronized关键字回顾

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nFnkA5FS-1646561428374)(并发编程.assets/1645958593904.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1BjpfaCR-1646561428375)(并发编程.assets/1645958647784.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vcuKVWZW-1646561428376)(并发编程.assets/1645958860848.png)]

2、 ? Lock接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RLLbzczS-1646561428376)(并发编程.assets/1645963661267.png)]

1、Lock和Synchronized区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oofa1QOz-1646561428378)(并发编程.assets/1645963672685.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ExfswdL3-1646561428378)(并发编程.assets/1645964173355.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c9Vf8Ozf-1646561428378)(并发编程.assets/1645964287594.png)]

2、实现类1

1、可重入锁(ReentrantLock)

​ lock() 上锁

​ unlock() 解锁

​ lockInterruptibly() 让一个线程不会一直等待锁的申请,转而去响应中断

lock() 方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待

**tryLock() ** 方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待

**tryLock(long time, TimeUnit unit) ** 方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly() 方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhQCsko5-1646561428379)(并发编程.assets/1645963929443.png)]

三、线程间的通信

synchronized用法

  • 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象
  • 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象
  • 修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象

synchronize线程通信演示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X2ezr55c-1646561428380)(并发编程.assets/1646005682542.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oSVI8yum-1646561428380)(并发编程.assets/1646006295214.png)]

所用方法(java.lang.Object):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MFQrJ5P4-1646561428381)(并发编程.assets/1646057539662.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aKAXXYjD-1646561428382)(并发编程.assets/1646057549579.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AgWrgIwE-1646561428383)(并发编程.assets/1646057564211.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P65IE1Fz-1646561428384)(并发编程.assets/1646057574494.png)]

虚假唤醒

wait 在哪里睡,在哪里醒,

​ 所以被唤醒时,在判断的方法体中 不会再经过判断条件,直接执行下面的代码,判断失效

解决办法: if——》while

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v7P1apdB-1646561428385)(并发编程.assets/1646016963567.png)]

Lock线程通信演示

所用方法:

1、java.util.concurrent.locks .Lock

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A3By2vuW-1646561428385)(并发编程.assets/1646057621988.png)]

2、java.util.concurrent.locks .Condition

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PtxcPefM-1646561428386)(并发编程.assets/1646057657771.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G9DsxsEX-1646561428386)(并发编程.assets/1646057671468.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QyAufiv5-1646561428386)(并发编程.assets/1646057681030.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Srsn7Ss3-1646561428387)(并发编程.assets/1646057689802.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUl2890W-1646561428387)(并发编程.assets/1646017480523.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PSwixKL0-1646561428387)(并发编程.assets/1646017543576.png)]

线程的定制化通信

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qBcnWx1D-1646561428388)(并发编程.assets/1646017970757.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3qNwlL7g-1646561428388)(并发编程.assets/1646018323747.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ipKAAwDr-1646561428389)(并发编程.assets/1646018598825.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CjYWE9at-1646561428389)(并发编程.assets/1646018730441.png)]

重复print5方法,变成 print10 print15方法

只是判断等待时、修改标志位时、通知线程时,稍作更改即可

四、生产者消费者模式

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据
.
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

五、创建线程的多种方式

继承Thread类

实现Runnable接口

使用Callable接口

使用线程池

六、集合的线程安全

1、 ArrayList不安全

1、演示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DJNjjIad-1646561428390)(并发编程.assets/1646040777201.png)]

获取内容时,可能还没存进去,或者正在存,报错(ConcurrentModificationException)

2、解决方案-Vector

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jthaCtZ8-1646561428390)(并发编程.assets/1646041237534.png)]

Vector不用的原因

1.因为vector是线程安全的,所以效率低,这容易理解,类似StringBuffer
2.Vector空间满了之后,扩容是一倍,而ArrayList仅仅是一半
3.Vector分配内存的时候需要连续的存储空间,如果数据太多,容易分配内存失败
4.只能在尾部进行插入和删除操作,效率低

3、解决方案Collections

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjszGFwp-1646561428391)(并发编程.assets/1646041755779.png)]

不常用

4、解决方案-CopyOnWriteArrayList(重点)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L8EWLv1e-1646561428391)(并发编程.assets/1646041845215.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S2Ax4ZRu-1646561428391)(并发编程.assets/1646042005503.png)]

缺点

内存占用问题 : CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象 可以通过压缩容器中的元素的方法来减少大对象的内存消耗

数据一致性问题 : CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性

2、HashSet不安全

1、演示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o5fyo5qx-1646561428392)(并发编程.assets/1646042838804.png)]

解决方案-CopyOnWriteArraySet

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FYPZJvuZ-1646561428392)(并发编程.assets/1646042928150.png)]

3、HashMap不安全

1、演示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZpeOvrOm-1646561428393)(并发编程.assets/1646049785177.png)]

2、解决方案-ConcurrentHashMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FtAq0QBP-1646561428393)(并发编程.assets/1646049990480.png)]

七、多线程锁

1、 锁的分类

1、乐观锁 & 悲观锁

两种锁只是一种概念

**乐观锁:**乐观锁认为一个线程去拿数据的时候不会有其他线程对数据进行更改,所以不会上锁。

实现方式:CAS机制、版本号机制

**悲观锁:**悲观锁认为一个线程去拿数据时一定会有其他线程对数据进行更改。所以一个线程在拿数据的时候都会顺便加锁,这样别的线程此时想拿这个数据就会阻塞。比如Java里面的synchronized关键字的实现就是悲观锁。实现方式:就是加锁。

2、独享锁 & 共享锁

两种锁只是一种概念

**独享锁:**该锁一次只能被一个线程所持有

**共享锁:**该锁可以被多个线程所持有

举例:

synchronized是独享锁;

可重入锁ReentrantLock是独享锁;

读写锁ReentrantReadWriteLock中的读锁ReadLock是共享锁,写锁WriteLock是独享锁。

独享锁与共享锁通过AQS(AbstractQueuedSynchronizer)来实现的,通过实现不同的方法,来实现独享或者共享。

3、互斥锁 & 读写锁

上面讲的独享锁/共享锁就是一种概念,互斥锁/读写锁是具体的实现。

互斥锁的具体实现就是synchronized、ReentrantLock。ReentrantLock是JDK1.5的新特性,采用ReentrantLock可以完全替代替换synchronized传统的锁机制,更加灵活。

读写锁的具体实现就是读写锁ReadWriteLock。

4、可重入锁

定义:对于同一个线程在外层方法获取锁的时候,在进入内层方法时也会自动获取锁。

优点:避免死锁

举例:ReentrantLock、synchronized

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ILB8O5od-1646561428394)(并发编程.assets/1646054717975.png)]

注意:使用ReentrantLock时,如果不主动释放锁,那后面的线程就拿不到锁,程序停滞

5、公平锁 & 非公平锁

**公平锁:**多个线程相互竞争时要排队,多个线程按照申请锁的顺序来获取锁。

**非公平锁:**多个线程相互竞争时,先尝试插队,插队失败再排队,比如:synchronized、ReentrantLock默认

公平锁 不会出现饿死现象(会判读该位置是否有人)

非公平锁 效率高

6、分段锁

分段锁并不是具体的一种锁,只是一种锁的设计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。CurrentHashMap底层就用了分段锁,使用Segment,就可以进行并发使用了,而HashMap确实非线程安全的,就差在了分段锁上。

7、偏向锁 & 轻量级锁 & 重量级锁

JDK 1.6 为了减少获得锁和释放锁所带来的性能消耗,在JDK 1.6里引入了4种锁的状态:无锁、偏向锁、轻量级锁和重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。

研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了不让这个线程每次获得锁都需要CAS操作的性能消耗,就引入了偏向锁当一个线程访问对象并获取锁时,会在对象头里存储锁偏向的这个线程的ID,以后该线程再访问该对象时只需判断对象头的Mark Word里是否有这个线程的ID,如果有就不需要进行CAS操作,这就是偏向锁。当线程竞争更激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待一会儿上一个线程就会释放锁,但是当自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁就是Synchronized,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。

2、死锁现象

1、死锁?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DKlUPBwb-1646561428394)(并发编程.assets/1646055383254.png)]

2、产生死锁的原因主要是:

(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。

3、 产生死锁的四个必要条件:

(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

4、验证是否死锁

(1)jps命令: 查看当前进程

(2)jstack命令: jvm自带的堆栈跟踪工具

3、Synchronized锁的八种情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDKzanFl-1646561428395)(并发编程.assets/1646050102778.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vhSeIUOy-1646561428395)(并发编程.assets/1646050223199.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h3sJAf3B-1646561428395)(并发编程.assets/1646050286786.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dbdy0WiW-1646561428396)(并发编程.assets/1646050432220.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-36govTVW-1646561428396)(并发编程.assets/1646050443651.png)]

1,2解释: Synchronized在方法上锁的是当前对象

3、解释: 普通方法不受影响,直接调用

4解释: Synchronized在方法上锁的是当前对象 两个对象,谁不等待,谁执行

5,6解释: 静态方法是类级元素,锁定的是当前类的字节码对象,只有一个

7,8解释: 静态锁定字节码对象,普通锁定当前对象,但是类加载最快

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9d4Jcc17-1646561428397)(并发编程.assets/1646053331808.png)]

八、Callable接口

1、创建线程的多种方式

继承Thread类

实现Runnable接口

使用Callable接口

使用线程池

2、Callable接口简介

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZEgbanXz-1646561428397)(并发编程.assets/1646058949102.png)]

3、Runnable接口和Callable接口

1、Callable接口有返回值

2、Callable接口能抛出异常

3、实现的方法名:(Callable接口 ——》call方法)(Runnable接口 ——》run方法)

4、Callable演示和FutureTask

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hyytTU7d-1646561428398)(并发编程.assets/1646061146312.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ystxLQBS-1646561428398)(并发编程.assets/1646061198406.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PjR9PVKl-1646561428399)(并发编程.assets/1646061467520.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9QrstXC-1646561428399)(并发编程.assets/1646059467768.png)]

  1. FutureTask执行多任务计算的使用场景

利用FutureTask和ExecutorService,可以用多线程的方式提交计算任务,主线程继续执行其他任务,当主线程需要子线程的计算结果时,在异步获取子线程的执行结果。

  1. FutureTask在高并发环境下确保任务只执行一次

在很多高并发的环境下,往往我们只需要某些任务只执行一次。这种使用情景FutureTask的特性恰能胜任。举一个例子,假设有一个带key的连接池,当key存在时,即直接返回key对应的对象;当key不存在时,则创建连接。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N0IuLNJh-1646561428400)(并发编程.assets/1646061046601.png)]

九、辅助类

1、CountDownLatch(减少计数)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xm9UiauU-1646561428400)(并发编程.assets/1646061901846.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cnmWMg0A-1646561428401)(并发编程.assets/1646061931351.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lpBHA08V-1646561428401)(并发编程.assets/1646061916931.png)]

该类的构造方法为
CountDownLatch(int count)构造一个用给定计数初始化的CountDownLatch在这里插入代码片

两个常用的主要方法
await() 使当前线程在锁存器倒计数至零之前一直在等待,除非线程被中断
countDown()递减锁存器的计数,如果计数达到零,将释放所有等待的线程

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句
具体步骤可以演化为定义一个类,减1操作,并等待到0,为0执行结果

通过具体的案例进行加深代码
6个同学陆续离开教室之后,班长才能锁门
如果不加 CountDownLatch类,会出现线程混乱执行,同学还未离开教室班长就已经锁门了

public class CountDownLatchDemo {
    //6个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) throws InterruptedException {
        
        //6个同学陆续离开教室之后
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");

            },String.valueOf(i)).start();
        }

        System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
    }
}
123456789101112131415

在这里插入图片描述

具体正确的案例代码

//演示 CountDownLatch
public class CountDownLatchDemo {
    //6个同学陆续离开教室之后,班长锁门
    public static void main(String[] args) throws InterruptedException {

        //创建CountDownLatch对象,设置初始值
        CountDownLatch countDownLatch = new CountDownLatch(6);

        //6个同学陆续离开教室之后
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");

                //计数  -1
                countDownLatch.countDown();

            },String.valueOf(i)).start();
        }

        //等待
        countDownLatch.await();

        System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
    }
}
12345678910111213141516171819202122232425

在这里插入图片描述

2、CyclicBarrier(循环栅栏)

该类是一个同步辅助类,允许一组线程互相等到,直到到达某个公共屏障点,在设计一组固定大小的线程的程序中,这些线程必须互相等待,这个类很有用,因为barrier在释放等待线程后可以重用,所以称为循环barrier

常用的构造方法有:
CyclicBarrier(int parties,Runnable barrierAction)创建一个新的CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动barrier时执行给定的屏障操作,该操作由最后一个进入barrier的线程操作

常用的方法有:
await()在所有的参与者都已经在此barrier上调用await方法之前一直等待

通过具体案例
集齐7颗龙珠就可以召唤神龙
完整代码

//集齐7颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {

    //创建固定值
    private static final int NUMBER = 7;

    public static void main(String[] args) {
        //创建CyclicBarrier
        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()+" 星龙被收集到了");
                    //等待
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}
123456789101112131415161718192021222324252627

在这里插入图片描述
总结:
CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作

3、Semaphore(信号灯)

一个计数信号量,从概念上将,信号量维护了一个许可集,如有必要,在许可可用前会阻塞每一个acquire(),然后在获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动

具体常用的构造方法有:
Semaphore(int permits)创建具有给定的许可数和非公平的公平设置的Semapore

具体常用的方法有:
acquire()从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
release()释放一个许可,将其返回给信号量

设置许可数量Semaphore semaphore = new Semaphore(3);
一般acquire()都会抛出异常,releasefinally中执行

通过具体案例
6辆汽车,停3个车位
完整代码:

//6辆汽车,停3个车位
public class SemaphoreDemo {
    public static void main(String[] args) {
        //创建Semaphore,设置许可数量
        Semaphore semaphore = new Semaphore(3);

        //模拟6辆汽车
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                try {
                    //抢占
                    semaphore.acquire();

                    System.out.println(Thread.currentThread().getName()+" 抢到了车位");

                    //设置随机停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));

                    System.out.println(Thread.currentThread().getName()+" ------离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}
1234567891011121314151617181920212223242526272829

代码截图:
在这里插入图片描述

十、 读写锁

1、简介

回顾悲观锁和乐观锁的概念
悲观锁:单独每个人完成事情的时候,执行上锁解锁。解决并发中的问题,不支持并发操作,只能一个一个操作,效率低

乐观锁:每执行一件事情,都会比较数据版本号,谁先提交,谁先提交版本号


新概念
表锁:整个表操作,不会发生死锁
行锁:每个表中的单独一行进行加锁,会发生死锁
读锁:共享锁(可以有多个人读),会发生死锁
写锁:独占锁(只能有一个人写),会发生死锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QSs4l80Y-1646561428402)(并发编程.assets/1646094286896.png)]

2、案例

读写锁:一个资源可以被多个读线程访问,也可以被一个写线程访问,但不能同时存在读写线程,读写互斥,读读共享

读写锁ReentrantReadWriteLock
读锁为ReentrantReadWriteLock.ReadLockreadLock()方法
写锁为ReentrantReadWriteLock.WriteLockwriteLock()方法

创建读写锁对象private ReadWriteLock rwLock = new ReentrantReadWriteLock();
写锁 加锁 rwLock.writeLock().lock();,解锁为rwLock.writeLock().unlock();
读锁 加锁rwLock.readLock().lock();,解锁为rwLock.readLock().unlock();

案例分析
模拟多线程在map中取数据和读数据
完整代码如图

//资源类
class MyCache {
    //创建map集合
    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()+" 正在写操作"+key);
            //暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            //放数据
            map.put(key,value);
            System.out.println(Thread.currentThread().getName()+" 写完了"+key);
        } catch (InterruptedException 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()+" 正在读取操作"+key);
            //暂停一会
            TimeUnit.MICROSECONDS.sleep(300);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName()+" 取完了"+key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放读锁
            rwLock.readLock().unlock();
        }
        return result;
    }
}

public class ReadWriteLockDemo {
    public static void main(String[] args) throws InterruptedException {
        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();
        }

        TimeUnit.MICROSECONDS.sleep(300);

        //创建线程取数据
        for (int i = 1; i <=5; i++) {
            final int num = i;
            new Thread(()->{
                myCache.get(num+"");
            },String.valueOf(i)).start();
        }
    }
}

通过这一章节和以上文章

3、锁的演变

  1. 无锁:多线程抢夺资源
  2. synchronized和ReentrantLock,都是独占,每次只可以一个操作,不能共享
  3. ReentrantReadWriteLock,读读可以共享,提升性能,但是不能多人写。缺点:造成死锁(一直读,不能写),读进程不能写,写进程可以读。
  4. 写锁降级为读锁(一般等级写锁高于读锁)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1XxM2CO3-1646561428402)(并发编程.assets/1646095251639.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qe5slAnw-1646561428403)(并发编程.assets/1646095281312.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FMz1Fv58-1646561428403)(并发编程.assets/1646095545929.png)]

具体第四步演练的代码
具体降级步骤
获取写锁->获取读锁->释放写锁->释放读锁

//演示读写锁降级
public class Demo1 {

    public static void main(String[] args) {
        //可重入读写锁对象
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//读锁
        ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//写锁

        //锁降级
        //1 获取写锁
        writeLock.lock();
        System.out.println("manongyanjiuseng");
        
        //2 获取读锁
        readLock.lock();
        System.out.println("---read");
        
        //3 释放写锁
        writeLock.unlock();

        //4 释放读锁
        readLock.unlock();
    }
}

代码截图
在这里插入图片描述

如果是读之后再写,执行不了
因为读锁权限小于写锁
需要读完之后释放读锁,在进行写锁

//2 获取读锁
readLock.lock();
System.out.println("---read");

//1 获取写锁
writeLock.lock();
System.out.println("manongyanjiuseng");

十一、阻塞队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zQEdoRYt-1646561428404)(并发编程.assets/1646098611663.png)]

而阻塞队列是共享队列(多线程操作),一端输入,一端输出
不能无限放队列,满了之后就会进入阻塞,取出也同理

  • 当队列是空的,从队列中获取元素的操作将会被阻塞
  • 当队列是满的,从队列中添加元素的操作将会被阻塞
  • 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
  • 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增

1 、种类

1.ArrayBlockingQueue
基于数组的阻塞队列
由数组结构组成的有界阻塞队列
-ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象无法并行

2. LinkedBlockingQueue
基于链表的阻塞队列
由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列

  • 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能

3.DelayQueue
使用优先级队列实现的延迟无界阻塞队列

  • DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞

4.PriorityBlockingQueue
基于优先级的阻塞队列
支持优先级排序的无界阻塞队列
不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者

5.SynchronousQueue
一种无缓冲的等待队列
相对于有缓冲的 BlockingQueue 来说,少了一个中间经销商的环节(缓冲区)
不存储元素的阻塞队列,也即单个元素的队列

声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。
公平模式和非公平模式的区别:
公平模式:SynchronousQueue 会采用公平锁,并配合一个 FIFO 队列来阻塞
多余的生产者和消费者,从而体系整体的公平策略;
非公平模式(SynchronousQueue 默认):SynchronousQueue 采用非公平锁,同时配合一个 LIFO 队列来管理多余的生产者和消费者

而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理

6.LinkedTransferQueue
由链表结构组成的无界阻塞 TransferQueue 队列
由链表组成的无界阻塞队列

  • 预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,生成一个节点(节点元素为 null)入队,消费者线程被等待在这个节点上,生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回

7.LinkedBlockingDeque
由链表结构组成的双向阻塞队列
阻塞有两种情况

  • 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException异常
  • 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数

2 、方法

在这里插入图片描述
创建阻塞队列 BlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

  • 加入元素System.out.println(blockingQueue.add("a"));,成功为true,失败为false
  • 检查元素System.out.println(blockingQueue.element());
  • 取出元素System.out.println(blockingQueue.remove());,先进先出

越界抛异常

第二种方法:
加入元素System.out.println(blockingQueue.offer("a"));
取出元素System.out.println(blockingQueue.poll());

越界加不进去false ,拿不出来null

第三种方法:
加入元素blockingQueue.put("a");
取出元素System.out.println(blockingQueue.take());

该方法加入元素或者取出元素,如果满了或者空了,还进行下一步加入或者取出操作,会出现阻塞的状态,而第一种方法是直接抛出异常

第四种方法:
加入元素System.out.println(blockingQueue.offer("a"));
该方法满了或者空了在进行会有阻塞,但可以加入参数,超时退出System.out.println(blockingQueue.offer("w",3L, TimeUnit.SECONDS));

十二、 线程池

回顾以前的连接池概念
连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度

特点:

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

具体架构:
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类
在这里插入图片描述
说明:Executors为工具类,I为接口类,C为实现类

10.1 种类与创建

  • Executors.newFixedThreadPool(int)一池N线程
ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LsHmrBLK-1646561428404)(并发编程.assets/1646100407502.png)]

  • Executors.newSingleThreadExecutor()一池一线程
 ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口
  • Executors.newCachedThreadPool()一池可扩容根据需求创建线程
 ExecutorService threadPool3 = Executors.newCachedThreadPool();

执行线程execute()
关闭线程shutdown()

线程池中的执行方法execute源代码为

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

void execute(Runnable command);参数为Runnable接口类,可以通过设置lambda

具体案例代码案例

//演示线程池三种常用分类
public class ThreadPoolDemo1 {
    public static void main(String[] args) {
        //一池五线程
        ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口

        //一池一线程
        ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口

        //一池可扩容线程
        ExecutorService threadPool3 = Executors.newCachedThreadPool();
        //10个顾客请求
        try {
            for (int i = 1; i <=10; i++) {
                //执行
                threadPool3.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //关闭
            threadPool3.shutdown();
        }

    }

}

10.2 底层原理

通过查看上面三种方式创建对象的类源代码
都有new ThreadPoolExecutor
具体查看该类的源代码,涉及七个参数

public ThreadPoolExecutor(int corePoolSize,
                         int maximumPoolSize,
                         long keepAliveTime,
                         TimeUnit unit,
                         BlockingQueue<Runnable> workQueue,
                         ThreadFactory threadFactory,
                         RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
      maximumPoolSize <= 0 ||
      maximumPoolSize < corePoolSize ||
     keepAliveTime < 0)
      throw new IllegalArgumentException();
      if (workQueue == null || threadFactory == null || handler == null)
         throw new NullPointerException();
       this.corePoolSize = corePoolSize;
       this.maximumPoolSize = maximumPoolSize;
       this.workQueue = workQueue;
       this.keepAliveTime = unit.toNanos(keepAliveTime);
       this.threadFactory = threadFactory;
       this.handler = handler;
}

具体代码中的七个参数讲解:
int corePoolSize, 常驻线程数量(核心)
int maximumPoolSize,最大线程数量
long keepAliveTime,TimeUnit unit,线程存活时间
BlockingQueue workQueue,阻塞队列(排队的线程放入)
ThreadFactory threadFactory,线程工厂,用于创建线程
RejectedExecutionHandler handler拒绝测试(线程满了)

具体工作流程是:

  • 在执行创建对象的时候不会创建线程
  • 创建线程的时候execute()才会创建
  • 先到常驻线程,满了之后再到阻塞队列进行等待,阻塞队列满了之后,在往外扩容线程,扩容线程不能大于最大线程数。大于最大线程数和阻塞队列之和后,会执行拒绝策略。

阻塞队列为3,常驻线程数2,最大线程数5
在这里插入图片描述
具体的拒绝策略有:

  1. 抛异常(默认)
  2. 谁调用找谁
  3. 抛弃最久执行当前
  4. 不理不问
    在这里插入图片描述

10.3 自定义线程池

实际在开发中不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,规避资源耗尽风险
在这里插入图片描述

ExecutorService threadPool = new ThreadPoolExecutor(
        2,
        5,
        2L,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
);

其他都同理,只是调用ThreadPoolExecutor类,自定义参数

完整代码演示

//自定义线程池创建
public class ThreadPoolDemo2 {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        //10个顾客请求
        try {
            for (int i = 1; i <=10; i++) {
                //执行
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" 办理业务");
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //关闭
            threadPool.shutdown();
        }
    }
}

十三、 Fork与Join分支

将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FxWC43dY-1646561428405)(并发编程.assets/1646103867828.png)] 该算法相当于递归,且是二分查找思路

class Fibonacci extends RecursiveTask<Integer> {
   final int n;
   Fibonacci(int n) { this.n = n; }
   Integer compute() {
     if (n <= 1)
        return n;
     Fibonacci f1 = new Fibonacci(n - 1);
     f1.fork();
     Fibonacci f2 = new Fibonacci(n - 2);
     return f2.compute() + f1.join();
   }
 }
123456789101112

  • ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
    RecursiveAction:用于没有返回结果的任务
    RecursiveTask:用于有返回结果的任务
  • ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行
  • RecursiveTask: 继承后可以实现递归(自己调自己)调用的任务

创建分支合并对象
通过该对象调用内部方法

具体案例:1加到100,相加两个数值不能大于10
完整代码如图

class MyTask extends RecursiveTask<Integer> {

    //拆分差值不能超过10,计算10以内运算
    private static final Integer VALUE = 10;
    private int begin ;//拆分开始值
    private int end;//拆分结束值
    private int result ; //返回结果

    //创建有参数构造
    public MyTask(int begin,int end) {
        this.begin = begin;
        this.end = end;
    }

    //拆分和合并过程
    @Override
    protected Integer compute() {
        //判断相加两个数值是否大于10
        if((end-begin)<=VALUE) {
            //相加操作
            for (int i = begin; i <=end; i++) {
                result = result+i;
            }
        } else {//进一步拆分
            //获取中间值
            int middle = (begin+end)/2;
            //拆分左边
            MyTask task01 = new MyTask(begin,middle);
            //拆分右边
            MyTask task02 = new MyTask(middle+1,end);
            //调用方法拆分
            task01.fork();
            task02.fork();
            //合并结果
            result = task01.join()+task02.join();
        }
        return result;
    }
}

public class ForkJoinDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建MyTask对象
        MyTask myTask = new MyTask(0,100);
        //创建分支合并池对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
        //获取最终合并之后结果
        Integer result = forkJoinTask.get();
        System.out.println(result);
        //关闭池对象
        forkJoinPool.shutdown();
    }
}

十四、异步回调

CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息

类中的具体引用类以及接口:
在这里插入图片描述
CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture 类

异步调用没有返回值方法runAsync
异步调用有返回值方法supplyAsync

主线程调用 get 方法会阻塞

具体完整代码演示:

//异步调用和同步调用
public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        //同步调用
        CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
            System.out.println(Thread.currentThread().getName()+" : CompletableFuture1");
        });
        completableFuture1.get();

        //mq消息队列
        //异步调用
        CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
            System.out.println(Thread.currentThread().getName()+" : CompletableFuture2");
            //模拟异常
            int i = 10/0;
            return 1024;
        });
        completableFuture2.whenComplete((t,u)->{
            System.out.println("------t="+t);
            System.out.println("------u="+u);
        }).get();

    }
}


具体whenComplete的源代码为:
t为返回结果,u为异常信息

public CompletableFuture<T> whenComplete(
        BiConsumer<? super T, ? super Throwable> action) {
        return uniWhenCompleteStage(null, action);
    }
1234

12.1 Future 与 CompletableFuture

对比这两种方法,一个为同步一个为异步

Futrue 在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone 方法来 判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成

(1)不支持手动完成
我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果,现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直等待它执行完成

(2)不支持进一步的非阻塞调用
通过 Future 的 get 方法会一直阻塞到任务完成,但是想在获取任务之后执行额外的任务,因为 Future 不支持回调函数,所以无法实现这个功能
(3)不支持链式调用
对于 Future 的执行结果,我们想继续传到下一个 Future 处理使用,从而形成一个链式的 pipline 调用,这在 Future 中是没法实现的。
(4)不支持多个 Future 合并
比如我们有 10 个 Future 并行执行,我们想在所有的 Future 运行完毕之后,执行某些函数,是没法通过 Future 实现的。
(5)不支持异常处理
Future 的 API 没有任何的异常处理的 api,所以在异步运行时,如果出了问题是不好定位的

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值