序言
本文主要介绍了,并发编程中问题,java并发机制的底层实现,内存模型,基础,锁,框架,原子类,并发工具类,线程池以及实战
本文中名词的定义借鉴了并发编程的艺术这本书以及个人工作多年工作经验对并发编程认识与总结
1.并发编程中问题
- 上下文切换
- 死锁
- 资源限制(硬件与软件)
1.1上下文切换
- 定义
即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现
这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切
换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个
任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这
个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换 - 如何避免
1)无锁并发
多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一
些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
2)·CAS算法
Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
3)使用最少线程
避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这
样会造成大量线程都处于等待状态
4)协程
在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
1.2死锁
- 定义
两个分别不同的线程分别等待,就会容易造成死锁 - 如何避免
1)一个线程同时获取多个锁
2)一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
3)尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
4)对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
1.3资源限制(硬件与软件)
- 定义
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资
源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。硬件资源限
制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接
数和socket连接数等。 - 如何避免
1)集群
2)资源复用
2.Java并发机制的底层实现原理
2.1volatile
- 定义
Java编程语言允许线程访问共享变量,为了
确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言
提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存
模型确保所有线程看到这个变量的值是一致的。 - 原理
总线锁与缓存一致性
对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据
写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操
作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一
致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当
处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状
态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存
里 - 作用
1)保证可见性
原理:要想实现多线程中对同一个变量进行操作,必须保证每个线程操作完成后,都要通知其它线程,数据已经发生改变,让其它线程在它改的基础上进行操作,这里就是我们所说的java内存模型(JMM)
2)禁止指令重排序(内存屏障)
原理:java程序在执行时,往往在编译与处理器时会发生指令重排序,可能导致安全问题,这时引入内存屏障来解决,告诉cpu不要给我重排序
2.2synchronized
- 定义
1)重量级锁
2)java1.6进行优化,引入偏向锁,轻量级锁,以及锁的存储结构和升级过程 - 使用形式
1)对于普通同步方法,锁是当前实例对象
2)对于静态同步方法,锁是当前类的Class对象
3)·对于同步方法块,锁是Synchonized括号里配置的对象。 - 原理
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结
束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有
一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter
指令时,将会尝试获取对象所对应的monitor的所有权 - 锁的升级过程
无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
1)偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁
2).轻量级锁
遇到线程竞争,偏向锁变为轻量级锁
3).重量级锁
线程大量自旋,变为重量级锁 - 锁的优缺点对比
2.3原子操作
- 定义
原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意
为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。让我们
一起来聊一聊在Intel处理器和Java里是如何实现原子操作的。 - 原理
在Java中可以通过锁和循环CAS的方式来实现原子操作。 - CAS问题
1)ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化
则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它
的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面
追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A(AtomicStampedReference)
2)循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
3)只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来
操作
3.java内存模型
3.1基础
1)并发编程的两个关键问题
线程之间如何通信及线程之间如何同步(这里的
线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程
中,线程之间的通信机制有两种:共享内存和消息传递。
3.2Java内存模型的抽象结构
3.3指令重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序
3.4什么是happen-before
- 定义
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
3.5各种锁内存语义
详细不在此介绍,都遵守JMM规则
4.Java并发编程基础
4.1线程的定义
现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作
系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light
Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局
部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉
到这些线程在同时执行。
4.2多线程的意义
1)更多的处理器核心
2)更快的响应时间
3)更好的编程模型
4.3线程的状态
- 初始状态
- 运行状态
- 堵塞状态
- 等待状态
- 终止状态
4.4线程间的通信
- volatile与synchronized
- 等待与通知(notify,wait)
- Thread.join
- 管道输入/输出流
- ThreadLocal的使用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构
5.java 中的锁
5.1 lock
- 定义
它一个接口,需要手动释放锁,可公平非公平
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock();
}
5.2 队列同步器
1)同步队列的基本结构
2)节点自旋获取同步状态
5.3 重入锁(ReentrantLock)
就是支持重进入的锁,它表示该锁能够支持一个线程对
资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择
- 实现重进入
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实
现需要解决以下两个问题。
1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再
次成功获取。
2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到
该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁
被释放时,计数自减,当计数等于0时表示锁已经成功释
5.4 读写锁(ReentrantReadWriteLock)
- 定义
之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线
程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读
线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写
锁,使得并发性相比一般的排他锁有了很大提升
5.5 LockSupport工具
里面包括阻塞线程与唤醒功能方法
5.6Condition接口
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁
6.Java并发容器和框架
6.1concurrentHashmap实现原理
在 JDK7 中,ConcurrentHashMap 使用“分段锁”机制实现线程安全,数据结构可以看成是"Segment数组+HashEntry数组+链表",一个 ConcurrentHashMap 实例中包含若干个 Segment 实例组成的数组,每个 Segment 实例又包含由若干个桶,每个桶中都是由若干个 HashEntry 对象链接起来的链表。
因为Segment 类继承 ReentrantLock 类,所以能充当锁的角色,通过 segment 段将 ConcurrentHashMap 划分为不同的部分,就可以使用不同的锁来控制对哈希表不同部分的修改,从而允许多个写操作并发进行,默认支持 16 个线程执行并发写操作,及任意数量线程的读操作。
6.2.java队列
-
定义
队列是一种特殊的线性表,遵循先入先出、后入后出的基本原则,一般来说,它只允许在表的前端进行删除操作,而在表的后端进行插入操作,但是java的某些队列运行在任何地方插入删除;比如我们常用的 LinkedList 集合,它实现了Queue 接口,因此,我们可以理解为 LinkedList 就是一个队列 -
阻塞队列
ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。 -
非阻塞队列
LinkedList
PriorityQueue
ConcurrentLinkedQueue
7.原子类
它保证了原子性与可见性常见如下:
- AtomicBoolean
- AtomicInteger
- AtomicLong
…
8.线程池
案例:并发从队列取值
- 线程池配置
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @author GuoZhong
* @description
* @date 2022/11/28 11:10
*/
@Configuration
public class ThreadPoolTaskExecuteConfig {
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
//设置线程池参数信息
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(50);
taskExecutor.setKeepAliveSeconds(60);
taskExecutor.setThreadNamePrefix("myExecutor--");
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
taskExecutor.setAwaitTerminationSeconds(60);
//修改拒绝策略为使用当前线程执行
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//初始化线程池
taskExecutor.initialize();
return taskExecutor;
}
}
- 业务代码
package com.example.demo.cron;
import com.example.demo.pojo.Task;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author GuoZhong
* @description
* @date 2022/11/28 11:21
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class ThreadCron {
private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
private static final int CAPACITY=600;
private static final int MAX_FOR_NUM=500;
private LinkedBlockingQueue<Integer> queue=new LinkedBlockingQueue<Integer>(CAPACITY);
/**
*功能描述 初始化队列
*/
@PostConstruct
public void initQueue() throws InterruptedException {
for(int i=0;i<MAX_FOR_NUM;i++){
queue.put(i);
}
}
@Scheduled(cron="0/1 * * * * ?")
public void countTask() throws InterruptedException {
Integer take = queue.take();
try {
threadPoolTaskExecutor.submit(()->{
show(take);
});
}catch (Exception e){
e.printStackTrace();
log.info("捕获拒绝异常");
}
}
private void show(Integer take) {
log.info("take:{}",take);
}
}
案例:计时器,不到5秒,进行重置
- 线程池配置
@Configuration
public class ThreadPoolTaskSchedulerConfig {
@Bean
public ThreadPoolTaskScheduler threadPoolTaskExecutor() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
//设置线程池参数信息
taskScheduler.setPoolSize(1);
taskScheduler.setThreadNamePrefix("timerScheduler--");
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
taskScheduler.setAwaitTerminationSeconds(60);
//修改拒绝策略为使用当前线程执行
taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//初始化线程池
taskScheduler.initialize();
return taskScheduler;
}
}
private final ThreadPoolTaskScheduler threadPoolTaskScheduler;
private void Timer(){
//周期计划对象
ScheduledFuture<?> schedule=null;
//创建任务
Runnable task=()-> {
log.info("通知信息");
};
Date startTime=new Date(5000L);
//间隔周期5s执行一次
if (null == schedule) {
//取消周期
schedule.cancel(true);
}
//取消周期
schedule.cancel(true);
schedule = threadPoolTaskScheduler.scheduleWithFixedDelay(task,startTime,5000L);
}
线程池参数介绍
**corePoolSize**:线程池最少维护的数量
**keepAliveSeconds**:允许的空闲时间
**maxPoolSize**:线程池维护现场的最大数量
**queueCapacity**:缓存队列
**rejectedExecutionHandler**:对拒绝task的处理策略
execute(Runable)方法执行过程
1.当线程池里的线程数量小于corePoolSize时,即使当前线程池里面的线程都出于空闲状态,也会新建线程来处理任务;
2.当线程池里的线程数量等于corePoolSize时,若缓存队列里面的数据未满则任务放入缓存队列里面等待
3.当线程池里的线程数量大于corePoolSize小于maxPoolSize时,若缓存队列里面的数量已满,则新建线程
4.当线程池里面的线程数量大于corePoolSize时,若线程处于空闲状态并且空闲时间超过keepAliveSeconds时,将会回收线程,可以动态的控制线程池里面的线程数量
5.当线程池里面的线程数等于maxPoolSize,并且缓存队列已满,则通过rejectedExecutionHandler设置的处理策略来处理任务;
拒绝策略有以下几种处理方式:
1.ThreadPoolExecutor.AbortPolicy:舍弃任务,并抛出异常
2.ThreadPoolExecutor.DiscardPolicy:舍弃任务,不抛出异常
3.ThreadPoolExecutor.DiscardOldestPolicy:丢弃缓存队列最旧(也就是排在队列前面的任务)的任务,并将此任务添加到缓存队列
4.ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务,(直接在executor处理的线程中执行该任务,若线程已关闭则舍弃该任务)