JUC简介
JUC
一. 概述
- JUC指的是JDK1.5中提供的一套并发包及其子包:
- java.util.concurrent
- java.util.concurrent.lock
- java.util.cncurrent.atomic
- 主要内容有:阻塞式队列、并发映射、锁、执行器服务、原子性操作。
二. 原子性操作
原子性操作实际上是保证了属性的原子性,底层是基于CAS+volatile来实现的
Ⅰ. 关于CAS
👉CAS
Ⅱ.关于volatile
volatile是java中的关键字之一,是Java中提供的用于保证线程通信间的轻量级通信机制。
- 特性:
- 保证线程的可见性。一个线程对主内存的数据做了改变,其他线程能够立即感知到这个改变。
- 对单个读/写具有原子性,但是复合操作除外,例如i++不保证线程的原子性。原子性指线程的执行过程不可拆分,换言之,线程在执行过程中不会中断。加锁就是为了保证原子性。
- 内存语义:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
- 实施机制
- 禁止指令重排。指令没有按照预定顺序调用执行,而是在底层产生了所谓的优化,导致顺序发生了改变。指令重排不能违背happen-before原则。
- 内存屏障
三. LOCK锁
Ⅰ. 锁一些概念
-
锁的公平和非公平原则:
公平锁:锁的获取顺序应该符合请求的绝对时间顺序,也就是FIFO。
非公平锁:只要CAS设置同步状态成功,则表示当前线程获取了锁
- 在资源有限的情况下,线程之间实际执行的次数并不均等,这种现象称之为非公平原则。在公平策略下,线程不能直接抢占资源,而是抢占入队顺序。此时线程之间实际执行次数大致相等,我们称之为公平策略。
- 相对而言,非公平的效率更高(不需要考虑调度问题)
-
锁的独占和共享
独占锁:独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。ReentrantLock 和 synchronized 都是独占锁
共享锁:享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
独享锁与共享锁都是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享ReentrantReadWriteLock中读锁是共享锁,写锁是独占锁。读锁的共享可以保证并发读是高效的,读写,写读,写写是互斥的。
-
锁的重入和非重入
可重入锁:可重入锁也叫做递归锁,指的是同一个线程T在进入外层函数A获得锁L之后,T继续进入内层递归函数B,也需要获取该锁L的代码时,在不释放锁L的情况下,可以重复获取该锁L。
非重入锁:非可重入锁也叫做自旋锁,对比上面,指的是同一个线程T在进入外层函数A获得锁L之后,T继续进入内层递归函数B时,仍然有获取该锁L的代码,必须要先释放进入函数A的锁L,才可以获取进入函数B的锁L。
-
锁的乐观和悲观
乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将乐观锁的核心算法是CAS,比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。(不加锁就修改)
悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。(加锁才修改)
-
读写锁
读锁:当线程获取读锁时,允许其他线程的读操作,不允许写操作。
写锁:当线程获取读写时,不允许其他线程的任何操作。
-
自旋
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
Ⅱ. ReentrantLock
JDK1.5增加了LOCK锁,可以通过显示定义同步锁对像实现同步,是对共享资源进行访问的工具。相比synchronized,LOCK更加精细灵活。唯一实现类:ReentrantLock
【特点】
- 可重入。(Synchronized同)
- 如果不指定,默认非公平。(Synchronized同)
- 独占(Synchronized同)
- 悲观(Synchronized同)
- 底层采用AQS实现。
【案例】
import java.util.concurrent.locks.ReentrantLock;
/**
* 银行账户类
* 此类为可变类,亦是线程不安全类。
* 若想变为线程安全类,需付出额外的方法
*
* 此例中,需要将修改balance的方法同步
* 若使用Synchronized同步,则锁是this
*
* 此例也可显示定义锁对象,来同步方法
* 注意要显示的释放锁
*/
public class Account{
private String accountNo;
private double balance;
//定义锁对象
private final ReentrantLock lock=new ReentrantLock();
public void draw(double drawAmount){
//加锁
lock.lock();
try{
if(drawAmount<balance){
System.out.println("目前余额:"+balance);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance-=drawAmount;
System.out.println("余额:"+balance);
}
else {
System.out.println("余额不足,提款失败");
}
}finally {
//修改完成,释放锁
lock.unlock();
}
}
}
Ⅲ. ReadWriteLock
ReadWriteLock:读写锁。在使用的时候先创建ReentrantReadWriteLock,通过这个对象获取读锁或者写锁,之后再加锁解锁或者解锁。
相比ReadWriteLock,ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
【关于StampedLock】
StampedLock是Java8新增的锁,在绝大多数场景下可以替代传统的读写锁。其在提供读写锁的同时,还支持优化读模式。优化读基于假设:大多数情况下读操作并不会和写操作冲突,所以可以先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入了,则尝试获取读锁。
Ⅳ.Condition
ConditionObject
是同步器AbstractQueuedSynchronizer
的内部类 ,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也会是比较合理的。
每个Condition对象都包含着一个队列(等待队列),是Condition对象实现等待/通知功能的关键。
-
等待队列是一个FIFO队列,队列的每个节点都包含一个线程引用, 线程就是在Condition对象中等待的线程,如果一个线程调用了
Condition.await()
方法,那么该线程将会释放锁、构造节点加入等待队列进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的经静态内部类AbstractQueuedSynchronizer.Node
一个Condition包含一个等待队列,Condition拥有首节点(fristWaiter)和尾节点(lastWriter)。当前线程调用
Condition.await()
方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。 -
调用Condition的
await()
方法,会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态,当从await()
方法返回时,当前线程一定获取了Condition相关联的锁,如果从队列 (同步队列和等待队列)的角度看
await()
方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。 -
调用Condition的
signal()
方法,将唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移动到同步队列中。
【案例】👉线程通信
Ⅴ. synchronized 和 ReentrantLock的区别
- synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。
- synchronized不需要显示的定义锁和释放锁。
- 既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
- ReentrantLock可以对获取锁的等待时间进行超时设置,这样就避免了死锁。
- 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
- 可以实现公平策略
- ReentrantLock可以获取各种锁的信息。
- ReentrantLock可以灵活地实现多路通知
四. BlockingQueue - 阻塞式队列
Ⅰ. 特点
-
满足队列特点:FIFO(First In First Out)
-
阻塞:如果队列为空,则试图获取元素的线程会被阻塞;如果队列已满,则试图放入元素的线程会被阻塞。
-
不允许元素为null(LinkedList允许)
-
重要方法
抛出异常 返回特殊值 永久阻塞 定时阻塞 添加 add - IllegalStateException offer - false put offer 获取 remove - NoSuchElementException poll - null take poll
Ⅱ. 常用的实现类
- ArrayBlockingQueue阻塞式顺序队列
- 底层基于数组存储数据
- 使用的时候需要指定容量,不能扩容
- 在多线程环境下不保证“公平性”
- 实现:ReentrantLock+Condition
- LinkedBlockingQueue阻塞式锁式队列
- 底层基于节点来存储数据
- 在使用的时候可以指定容量也可以不指定。如果指定容量,则容量不可变;如果不指定容量,则容量默认为Integer.MAX_VALUE = 231-1不可变。因为实际开发中,一般不会在队列中存储21亿个元素,所以一般认为此时的容量是无限的
- PriorityBlockingQueue具有优先级的阻塞式队列:
- 底层基于节点来存储数据
- 使用的时候可以指定容量也可以不指定。如果不指定则默认初始容量是11
- PriorityBlockingQueue会对放入的元素来进行排序,默认情况下元素采用自然顺序升序排序,要求元素对应的类实现Comparable接口,覆盖compareTo方法指定比较规则。
- SynchronousQueue 同步队列
- 在使用的时候不需要指定容量,默认容量为1且只能为1
- 应用:交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务
另:BlockingDeque阻塞式双端队列
- 允许从两端放入/获取元素。
- 遵循阻塞特点,在使用的时候需要指定容量。