Java并发API设计原理案例分析

1,550 阅读12分钟

目录

一、开篇

二、并发与并行

三、并发程序带来关键问题

1、数据竞争

2、死锁

3、活锁

4、资源不足

5、优先权反转

四、Java并发API(详细)

1、基本并发类

2、同步机制

3、执行器

4、Fork/Join框架

5、并发数据结构

五、并发设计模式

1、信号模型

2、会合模式

3、互斥模式

4、读写锁模式

5、线程池模式

6、线程局部存储模式

六、最后总结

一、开篇

在我之前写的许多关于Java网络编程的博文中,已经初步使用了多线程的技术,是java并发的相关应用案例。而现在,需要学习一些关于并发程序设计的原理,弄懂来龙去脉,相对更加深入地理解并发设计原理。而且我发现,前面学习Java网络编程之后,有了实践性的理解,再学习其相关原理,比较容易理解原理方面的知识。

这一篇记录一下我学习的重要知识,整理出来感觉更容易看到,也是方便以后重温学习查看!

二、并发与并行

很奇妙,并发和并行两个概念在操作系统的课程上有学习过,当时只是在操作系统层面上理论的学习,未涉及实际应用场景和编程语言。现在重温知识进一步学习,发现理解起来更实际了。

并发和并行概念有许多定义,整理一下好理解的几个:

并发是指2个或多个活动在同一时间间隔内发生,在多道程序设计中,是指在同一时间段内有多个程序任务同时运行,也就是宏观上同时执行,微观上是串行的。

并行是指在微观上多个任务同时执行,发生在同一个时间点上。显然,要求多个程序任务并行运行,就需要多个处理器,在单处理器系统中,只有并发而无并行

在单个处理器上采用单核执行多个任务即为并发,操作系统的调度程序会很快从一个任务切换到另一个任务,因此看起来所有任务都是同时运行的。而同一时间在不同处理器或处理器核心上同时执行多个任务,就是并行

最关键的还是并发原理以及设计,在并发中一个重要概念**“同步”**由此引出。

同步是一种协调两个或多个任务以获得预期结果的机制。包括:

  1. 控制同步:任务依赖关系。当一个任务的执行需要依赖于另一个任务的结束时,第二个任务不能在前者完成之前开始。
  2. 数据访问同步:当两个或多个任务访问共享变量时,在任意时间里,只有一个任务可以访问该变量。

与同步密切相关的一个概念是临界段,它是一段代码,用来保证在任意时间内只有一个任务能够访问共享资源。而互斥就是用来保证这个临界段的机制。

三、并发程序带来关键问题

并发程序的编写设计需要在同步/互斥机制上做许多工作,才能保证并发程序正确执行。所以,需要关注下面几个关键问题。

1、数据竞争

简单来说,就是多个任务对共享变量进行写入操作,而没有同步机制的临界段作为约束,程序就存在数据竞争。

public class Account{
    private float balance;
    public void modify(float difference){
        float value=this.balance;
        this.balance=value+difference;
    }
}

例如,上面的例子,如果存在两个任务执行同一个Account对象进行modify操作,初始balance=1000,最终应该是3000,但是如果两个任务同时执行了modify方法的第一条语句,又同时执行第二条语句,结果变为2000。因此,Account类不是线程安全的,没有实现原子操作和同步机制。

2、死锁

简单来讲,就是两个或多个任务正在等待另一线程释放的某个资源,而这个线程也正在等待前面的任务释放的资源,这样的并发程序就出现了死锁。

完全满足以下条件时候,就会导致死锁。

  1. 互斥条件:死锁中的资源是不可共享的,一个时间段内只有一个任务可以使用该资源。
  2. 请求和保持条件:任务已经保持了至少一个资源,但又提出新的请求,而该资源需要等待释放,此时该任务阻塞等待,同时不会释放自己获得的资源。
  3. 非剥夺条件:资源只能被那些持有他们的任务释放。
  4. 循环等待条件:任务1正等待任务2占用的资源,任务2又正在等待任务3占用的资源,……,这样出现循环等待。

处理死锁的方法有:预防、避免、检测、解除。这四个基本方法都有具体实现的算法。

3、活锁

如果系统中有两个任务,总是因为对方的行为而改变自己的状态,就会出现活锁。

举个栗子,任务1、2都需要资源1、2,此时任务1拥有资源1并加锁,任务2拥有资源2并加锁。当他们无法访问所需资源时候,就释放自己的资源并开始新的循环,这样无限持续下去,两个任务都不会结束自己的执行过程。

4、资源不足

活锁就是一种资源不足的情况,当任务在系统中无法获取所需资源继续执行任务。

解决方法就是要确保公平原则

5、优先权反转

当一个低优先权任务持有高优先权任务所需资源时,就会发生优先权反转,低优先权的任务会提前执行。

四、Java并发API(详细)

java包含了丰富的并发API,在并发程序设计时候可以灵活使用。

1、基本并发类

  1. Thread类:描述了执行并发Java应用程序的所有线程。
  2. Runnable接口:Java中创建并发程序的另一种方式。
  3. ThreadLocal类:用于存放从属于某个线程的变量。(没有同步机制时使用)
  4. ThreadFactory接口:实现Factory设计模式的基类,用以创建定制线程。

2、同步机制

Java并发的同步机制支持定义访问某个共享资源的临界段;在某一共同点上同步不同的任务。

  1. sychronized关键字:可以在一个代码块或完整方法中定义一个临界段。
  2. Lock接口:提供更丰富灵活的同步操作。其中ReentantLock用于实现一个条件关联锁;ReentantRead-WriteLock将读写操作分离;StampedLock包括了控制读/写访问的模式,是Java8新增的特性。

以下几个比较陌生:

  1. CountDownLatch类:允许多个任务等待多项操作的结束。
  2. CyclicBarrier类:允许多线程在某个共同点上进行同步。
  3. Phaser类:允许控制那些被分为多阶段的任务的执行。所有任务在完成当前阶段分任务前,不能进入下个阶段。

3、执行器

  1. Executor接口和ExecutorService接口:包括共有的execute方法。
  2. ThreadPoolExecutor类:可以定义线程池执行器任务的最大数目。
  3. ScheduledThreadPoolExecutor类:一种特殊的执行器,在一段延迟之后执行任务或周期性执行任务。
  4. Callable接口:提供返回值的Runnable接口的代替接口。
  5. Future接口:包含获取Callable返回值并控制其状态的方法。

4、Fork/Join框架

该框架定义了一种特殊的执行器,尤其针对分治求解问题,提供了一种优化机制,开销小。主要类和接口:

  1. ForkJoinPool:实现了用于运行任务的执行器。
  2. ForkJoinTask:可以在上述类中执行的任务。
  3. ForkJoinWorkerThread:准备在1类中执行任务的线程。

5、并发数据结构

Java中常用的数据结构如:ArrayList、Map、HashMap等都不能在并发程序中使用,因为没有采取同步机制,不是线程安全的。如果自行采用同步机制,程序计算开销增大。

因此,Java提供了并发程序中特殊的数据结构,属于线程安全,主要有两个类别:

  1. 阻塞型数据结构:包含了阻塞调用任务的方法,比如当获取值时候该数据结果为空。
  2. 非阻塞型数据结构:操作可以立即进行,不会阻塞调用任务。

常用的并发数据结构(线程安全):

  1. ConcurrentHashMap:非阻塞哈希表(关键字Concurrent开头的其他数据结构)

  2. ConcurrentLinkedDeque:非阻塞型列表

  3. LinkedBlockQueue:阻塞型队列

  4. CopyOnWriteArrayList:读读共享、写写互斥、读写互斥。

  • 实现了List接口
  • 内部持有一个ReentrantLock lock = new ReentrantLock();
  • 底层是用volatile transient声明的数组 array
  • 读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array

5.PriorityBlockingQueue:阻塞型队列,基于优先级对元素进行排序。

6.AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference:基本Java数据类型的原子实现。(可使用原子变量代替同步)

五、并发设计模式

1、信号模型

实现该模式课采用信号量或者互斥,Java中的ReentrantLock或Semaphore,或者Object类的wait和notify方法。

public void task1(){
    section1();
    commonObject.notify();
}

public void task2(){
    commonObject.wait();
    section2();
}

section2方法总是在section1之后执行。

2、会合模式

信号模式的推广,第一个任务将等待第二个任务某个活动的完成,而第二个任务也在等待第一个任务某个活动的完成,区别在于使用到两个对象。

public void task1(){
    section1_1();
    commonObject1.notify();
    commonObject2.wait();
    section1_2();
}

public void task2(){
    section2_1();
    commonObject2.notify();
    commonObject1.wait();
    section2_2();
}

其中的语句顺序不能修改,否则可能出现死锁。

section2_2总是在section1_1后执行, section1_2总是在section2_1后执行.

3、互斥模式

互斥机制用以实现临界段,确保操作相互排斥。

public void task1() {
    preSection();
    try {
        lockObject.lock();//临界段开始
        section();
    }catch (Exception e){
        ……
    }finally {
        lockObject.unlock();//临界段结束
        postSection();
    }
}

4、读写锁模式

该模式定义了一种特殊的锁,包含两个内部锁:一个用于读操作,一个用于写操作。

锁的特点:读读共享、写写互斥、读写互斥。

Java并发API的ReentrantReadWriteLock类实现了这种模式

5、线程池模式

该模式应用广泛,减少了每次为每个任务创建线程的开销,它是由一个线程集合和一个待执行任务队列构成。ExceutorService接口可以实现该模式。

6、线程局部存储模式

Java中的ThreadLocal类实现了线程局部变量,可以使用线程局部存储,每个线程会访问该变量不同实例。

六、最后总结

并发算法设计遵循:

1、使用线程安全的Java并发API

2、在静态类和共享场合使用局部线程变量

3、避免死锁:对锁排序

4、使用原子变量

这一篇我记录了学习并发设计原理的关键知识,还有一些常用重要的Java并发编程API,在学习理论知识后,对这个部分有了更深层的理解,如果能够加上实践项目或者具体地案例分析,才能算真正掌握。因此,我也找了比较实际的案例进行实践——文件搜索,将在下一篇的博客进行详细记录,希望理论+实践结合,知识的理解更进一步!

如果觉得不错欢迎“一键三连”哦,点赞收藏关注,有问题直接评论,交流学习!

本文首发我的CSDN博客:blog.csdn.net/Charzous/ar…