Java并发编程 面试题

基础知识点

进程

我们自己写的程序,也就是所谓的用户程序是由操作系统来管理的,人们把一个执行着的程序叫做一个进程(英文名:Process),每个进程都有这么两个特点:
1.资源所有权
程序在运行过程中需要一定的资源,比如内存、I/O啥的,这些东西不能在不同进程间共享,假如一个进程占了另一个进程的内存,那另一个进程的数据不就丢失了么;一个进程正在使用打印机输出东西,另一个进程也使用的话,不就尴尬了么。所以进程所拥有的这些资源是不能共享的,而这种资源分配的活是由操作系统来管理的。
2.能作为操作系统调度/执行的单位
操作系统会为它管理的进程分配时间片,来调度哪个进程应该被处理器处理,哪个应该先休息一会儿。

现代的好多操作系统已经把这两个特点给拆开了,可以被调度和执行的单位通常被称作线程或者轻量级进程,而拥有资源所有权的单位通常被称为进程。所以我们现在电脑里每个运行着的程序都是一个进程,可以打开你的任务管理器(windows)或者活动监视器(mac),看到我们的电脑里其实有好多好多进程喔,什么QQ、微信、音乐播放器、视频播放器啥的。

进程的状态

Java并发编程 面试题_第1张图片

Java并发编程 面试题_第2张图片

线程

Java并发编程 面试题_第3张图片

任务

Java并发编程 面试题_第4张图片

Java并发编程 面试题_第5张图片

线程的休眠

Java并发编程 面试题_第6张图片

守护线程

Java并发编程 面试题_第7张图片

共享变量的含义

Java并发编程 面试题_第8张图片

i++这个操作不是一个原子性操作

Java并发编程 面试题_第9张图片

Java并发编程 面试题_第10张图片

为什么一个对象就可以当作一个锁呢?我们知道一个对象会占据一些内存,这些内存地址可是唯一的,也就是说两个对象不能占用相同的内存。真实的对象在内存中的表示其实有对象头和数据区组成的,数据区就是我们声明的各种字段占用的内存部分,而对象头里存储了一系列的有用信息,其中就有几个位代表锁信息,也就是这个对象有没有作为某个线程的锁的信息。

synchronized(同步执行)

如果一个线程获取某个锁之后,就相当于把厕所门儿给锁上了,其他的线程就不能获取该锁了,进不去厕所只能干等着,也就是这些线程处于一种阻塞状态,直到已经获取锁的线程把该锁给释放掉,也就是把厕所门再打开,某个线程就可以再次获得锁了。这样线程们按照获取锁的顺序执行的方式也叫做同步执行(英文名就是synchronized),这个被锁保护的代码块也叫做同步代码块,我们也会说这段代码被这个锁保护。由于如果线程没有获得锁就会阻塞在同步代码块这,所以我们需要格外注意的是,在同步代码块中的代码要尽量的短,不要把不需要同步的代码也加入到同步代码块,在同步代码块中千万不要执行特别耗时或者可能发生阻塞的一些操作,比如I/O操作啥的。

同步方法

Java并发编程 面试题_第11张图片

内存可见性

地址

一个程序的好坏

一个程序的好坏可以从两个方面来考虑,一方面是从运行时间上考虑,也就是对于给定的任务,在资源一定的情况下,完成任务所用的时间长短;另一方面是从吞吐量上考虑,也就是处对于给定的时间,在资源一定的情况下,可以完成任务的数量多少。如果一个程序的运行时间越短,吞吐量越大,我们就说这个程序的性能越好。很显然,为了提高处理器的利用率而引入的线程是为了让程序的性能提高,但是线程本身会存在着一些开销,如果引入线程的开销大于提升处理器利用率的开销,程序的性能是会降低的,所以我们需要分析一下线程有哪些开销,并且针对这些开销来做一些工作来提升并发程序的性能。

阻塞队列的take和put方法

地址

CPU上下文切换

地址

锁的重入

Java并发编程 面试题_第12张图片

面试

地址

线程池工作原理

全面理解Java内存模型

为什么使用线程池?

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

线程池工作原理

线程池详解
线程池核心参数
二、线程池工作流程

(1)当线程池小于corePoolSize时,,即使此时线程池中存在空闲线程,新提交任务将创建一个新线程执行任务

2)如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;

3)如果阻塞队列满了,那就创建新的线程执行当前任务,直到线程池中的线程数达到maxPoolSize,这时再有任务来,由饱和策略来处理提交的任务

Java线程池种类、区别和适用场景

地址

线程的生命周期

下图是进程的状态,由于线程只是单纯的继承了进程中调度和执行的特性,所以原先进程拥有的状态,现在线程—样拥有,也就是:创建、就绪、执行、阻塞、退出。

Java并发编程 面试题_第13张图片

Java并发编程 面试题_第14张图片

线程中断

地址

创建线程有几种方式

地址

死锁

举一个很好的例子:
Java并发编程 面试题_第15张图片

Java并发编程 面试题_第16张图片

何为死锁:每个线程都拥有其他线程需要的资源,同时又等待其他线程已经拥有的资源,并且每个线程在获得全部需要的资源之前不会释放已经拥有的资源,若干线程之间形成一种头尾相接的循环等待资源关系

前人已经认真的总结过产生死锁的几个必要条件:这四个条件跟上面那句话一一对应
互斥条件:一个资源每次只能被一个线程使用。
不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

只有这4个条件全部成立,死锁的情况才有可能发生。听清楚了,我说的是才有可能发生。因为一般情况下,一个线程持有资源的时间并不会太长,所以一般并不会发生死锁情况,但是如果并发程度很大,也就是非常多的线程在同时竞争资源,如果这四个条件都成立,那么发生死锁的概率将会很大,重要并且可怕的是:一旦系统进入死锁状态,将无法恢复,只能重新启动系统。

如何避免死锁:
地址

1.死锁预防-破坏死锁产生的四个必要条件(线程申请资源前)
2.避免死锁-使用银行算法在使用前进行判断,只允许不会产生死锁的进程申请资源(线程申请资源中)
3 .死锁检测与解除 ----- 在检测到运行系统进入死锁,进行恢复.(线程申请到资源后)

线程中start和run的区别

添加链接描述

线程都有哪些方法

1.获取线程ID
Java并发编程 面试题_第17张图片
2.获取和设置线程名称
Java并发编程 面试题_第18张图片

3.获取和设置线程优先级
Java并发编程 面试题_第19张图片

4,休眠
Java并发编程 面试题_第20张图片

5.获取当前正在执行的线程

Java并发编程 面试题_第21张图片

volatile底层原理

Java并发编程 面试题_第22张图片

首先我们先了解下内存屏障,上面讲到了,通过内存屏障可以禁止特定类型处理器的重排序,从而让程序按我们预想的流程去执行。内存屏障,又称内存栅栏,是一个CPU指令,基本上它是一条这样的指令:
1.保证特定操作的执行顺序。
2.影响某些数据(或则是某条指令的执行结果)的内存可见性。

编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。

Memory Barrier所做的另外一件事是强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入 cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。

和java有什么关系?上面java内存模型中讲到的volatile是基于Memory Barrier实现的。

如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,就可以保证:
1.一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
2.在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。

Synchronized的作用有哪些

地址1

ReentrantLock 是如何实现可重入性的

地址

ReentrantLock和synchronized区别

地址

wait()/notify()机制

就是一个线程在获取到锁之后,如果指定条件不满足的话,应该主动让出锁,然后到专门的等待区等待,直到某个线程完成了指定的条件,再通知一下在等待这个条件完成的线程,让它们继续执行。
故事背景:如果你觉得上边这句话比较绕的话,我来给你翻译一下:当上狗哥获取到厕所门锁之后,如果厕所处于不可用状态,那就主动让出锁,然后到等待上厕所的队伍里排队等待,直到维修工把厕所修理好,把厕所的状态置为可用后,维修工再通知需要上厕所的人,然他们正常上厕所。

为了实现这个构想:java里提出了一套叫wait/notify的机制。当一个线程获取到锁之后,如果发现条件不满足,那就主动让出锁,然后把这个线程放到一个等待队列里等待去,等到某个线程把这个条件完成后,就通知等待队列里的线程他们等待的条件满足了,可以继续运行啦!
如果不同线程有不同的等待条件肿么办,总不能都塞到同一个等待队列里吧?是的,java里规定了每一个锁都对应了一个等待队列,也就是说如果一个线程在获取到锁之后发现某个条件不满足,就主动让出锁然后把这个线程放到与它获取到的锁对应的那个等待队列里,另一个线程在完成对应条件时需要获取同一个锁,在条件完成后通知它获取的锁对应的等待队列。这个过程意味着锁和等待队列建立了一对一关联。

Java并发编程 面试题_第23张图片

wait(),notify()和suspend(),resume()之间的区别

地址

Runnable和 Callable有什么区别

(1) Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void)

(2)Callable规定的方法是call(),Runnable规定的方法是run()

(3)call方法可以抛出异常,run方法不可以

(4)加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法

volatile和synchronized的区别是什么

1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
4.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

线程执行顺序怎么控制?

地址

守护线程

护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程,这是它的作用——而其他的线程只有一种,那就是用户线程。所以java里线程分2种,

1、守护线程,比如垃圾回收线程,就是最典型的守护线程。

2、用户线程,就是应用程序里的自定义线程。

用户也可以在应用程序代码自定义守护线程,只需要调用Thread类的设置方法设置一下即可

线程间通信方式

1.通过synchronized关键字这种方式来实现线程间的通信。
2.通过volatile关键字这种方式来实现线程间的通信。
3.wait/notify机制
4.管道通信:就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信
分布式系统中说的两种通信机制:共享内存机制和消息通信机制,
synchronized、volatile属于有共享内存机制,它们通过判断这个“共享的条件变量“是否改变了,来实现进程间的交流。
而管道通信,更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个。

实现互斥的几种方式(思维导图)

Java并发编程 面试题_第24张图片

Threadlocal原理

地址
地址

ThreadLocal内存泄漏的原因

在线程池中线程的存活时间太长,往往都是和程序同生共死的,这样 Thread 持有的 ThreadLocalMap 一直都不会被回收,再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。
Entry 中的 Value 是被 Entry 强引用的,即便 value 的生命周期结束了,value 也是无法被回收的,导致内存泄露。

ThreadLocal使用场景有哪些

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

锁的分类

一. 公平锁和非公平锁

公平锁表示线程获取锁顺序是按照线程加锁的顺序来分配的,即FIFO顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的。有可能后申请的线程比先申请的线程优先获取锁,可能会造成优先级反转或者饥饿现象。
在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。
对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

二.共享锁和独享锁

共享锁也叫S锁,读锁,该锁可以被多个线程持有;

独享锁也叫X锁,写锁,排他锁,该锁只能被一个线程持有。

共享锁【S锁】
若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

排他锁【X锁】
又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。

对于ReentrantLock和Synchronized而言,是独享锁,读读、读写、写写的过程都是互斥的。对于ReadWriteLock而言,读锁是共享锁,写锁是独享锁,读锁的共享锁可以保证并发读是非常高效的,在读写锁中,读读不互斥、读写、写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

读写锁ReentrantReadWriteLock的应用场景:

读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,我们只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁。

三. 乐观锁和悲观锁
悲观锁:
总是假设最坏的情况,每次拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想要拿到它的数据就会被一直阻塞直到它拿到锁,传统的关系型数据库里面就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。再比如java里面的synchronized关键字的实现也是悲观锁。
乐观锁:
顾名思义,很乐观,每次拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会去判断一下别人有没有修改这个数据,可以使用版本号等机制。乐观锁适用于多读的应用场景,这样可以提高吞吐量,像数据库提供的类似于write_condition机制就是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
4.1 悲观锁的缺点
悲观锁通过加锁的方式限制其他人对数据的操作,而乐观锁不会加锁,也就放宽了别人对数据的访问。使用悲观锁会引发一些问题:
在多线程竞争下,加锁、释放锁会造成比较多的上下文切换和调度延时,引起性能问题;
一个线程持有锁,会导致其他所有需要此锁的线程挂起;
如果一个优先级高的线程等待一个优先级底的线程的锁,会导致优先级倒置,引起性能风险。

对比于悲观锁的这些问题,一个有效的方式就是乐观锁。其实乐观锁就是:每次不加锁而是假设没有并发冲突而去完成某项操作,如果因为并发冲突失败就重试,直到成功为止。
4.2 乐观锁的一种实现方式:CAS

CAS的全称是Compare And Swap,比较和替换。CAS操作包括三个操作数内存值(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。一般配合死循环来不断尝试更新值,直到成功。

相对于synchronized这种阻塞算法,CAS是一种非阻塞算法的常用实现。

CAS实现的过程是:调用java的JNI接口–> 调用c接口 --> 汇编语言调用CPU指令(关键指令:cmpxchg)

CAS的缺点

ABA问题:意思是说当一个线程获取当前的值是A,此时另一个线程先将A变成B,再变成A,之前的线程继续执行,发现值没变还是A,就继续执行更新操作。这样可能会引发一些潜在问题,问题实例可以参考引用。通常各种乐观锁的实现用版本戳来对记录或者对象进行标记,来避免ABA问题,比如可以使用时间戳。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
循环时间开销大:不成功就会一直循环直到成功,如果长时间不成功会给CPU带来非常大的执行开销。如果JVM支持pause指令那么可以一定程度上减少开销。
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

CAS和synchronized的使用场景:

对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

补充: synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

总结一下就是:线程冲突小的情况下使用CAS,线程冲突多的情况下使用synchronized。

CAS

对比于悲观锁的这些问题,一个有效的方式就是乐观锁。其实乐观锁就是:每次不加锁而是假设没有并发冲突而去完成某项操作,如果因为并发冲突失败就重试,直到成功为止。
乐观锁的一种实现方式:CAS

CAS的全称是Compare And Swap,比较和替换。CAS操作包括三个操作数内存值(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。一般配合死循环来不断尝试更新值,直到成功。

相对于synchronized这种阻塞算法,CAS是一种非阻塞算法的常用实现。

CAS实现的过程是:调用java的JNI接口–> 调用c接口 --> 汇编语言调用CPU指令(关键指令:cmpxchg)

CAS的缺点

ABA问题:意思是说当一个线程获取当前的值是A,此时另一个线程先将A变成B,再变成A,之前的线程继续执行,发现值没变还是A,就继续执行更新操作。这样可能会引发一些潜在问题,问题实例可以参考引用。通常各种乐观锁的实现用版本戳来对记录或者对象进行标记,来避免ABA问题,比如可以使用时间戳。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
循环时间开销大:不成功就会一直循环直到成功,如果长时间不成功会给CPU带来非常大的执行开销。如果JVM支持pause指令那么可以一定程度上减少开销。
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

并发工具类

地址

原子变量类

为何需要原子变量类

相关操作

AQS原理?

地址

地址
AQS原理简述思路:
首先我们很明显需要一个状态变量private volatile int state(它就是所谓的同步状态)来表示资源的大小
Java并发编程 面试题_第25张图片
我们会通过acquire方法,acquire方法实际上是通过tryAcquire方法来获取同步状态的,如果tryAcquire方法返回true则结束,如果返回false则继续执行。这个tryAcquire方法就是我们自己规定的获取同步状态的方式。假设现在有一个线程已经获取到了同步状态,而线程t1同时调用tryAcquire方法尝试获取同步状态,结果就是获取失败,会先执行addWaiter方法,这个addWaiter方法就是向队列中插入节点的方法**,也就是说在队列为空的时候会先让head和tail引用指向同一个节点后再进行插入操作**,而这个节点竟然就是简简单单的new Node(),真是没有任何添加剂呀~ 我们先把这个节点称为0号节点吧,这个节点的任何一个字段都没有被赋值,所以在第一次节点插入后,队列其实长这样
Java并发编程 面试题_第26张图片Java并发编程 面试题_第27张图片现在我们重点关注waitStauts为0或者-1的情况。目前我们的当前节点是节点1,它对应着当前线程,当前节点的前一个节点是0号节点。在一开始,所有的Node节点的waitStatus都是0,所以在第一次调用shouldParkAfterFailedAcquire方法时,当前节点的前一个节点,也就是0号节点的waitStatus会被设置成Node.SIGNAL立即返回false,这个状态的意思就是说0号节点后边的节点都处于等待状态,现在的队列已经变成了这个样子
Java并发编程 面试题_第28张图片

如果此时再新来一个线程t2调用acquire方法要求获取同步状态的话,它同样会被包装成Node插入同步队列的,效果就像下图一样:
Java并发编程 面试题_第29张图片

如果一个线程在各种acquire方法中获取同步状态失败的话,会被包装成Node节点放到同步队列,这个可以看作是一个插入过程。有进就有出,如果一个线程完成了独占操作,就需要释放同步状态,同时把同步队列第一个(非0号节点)节点代表的线程叫醒,在我们上边的例子中就是节点1,让它继续执行,这个释放同步状态的过程就需要调用release方法了:这个方法会用到我们在AQS子类里重写的tryRelease方法

我们现在的头节点head指向的是0号节点,它的状态为-1,所以它的waitStatus首先会被设置成0,接着它的后继节点,也就是节点1代表的线程会被这样调用LockSupport.unpark(s.thread),这个方法的意思就是唤醒节点1对应的线程t1,把节点1的thread设置为null并把它设置为头节点,修改后的队列就长下边这样:
Java并发编程 面试题_第30张图片
与独占模式不同的一点是,共享模式可能同时会有多个线程释释放同步状态,也就是可能多个线程会同时移除同步队列中的阻塞节点,哈哈,如何保证移除过程的安全性?这个问题就不看源码了,大家自己尝试着写写。

当一个线程因为某个条件不能满足时就可以在持有锁的情况下调用该锁对象的wait方法,之后该线程会释放锁并进入到与该锁对象关联的等待队列中等待;如果某个线程完成了该等待条件,那么在持有相同锁的情况下调用该锁的notify或者notifyAll方法唤醒在与该锁对象关联的等待队列中等待的线程。
显式锁的本质其实是通过AQS对象获取和释放同步状态,而内置锁的实现是被封装在java虚拟机里的,我们并没有唠叨过,这两者的实现是不一样的。而wait/notify机制只适用于内置锁,在显式锁里需要另外定义一套类似的机制,在我们定义这个机制的时候需要整清楚:在获取锁的线程因为某个条件不满足时,应该进入哪个等待队列,在什么时候释放锁,如果某个线程完成了该等待条件,那么在持有相同锁的情况下怎么从相应的等待队列中将等待的线程从队列中移出。
为了定义这个等待队列,设计java的大叔们在AQS中添加了一个名叫ConditionObject的成员内部类:

public abstract class AbstractQueuedSynchronizer {

    public class ConditionObject implements Condition, java.io.Serializable {
        private transient Node firstWaiter;
        private transient Node lastWaiter;

        // ... 为省略篇幅,省略其他方法
    }
}

很显然,这个ConditionObject维护了一个队列,firstWaiter是队列的头节点引用,lastWaiter是队列的尾节点引用。但是节点类是Node?对,你没看错,就是我们前边分析的同步队列里用到的AQS的静态内部类Node
也就是说:AQS中的同步队列和自定义的等待队列使用的节点类是同一个
ReentrantLock通过newCondition方法来获取到等待队列
我们以下面的代码为例

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

于在初始状态下,没有线程去竞争锁,所以同步队列是空的也没有线程因某个条件不成立而进入等待队列,所以等待队列也是空的,ReentrantLock对象、AQS对象以及等待队列在内存中的表示就如图:
Java并发编程 面试题_第31张图片

当然,这个newCondition方法可以反复调用,从而可以通过一个锁来生成多个等待队列

那接下来需要考虑怎么把线程包装成Node节点放到等待队列的以及怎么从等待队列中移出了。ConditionObject成员内部类实现了一个Condition的接口,这个接口提供了下边这些方法:
可以看到,Condition中的await方法和内置锁对象的wait方法的作用是一样的,都会使当前线程进入等待状态,signal方法和内置锁对象的notify方法的作用是一样的,都会唤醒在等待队列中的线程。

像调用内置锁的wait/notify方法时,线程需要首先获取该锁一样,调用Condition对象的await/siganl方法的线程需要首先获得产生该Condition对象的显式锁。它的基本使用方式就是:通过显式锁的 newCondition 方法产生Condition对象,线程在持有该显式锁的情况下可以调用生成的Condition对象的 await/signal 方法,一般用法如下:

Lock lock = new ReentrantLock();

Condition condition = lock.newCondition();

//等待线程的典型模式
public void conditionAWait() throws InterruptedException {
    lock.lock();    //获取锁
    try {
        while (条件不满足) {
            condition.await();  //使线程处于等待状态
        }
        条件满足后执行的代码;
    } finally {
        lock.unlock();    //释放锁
    }
}

//通知线程的典型模式
public void conditionSignal() throws InterruptedException {
    lock.lock();    //获取锁
    try {
        完成条件;
        condition.signalAll();  //唤醒处于等待状态的线程
    } finally {
        lock.unlock();    //释放锁
    }
}

这里需要特别注意的是:同步队列是一个双向链表,prev表示前一个节点,next表示后一个节点,而等待队列是一个单向链表,使用nextWaiter表示下一个节点,这是它们不同的地方
以上就是Condition机制的原理和用法,它其实是内置锁的wait/notify机制在显式锁中的另一种实现,不过原来的一个内置锁对象只能对应一个等待队列,现在一个显式锁可以产生若干个等待队列,我们可以根据线程的不同等待条件来把线程放到不同的等待队列上去

手撕生产者消费者

地址

如何解决生产者消费者问题

生产者与消费者问题解决方案

管程法

Java并发编程 面试题_第32张图片

Java并发编程 面试题_第33张图片

  • 代码
package com.ryh.lock;

/**
 * @author renyuhua
 * @date 2021年08月15日 15:35
 */
//测试生产者消费者模型,利用缓冲区解决:管程法

    //生产者,消费者,产品,缓冲区
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Productor(container).start();
        new Consumer(container).start();
    }
}

//生产者
class Productor extends Thread{
    SynContainer container;
    
    public Productor(SynContainer container){
        this.container = container;
    }
    
    //生产
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生产了"+i+"只鸡");
        }
    }
}

//消费者
class Consumer extends Thread{
    SynContainer container;

    public Consumer(SynContainer container){
        this.container = container;
    }

    //消费
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了--->"+container.pop().id+"只鸡");
        }
    }
}

//产品
class Chicken{
    int id;//产品编号

    public Chicken(int id){
        this.id = id;
    }
}

//缓冲区
class SynContainer{

    //需要一个容易大小
    Chicken[] chickens = new Chicken[10];
    //容量计数器
    int count = 0;

    //生产者放入产品
    public synchronized void push(Chicken chicken){
        //如果容器满了,就需要等待消费者消费
        if (count==chickens.length){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //通知消费者消费,生产等待
        }
        //如果没有满,我们就需要丢入产品
        chickens[count]=chicken;
        count++;

        //可以通知消费者消费了
        this.notifyAll();
    }
    //消费者消费产品
    public synchronized Chicken pop(){
        //判断者能否消费
        if (count==0){
            //等待生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果可以消费
        count--;
        Chicken chicken = chickens[count];

        //吃完了,通知生产者生产
        this.notifyAll();
        return chicken;
    }

}


信号灯法(加一个标志位)

package com.ryh.lock;

/**
 * @author renyuhua
 * @date 2021年08月15日 21:16
 */
//测试生产者消费者问题:信号灯法,标识位解决
public class TestPc2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生产者-->演员
class Player extends Thread{
    TV tv;
    public Player(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i%2==0){
                this.tv.play("快乐大本营播放中");
            }else {
                this.tv.play("抖音记录美好生活");
            }
        }
    }
}

//消费者-->观众
class Watcher extends Thread{
    TV tv;
    public  Watcher(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//产品-->节目
class TV{
    //演员表演,观众等待 T
    //观众观看,演员等待 F
    String voice;//表演的节目
    boolean flag = true;

    //表演
    public synchronized void play(String voice){

        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("演员表演了"+voice);
        //通知观众观看
        this.notifyAll();//通知唤醒
        this.voice = voice;
        this.flag = !this.flag;//取反
    }

    //观看
    public synchronized void watch(){
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了:"+voice);
        //通知演员表演
        this.notifyAll();
        this.flag = !this.flag;//取反
    }
}

进程间的通信方式

地址

通俗理解进程和线程的区别

看了一遍排在前面的答案,类似”进程是资源分配的最小单位,线程是CPU调度的最小单位“这样的回答感觉太抽象,都不太容易让人理解。
做个简单的比喻:进程=火车,线程=车厢
1.线程在进程下行进(单纯的车厢无法运行)
2.一个进程可以包含多个线程(一辆火车可以有多个车厢)
3.不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
4.进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
5.进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
6.进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
7.进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”
7.进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

进程的地址空间概述

地址

Java提供的并发工具类?

多线程会存在哪些问题?

给变量加锁有哪几种方式

你可能感兴趣的:(并发,Java学习,java,jvm,开发语言)