JUC快速入门各个知识点汇总(上)
@[toc]
前言
这段时间学习了下JUC,通过视频+博客文章+书籍让我对于一些并发知识的学习有了大致的了解,本章节内容demo
示例见:Gitee-JUC学习。
博主文章汇总:博客目录索引(持续更新)
各类锁汇总
相关锁知识点
排他锁:同一时刻只允许一个线程访问共享资源。。Java中synchronized
和ReentrantLock
实现。
读写锁:适用于共享资源读多写少
的场景,分别读锁,写锁。Java中,如ReadWriteLock
接口,包含实现类ReentrantReadWriteLock
(包含读锁,写锁)。
- 读操作(共享锁):同一时间允许多个线程对同一共享资源进行读操作。同一时刻所有线程的写操作会被阻塞。
- 写操作(排他锁、独占锁):同一时间允许一个线程对同一共享资源进行写操作。同一时刻其他线程的读操作会被阻塞。
公平锁与非公平锁:一般情况下非公平锁性能比非公平锁高
private ReentrantLock lock = new ReentrantLock();//默认为非公平锁
private ReentrantLock lock1 = new ReentrantLock(true);//设置为公平锁
- 非公平锁:一般默认是非公平锁,就是对于不同的线程请求获取同一个锁并不是公平来分配的,一般都是在这个锁的等待队列中随机挑选一个,并且与优先级也是有一点关系的,这种方式的话对于某些线程可能是不太公平的。
- 公平锁:很公平的,按照时间的先后顺序,保证先到先得,后到后得,特点是不会产生饥饿现象,只要你排队最终还是能够等到资源的。
- 哪些具有公平、非公平锁?如
ReentrantLock
,ReentrantReadWriteLock
,默认是非公平锁。
- 哪些具有公平、非公平锁?如
互斥锁:对象互斥锁,用来保证共享数据操作的完整性,每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。实现同步方法。
自旋锁:与互斥锁相同,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。
- 如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;
- 如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
- 实际应用:原子包中的原子类许多方法都采用了自旋的操作,如原子引用
AtomicReference
类中的getAndSet()
,以及通过使用cas操作我们来自定义锁,如下面一小节使用。
可重入锁与不可重入锁
可重入锁:可重入锁是可以进行反复进入的(上A锁内可以多次使用A锁,但需要注意释放否则会有阻塞),仅局限于在一个线程中。
- 如
synchronized
、ReentrantLock
不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
- 可定义实现。可见 Java不可重入锁和可重入锁理解
可重入锁(递归锁)
demo见demo9中的
SynchronizedTest.java
、`ReentrantLockTest.java
- 例如
synchonzied
关键字(可自动释放锁)、ReentrantLock
(需要手动上锁解锁)都是可重入锁。
/**
* @ClassName Test
* @Author ChangLu
* @Date 2021/4/6 9:33
* @Description Synchronzied(可重入锁)测试
*/
//测试synchronized本质可重入锁
public class SynchronizedTest {
public static void main(String[] args) {
new SynchronizedTest().use1();
}
//锁为LockTest实例
public synchronized void use1(){
System.out.println("首次上锁使用use1()方法");
use2();//调用了同步方法
}
//该方法锁依旧为LockTest实例
public synchronized void use2(){
System.out.println("二次上锁使用use2()方法");
}
}
/**
* @ClassName ReentrantLockTest
* @Author ChangLu
* @Date 2021/4/6 9:40
* @Description ReentrantLock(可重入锁)测试
*/
public class ReentrantLockTest {
//创建一个可重入锁
private ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
new ReentrantLockTest().use1();
}
public void use1() {
lock.lock();//上锁
try {
System.out.println("use1()方法使用");
use2();//使用use2()方法
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
}
public void use2() {
lock.lock();//上锁
try {
System.out.println("use2()方法使用");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//解锁
}
}
}
说明:通过上面例子能够看到在一个线程中若某一方法使用了锁A,在其中也可以重复使用锁A,但需要注意的是ReentrantLock
需要手动释放,上多少锁就要释放多少锁!synchronized
不用担心因为其是自动释放锁的。
- 获取锁次数>释放锁次数(一个线程中):由于释放其中锁相当于线程还持有这个锁,其他线程无法进入临界区,就会阻塞。
- 获取锁次数<释放锁次数(一个线程中):会报出一个异常
java.lang.IllegalMonitorStateException
(非法监控状态异常)。
结论:使用可重入锁在一个线程中可以多次获得同一把锁,那么也需要释放同样把锁,否则会出现阻塞或者异常问题。
乐观锁与悲观锁
乐观锁与悲观锁
悲观锁:很悲观,每次拿数据时都会认为其他别的线程会修改该数据,因此会上锁(操作之前上锁)。抢到锁的线程执行过程中,其他想要获得该锁的线程都是阻塞挂起状态。
核心
:不支持多并发,是单线程操作,通过抢占时间片方式来获取锁的使用权,并发变串行。优点
:保证了线程安全和数据安全。应用场景
:适用于多写的场景,例如mysql中的行锁、表锁、读锁、写锁,java中的synchronized
关键字。
乐观锁:很乐观,每次拿数据都会认为别的线程不会修改该数据,因此不会给数据上锁。自旋锁也是乐观锁一种。
- 额外操作:虽说不会上锁,但会在数据更新时判断在此期间其他线程有没有对该数据做更新,最终通过线程的逐一更新获取数据的最终值。
- 判断是否更新的操作哪些?①version版本号机制(运用于SQL命令,对应SQL语句可见面试必问的CAS,你懂了吗?)、②CAS算法。
核心
:支持多线程并发,每个线程在不同的时间节点对数据做更新操作,每次更新时候会判断其他线程是否对数据做更新。应用场景
:适用于多读的场景,获取数据不再创建、销毁锁,减少了使用锁的情况,加大数据的吞吐量。如Redis
等非关系型数据库。(Redis是单线程操作,将事务封闭在单一线程中,避免了线程的安全问题)
自旋锁(含自定义自旋锁)
自旋锁:一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁,若是线程A首先获得了自旋锁,接着线程B也想获得该自旋锁,此时由于该自旋锁已经有保持着,那么线程B的获取锁操作会一直自旋在那里,一直到自旋锁的保持者A释放了该锁,线程B才能获取到!
JDK中自旋锁的实例说明
在原子包java.util.concurrent.atomic
中的getAndSet()方法中有自旋锁的体现:
//AtomicReference类(原子包下)
public final V getAndSet(V newValue) {
return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
}
//Unsafe类
public final Object getAndSetObject(Object var1, long var2, Object var4) {
Object var5;
//do while形式为自旋锁的一种体现
do {
var5 = this.getObjectVolatile(var1, var2);//获取对应原子类实例中的值
} while(!this.compareAndSwapObject(var1, var2, var5, var4));//进行比较交换操作(cas操作),若是比较交换失败,会不断重复执行直至交换值为止!
return var5;
}
说明:这是官方提供的一种自旋锁的体现,我们也可以进行自定义锁来模拟上锁、解锁操作(借助cas)。
自定义锁(自旋锁)
demo见demo9中的
MySpinLock.java
程序描述:自定义自旋锁,并且自定义其中的上锁与解锁,通过使用AtomicReference
中的cas
操作,在上锁过程中将执行线程作为原子引用的值(null->执行线程),其是while()
循环操作。解锁操作就是将原子引用类中的值设置为null
(执行线程->null)即可表示解锁。
- 若已经有一个线程占有了自旋锁,那么其他线程想要使用该锁的需要不断的进行重复请求,保持自旋的状态。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* @ClassName SpinLockTest
* @Author ChangLu
* @Date 2021/4/6 10:20
* @Description 自定义自旋锁(借助cas)
*/
public class MySpinLock {
//默认其中值为null
AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();
//上锁
public void myLock() {
System.out.println(Thread.currentThread().getName()+"开始准备上锁");
//只有当其中值为null时才能够进行更改,相当于进行上锁
//若是一直执行不了cas操作,那么就会一直处于循环阻塞
while (!threadAtomicReference.compareAndSet(null,Thread.currentThread())){
//用于提示阻塞效果!!!进行延时,若是过多调用compareAndSet程序会终止
System.out.println(Thread.currentThread().getName()+"阻塞等待锁中....");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"已经上锁");
}
//解锁
public void myUnlock(){
//将原子引用中的线程设置为null,表示为解锁
threadAtomicReference.compareAndSet(Thread.currentThread(),null);
System.out.println(Thread.currentThread().getName()+"已解锁");
}
//测试一下自定义自旋锁通过使用cas来进行上锁、解锁
public static void main(String[] args) throws InterruptedException {
//自定义锁
MySpinLock lock = new MySpinLock();
new Thread(()->{
lock.myLock();//进行上锁
try {
System.out.println(Thread.currentThread().getName()+"执行任务....");
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnlock();//进行解锁
}
},"A").start();
//确保线程A先执行
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
lock.myLock();//进行上锁
try {
System.out.println(Thread.currentThread().getName()+"执行任务....");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnlock();//进行解锁
}
},"B").start();
}
}
- 线程B需要在线程A释放了自旋锁之后才能够执行。
知识补充
Java
无法开线程,Thread
线程中start
方法中实际上调用的是本地方法native void start0()
,该方法是调用的本地C++方法。
线程创建规范:一般对于多线程使用时调用的方法其类不应该在Thread
实现类或者是实现Runnable
接口的类中。这样会造成代码的耦合性。
//一般如下写
public class Main {
public static void main(String[] args) {
new Thread(()->{....}).start();
}
}
class OtherClass{
public void test(){
...
}
}
Runtime
可通过该类来动态获取一些虚拟机的信息,例如处理器数量等
Runtime
类:运行时类。
介绍:每个Java
应用程序都有一个Runtime
类的Runtime
,允许应用程序与运行应用程序的环境进行接口。
//cpu密集型、IO密集型
System.out.println(Runtime.getRuntime().availableProcessors());//返回可用于Java虚拟机的处理器数量,即你电脑的核心数量
应用场景:在创建线程池时可以进行使用其来动态获取对应java虚拟机可用的处理器数量。
上下文切换
在进程中包含了多个线程,线程可以进行并发执行,即不断的进行上下文切换不同的线程执行,宏观上来看是在同时进行的,实际上是在不断交替执行,每次交替的时间很短几十毫秒而已,就会给我们一种错觉是在同时进行的。
那么我们来了解一下上下文切换做了哪些操作呢?
- 调度操作只能由核心态来执行,所以在线程不断被调度获取对应的时间片这个过程实际上就是用户态与核心态不断切换的过程,如上首先由核心态进行调度指定的时间片,接着切换为用户态来执行指定的迅雷线程,当执行完之后挂起保存对应线程的信息又切换为核心态进行调度再切换为用户态执行QQ线程,这一切换过程指的就是上下文切换。
上下文切换过程中需要进行挂起线程,保存线程信息,当重新执行被挂起线程还要进行load加载等操作,即进行切换核心态、用户态操作,实际上是特别耗资源的操作。
- 一定有用户态与内核态的相互转化。
CPU多层缓存架构
介绍CPU的三级缓存
速度排名:CPU->内存->硬盘
CPU处理内存数据时,为了提升运行性能,设计了多级缓存策略,在每个CPU中都包含了一级缓存、二级缓存、三级缓存,若是CPU想要处理某个值时首先会去一级缓存找,若找不到去二级缓存,再找不到去三层,若还是找不到就会去主存中找:
- CPU查找顺序:
CPU->L1->L2->L3->内存->硬盘
- 图片引用 b站IT楠老师—java多线程 视频中的课件
缓存一致性协议
在执行程序时,每一条指令都是在CPU中执行的,执行指令过程中会包含读取与写入的操作。一般来说程序运行过程中的临时数据都存放在主存(即内存)中,由于CPU执行速度很快,但每次从内存读取数据和向内存写入数据的过程与CPU执行指令速度比起来要慢的多,若是每次对数据的操作都与内存进行交互的话,会大大降低指令执行的速度,为了提升性能,CPU中出现了高速缓存(cache
,现在一般都有三级缓存)!
- CPU有了高速缓存之后,但程序在运行时,会将运算需要的数据从主存中复制一份到CPU的高速缓存中,接着进行读取与写入操作即就从其高速缓存中进行,当运算结束后,会将高速缓存中的数据刷新到主存中。
举例执行i=i+1
,该条指令在单线程中并不会有问题,但是在多线程中就会出现线程安全问题!
问题描述:由于是多线程,可能多个线程会同时拷贝一份主存中的对应变量,接着在线程中不断对对应自己线程的副本进行读取写入操作,当多个线程执行完之后,重新刷新高速缓存中的值到主存,这时候就会出现缓存不一致问题!
解决缓存不一致问题方法:硬件层面提供方法
- 通过在总线加LOCK#锁的方式。
- 通过缓存一致性协议。
由于使用第一种方法会造成在锁住总线期间,其他CPU无法访问内存,导致效率低下,接着就出现了缓存一致性协议!这里介绍Intel的MESI
协议:
MESI协议 规定每条缓存都有一个状态位(额外两位表示),该状态位可对应四种状态:
①修改态(Modified):此缓存被修改过,与主存数据不一致,为此缓存专有。
②专有态(Exclusive):此缓存与主内存一致,但是其他CPU中没有。
③共享态(Shared):此缓存与主内存一致,但也出现在其他缓存中。
④无效态(Invalid):此缓存无效,需要从主内存重新读取
核心思想:当CPU写数据时,若发现操作的变量是共享变量(其他线程也包含该副本),会发出信号通知其他CPU将该变量的缓存行执行无效状态,当其他CPU读取这个变量时会发现自己缓存中的对应变量缓存是无效的,那么就会从内存中重新读取。
说明:这仅仅是CPU
硬件层面的,对于Java
的话需要去知晓对应的Java
内存模型(JMM
)对应的规范说明。
导致的问题
伪共享
:缓存中的数据与主存中的数据不是实时同步的,各个CPU缓存之间的数据也不是同步的,在同一时间点,各个CPU所看到的同一内存地址的数据可能是不一致的。
指令重排
:CPU为了提升指令执行的性能,会对一些没有依赖关系的指令进行指令重排,在多核多线程情况下就可能会因为指令重排的问题导致问题出现,即线程安全问题。
一、初识JUC
1.1、JUC是什么?
并发编程的本质:充分利用CPU的效率。
JUC
:就是java.util .concurrent
工具包的简称,是一个处理线程的工具包,在jdk1.5
版本时就引出的。其有三个主要的包分别是:并发包、原子包、锁。
- 第四个包是函数式接口包,在其他三个并发包中经常使用。
1.2、JUC三个包介绍
java.util.concurrent包
认识
java.util.concurrent
包
在这个包下包含了阻塞队列(双端队列)、完成的未来、并发集合、线程池等接口;包含了对应的实现类,如一些处理并发的集合类、数组阻塞队列、并发HashMap、List、Set集合、同步工具辅助类、并行计算池等等。
TimeUnit
:枚举类,表示给定粒度单位的持续时间,包含纳秒、毫秒、分钟、小时、天对应的实例。内部封装了sleep()
方法,使我们设置睡眠时间更加的方便。
TimeUnit.DAYS.sleep(3);//睡三天
TimeUnit.HOURS.sleep(3);//睡3小时
java.util.concurrent.locks包(含两个模板)
认识
java.util.concurrent.locks
包
首先看下该包中的有哪些接口与实现类:看这名字就知道是包中都是一些锁的相关内容
Condition
:精确通知Lock
:lock接口,下面是对应的一些实现类ReentrantLock
:可重入锁(常用)ReentrantReadWriteLock.ReadLock
:可重用读写锁的读锁,用于处理高性能ReentrantReadWriteLock.WriteLock
:可重用读写锁的写锁
ReadWriteLock
:读写锁
介绍一下Lock
接口中的常用方法:
lock()
:上锁。tryLock()
:方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。tryLock(long time, TimeUnit unit)
:方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
unlock()
:释放锁。
注意点:在并发包中提供了一些锁,如可重入锁,读写锁,这些锁与synchronized关键字不同的是,其需要手动上锁与解锁,一般在try-catch-finally
中的finally
里释放锁,核心代码写在try
中。
模板1如下:
//主要三步骤:①Lock锁实例化 ②上锁 ③释放锁
class LockDemo{
Lock lock = new ReentrantLock();//①可重入锁实例化
//使用lock锁
public void sellTicket(){
lock.lock();//②上锁
try {
....//核心代码
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//③释放锁
}
}
}
- 不释放锁的话会出现死锁状况。Lock锁在异常情况下不会释放锁,因此将释放锁的操作放在finally()中。
模板2如下:
if(lock.tryLock()){//尝试获取锁,可设置时间
try {
....//核心代码
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//③释放锁
}
}else{
//如果不能获取锁,则直接做其他业务
}
上面可以说是可重入锁的小例子,我们看下可重入锁ReentrantLock
的创建实例时的源码:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();//创建ReentrantLock时默认默认使用的是非公平锁
}
//可通过传入一个布尔值来创建公平或非公平锁
//true:公平锁 false:非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
java.util.concurrent.atomic包
该包中主要都有一些原子类,一般在并发中用于代替一些基本类型、引用类型的复合操作,避免出现线程安全问题:
分类如下:
1.3、synchronized与lock区别
之前我们让线程进行同步主要使用的是synchronized
关键字,在并发包中提供了一些锁来提升并发时的性能,并且在并发过程中能够通过锁来做更多细粒度的事情以及实现一些扩展功能,首先我们来看下synchronized
关键字与使用lock
实现类的区别?
1、Synchronized
是内置的java
关键字;Lock
是一个java
类。
2、Synchronized
无法判断获取锁的状态;Lock
可以判断是否获取到了锁(tryLock
)。
3、Synchronized
会自动释放锁;Lock
必须要手动释放锁,否则出现死锁。
4、Synchronized
线程1(获得锁,阻塞) 线程2(无法获取锁一直等待下去);Lock 通过设置不同锁就不会一直等待下去。
5、Synchronized
本身是可重入锁,不可以中断,非公平锁;Lock
可重入锁,可以判断锁,可设置公平或非公平。
二、生产者消费者问题
2.1、synchronized实现
demo见demo2目录中的
Synchronizeddemo1.java
程序描述:通过使用add()
方法作为生产操作、minus()
模拟消费操作。
class Data{
private int num;
//+1操作
public synchronized void add() throws InterruptedException {
//num≠0阻塞释放锁
if (num != 0){
wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"=>"+num);
notifyAll();
}
//-1操作
public synchronized void minus() throws InterruptedException {
//num为0阻塞释放锁
if (num == 0){
wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"=>"+num);
notifyAll();
}
}
测试程序:
public class SynchronizedDemo1 {
public static void main(String[] args) {
Data data = new Data();
//一个线程为生产者、一个线程为消费者
//仅有两个线程时不会出现问题,当出现三四个线程时会出现安全问题
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.minus();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
// new Thread(()->{
// for (int i = 0; i < 20; i++) {
// try {
// data.add();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// },"C").start();
//
// new Thread(()->{
// for (int i = 0; i < 20; i++) {
// try {
// data.minus();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// },"D").start();
}
}
说明:在上面代码中会出现线程安全问题
- 当有两个线程时(分为作为消费、生产且消费、生产次数一致时)不会出现线程安全问题(若次数不一致有阻塞问题);
- 若有四个线程时(如两个线程进行消费、两个线程进行生产)则会出现线程安全问题(也可能会有阻塞问题)。
以上代码两个线程不会出现安全问题:
若是四个线程就会出现安全问题,甚至还可能出现阻塞问题:
问题原因:首先wait()
时是该线程进入阻塞并且释放锁,在四个线程(其中两个生产、两个消费),若是一个生产线程进行if()
判断通过进入阻塞释放锁,此时另一个生产线程也进行if()
判断进入阻塞释放锁,说明此时num>0
,接着消费线程只会判断num==0
才会阻塞否则进行了num--
,使用notifyAll()
将其他两个生产线程同时唤醒此时由于只是一层if
判断所以会直接执行下面的num++
,就会导致出现进行2次相加甚至是多次相加。
解决方案:
- ①使用
if..else..
,将下面执行操作放置到else中,不过这样的话会有很多生产、消费操作无效。 - ②直接将
if
直接改为while
,当被唤醒时会再次判断是否满足条件,就不会出现线程安全问题。
demo见demo2目录下的
Synchronizeddemo2.java
,这里仅列出更改部分代码:
class Data2{
private int num;
//+1操作
public synchronized void add() throws InterruptedException {
//使用while确保在被唤醒时不会直接执行下面num++
while (num != 0){
wait();
}
num++;
System.out.println(Thread.currentThread().getName()+"=>"+num);
notifyAll();
}
//-1操作
public synchronized void minus() throws InterruptedException {
//使用while确保在被唤醒时不会直接执行下面代码
while (num == 0){
wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"=>"+num);
notifyAll();
}
}
测试代码:使用四个线程来测试,效果如下
说明:可以看到修改为while()
过后解决了多个生产线程、消费线程出现的线程安全问题!
2.2、Lock实现(ReentrantLock)
实现生产者消费者
使用synchronized
实现生产者消费者,需要使用到wait()
与notify()
配合进行。
现在使用并发包中的Lock
锁实现时,同样也需要配合Condition
类中的方法来进行阻塞、唤醒操作达到与之前同样的效果。
Lock
锁搭配使用的await()
方法、signalAll()
方法都是通过Condition
实例调用的。Lock
相关锁、Condtion
类都是属于java.util.concurrent.locks
包下的。
demo见demo2目录中的
LockDemo1.java
程序描述:其中Condition
实例是通过ReentrantLock
实例(可重入锁,该实例为非公平锁)调用newCondition()
获取,其await()
与signalAll()
使用效果与之前wait()
与notify()
方法效果一致,代码如下:
class Data3{
private int num;
private Lock lock = new ReentrantLock();//①可重入锁实例化
Condition condition = lock.newCondition();//通过指定锁来获取Condition实例(ConditionObject实例)
//生产
public void add() throws InterruptedException {
lock.lock();//上锁
try {
//核心业务
while (num != 0) {
condition.await();//阻塞
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
condition.signalAll();//唤醒所有等待线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
//消费
public void minus() throws InterruptedException {
lock.lock();//上锁
try {
while(num == 0){
condition.await();//阻塞等待
}
num--;
System.out.println(Thread.currentThread().getName()+"=>"+num);
condition.signalAll();//唤醒所有线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
}
Lock
锁使用过程一般是先上锁,接着将释放锁写在finally
中,核心业务一般写在try()
中,await()
与signalAll()
用于与之前使用的wait()
与notifyAll()
方式相同。
测试程序:
public class LockDemo {
public static void main(String[] args) {
Data3 data = new Data3();
//多个生产线程、消费线程测试
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.minus();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 20; i++) {
try {
data.minus();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
- 可以看到输出结果与之前的大致相同,但是线程的执行是随机的状态,通过Lock锁实现还可以对其线程的顺序进行指定。
通过Condition实现精准通知唤醒
demo见demo2目录下的
LockDemo2.java
Condition
(同步监视器):精准通知和唤醒线程,能够更好的控制Lock。
下面的例子目的是想通过Condition
同步监视器来精确通知唤醒指定线程,来通过多线程精确执行指定任务:
//多线程下执行顺序为A->B->C
class Work{
private Lock lock = new ReentrantLock();//可重入锁
//创建三个同步监视器,通过对某个同步监视器进行阻塞唤醒来达到指定任务执行
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
//通过num的值来让指定线程进入到阻塞状态
private int num = 1;
public void printA(){
lock.lock();//上锁
try {
//业务(判断、执行、通知)
while (num != 1){
condition1.await();//进入阻塞
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
num = 2;
condition2.signal();//唤醒condition2
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
public void printB(){
lock.lock();//上锁
try {
//核心业务
while (num != 2){
condition2.await();//进入阻塞
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBB");
num = 3;
condition3.signal();//唤醒condition3
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
public void printC(){
lock.lock();//上锁
try {
while (num != 3){
condition3.await();//进入阻塞
}
System.out.println(Thread.currentThread().getName()+"=>CCCCCCC");
num = 1;
condition1.signal();//唤醒condition1
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//释放锁
}
}
}
- 可以将其想象为一条生产线:下单->支付->交易->物流,不过实际这种场景并不会像这么简单。
测试程序:
public class LockDemo2 {
public static void main(String[] args) {
Work work = new Work();
//三个线程进行执行
new Thread(()->{for (int i = 0; i < 20; i++) work.printA();},"1").start();
new Thread(()->{for (int i = 0; i < 20; i++) work.printB();},"2").start();
new Thread(()->{for (int i = 0; i < 20; i++) work.printC();},"3").start();
}
}
三、8锁问题
理清synchronized
使用于普通方法、静态方法时使用什么作为锁的概念即可!
- 作用于普通方法,将
this
(调用方法的实例本身)作为锁。如:public synchronized void method(){}
- 作用于静态方法,将其方法
类.class
作为锁。如:public synchronized static void method(){}
可通过8个不同例子来进行练习:关于8锁问题详细介绍
参考资料
[1]. 并发容器之CopyOnWriteArrayList
[3]. ConcurrentModificationException异常原因和解决方法
[4]. FutureTask的用法及两种常用的使用场景 实际使用的场景
[5]. jdk1.8 探讨FutureTask的两个问题 只执行1次与get()获取返回值阻塞问题
[6]. CountDownLatch的两种使用场景 包含源码解析、使用场景及与join区别
[7]. CountDownLatch踩过的坑 解决dubbo线程池满的线上问题
[8]. 深入理解CyclicBarrier原理 包含源码分析
[9]. 理解Semaphore及其用法详解
[10]. 读写锁ReadWriteLock的实现原理 读写锁详细介绍,含源码
[11]. java并发之SynchronousQueue实现原理
[12]. Executors的四种线程池 简单介绍原理、应用场景及执行流程
[13]. 线程池使用:CPU密集型和IO密集型 介绍了两种类型设置线程的数量,根据应用情况来定,其中还介绍了一些场景分析
[14]. 【并发编程】IO密集型和CPU密集型任务 两种类型描述的比较清晰
[15]. Fork/Join框架基本使用 包含原理分析、执行过程,fork()与join()方法,两个例子说明,包含一个使用ForkJoin来解决归并排序问题性能更高(并行处理)
[16]. JDK1.8新特性CompletableFuture总结
[17]. 面试必问的CAS,你懂了吗? 包含源码分析最主要的是其中的汇编源码分析,三个缺点的详细说明
[18]. Java并发之CAS理解 描述简洁,也推荐看
[19]. 悲观锁 & 乐观锁的原理及应用场景 两种锁的介绍说明及应用场景
[20]. 自旋锁—百度百科
[21]. 缓存一致性协议(MESI协议) 介绍了CPU缓存一致性协议
- 点赞
- 收藏
- 关注作者
评论(0)