资讯专栏INFORMATION COLUMN

Java 并发编程

nihao / 1295人阅读

摘要:并发编程的核心是为了提高电脑资源的利用率,因为现代操作系统都是多核的,可以同时跑多个线程。合理配置线程池,密集型任务配置少数线程池如个数,密集型任务配置多一点的线程池如个数,其次是使用有界队列即使发现错误。

并发编程的核心是为了提高电脑资源的利用率,因为现代操作系统都是多核的,可以同时跑多个线程。那么是不是线程越多越好? 由于线程的切换涉及上下文的切换,所谓上下文就是线程运行时需要的资源,系统要分配给它消耗时间。所以为了减少上下文的切换,我们有以下几种方法:

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 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 后台周期执行任务

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68960.html

相关文章

  • Java多线程学习(七)并发编程中一些问题

    摘要:相比与其他操作系统包括其他类系统有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。因为多线程竞争锁时会引起上下文切换。减少线程的使用。很多编程语言中都有协程。所以如何避免死锁的产生,在我们使用并发编程时至关重要。 系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)syn...

    dingding199389 评论0 收藏0
  • Java多线程学习(七)并发编程中一些问题

    摘要:因为多线程竞争锁时会引起上下文切换。减少线程的使用。举个例子如果说服务器的带宽只有,某个资源的下载速度是,系统启动个线程下载该资源并不会导致下载速度编程,所以在并发编程时,需要考虑这些资源的限制。 最近私下做一项目,一bug几日未解决,总惶恐。一日顿悟,bug不可怕,怕的是项目不存在bug,与其惧怕,何不与其刚正面。 系列文章传送门: Java多线程学习(一)Java多线程入门 Jav...

    yimo 评论0 收藏0
  • 并发编程 - 探索一

    摘要:并发表示在一段时间内有多个动作存在。并发带来的问题在享受并发编程带来的高性能高吞吐量的同时,也会因为并发编程带来一些意想不到弊端。并发过程中多线程之间的切换调度,上下文的保存恢复等都会带来额外的线程切换开销。 0x01 什么是并发 要理解并发首选我们来区分下并发和并行的概念。 并发:表示在一段时间内有多个动作存在。 并行:表示在同一时间点有多个动作同时存在。 例如:此刻我正在写博客,但...

    pcChao 评论0 收藏0
  • 多线程编程完全指南

    摘要:在这个范围广大的并发技术领域当中多线程编程可以说是基础和核心,大多数抽象并发问题的构思与解决都是基于多线程模型来进行的。一般来说,多线程程序会面临三类问题正确性问题效率问题死锁问题。 多线程编程或者说范围更大的并发编程是一种非常复杂且容易出错的编程方式,但是我们为什么还要冒着风险艰辛地学习各种多线程编程技术、解决各种并发问题呢? 因为并发是整个分布式集群的基础,通过分布式集群不仅可以大...

    mengera88 评论0 收藏0
  • 并发

    摘要:表示的是两个,当其中任意一个计算完并发编程之是线程安全并且高效的,在并发编程中经常可见它的使用,在开始分析它的高并发实现机制前,先讲讲废话,看看它是如何被引入的。电商秒杀和抢购,是两个比较典型的互联网高并发场景。 干货:深度剖析分布式搜索引擎设计 分布式,高可用,和机器学习一样,最近几年被提及得最多的名词,听名字多牛逼,来,我们一步一步来击破前两个名词,今天我们首先来说说分布式。 探究...

    supernavy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<