JUC概述
JUC简介
- 在Java中,线程部分是一个重点,本篇文章说的JUC也是关于线程的。JUC就是java.util.concurrent工具包的简称。这是一个处理线程的包,JDK1.5开始出现的。
进程与线程
- 进程(Process)
- 计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
- 线程(thread)
- 操作系统能够进行运算调度的最小单位。它包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
- 进程:值在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程———资源分配的最小单位
- 线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程———程序执行的最小单位
- 管程
- Monitor(监视器),也就是我们平时所说的锁
- Monitor其实是一种同步机制,他的义务是保证同一时间只有一个线程可以访问被保护的数据和代码
- JVM中同步是基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象
- Monitor对象会和java对象一同创建并销毁,它的底层是由C++实现的
Object o = new Object();
new Thread(()->{
synchronized (o){
}
},"t1").start();
}
线程的状态
wait/sleep
- sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用
- sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)
并发与并行
卖票
/**
* 三个售票员 卖 30张票
*/
public class SaleTicket {
public static void main(String[] args) {
//资源类
final Ticket ticket = new Ticket();
/**
* 使用匿名内部类实现Runnable接口
*/
new Thread(new Runnable() {
public void run() {
while (ticket.getTicketCount() > 0){
ticket.sale();
}
}
}, "售票员1").start();
new Thread(new Runnable() {
public void run() {
while (ticket.getTicketCount() > 0){
ticket.sale();
}
}
}, "售票员2").start();
new Thread(new Runnable() {
public void run() {
while (ticket.getTicketCount() > 0){
ticket.sale();
}
}
}, "售票员3").start();
}
}
class Ticket{
private int ticketCount = 30;
public synchronized void sale(){
if (ticketCount > 0){
System.out.println(Thread.currentThread().getName()+"卖出第"+(ticketCount--)+"张票,剩余"+ticketCount+"张票");
}
}
public int getTicketCount() {
return ticketCount;
}
}
class Ticket{
private int ticketCount = 30;
//可重入锁
private Lock lock = new ReentrantLock();
public void sale(){
//加锁
lock.lock();
try {
if (ticketCount > 0){
System.out.println(Thread.currentThread().getName()+"卖出第"+(ticketCount--)+"张票,剩余"+ticketCount+"张票");
}
}finally {
//解锁
lock.unlock();
}
}
public int getTicketCount() {
return ticketCount;
}
}
线程间的通信
生产者消费者通信
- 现有两个线程,可以操作初始值为0的一个变量,实现一个线程对变脸加一,一个线程实现对变量减一,实现交替来5轮
//空调
class AirConditioner{
//空调度数
private int number = 0;
//加一度
public synchronized void increment() throws InterruptedException {
//判断number是否为0,如果不为0则等待
if (number != 0){
this.wait();
}
//加一度
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//通知另一个线程可以减1了
this.notifyAll();
}
//减一度
public synchronized void decrement() throws InterruptedException {
if (number == 0){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}
}
/**
* 现在有两个线程,可以操作初始值为0的一个变量
* 实现一个线程对变量加1,一个线程对该变量减1
* 实现交替,来5轮
*/
public class ThreadWaitNotifyDemo {
public static void main(String[] args) {
//空调资源类
AirConditioner airConditioner = new AirConditioner();
//线程A每次加一,来10轮
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
airConditioner.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
//线程B每次减一,来10轮
new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
airConditioner.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
A 1
B 0
A 1
B 0
A 1
B 0
A 1
B 0
A 1
B 0
- 新增两个线程,两个线程加一,两个线程减一。则会出现虚假唤醒的情况
- 例子:变量为0线程1判断通过则加一然后唤醒其他线程(此时应该唤醒减一的线程),唤醒了所有的线程,并且另一个加一的线程执行了,则变量变为2
- 解决方法,使用while提换掉if,当被唤醒之后再进行判断是否执行
//空调
class AirConditioner{
//空调度数
private int number = 0;
//加一度
public synchronized void increment() throws InterruptedException {
//判断number是否为0,如果不为0则等待
while (number != 0){
this.wait();
}
//加一度
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//通知另一个线程可以减1了
this.notifyAll();
}
//减一度
public synchronized void decrement() throws InterruptedException {
while (number == 0){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
this.notifyAll();
}
}
- 使用可重入锁ReentrantLock替换synchronized
//空调
class AirConditioner{
//空调度数
private int number = 0;
//可重入锁
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//加一度
public void increment() throws InterruptedException {
//加锁
lock.lock();
try{
//判断number是否为0,如果不为0则等待
while (number != 0){
condition.await();
}
//加一度
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//通知另一个线程可以减1了
condition.signal();
}finally {
lock.unlock();
}
}
//减一度
public synchronized void decrement() throws InterruptedException {
//加锁
lock.lock();
try{
//判断number是否为0,如果为0则等待
while (number == 0){
condition.await();
}
//加一度
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//通知另一个线程可以加1了
condition.signal();
}finally {
lock.unlock();
}
}
}
精确通知顺序访问
- 案例
- 多线程之间按顺序,实现A-B-C
- A打印5次,B打印10次,C打印15次,来10轮
class ShareResource{
private int number = 1; //1 A打印 2 B打印 3 C打印
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
public void print5(){
lock.lock();
try{
while (number != 1){
conditionA.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+(i+1));
}
//将标志位改为2B打印10次
number = 2;
//唤醒conditionB
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print10(){
lock.lock();
try{
while (number != 2){
conditionB.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+(i+1));
}
//将标志位改为2B打印10次
number = 3;
//唤醒conditionC
conditionC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void print15(){
lock.lock();
try{
while (number != 3){
conditionC.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+(i+1));
}
//将标志位改为2B打印10次
number = 1;
//唤醒conditionA
conditionA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class ThreadOrderAccess {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(()->{
for (int i = 0; i < 10; i++) {
shareResource.print5();
}
},"A").start();
new Thread(()-> {
for (int i = 0; i < 10; i++) {
shareResource.print10();
}
},"B").start();
new Thread(()-> {
for (int i = 0; i < 10; i++) {
shareResource.print15();
}
},"C").start();
}
}
synchronized锁
- 一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能等待,换句话说,某一时刻内,只能有唯一一个线程去访问这些synchronized方法锁时当前对象this,被锁定后,其他线程都不能进入到当前对象的其他的synchronized方法
CopyOnWriteArrayList
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class ListDemo {
public static void main(String[] args) {
//高并发下线程安全的集合
List<String> list = new CopyOnWriteArrayList();
for (int i = 0; i < 3; i++) {
new Thread(()->{
list.add(System.currentTimeMillis()+"");
System.out.println(list);
},i+"").start();
}
}
}
- CopyOnWriteArrayList.add()
public boolean add(E e) {
//可重入锁
final ReentrantLock lock = this.lock;
//上锁
lock.lock();
try {
//获取到集合的值
Object[] elements = getArray();
int len = elements.length;
//拷贝数组的值并扩容(+1)
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新数组赋值上新增的值
newElements[len] = e;
//将新的数组赋给集合中的数组对象
setArray(newElements);
return true;
} finally {
//解锁
lock.unlock();
}
}
- CopyOnWrite容器即写时赋值的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后新的容器Object[] newElements里添加元素,添加元素之后,再将原容器的引用指向新的容器setArray(newElements)。这样做的好处是可以对CopyWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyWrite容器也是一种读写分离的思想,读和写不同的容器。
- CopyOnWriteArraySet(JUC包下面线程安全的set)
- ConcurrentHashMap(JUC包下线程安全的HashMap)
- HashMap默认大小为16,负载因子0.75,扩容为两倍
- ArrayList默认大小10,扩容为1.5倍
Callable
- Callable和Runable接口的区别
- 有返回值
- 可以起多个线程执行不同的操作,然后再在最后调用get方法进行汇总
- 抛出异常
- 方法不一样,Callable是call,Runable是run
class Thread01 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("启动一个线程");
return 0;
}
}
public class MyThread {
public static void main(String[] args) throws Exception {
FutureTask futureTask = new FutureTask(new Thread01());
Thread thread = new Thread(futureTask,"线程A");
thread.start();
//获取call方法返回值
System.out.println(futureTask.get());
}
}
CountDownLatch(控制线程顺序)
- CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。其他线程调用cuntDown方法会将计数器减1,当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"离开教室");
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t"+"班长关门");
Thread-0 离开教室
Thread-5 离开教室
Thread-2 离开教室
Thread-3 离开教室
Thread-1 离开教室
Thread-4 离开教室
main 班长关门
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙");
});
for (int i = 0; i < 7; i++) {
final int temp = i+1;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"收集到第"+temp+"颗龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
Thread-0 收集到第1颗龙珠
Thread-2 收集到第3颗龙珠
Thread-4 收集到第5颗龙珠
Thread-3 收集到第4颗龙珠
Thread-1 收集到第2颗龙珠
Thread-6 收集到第7颗龙珠
Thread-5 收集到第6颗龙珠
召唤神龙
Semaphore(信号量)
- 在信号量上我们定义两种操作
- acquire(获取)当一个线程调用acquire操作时,他要么成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时
- release(释放)实际上会将信号量的值加1,然后唤醒等待的线程
- 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另个一个用于经发线程数的控制
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
//抢占到车位 车位减1,没抢到则等待
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t抢占到了车位");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"\t释放掉车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放掉车位,车位+1
semaphore.release();
}
}).start();
}
ReadWriteLock(读写锁)
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//自定义缓存
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
//写锁
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName()+"\t写入数据"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t写入完成"+key);
//解锁
readWriteLock.writeLock().unlock();
}
public Object get(String key){
try{
//读锁
readWriteLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+"\t读取数据"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t读取数据"+key);
return o;
}finally {
readWriteLock.readLock().unlock();
}
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
int temp = i;
new Thread(()->{
myCache.put(temp+"",temp);
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
BlockingQueue(阻塞队列)
- 当队列是空的,从队列中获取元素的操作将会被阻塞
- 当队列是满的,从队列中添加元素的操作会被阻塞
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素队列
- LinkedTransferQueue:由链表组成的无界阻塞队列
- LinkedBlockingDeque:由链表组成的双向阻塞队列
方法类型 |
抛出异常 |
特殊值 |
阻塞 |
超时 |
插入 |
add(e) |
offer(e) |
put(e) |
offer(e,time,unit) |
移除 |
remove() |
poll() |
take() |
poll(time,unit) |
检查 |
element() |
peek() |
不可用 |
不可用 |
- 抛出异常
- 当阻塞队列满时,再往队列里add插入元素会抛出java.lang.IllegalStateException: Queue full
- 当祖列队列空时,再往队列remove移除元素会抛出NoSuchElementException
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue(3);
blockingQueue.add("a");
blockingQueue.add("b");
blockingQueue.add("c");
//java.lang.IllegalStateException: Queue full 队列已满,再添加d会抛出异常
//blockingQueue.add("d");
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
//java.util.NoSuchElementException 当队列中没有元素再移除元素,抛出异常
//System.out.println(blockingQueue.remove());
blockingQueue.add("a");
//a 检查队首元素,如果队列中没有元素则抛出异常java.util.NoSuchElementException
System.out.println(blockingQueue.element());
- 特殊值
- 插入方法,成功true,失败false
- 移除方法,成功返回出队元素,队列里没有就返回null
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue(3);
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("a"));
//false 当队列满再插入元素则返回false
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
//null 当队列元素为空时,再移除元素则返回null
System.out.println(blockingQueue.poll());
//null 当队列元素为空时,检查队首元素返回null
System.out.println(blockingQueue.peek());
- 阻塞
- 插入方法,队列满时则会一直阻塞,直到队列移除一个元素
- 移除方法,队列空时则会一直阻塞,直到队列中有一个元素
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue(3);
blockingQueue.put("a");
blockingQueue.put("b");
blockingQueue.put("c");
//队列满时,在添加元素则会阻塞,直到队列移除掉一个元素
//blockingQueue.put("d");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//队列空时,在移除元素则会阻塞,直到队列中有元素
//System.out.println(blockingQueue.take());
- 超时
- 插入元素,成功true,失败则阻塞,带阻塞时间
- 移除元素,成功返回移除的元素,失败则阻塞,带阻塞时间
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue(3);
blockingQueue.offer("a",3, TimeUnit.SECONDS);
blockingQueue.offer("b",3, TimeUnit.SECONDS);
blockingQueue.offer("c",3, TimeUnit.SECONDS);
//当队列为满时,阻塞,阻塞时间为3秒
blockingQueue.offer("d",3, TimeUnit.SECONDS);
blockingQueue.poll(3,TimeUnit.SECONDS);
blockingQueue.poll(3,TimeUnit.SECONDS);
blockingQueue.poll(3,TimeUnit.SECONDS);
//当队列为空时,阻塞,阻塞时间3秒
blockingQueue.poll(3,TimeUnit.SECONDS);
线程池
- 线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
- 线程复用;控制最大并发数;管理线程;
- 降低了资源的消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立刻执行
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控
//5个线程的线程池,固定线程数量
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);
//一个线程池一个线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
//可自动扩容的线程池
ExecutorService threadPool3 = Executors.newCachedThreadPool();
try{
for (int i = 0; i < 10; i++) {
threadPool3.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
}finally {
threadPool3.shutdown();
}
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- 线程池参数
- corePoolSize:线程池中的常驻核心线程数
- maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
- keepAliveTime:多余的空闲线程的存活时间,当线程池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
- handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如果来拒绝请求执行的runnable的策略
- 在创建了线程池后,开始等待请求
- 当调用execute()方法添加一个请求任务时,线程池会做出如下判断
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 如果这个时候队列满了且正在运行的数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
- 如果队列满了且正在运行的线程数量等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
- 当一个线程池完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断
- 如果当前线程的线程数大于corePoolSize,那么这个线程就被停掉
- 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小
- 拒绝策略
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:调用者运行,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者(由调用者执行,如main线程),从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
2,
TimeUnit.SECONDS,new LinkedBlockingQueue(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try{
for (int i = 0; i < 10; i++) {
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t执行操作");});
}
}finally {
threadPoolExecutor.shutdown();
}