Java 并发编程

并发编程的核心是为了提升电脑资源的利用率,由于现代操做系统都是多核的,能够同时跑多个线程。那么是否是线程越多越好? 因为线程的切换涉及上下文的切换,所谓上下文就是线程运行时须要的资源,系统要分配给它消耗时间。因此为了减小上下文的切换,咱们有如下几种方法:java

  • 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 后台周期执行任务
相关文章
相关标签/搜索