并发编程的核心是为了提高电脑资源的利用率,因为现代操作系统都是多核的,可以同时跑多个线程。那么是不是线程越多越好? 由于线程的切换涉及上下文的切换,所谓上下文就是线程运行时需要的资源,系统要分配给它消耗时间。所以为了减少上下文的切换,我们有以下几种方法:
- CAS算法
- 协程,单线程里实现多任务调度
- 避免创建不需要的线程因此
协程和线程区别:每个线程OS会给它分配固定大小的内存(一般2MB)来存储当前调用或挂起的函数的内部变量,固定大小的栈意味着内存利用率很低或有时面对复杂函数无法满足要求,协成就实现了可动态伸缩的栈(最小2KB,最大1GB).其二OS线程受操作系统调度,调度时要将当前线程状态存到内存,将另一个线程执行指令放到寄存器,这几步很耗时。Go调度器并非硬件调度器,而是Go语言内置的一中机制,因此goroutine调度时则不需要切换上下文。
Java并发机制的底层实现原理,java代码编译成字节码后加载到JVM中,JVM执行字节码最终转化成汇编命令在CPU上运行,因此Java所使用的并发机制依赖JVM的实现和CPU指令。Java大部分并发容器和框架都依赖于volatile和原子操作的实现原理。
- volatile:被volatile修身的变量在进行写操作时会多出一行以Lock为前缀的汇编代码,Lock前缀的指令在多核处理器下执行两件事情,1.将当前处理器缓存行(缓存可分配的最小单元)的数据写入到系统内2.写回内存的操作使其它处理器地址为该缓存的内存无效。这两条保证了所谓的可见性
- 原子操作的实现:首先看一看处理器是如何实现原子操作的,有两核CPU1和CPU2,两个处理器同时对数据i进行操作,CPU采取总线锁使得一个数据不能同时被多个处理器操作。大概原理就是使用处理器提供的一个LOCK信号,一个处理器在总线上输出此信号时另一个处理器的请求被阻塞住。这样会导致别的处理器不能处理其它内存地址的数据,因为总线锁开销比较大出现了缓存锁,使得CPU1修改缓存行1中数据时若使用了缓存锁定,那么CPU2就不能再缓存该缓存。处理器提供了一系列命令支持这两种机制,如BTS,XADD等,被这些指令操作的内存区域就会加锁,使其它处理器不能同时访问。
Java内存模型
Java之间通过共享内存进行通信,处理器和编译器为了提高性能会对指令进行重排序,这在单线程情况下不会发生异常,但是在多线程下就会造成结果的不一致
int a=0;
public int calculate(){
a=1; 1
boolean flag=true; 2
if(flag){
return a*a;
}
return 0;
}
现有两个线程执行这段代码,线程A执行时对指令进行了重排序先制行 2 在执行 1,在中间线程B插入了进来此时a=1值还没被写入导致返回结果为0发生错误。
处理器遵循as-if-serial语义,即不管如何重排序结果不变,但是多线程情况下会出现错误
为了避免重排序,Java引入了volatile变量,使得语句在操作被volatile修饰的变量时禁止指令重排序。在执行指令时插入内存屏障也就是这个目的,最关键的是volatile的读/写内存语义如下
- 写语义:写一个volatile变量时会把线程对应本地内存的值刷新到主存中
- 读语义:读一个volatile变量时会把本地内存的值设置为无效,从主存中读
volatile的缺陷在于改这个动作是不完全的,因此又提出了CAS机制,CAS会使用处理器提供的机器级别的原子命令(CMPXCHG),原子执行读-改-写操作。Java concurrent包中一个通用化的实现模式就是结合两者,步骤如下
- 声明共享变量为volatile
- 使用CAS实现线程间的同步和通信,(自旋乐观锁,性能大大提升)
Java线程池
线程池的核心作用就是维护固定的几个线程,有任务来的时候直接使用避免创建/销毁线程导致的额外开销。 线程池执行流程如下:
提交任务-->核心线程池已满? 是 提交任务到消息队列--->队列已满? 是 按指定策略执行
否 创建线程执行任务 否 加进队列
了解了线程池的原理最重要的就是如何是去使用它,而使用的关键就是参数的设置。
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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
以上是ThreadPoolExecutor的构造函数,我们逐一看一看各参数的含义
- corePoolSize 一直维护的线程数
- maximumPoolSize 最大线程数
- keepAliveTime 多余线程存活的时间(实际线程数比corePool多的那部分)
- workQueue 存储线程的队列,可选择ArrayBlockingQueue等
- threadFactory 创建线程时的用到的工厂,可通过自定义工厂创建更有意义的线程名称
- handler 队列满时采取的策略 有AbortPolicy(直接抛出异常)/CallerRunsPolicy(只用调用者所在的线程执行)等等
提交线程池有两个方法,一个是submit这个不需要返回值,一个是submit会返回一个future对象,并通过future的get()方法获取返回值(该方法会阻塞直到线程完成任务)。
合理配置线程池,CPU密集型任务配置少数线程池如N(CPU个数)+1,I/O密集型任务配置多一点的线程池如2N(CPU个数),其次是使用有界队列即使发现错误。
Executor框架
在HotSpot VM的线程模型中,Java线程被一对一的映射成本地操作系统的线程,操作系统会调度线程把它们分配给可用的CPU。在上层Java通过用户级调度器Executor将任务映射为几个线程,在下层操作系统内核将这些线程映射到硬件处理器上面。
Executor的出现将任务与如何执行任务分离开了,避免了每创建一个线程就要执行它。Executor的整个架构有一下几个要点
- 实现了Runnable和Callable的对象可提交到Executor运行
- 可返回Future获取线程执行后的返回值
- 内部维护一个线程池(上面介绍的)来处理提交过来的任务
Executor最核心的就是ThreadPoolExecutor,下面介绍以下以及各自使用场景
- FixedThreadPool 固定线程个数,用于高负载的服务器,满足资源的管理需求
- SingleThreadPool 单个线程,保证顺序的执行任务
- CachedThreadPool 大小无界的线程池,使用负载比较轻的服务器
- ScheduledThreadPoolExecutor 后台周期执行任务
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。