zoukankan      html  css  js  c++  java
  • java并发编程

    线程有哪些基本状态?

     

    Java 线程状态变迁如下图所示

    由上图可以看出:

    线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 cpu 时间片(timeslice)后就处于 RUNNING(运行) 状态。

    操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJavaJava Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

    同步:执行一个操作之后,等待结果,然后才继续执行后续的操作。

    异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作。

    阻塞:进程给CPU传达一个任务之后,一直等待CPU处理完成,然后才执行后面的操作。

    非阻塞:进程给CPU传达任务后,继续处理后续的操作,隔断时间再来询问之前的操作是否完成。这样的过程其实也叫轮询。

    ----------------------------------------------------------------------------参考《Java高并发编程详解 多线程与架构设计----------------------------------------------------------------------------

    1 ObservableThread 监控任务的生命周期

    观察者模式遇到thread

    2 Single Thread Execution 设计模式

    Single Thread Execution模式是指在同一时刻只能有一个线程去访问共享资源,就像独木桥一样每次只允许一人通行,简单来说,Single Thread Execution就是采用排他式的操作保证在同一时刻只能有一个线程访问共享资源。

    3 读写锁分离设计模式

    Synchronized StampedLock ReadWriteLock

    4 不可变对象设计模式

    无论是synchronized关键字还是显式锁Lock,都会牺牲系统的性能,不可变对象的设计理念在这几年变得越来越受宠,其中Actor模型(不可变对象在Akka、jActor、Kilim等Actor模型框架中得到了广泛的使用)等都是依赖于不可变对象的设计达到lock free(无锁)的。

    Java核心类库中提供了大量的不可变对象范例,其中java.lang.String的每一个方法都没有同步修饰,可是其在多线程访问的情况下是安全的,Java8中通过Stream修饰的ArrayList在函数式方法并行访问的情况下也是线程安全的,所谓不可变对象是没有机会去修改它,每一次的修改都会导致一个新的对象产生,比如 String s1 =“Hello”;s1=s1+”world”两者相加会产生新的字符串。

    5 future设计模式

    假设有个任务需要执行比较长的的时间,通常需要等待任务执行结束或者出错才能返回结果,在此期间调用者只能陷入阻塞苦苦等待,对此,Future设计模式提供了一种凭据式的解决方案。

    JDK1.8时引入了CompletableFuture,其结合函数式接口可实现更强大的功能。

    6 Guarded Suspension 确保挂起设计模式

    当线程在访问某个对象时,发现条件不满足,就暂时挂起等待条件满足时再次访问,这一点和 Balking 设计模式刚好相反(Balking 在遇到条件不满足时会放弃)。

    7 线程上下文设计模式

    虽然有些时候后一个步骤未必会需要前一个步骤的输出结果,但是都需要将context从头到尾进行传递,假如方法参数比较少还可以容忍,如果方法参数比较多,在七八次的调用甚至十几次的调用,都需要从头到尾地传递context,很显然这是一种比较烦琐的设计,那么我们就可以尝试采用线程的上下文设计来解决这样的问题。

    8 Balking (犹豫)设计模式

    多个线程监控某个共享变量,A线程监控到共享变量发生变化后即将触发某个动作,但是此时发现有另外一个线程B已经针对该变量的变化开始了行动,因此A便放弃了准备开始的工作,我们把这样的线程间交互称为Balking(犹豫)设计模式。

    9 Latch (门阀)设计模式

    若干线程并发执行某个特定的任务,然后等到所有的子任务都执行结束之后再统一汇总,比如用户想要查询自己三年以来银行账号的流水,为了保证运行数据库的数据量在一个恒定的范围之内,通常数据只会保存一年的记录,其他的历史记录或被备份到磁盘,或者被存储于hive数据仓库,或者被转存至备份数据库之中,总之想要三年的流水记录,需要若干个渠道的查询才可以汇齐。如果一个线程负责执行这样的任务,则需要经历若干次的查询最后汇总返回给用户,很明显这样的操作性能低下,用户体验差,如果我们将每一个渠道的查询交给一个线程或者若干个线程去查询,然后统一汇总,那么性能会提高很多,响应时间也会缩短不少。

    10 Thread-Per-Message 模式

    Thread-Per-Message的意思是为每一个消息的处理开辟一个线程使得消息能够以并发的方式进行处理,从而提高系统整体的吞吐能力。这就好比电话接线员一样,收到的每一个电话投诉或者业务处理请求,都会提交对应的工单,然后交由对应的工作人员来处理。

    11 Two Phase Termination (两阶段终止)设计模式

    当一个线程正常结束,或者因被打断而结束,或者因出现异常而结束时,我们需要考虑如何同时释放线程中资源,比如文件句柄、socket套接字句柄、数据库连接等比较稀缺的资源。Two Phase Termination设计模式

    12 Worker-Thread (流水线)设计模式

    Worker-Thread模式有时也称为流水线设计模式,这种设计模式类似于工厂流水线,上游工作人员完成了某个电子产品的组装之后,将半成品放到流水线传送带上,接下来的加工工作则会交给下游的工人。

    13 Active Objects (主动对象)设计模式

    Active是“主动”的意思,Active Object是“主动对象”的意思,所谓主动对象就是指其拥有自己的独立线程,比如java.lang.Thread实例就是一个主动对象,不过Active Object Pattern不仅仅是拥有独立的线程,它还可以接受异步消息,并且能够返回处理的结果。System.gc()方法就是一个“接受异步消息的主动对象”。

    标准的Active Objects要将每一个方法都封装成Message,然后提交至Message 队列中,这样的做法有点类似于远程方法调用(RPC : Remote Process Call)。如果某个接口的方法很多,那么需要封装很多的Message类;同样如果有很多接口需要成为Active Object,则需要封装成非常多的Message类,这样显然不是很友好。更加通用的ActiveObject框架,可以将任意的接口转换成Active Object。使用JDK动态代理的方式实现一个更为通用的Active Objects,可以将任意接口方法转换为Active Objects,当然如果接口方法有返回值,则必须要求返回Future类型才可以,否则将会抛出IllegalActiveMethod异常。

    14 Event Bus (事件总线)设计模式

    异步EventBus

    15 Event Driven (事件驱动)设计模式

    EDA (Event-Driven Architecture))是一种实现组件之间松耦合、易扩展的架构方式,在本节中,我们先介绍EDA的基础组件,让读者对EDA设计架构方式有一个基本的认识,一个最简单的EDA设计需要包含如下几个组件。
    Events:需要被处理的数据。
    Event Handlers:处理Events的方式方法。
    Event Loop:维护Events和 Event Handlers之间的交互流程。

    异步EDA框架设计

    ----------------------------------------------------------------------------参考《Java高并发编程详解 深入理解并发核心库---------------------------------------------------------------------------- 

    CountDownLatch(倒计数门阀)

    JDK官方:“CountDownLatch是一个同步助手,允许一个或者多个线程等待一系列的其他线程执行结束”。

    CyclicBarrier(循环屏障)

    CyclicBarrier与CountDownLatch非常类似,但是它们之间的运行方式以及原理还是存在着比较大的差异的,并且CyclicBarrier所能支持的功能CountDownLatch是不具备的。比如,CyclicBarrier可以被重复使用,而CountDownLatch当计数器为0的时候就无法再次利用。

    Exchanger(交换器)

    Exchanger简化了两个线程之间的数据交互,并且提供了两个线程之间的数据交换点,Exchanger等待两个线程调用其exchange()方法。调用此方法时,交换机会交换两个线程提供给对方的数据。

    Semaphore(信号量)

    Semaphore(信号量)是一个线程同步工具,主要用于在一个时刻允许多个线程对共享资源进行并行操作的场景。

    phaser(阶段器)

    Phaser 是在JDK 1.7版本中才加入的。Phaser同样也是一个多线程的同步助手工具,它是一个可被重复使用的同步屏障,它的功能非常类似于本章已经学习过的CyclicBarrier 和CountDownLatch 的合集,但是它提供了更加灵活丰富的用法和方法,同时它的使用难度也要略微大于前两者。

    Lock&ReentrantLock

    Lock接口是对锁操作方法的一个基本定义,它提供了synchronized关键字所具备的全部功能方法,另外我们可以借助于Lock创建不同的Condition对象进行多线程间的通信操作,与关键字synchronized进行方法同步代码块同步的方式不同,Lock提供了编程式的锁获取(lock())以及释放操作(unlock())等其他操作。

    ReadWriteLock&ReentrantReadWriteLock

    与ReentrantLock一样,ReentrantReadWriteLock的使用方法也是非常简单的,只不过在使用的过程中需要分别派生出“读锁”和“写锁”,在进行共享资源读取操作时,需要使用读锁进行数据同步,在对共享资源进行写操作时,需要使用写锁进行数据一致性的保护。

    Condition详解

    如果说显式锁Lock可以用来替代synchronized关键字,那么Condition接口将会很好地替代传统的、通过对象监视器调用wait()、notify()、notifyAll()线程间的通信方式。Condition对象是由某个显式锁Lock创建的,一个显式锁Lock可以创建多个Condition对象与之关联,Condition 的作用在于控制锁并且判断某个条件(临界值)是否满足,如果不满足,那么使用该锁的线程将会被挂起等待另外的线程将其唤醒,与此同时被挂起的线程将会进入阻塞队列中并且释放对显式锁Lock的持有,这一点与对象监视器的wait()方法非常类似。

    建议使用Condition的方式完全替代对象(Object Monitor)监视器的使用。由于Condition的卓越表现,除了广泛应用于开发中之外,JDK本身的很多类的底层都是采用Condition来实现的。

    StampedLock

    如果对读写锁使用不得当,则还会引起饥饿写的情况发生,所谓的饥饿写是指在使用读写锁的时候,读线程的数量远远大于写线程的数量,导致锁长期被读线程霸占,写线程无法获得对数据进行写操作的权限从而进入饥饿的状态(当然可以在构造读写锁时指定其为公平锁,读写线程获得执行权限得到的机会相对公平,但是当读线程大于写线程时,性能效率会比较低下)。因此在使用读写锁进行数据一致性保护时请务必做好线程数量的评估(包括线程操作的任务类型)。

    针对这样的问题,JDK1.8版本引入了StampedLock,该锁由一个long型的数据截(stamp)和三种模型构成,当获取锁(比如调用readLock(),writeLock())的时候会返回一个 long型的数据戳( stamp),该数据戳将被用于进行稍后的锁释放参数。如果返回的数据戳为0(比如调用tryWriteLock()),则表示获取锁失败,同时StampedLock还提供了一种乐观读的操作方式。

    Guava之Monitor

    无论使用对象监视器的wait notify/notifyAll还是Condition的 await signall signalAll方法调用,我们首先都会对共享数据的临界值进行判断,当条件满足或者不满足的时候才会调用相关方法使得当前线程挂起,或者唤醒wait队列/set中的线程,因此对共享数据临界值的判断非常关键,Guava的 Monitor工具提供了一种将临界值判断抽取成Guard 的处理方式,可以很方便地定义若干个 Guard也就是临界值的判断,以及对临界值判断的重复使用,除此之外Monitor还具备synchronized关键字和显式锁Lock的完整语义。

    当对共享数据进行操作之前,首先需要获得对该共享数据的操作权限(也就是获取锁的动作),然后需要判断临界值是否满足,如果不满足,则为了确保数据的一致性需要将当前线程挂起(对象监视器的wait set或者Condition 的阻塞队列),这样的动作,前文中已经练习过很多次了,Monitor 以及 Monitor Guard则很好地将类似的一系列动作进行了抽象,隐藏了锁的获取、临界值判断、线程挂起、阻塞线程唤醒、锁的释放等操作。

    Guava之RateLimiter

    RateLimiter,顾名思义就是速率(Rate)限流器(Limiter),事实上它的作用正如名字描述的那样,经常用于进行流量、访问等的限制,这一点与Semaphore非常类似,但是它们的关注点却完全不同,RateLimiter关注的是在单位时间里对资源的操作速率(在RateLimiter内部也存在许可证(permits)的概念,因此可以理解为在单位时间内允许颁发的许可证数量),而Semaphore则关注的是在同一时间内最多允许多少个许可证可被使用,它不关心速率而只关心个数。

    RateLimiter-----漏桶算法

    因此在一个提供高并发服务的系统中,若系统无法承受更多的请求,则对其进行降权处理(直接拒绝请求,或者将请求暂存起来等稍后处理),这是一种比较常见的做法,漏桶算法作为一种常见的限流算法应用非常广泛

    1 无论漏桶进水速率如何,漏桶的出水速率永远都是固定的。
    2 如果漏桶中没有水流,则在出水口不会有水流出。
    3 漏桶有一定的水容量。
    4 如果流入水量超过漏桶容量,则水将会溢出(降权处理)。

    令牌环桶算法

    令牌环桶与漏桶比较类似,漏桶对水流进入的速度不做任何限制,它只对水流出去的速率是有严格控制的,令牌环桶则与之相反,在对某个资源或者方法进行调用之前首先要获取到令牌也就是获取到许可证才能进行相关的操作,否则将不被允许。比如,常见的互联网秒杀抢购等活动,商品的数量是有限的,为了防止大量的并发流量进入系统后台导致普通商品消费出现影响,我们需要对类似这样的操作增加令牌授权、许可放行等操作,这就是所谓的令牌环桶。

    1 根据固定的速率向桶里提交数据。
    2 口新加数据时如果超过了桶的容量,则请求将会被直接拒绝。
    3 如果令牌不足,则请求也会被拒绝(请求可以再次尝试)。

    java并发包之并发容器
    基本链表

    优先级链表

    跳表(SkipList)

    平衡树的另一个选择,也是redis的主要数据结构之一。

    增加多个层级进行数据存储和查找,这种以空间换取时间的思路能够加快元素的查找速度,跳表( skiplist)正是受这种多层链表的想法启发而设计出来的。实际上,上面每一层链表的节点个数,都会是下面一层节点个数的一半左右,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。另外,跳表中的元素在插入时就已经是根据排序规则进行排序的,在进行查找时无须再进行排序。

    BlockingQueue(阻塞队列)

    以下七种队列都可称为BlockingQueue,所谓Blocking Queue是指其中的元素数量存在界限,当队列已满时(队列元素数量达到了最大容量的临界值),对队列进行写入操作的线程将被阻塞挂起,当队列为空时(队列元素数量达到了为0的临界值),对队列进行读取的操作线程将被阻塞挂起。

    ArrayBlockingQueue

    基于数组实现的FIFO“有边界”队列

    PriorityBlockingQueue

    优先级阻塞队列是一个“无边界”阻塞队列,与优先级链表类似的是,该队列会根据某种规则(Comparator)对插入队列尾部的元素进行排序,因此该队列将不会遵循FIFO ( first-in-first-out)的约束。

    LinkedBlockingQueue

    LinkedBlockingQueue是“可选边界”基于链表实现的FIFO队列。

    DelayQueue

    DelayQueue也是一个实现了BlockingQueue接口的“无边界”阻塞队列。

    SynchronousQueue

    尽管SynchronousQueue是一个队列,但是它的主要作用在于在两个线程之间进行数据交换,区别于Exchanger的主要地方在于(站在使用的角度)SynchronousQueue所涉及的一对线程一个更加专注于数据的生产,另一个更加专注于数据的消费(各司其职),而Exchanger则更加强调―对线程数据的交换。

    LinkedBlockingDeque

    LinkedBlockingDeque是一个基于链表实现的双向(Double Ended Queue,Deque)阻塞队列,双向队列支持在队尾写人数据,读取移除数据;在队头写入数据,读取移除数据。LinkedBlockingDeque实现自BlockingDeque ( BlockingDeque 又是BlockingQueue的子接口),并且支持可选“边界”,与LinkedBlockingQueue一样,对边界的指定在构造LinkedBlockingDeque时就已经确定了。

    LinkedTransferQueue

    TransferQueue是一个继承了BlockingQueue的接口,并且增加了若干新的方法。LinkedTransferQueue是 TransferQueue接口的实现类,其定义为一个无界的队列,具有FIFO的特性。

    以上7种 BlockingQueue之间的继承关系

    ConcurrentHashMap

    在 JDK 1.8版本中几乎重构了ConcurrentHashMap 的内部实现,摒弃了segment 的实现方式,直接用table数组存储键值对,在JDK1.6中,每个bucket中键值对的组织方式都是单向链表,查找复杂度是O(n),JDK1.8中当链表长度超过8时,链表转换为红黑树,查询复杂度可以降低到O(log n),改进了性能。利用CAS+Synchronized可以保证并发更新的安全性,底层则采用数组+链表+红黑树(提高检索效率)的存储结构。

    ConcurrentSkipListMap

    ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表。内部是SkipList(跳表)结构实现,在理论上,其能够在O(log(n))时间内完成查找、插入、删除操作。调用ConcurrentSkipListMap的 size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素的个数,这个操作是个O(log(n))的操作。
    在读取性能上,虽然ConcurrentSkipListMap不能与ConcurrentHashMap相提并论,但是ConcurrentSkipListMap存在着如下两大天生的优越性是ConcurrentSkipListMap所不具备的。
    第一,由于基于跳表的数据结构,因此ConcurrentSkipListMap的key是有序的。
    第二,ConcurrentSkipListMap支持更高的并发,ConcurrentSkipListMap的存取时间复杂度是o (log(n)),与线程数几乎无关,也就是说,在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出它的优势。

    写时考贝算法(Copy On Write)

    并发容器—CopyOnWrite容器,简称COw,该容器的基本实现思路是在程序运行的初期,所有的线程都共享一个数据集合的引用。所有线程对该容器的读取操作将不会对数据集合产生加锁的动作,从而使得高并发高吞吐量的读取操作变得高效,但是当有线程对该容器中的数据集合进行删除或增加等写操作时才会对整个数据集合进行加锁操作,然后将容器中的数据集合复制一份,并且基于最新的复制进行删除或增加等写操作,当写操作执行结束以后,将最新复制的数据集合引用指向原有的数据集合,进而达到读写分离最终一致性的目的。

    CopyOnWrite常常被应用于读操作远远高于写操作的应用场景中。

    线程池(Executor& ExecutorService

        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                    2,
                    4,
                    30,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(10),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.DiscardPolicy()
            );
    
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("execute the runnable task");
                }
            });
    
            Future<String> future = executor.submit(new Callable<String>() {
                @Override
                public String call() {
                    return "execute the callable task and this is the result";
                }
            });
            System.out.println(future.get());
    
            AtomicDouble result = new AtomicDouble();
            Future<AtomicDouble> f = executor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(20);
                        result.set(35.34D);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, result);
            System.out.println("The task result: " + future.get());
            System.out.println("The task is done? " + future.isDone());
        }

    Guava的Future 

    Future一般是被用于ExecutorService提交任务之后返回的“凭据”,本节对Executor-Service中所有涉及Future相关的执行方法都做了比较详细的讲解,Java中的Future不支持回调的方式,这显然不是一种完美的做法,调用者需要通过get方法进行阻塞方式的结果获取,因此在Google Guava工具集中提供了可注册回调函数的方式,用于被动地接受异步任务的执行结果,这样一来,提交异步任务的线程便不用关心如何得到最终的运算结果。

    虽然Google Guava的 ListenableFuture是一种优雅的解决方案,但是CompletablFuture则更为强大和灵活,目前业已成为使用最广的Future实现之一。

    Fork/Join Framework

    Fork/Join框架是在 JDK1.7版本中被Doug Lea引入的,Fork/Join计算模型旨在充分利用多核CPU的并行运算能力,将一个复杂的任务拆分(fork)成若干个并行计算,然后将结果合并(join)。

    CompletionService

    Future除了“调用者线程需要持续对其进行关注才能获得结果”这个缺陷之外,还有一个更为棘手的问题在于,当通过ExecutorService的批量任务执行方法 invokeAll来执行一批任务时,无法第一时间获取最先完成异步任务的返回结果。

    CompletionService则采用了异步任务提交和计算结果Future解耦的一种设计方式,在 CompletionService中,我们进行任务的提交,然后通过操作队列的方式(比如take或者poll)来获取消费 Future。

    CompletableFuture

    CompletableFuture是自JDK1.8版本中引入的新的Future,常用于异步编程之中,所谓异步编程,简单来说就是:“程序运算与应用程序的主线程在不同的线程上完成,并且程序运算的线程能够向主线程通知其进度,以及成功失败与否的非阻塞式编码方式”,无论是ExecutorService还是CompletionService,都需要主线程主动地获取异步任务执行的最终计算结果,如此看来,Google Guava所提供的ListenableFuture更符合这段话的描述,但是ListenableFuture无法将计算的结果进行异步任务的级联并行运算,甚至构成一个异步任务并行运算的 pipeline,但是这一切在CompletableFuture中都得到了很好的支持。
    CompletableFuture实现自CompletionStage接口,可以简单地认为,该接口是同步或者异步任务完成的某个阶段,它可以是整个任务管道中的最后一个阶段,甚至可以是管道中的某一个阶段,这就意味着可以将多个CompletionStage链接在一起形成一个异步任务链,前置任务执行结束之后会自动触发下一个阶段任务的执行。另外,CompletableFuture还实现了Future接口,所以你可以像使用Future一样使用它。

    Stream 

    1.8版本中,Stream为容器的使用提供了新的方式,它允许我们通过陈述式的编码风格对容器中的数据进行分组、过滤、计算、排序、聚合、循环等操作。

        public static void main(String[] args) throws IOException {
            List<String> list = Files.lines(Paths.get("123.txt"), StandardCharsets.UTF_8).collect(Collectors.toList());
            list.forEach(System.out::println);
        }

    stream主要分为两类操作 Intermediate 和 Terminal

    Collector

    Parallel Stream(并行流)

    对集合中、IO中、数组中等其他的元素以并行的方式计算处理元素数据。在并行流的处理过程中,元素会被拆分为多个元素块( chunks),每一个元素块都包含了若干元素,该元素块将被一个独立的线程运算,当所有的元素块被不同的线程运算结束之后,结果汇总将会作为最后的结果,这一切的一切都是由并行流( Parallel Stream)替我们完成

    Spliterator

    Spliterator也是Java8引入的一个新的接口,其主要应用于Stream中,尤其是在并行流进行元素块拆分时主要依赖于Spliterator的方法定义,这与我们在ForkJoinPool中进行子任务拆分是一样的,只不过对Spliterator的引入将任务拆分进行了抽象和提取

    Metrics

    Metrics最早是在Java的另外一个开源项目dropwizard中使用,主要是为了提供对应用程序各种关键指标的度量手段以及报告方式,由于其内部的度量手段科学合理,源码本身可扩展性极强,现在已经被广泛使用在各大框架平台中,比如我们常见的Kafka,Apache Storm,Spring Cloud等。

  • 相关阅读:
    2015.4.16-C#中ref和out的区别
    2015.4.10-SQL 高级查询(二)
    2015.4.10-C#入门基础(三)
    2015.4.8-C#入门基础(二)
    2015.4.7-C#入门基础(一)
    2015.4.2-SQL 简单语句(一)
    对GridView的某一列行进行操作。。
    jquery获取GridView中RadioButton选中的数据
    Ajax获取前台的数据
    前台验证
  • 原文地址:https://www.cnblogs.com/xiaomaoyvtou/p/9373825.html
Copyright © 2011-2022 走看看