文章目录
1. 谈谈volatile的理解
volatile是java虚拟机提供的轻量级同步机制,主要特点有三个:
- 保证线程之间的可见性
- 禁止指令重排
- 不保证原子性
谈到volatile关键字,就不得不首先提到java的内存模型(JMM)
1.1 什么是JMM
JMM是java提供的一种抽象模型,主要是通过一组规范来定义程序中的变量的访问方式。(包括实例字段、静态字段等),JMM主要用于线程间的同步,主要做了如下规定:
- 线程解锁前,必须把共享变量刷新到主内存。
- 线程加锁之前,必须读取主内存的最新值到工作内存。
- 加锁、解锁都是操作的同一把锁。
上面提到了主内存和工作内存又是什么东西呢?
JVM运行程序的实体都是线程,每次创建线程的时候,JVM都会给线程创建属于自己的工作内存,注意工作内存是该线程独有的,也就说别的线程无法访问工作内存中的信息。而Java内存模型中规定所有的变量都存储在主内存中,主内存是多个线程共享的区域,线程对变量的操作(读写)必须在工作内存中进行。
比如:如果要操作一个变量,首先需要将变量从主内存中拷贝到自己的工作内存,然后对变量操作,操作完成之后再写会主内存,而不能直接操作主内存中的变量。各个线程中的工作内存存储的都是主内存变量的副本。
上面这种更新或者说读取变量的方式在单线程下不存在问题,但是在多线程下呢?多个线程工作去读取或修改一个变量,如何保证每个线程都能够读取到正确的值呢?
设想一下:存在两个线程A、B,同时从主线程中获取一个对象(i = 25),某一刻,A、B的工作线程中i都是25,A效率比较高,片刻,改完之后,马上将i更新到了主内存,但是此时B是完全没有办法i发生了变化,仍然用i做一些操作。问题就发生了,B线程没有办法马上感知到变量的变化!!
1.1.1 可见性
Volatile就是解决该问题的,保证A线程修改值之后,B线程能够知道参数已经修改了,这就是线程间的可见性。 A修改共享变量i之后,B马上可以感知到该变量的修改。
可见性demo:
第一次number没有加上volatile关键字
import lombok.Data;
import java.util.concurrent.TimeUnit;
/**
* @author lanzhou
* @time 2020/3/15 16:38
* @Description volatile的可见性
*/
public class Juc001VolatileSee {
public static void main(String[] args) {
Resource resource = new Resource();
// 创建一个线程
new Thread(()->{
System.out.println(Thread.currentThread().getName()+ " start");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改资源中的数据
resource.addNum();
System.out.println(Thread.currentThread().getName()+ " update number to " + resource.getNumber());
},"Thread one").start();
// 判断number是否等于0,如果等于零,则主线程一直循环等待中
while (resource.getNumber() == 0){
}
// 观察主线程是否能够感知到number的变化,如果感知到了(可见性),则会到达该步。
System.out.println(Thread.currentThread().getName()+ " finish");
}
}
/**
* 定义线程操作的资源
*/
@Data
class Resource{
private int number = 0;
public void addNum(){
this.number = 60;
}
}
结果如下:
加上volatile关键字之后的执行结果:
1.1.2 原子性
JMM的目的是解决原子性,但volatile不保证原子性。如何保证原子性,下文再继续讲解。
为什么volatile无法保证原子性呢?
因为上述的Java的内存模型的存在,修改一个i的值并不是一步操作,过程可以分为三步:
- 从主内存中读取值,加载到工作内存
- 工作内存中对i进行自增
- 自增完成之后再写回主内存。
每个线程获取主内存中的值修改,然后再写回主内存,多个线程执行的时候,存在很多情况的写值的覆盖。
用下面的例子测试volatile是否保证原子性。
import lombok.Data;
/**
* @author lanzhou
* @time 2020/3/15 17:14
* @Description JMM原子性模拟
*/
public class Juc002VolatileAtomic {
public static void main(String[] args) {
AtomicResource resource = new AtomicResource();
// 利用for循环创建20个线程,每个线程自增100次
for(int i = 0; i < 20; i++){
new Thread(()->{
for (int j = 0; j < 100; j++) {
resource.addNum();
}
},String.valueOf(i)).start();
}
// 用该方法判断上述20线程是否计算完毕,如果小于2,则说明计算线程没有计算完,则主线程暂时让出执行时间
while (Thread.activeCount() > 2){
Thread.yield();
}
// 查看number是否可以保证原子性,如果可以保证则输出的值则为2000
System.out.println("Result = "+resource.getNumber());
}
}
@Data
class AtomicResource{
volatile int number = 0;
public void addNum(){
number++;
}
}
结果如下:
Result = 1906
Process finished with exit code 0
接回上面的问题,如何保证原子性呢?
Juc下面提供了多种方式,比较轻量级的有Atomic类的变量,更重量级的有Synchronized关键字修饰,下面用Atomic来保证原子性的测试:
import lombok.Data;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author lanzhou
* @time 2020/3/15 17:43
* @Description 利用Atomic来保证原子性
*/
public class Juc003VolatileAtomic {
public static void main(String[] args) {
AtomicResource resource = new AtomicResource();
// 利用for循环创建20个线程,每个线程自增100次
for(int i = 0; i < 20; i++){
new Thread(()->{
for (int j = 0; j < 100; j++) {
resource.addNum();
}
},String.valueOf(i)).start();
}
// 用该方法判断上述20线程是否计算完毕,如果小于2,则说明计算线程没有计算完,则主线程暂时让出执行时间
while (Thread.activeCount() > 2){
Thread.yield();
}
// 查看number是否可以保证原子性,如果可以保证则输出的值则为2000
System.out.println("Result = "+resource.getNumber());
}
}
@Data
class AtomicResource{
AtomicInteger number = new AtomicInteger();
public void addNum(){
number.getAndIncrement();
}
}
输出结果如下:
Result = 2000
Process finished with exit code 0
1.1.3 禁止指令重排
计算机在底层执行程序的时候,为了提高效率,经常会对指令做重排序,一般重排序分为三种
- 编译器优化的重排序
- 指令并行的重排
- 内存系统的重排
单线程下,无论怎么样重排序,最后执行的结果都一致的,并且指令重排遵循基本的数据依赖原则,数据需要先声明再计算;多线程下,线程交替执行,由于编译器存在优化重排,两个线程中使用的变量能够保证一致性是无法确定的,结果无法预测。
volatile可以防止指令重排情况。
禁止指令重排底层原理:
利用内存屏障来实现,通过插入内存屏障禁止在内存屏障前后的指令执行重排序的优化。内存屏障主要有两个功能:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性。
Volatile与内存屏障又是如何起着作用的呢?
对于Volatile变量进行写操作时,会在写操作后面加上一个store屏障指令,将工作内存中的共享变量值即可刷新到主内存;
对于Volatile变量进行读操作时,会在读操作前面加入一个load屏障指令,读取之前马上读取主内存中的数据。
1.2 工作哪用到指令重排
单例模式在多线程的安全性保证。
在查看多线程之前,首先来回归一下单线程下的单例模式:
// 懒汉模式
public class Juc004SingletonDemo {
/**
* 私有化构造方法、只会构造一次
*/
private Juc004SingletonDemo(){
System.out.println("构造方法");
}
private static Juc004SingletonDemo instance = null;
public static Juc004SingletonDemo getInstance(){
if(instance == null){
instance = new Juc004SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
// 如果是两次都构建同一个,则会返回true
System.out.println(Juc004SingletonDemo.getInstance() == Juc004SingletonDemo.getInstance());
}
}
输出结果如下:
构造方法
true
懒汉模式的单例:
public class Juc004SingletonDemoHunger {
/**
* 私有化构造方法
*/
private Juc004SingletonDemoHunger(){
}
private static Juc004SingletonDemoHunger instance = new Juc004SingletonDemoHunger();;
public static Juc004SingletonDemoHunger getInstance(){
return instance;
}
public static void main(String[] args) {
System.out.println(Juc004SingletonDemoHunger.getInstance() == Juc004SingletonDemoHunger.getInstance());
}
}
输出结果:
true
但是如果在多线程下呢?
public class Juc004SingletonMultiThread {
/**
* 私有化构造方法、只会构造一次
*/
private Juc004SingletonMultiThread(){
System.out.println("构造方法");
}
private static Juc004SingletonMultiThread instance = null;
public static Juc004SingletonMultiThread getInstance(){
if(instance == null){
instance = new Juc004SingletonMultiThread();
}
return instance;
}
public static void main(String[] args) {
// new 30个线程,观察构造方法会创建几次
for (int i = 0; i < 30; i++) {
new Thread(()->{
Juc004SingletonMultiThread.getInstance();
},String.valueOf(i)).start();
}
}
}
构造方法
构造方法
构造方法
构造方法
奇怪的是,构造方法竟然执行了四次,说明并不是真正的new一个对象,并不是真的线程安全的*(当然每个人测试的结果是不一样的)*
如何解决上述问题呢?
- 在
getInstance()
方法中加上Synchronized
关键字来保证线程安全。 - 线程安全的双重检查来保证。(在对象的加锁前后都加上非空检查)
双重检查的单例模式:
public class Juc004SingletonMultiThread {
/**
* 私有化构造方法、只会构造一次
*/
private Juc004SingletonMultiThread(){
System.out.println("构造方法");
}
private static Juc004SingletonMultiThread instance = null;
public static Juc004SingletonMultiThread getInstance(){
if(instance == null){
synchronized (Juc004SingletonMultiThread.class){
if(instance == null){
instance = new Juc004SingletonMultiThread();
}
}
}
return instance;
}
public static void main(String[] args) {
// new 30个线程,观察构造方法会创建几次
for (int i = 0; i < 30; i++) {
new Thread(()->{
Juc004SingletonMultiThread.getInstance();
},String.valueOf(i)).start();
}
}
}
单例这种双重检查的近线程安全的单例模式也有可能出现问题,因为底层存在指令重排,检查的顺序可能发生了变化,可能会发生读取到的instance !=null,但是instance的引用对象可能没有完成初始化。,导致另一个线程读取到了还没有初始化的结果。
为什么会发生以上的情况呢?得来分析一个对象的初始化过程。
public static Juc004SingletonMultiThread getInstance(){ // step 1
if(instance == null){ // step 2
synchronized (Juc004SingletonMultiThread.class){ // step 3
if(instance == null){ // step 4
instance = new Juc004SingletonMultiThread(); // step 5
}
}
}
return instance;
}
第五步初始化过程会分为三步完成:
- 分配对象内存空间
memory = allocate()
- 初始化对象
instance(memory)
- 设置instance指向刚分配的内存地址,此时
instance = memory
再使用该初始化完成的对象,似乎一起看起来是那么美好,但是计算机底层编译器想着让你加速,则可能会自作聪明的将第三步和第二步调整顺序(重排序),优化成了
1. memory = allocate() 分配对象内存空间
3. instance = memory 设置instance指向刚分配的内存地址,此时对象还没有哦
2. instance(memory) 初始化对象
这种优化在单线程下还不要紧,因为第一次访问该对象一定是在这三步完成之后,但是多线程之间存在如此多的的竞争,如果有另一个线程在重排序之后的3后面访问了该对象则有问题了,因为该对象根本就完全初始化的。具体可以看看下图:
但是上述问题在单线程下不存在该问题,只有涉及到多线程下才会发生。
为了解决该问题可以从两个角度解决问题,
1. 不允许2和3进行重排序
2. 允许2和3重排序,但是不允许其他线程看到这个重排序。
因此可以加上Volatile关键字防止指令重排。
public class Juc004SingletonMultiThread {
/**
* 私有化构造方法、只会构造一次
*/
private Juc004SingletonMultiThread(){
System.out.println("构造方法");
}
private static volatile Juc004SingletonMultiThread instance = null;
public static Juc004SingletonMultiThread getInstance(){
if(instance == null){
synchronized (Juc004SingletonMultiThread.class){
if(instance == null){
instance = new Juc004SingletonMultiThread();
}
}
}
return instance;
}
public static void main(String[] args) {
// new 30个线程,观察构造方法会创建几次
for (int i = 0; i < 30; i++) {
new Thread(()->{
Juc004SingletonMultiThread.getInstance();
},String.valueOf(i)).start();
}
}
}
2. 如何理解CAS
2.1 是什么
书面上的话:比较并交换(Compare And Swap),主要是为了解决原子性问题,同时又不想利用重量级的锁。
通俗的话说:如果想更新一个值,想看看该值是否等于某个值,如果等于则更新该值。
下面看一个简单的例子:
其中Atomic
类是使用CAS思想最广泛的类,具体的思想和上面一样,源码此处先不分析。
public class Juc005Cas {
public static void main(String[] args) {
AtomicInteger integer = new AtomicInteger(10);
// compareAndSet如果更新成功,则返回true,如果更新失败,则返回false
System.out.println(integer.compareAndSet(10,11));
System.out.println(integer.get());
System.out.println(integer.compareAndSet(10,15)); // 这一次数值已经变成了11,先比较发现,诶,库里面不是10,直接跳过
System.out.println(integer.get());
}
}
输出结果如下:
true
11
false
11
2.2 为什么可以保证原子性
底层源码:
/**
* 该方法本身是调用unsafe中的compareAndSwapInt()方法
* this 表示当前对象
* valueOffset 内存地址
* expect 内存中的值
* update 更新值
*
**/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// unsafe.class
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
CAS本身通过本地(native)方法来调用,该类可以直接操作底层内存的数据,Unsafe中所有的方法都是用native修饰,可以直接调用操作系统底层资源执行任务。
其中变量valueOffset
表示该对象值在内存中的偏移地址,用来寻找数据的地址。
在底层判断内存中某个位置的值是否为预期值、如果是则更新为新的值,该比较和赋值动作是原子的。是完全依赖于硬件的功能,通过底层硬件实现原子的功能,该过程的执行是不允许中断的,不会造成所谓的数据不一致问题。
2.3 CAS缺点
- 循环开销大:如果比较的时候一直不等于预期值,则会一直循环等待,直到比较成功为止,该过程会给CPU带来较大开销。
- 只能保证一个共享变量的原子操作。对于多个共享变量无法保证原子性,因为每次比较的都是一个元素。
- ABA问题。
2.4 ABA问题与底层原子引用及如何解决
1 ABA问题是什么?
比如数值i = 5
,A线程本来想改成10,在更改之前,B线程先将i先由5变成了6,再更新成5,但是A线程并不知道i
发生过改变,仍然将i改成10。尽管最后的结果没有问题,但是整个过程还是不对的。
public class Juc005CASImprove {
static AtomicReference<Integer> reference = new AtomicReference <>(100);
public static void main(String[] args) {
new Thread(()->{
reference.compareAndSet(100,111);
reference.compareAndSet(111,100);
},"Thread One").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 还是可以更新成功
System.out.println(reference.compareAndSet(100,222)+" "+ reference.get());
},"Thread Two").start();
}
}
2 原子引用
将需要修改的一系列值保证成一个类,每次不是比较值,而是比较一个引用的整体,然后每次保证引用的原子性。
public class Juc005CASAbaProblem {
public static void main(String[] args) {
User a = new User("a",12);
User b = new User("b",12);
AtomicReference<User> userReference = new AtomicReference <>();
userReference.set(a);
System.out.println(userReference.compareAndSet(a,b) +" "+userReference.get().toString());
}
}
3. 如何解决ABA问题
一般都是利用时间戳作为版本,保证每次修改的值都是独一无二的。
可以将元素与版本号构成一个对象,每次保证引用的原子性。
public class Juc005CASImprove {
static AtomicReference<Integer> reference = new AtomicReference <>(100);
/**
* 初始化时间值及初始化时间戳
*/
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference <>(100,1);
public static void main(String[] args) {
/**
* 利用第一个线程模拟ABA问题
*/
new Thread(()->{
int initStamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+" 第0次版本号:"+initStamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模拟ABA问题100->101->100
System.out.println(Thread.currentThread().getName()+" 第一次版本号:"+stampedReference.getStamp());
stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" 第二次版本号:"+stampedReference.getStamp());
stampedReference.compareAndSet(101,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" 第三次版本号:"+stampedReference.getStamp());
},"Thread Three").start();
/**
* 利用另一个线程尝试修改
*/
new Thread(()->{
int initStamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+" 第0次版本号:"+initStamp);
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
Boolean result = stampedReference.compareAndSet(100,2019,initStamp,stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+" 是否更新成功,"+result);
System.out.println(stampedReference.getReference());
},"Thread Four").start();
}
}
3. ArrayList是线程不安全的,编码写出不安全案例并给出解决方法
HashSet及HashMap同理
案例
现象如下:
public class Juc006ArrayListUnsafe {
public static void main(String[] args) {
List<String> lists = new ArrayList <>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
lists.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(lists);
},String.valueOf(i)).start();
}
}
}
结果会报异常:
java.util.ConcurrentModificationException 并发修改的异常
为什么会产生这种问题呢?
因为在多线程下,每次往容器中写数据时,不保证顺序,谁抢占到了容器谁开始写入数据,因此可能存在覆盖情况,导致每次执行的结果都不一致。
如何解决这种问题呢?
- Vector集合类。但是该类在方法上加上Synchronize关键字,保证线程安全。不推荐,重量级,效率低。
Collections.synchronizedList
保证线程安全。
// 保证线程安全
List<String> lists = Collections.synchronizedList(new ArrayList <>());
- 利用写时复制的集合类。
CopyOnWriteArrayList
List<String> lists = new CopyOnWriteArrayList <>();
写时复制类似于将读数据和写数据过程分离开来。
A线程和B线程都开始写数据,A、B每次写数据之前,都需要拿到一个许可证(类似于锁),主内存中数据复制到工作内存中,然后再进行修改,修改完毕之后将容器的引用指向新的数据集,然后再允许别的线程修改。
public boolean add(E e) {
// 先获取锁,也就是前文说的许可证
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 将原数组复制一份
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 添加新值
newElements[len] = e;
// 将数组索引指向新数组
setArray(newElements);
return true;
} finally {
// 最后释放锁
lock.unlock();
}
}
4. 锁的理解
公平锁、非公平锁、可重入锁(递归锁)、自旋锁,并手写一个自旋锁。
1 公平锁、非公平锁
抢占资源的顺序按照先后顺序依次获取,保证公平性,但是损失了效率。
非公平锁:抢占式的获取资源,哪个线程抢到资源哪个开始执行,也有可能存在一个线程一直获得资源,提高了效率。Synchronized是非公平锁。
上述只是简单的说公平锁的含义,具体的底层内容详见:JUC—ReentrantLock核心知识讲解
2 可重入锁
又称为递归锁:同一线程外层函数获取锁之后,内部函数仍然可以获取锁对象,线程可以进入任何一个已经拥有的锁所同步的代码块。
ReentrantLock和Synchronized两个都是典型的可重入锁。
可重入锁最大的作用可以避免死锁
3 自旋锁
尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁,可以减少线程上下文切换的消耗,但是循环也比较消耗CPU。
// CAS是最常用的自旋锁原理实践
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
// 一直循环
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
自旋锁实现:
public class Juc007LockSpinLock {
AtomicReference<Thread> reference = new AtomicReference <>();
/**
* 获取锁
*/
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(thread.getName() +" come in ....");
// 如果更新成功,则跳出while循环;如果失败,则一直循环等待
while(!reference.compareAndSet(null,thread)){
System.out.println(thread.getName() +" I am waiting....");
}
}
/**
* 释放锁
*/
public void unLock(){
Thread thread = Thread.currentThread();
//如果是当前线程获取锁,则尝试释放锁
reference.compareAndSet(thread,null);
System.out.println(thread.getName()+" unlock finished....");
}
/**
* new两个线程去尝试获取锁及释放锁
* @param args
*/
public static void main(String[] args) {
Juc007LockSpinLock spinLock = new Juc007LockSpinLock();
new Thread(()->{
spinLock.lock();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLock.unLock();
},"Thread One").start();
// 主线程等待1s
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
spinLock.lock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLock.unLock();
},"Thread Two").start();
}
}
4 独占锁(写锁)/共享锁(读锁)
独占锁:该锁对象一次只能被一个线程持有。上面说的ReentrantLock
及Synchronized
都是独占锁。
共享锁:一个锁对象可以被多个线程持有。
ReentrantReadWriteLock
该锁读的时候是共享锁,写的时候是独占锁。
5 synchronized和lock有什么区别?用lock有什么好处?
synchronized
是java
的关键字,是JVM
层面控制(底层是monitor
实现的),lock
是具体类的api
层面的锁(java.util.concurrent.locks.Lock
)。synchronized
不需要用户手动释放锁,当synchronized
代码执行完毕系统会自动让线程释放对锁的占有。ReentrantLock
则需要用户去手动释放锁,如果不手动释放锁,则可能会造成死锁现象。- 等待是否中断。
synchronized
不可中断,除非正常执行完或者抛出异常;lock可以设置超时中断。 - 加锁是否可以公平。
synchronized
默认是非公平锁,而lock可以自己设置公平与否,默认是非公平锁。 - 锁是否可以绑定多个条件
condition
。synchronized
无法绑定多个条件,而lock可以分组唤醒线程,可以做到精确唤醒某一个线程,而不是像synchronized随机唤醒任意一个线程。
可以利用lock实现精确的唤醒,比如先执行A线程,打印5次,再执行B线程打印10次,再执行C线程打印15次
public class Juc010SynAndLockCompare {
public static void main(String[] args){
CompareResource resource = new CompareResource();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
resource.print5();
} catch (Exception e) {
e.printStackTrace();
}
}
},"Thread One").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
resource.print10();
} catch (Exception e) {
e.printStackTrace();
}
}
},"Thread Two").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
resource.print15();
} catch (Exception e) {
e.printStackTrace();
}
}
},"Thread Three").start();
}
}
class CompareResource{
// 利用number来表示不同的线程
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5() {
lock.lock();
try {
// 循环判断
while(1 != number){
condition1.await();
}
// 干活
for(int i = 0; i < number *5; i++){
System.out.println(Thread.currentThread().getName() +" doing....");
}
// 唤醒下一个线程
number = 2;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print10() {
lock.lock();
try {
// 循环判断
while(2 != number){
condition2.await();
}
// 干活
for(int i = 0; i < number *5; i++){
System.out.println(Thread.currentThread().getName() +" doing.....");
}
// 唤醒下一个线程
number = 3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print15() {
lock.lock();
try {
// 循环判断
while(3 != number){
condition3.await();
}
// 干活
for(int i = 0; i < number *5; i++){
System.out.println(Thread.currentThread().getName() +" doing....");
}
// 唤醒下一个线程
number = 1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
6 Juc中常见的类CountDownLatch、CyclicBarrier、Semaphore的理解?
1. CountDownLatch
多个线程都执行完成时某一个线程才允许工作,否则该线程必须处于阻塞状态,其他线程工作完时,则latch-1,直到latch==0时,等待的线程才开始工作。
比如:只有其他人都走了(都完成),才可以关灯
public class Juc008CountDownLatch {
public static void main(String[] args) throws Exception{
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+ " doing own work");
latch.countDown();
},String.valueOf(i)).start();
}
// 其他线程工作的时候,主线程处于阻塞状态,直到latch==0
latch.await();
System.out.println("main Thread working....");
}
}
2. CyclicBarrier
该类与CyclicBarrier
相反,只有当所有线程都达到某个状态时,才允许所有线程开始执行,否则所有的线程都必须处于等待状态。
比如:所有的人到了才可以开饭(才可以一起做某事)。
public class Juc008CyclicBarrier {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(7, ()->{
System.out.println(Thread.currentThread().getName() +" starting.....");
});
for (int i = 1; i <= 7; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" waiting....");
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
3. Semaphore
之前有一篇仔细研究底层的原理文章,移步至:Semaphore
7. 谈谈阻塞队列
之前写过一篇基本的阻塞队列文章,阻塞队列
简单而言:本质上是一个队列,但是与普通队列有不同之处,阻塞队列,只有队列非空(有元素)时才可以获取元素,当队列非满的时候才可以添加元素,否则只能阻塞等待。
为什么需要阻塞队列呢?
在多线程领域,所谓阻塞,在某些情况下会挂起线程,一旦条件满足,被挂起的线程又会自动被唤醒。
通常我们需要手动处理何时阻塞线程、何时唤醒线程,但是有了阻塞队列之后,根本不需要关心该问题,一切都有BlockingQueue
实现。
核心方法同样可以参考:阻塞队列
主要的实现类:
主要运用:生产者消费者模式
传统的生产者消费者写法:
/**
* @Description 生产者消费者模式
* 线程 操作 (方法) 资源类
* 判断 干活 通知
* 防止虚假唤醒机制
*/
public class Juc009ProducerAndConsumer {
public static void main(String[] args) {
Food food = new Food();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
food.produce();
} catch (Exception e) {
e.printStackTrace();
}
}
},"生产者").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
food.consumer();
} catch (Exception e) {
e.printStackTrace();
}
}
},"消费者").start();
}
}
class Food{
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void produce() throws Exception{
lock.lock();
try {
// 1. number等于0,才开始生产,不等于0,则生产者处于等待状态
// 这里必须用while判断,否则可能产生虚假唤醒
while(number != 0){
condition.await();
}
// 2. 等于0,则开始干活
number++;
System.out.println("生产者生产.... " + number);
// 3. 生产者生产完了之后,需要通知唤醒消费者
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void consumer() throws Exception{
lock.lock();
try {
// 1. number ==0 ,生产者等待
while(number == 0){
condition.await();
}
// 2. 如果number != 0,则消费者消费
number--;
System.out.println("消费者消费.... " + number);
// 3. 消费者消费之后,通知唤醒生产者
condition.signalAll();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
利用阻塞队列实现生产者消费者模式
public class Juc011ProducerAndConsumerImprove {
public static void main(String[] args) {
ShareResource resource = new ShareResource(new ArrayBlockingQueue <>(10));
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 启动啦....");
try {
resource.produce();
} catch (Exception e) {
e.printStackTrace();
}
},"Producer").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 启动啦....");
try {
resource.consumer();
} catch (Exception e) {
e.printStackTrace();
}
},"Consumer").start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
resource.stop();
}
}
class ShareResource{
/**
* 利用flag来控制生产消费
*/
private volatile boolean flag = true;
/**
* 生产和消费的东西
*/
private AtomicInteger value = new AtomicInteger();
BlockingQueue<String> queue = null;
public ShareResource(BlockingQueue<String> queue){
this.queue = queue;
}
/**
* 生产方法
*/
public void produce() throws Exception{
String data = null;
Boolean result = false;
/**
* 如果为true,则开始生产,并且加入队列中
*/
while (flag){
data = value.incrementAndGet()+"";
result = queue.offer(data, 2L,TimeUnit.SECONDS);
if(result){
System.out.println(Thread.currentThread().getName()+" 生产成功!");
}else{
System.out.println(Thread.currentThread().getName()+" 生产失败!");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName()+" 整个生产过程都被终止.....");
}
/**
* 消费者方法
*/
public void consumer() throws Exception{
String result = null;
while(flag){
result = queue.poll(2L,TimeUnit.SECONDS);
if(result == null || result.equalsIgnoreCase("")){
flag = false;
System.out.println(Thread.currentThread().getName()+" 超时未获取到资源...!");
}else {
System.out.println(Thread.currentThread().getName()+" 消费成功!");
}
}
}
public void stop(){
flag = false;
System.out.println("终止了.............");
}
}
8. Callable接口理解
是什么?
callable同样是一个线程的接口,但是可以自己定义返回值。
/**
* 与Runnable接口区别在于可以接受返回值
*/
class OwnCall implements Callable<Integer>{
/**
* 返回值与Callable中泛型类型有关
*
* @return
* @throws Exception
*/
@Override
public Integer call() throws Exception {
System.out.println(".......");
return 1;
}
}
下面可以获取线程的返回结果:
public class Juc012CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask <>(new OwnCall());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
为什么?
因为Callable可以获取线程的计算结果,并且可以做到多个线程之间并行进行,比如先执行主线程,等该线程执行完了之后,在获取FutureTask的执行结果。
9. 线程池用过吗?ThreadPoolExecutor谈谈你的理解?
详细的可以参考之前的博文:JUC—线程池核心类ThreadPoolExecutor源码解析,对于线程池有更加准确的讲解。
9.1 为什么用线程池
线程的生成和销毁都是需要消耗系统的资源,因此可以提前准备好一堆可以使用的线程、供任务调度的使用。主要有以下几点好处:
- 降低资源的消耗。通过重复利用已经创建的线程降低线程创建和线程销毁的资源损耗。
- 提高响应速度。当任务达到时,不需要等待线程创建就可以执行
- 提供线程的可管理性。线程池本身提供了很多方法供用户监控、设置线程池。