专栏名称: 老马说编程
从入门到高级, 深入浅出, 老马和你一起探索编程及计算机技术的本质, 篇篇原创, 用心写作。
目录
相关文章推荐
码农翻身  ·  11w*14薪,进DeepSeek了! ·  7 小时前  
程序员小灰  ·  蔚来汽车裁员约10%,20分钟完成裁员。。。 ·  3 天前  
OSC开源社区  ·  使用DeepSeek拯救数据中台 ·  昨天  
程序员的那些事  ·  误杀!微软道歉了! ·  3 天前  
51好读  ›  专栏  ›  老马说编程

(83) 并发总结 / 计算机程序的思维逻辑

老马说编程  · 公众号  · 程序员  · 2017-04-15 20:51

正文

65节 82节 ,我们用了18篇文章讨论并发,本节进行简要总结。


多线程开发有两个核心问题,一个是竞争,另一个是协作。竞争会出现线程安全问题,所以,本节首先总结线程安全的机制,然后是协作的机制。管理竞争和协作是复杂的,所以Java提供了更高层次的服务,比如并发容器类和异步任务执行服务,我们也会进行总结。本节纲要如下:

  • 线程安全的机制

  • 线程的协作机制

  • 容器类

  • 任务执行服务


线程安全的机制

线程表示一条单独的执行流, 每个线程有自己的执行计数器,有自己的栈,但可以共享内存,共享内存是实现线程协作的基础,但共享内存有两个问题,竞态条件和内存可见性,之前章节探讨了解决这些问题的多种思路:

  • 使用synchronized

  • 使用显式锁

  • 使用volatile

  • 使用原子变量和CAS

  • 写时复制

  • 使用ThreadLocal


synchronized

synchronized 简单易用,它只是一个关键字,大部分情况下,放到类的方法声明上就可以了,既可以解决竞态条件问题,也可以解决内存可见性问题。


需要理解的是,它保护的是对象,而不是代码,只有对同一个对象的synchronized方法调用,synchronized才能保证它们被顺序调用。对于实例方法,这个对象是this,对于静态方法,这个对象是类对象,对于代码块,需要指定哪个对象。


另外,需要注意,它不能尝试获取锁,也不响应中断,还可能会死锁。不过,相比显式锁,synchronized简单易用,JVM也可以不断优化它的实现,应该被优先使用。


显式锁

显式锁 是相对于synchronized隐式锁而言的,它可以实现synchronzied同样的功能,但需要程序员自己创建锁,调用锁相关的接口,主要接口是Lock,主要实现类是ReentrantLock。


相比synchronized,显式锁支持以非阻塞方式获取锁、可以响应中断、可以限时、可以指定公平性、可以解决死锁问题,这使得它灵活的多。


在读多写少、读操作可以完全并行的场景中,可以使用读写锁以提高并发度,读写锁的接口是ReadWriteLock,实现类是ReentrantReadWriteLock。


volatile

synchronized和显式锁都是锁,使用锁可以实现安全,但使用锁是有成本的,获取不到锁的线程还需要等待,会有线程的上下文切换开销等。保证安全不一定需要锁。如果共享的对象只有一个,操作也只是进行最简单的get/set操作,set也不依赖于之前的值,那就不存在竞态条件问题,而只有内存可见性问题,这时,在变量的声明上加上volatile就可以了。


原子变量和CAS

使用volatile,set的新值不能依赖于旧值,但很多时候,set的新值与原来的值有关,这时,也不一定需要锁,如果需要同步的代码比较简单,可以考虑 原子变量 ,它们包含了一些以原子方式实现组合操作的方法,对于并发环境中的计数、产生序列号等需求,考虑使用原子变量而非锁。


原子变量的基础是CAS,比较并设置,一般的计算机系统都在硬件层次上直接支持CAS指令。通过循环CAS的方式实现原子更新是一种重要的思维,相比synchronized,它是乐观的,而synchronized是悲观的,它是非阻塞式的,而synchronized是阻塞式的。CAS是Java并发包的基础,基于它可以实现高效的、乐观、非阻塞式数据结构和算法,它也是并发包中锁、同步工具和各种容器的基础。


写时复制

之所以会有线程安全的问题,是因为多个线程并发读写同一个对象,如果每个线程读写的对象都是不同的,或者,如果共享访问的对象是只读的,不能修改,那也就不存在线程安全问题了。


我们在介绍容器类 CopyOnWriteArrayList 和CopyOnWriteArraySet时介绍了写时复制技术,写时复制就是将共享访问的对象变为只读的,写的时候,再使用锁,保证只有一个线程写,写的线程不是直接修改原对象,而是新创建一个对象,对该对象修改完毕后,再原子性地修改共享访问的变量,让它指向新的对象。


ThreadLocal

ThreadLocal 就是让每个线程,对同一个变量,都有自己的独有拷贝,每个线程实际访问的对象都是自己的,自然也就不存在线程安全问题了。


线程的协作机制

多线程之间的核心问题,除了竞争,就是协作。我们在 67节 68节 介绍了多种协作场景,比如生产者/消费者协作模式、主从协作模式、同时开始、集合点等。 之前章节探讨了协作的多种机制:

  • wait/notify

  • 显式条件

  • 线程的中断

  • 协作工具类

  • 阻塞队列

  • Future/FutureTask


wait/notify

wait/notify 与synchronized配合一起使用,是线程的基本协作机制,每个对象都有一把锁和两个等待队列,一个是锁等待队列,放的是等待获取锁的线程,另一个是条件等待队列,放的是等待条件的线程,wait将自己加入条件等待队列,notify从条件等待队列上移除一个线程并唤醒,notifyAll移除所有线程并唤醒。


需要注意的是,wait/notify方法只能在synchronized代码块内被调用,调用wait时,线程会释放对象锁,被notify/notifyAll唤醒后,要重新竞争对象锁,获取到锁后才会从wait调用中返回,返回后,不代表其等待的条件就一定成立了,需要重新检查其等待的条件。


wait/notify方法看上去很简单,但往往难以理解wait等的到底是什么,而notify通知的又是什么,只能有一个条件等待队列,这也是wait/notify机制的局限性,这使得对于等待条件的分析变得复杂, 67节 68节 通过多个例子演示了其用法,这里就不赘述了。


显式条件

显式条件 与显式锁配合使用,与wait/notify相比,可以支持多个条件队列,代码更为易读,效率更高,使用时注意不要将signal/signalAll误写为notify/notifyAll。


中断

Java中取消/关闭一个线程的方式是 中断 ,中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出,线程在不同状态和IO操作时对中断有不同的反应,作为线程的实现者,应该提供明确的取消/关闭方法,并用文档清楚描述其行为,作为线程的调用者,应该使用其取消/关闭方法,而不是贸然调用interrupt。


协作工具类

除了基本的显式锁和条件,针对常见的协作场景,Java并发包提供了多个 用于协作的工具类


信号量类Semaphore用于限制对资源的并发访问数。


倒计时门栓CountDownLatch主要用于不同角色线程间的同步,比如在"裁判"-"运动员"模式中,"裁判"线程让多个"运动员"线程同时开始,也可以用于协调主从线程,让主线程等待多个从线程的结果。


循环栅栏CyclicBarrier用于同一角色线程间的协调一致,所有线程在到达栅栏后都需要等待其他线程,等所有线程都到达后再一起通过,它是循环的,可以用作重复的同步。


阻塞队列

对于最常见的生产者/消费者协作模式,可以使用 阻塞队列 ,阻塞队列封装了锁和条件,生产者线程和消费者线程只需要调用队列的入队/出队方法就可以了,不需要考虑同步和协作问题。


阻塞队列有普通的先进先出队列,包括基于数组的ArrayBlockingQueue和基于链表的LinkedBlockingQueue/LinkedBlockingDeque,也有基于堆的优先级阻塞队列PriorityBlockingQueue,还有可用于定时任务的延时阻塞队列DelayQueue,以及用于特殊场景的阻塞队列SynchronousQueue和LinkedTransferQueue。


Future/FutureTask

在常见的主从协作模式中,主线程往往是让子线程异步执行一项任务,获取其结果,手工创建子线程的写法往往比较麻烦,常见的模式是使用 异步任务执行服务 ,不再手工创建线程,而只是提交任务,提交后马上得到一个结果,但这个结果不是最终结果,而是一个Future,Future是一个接口,主要实现类是FutureTask。


Future封装了主线程和执行线程关于执行状态和结果的同步,对于主线程而言,它只需要通过Future就可以查询异步任务的状态、获取最终结果、取消任务等,不需要再考虑同步和协作问题。


容器类

线程安全的容器有两类,一类是同步容器,另一类是并发容器。在 理解synchronized一节 ,我们介绍了同步容器。关于并发容器,我们介绍了:


同步容器

Collections类中有一些静态方法,可以基于普通容器返回线程安全的同步容器,比如:

public static Collection synchronizedCollection(Collection c)

public static List synchronizedList(List list)
public static Map synchronizedMap(Map m)


它们是给所有容器方法都加上synchronized来实现安全的。同步容器的性能比较低,另外,还需要注意一些问题,比如复合操作和迭代,需要调用方手工使用synchronized同步,并注意不要同步错对象。


而并发容器是专为并发而设计的,线程安全、并发度更高、性能更高、迭代不会抛出ConcurrentModificationException、很多容器以原子方式支持一些复合操作。


写时拷贝的List和Set

CopyOnWriteArrayList基于数组实现了List接口,CopyOnWriteArraySet基于CopyOnWriteArrayList实现了Set接口,它们采用了写时拷贝,适用于读远多于写,集合不太大的场合。不适用于数组很大,且修改频繁的场景。它们是以优化读操作为目标的,读不需要同步,性能很高,但在优化读的同时就牺牲了写的性能。







请到「今天看啥」查看全文


推荐文章
码农翻身  ·  11w*14薪,进DeepSeek了!
7 小时前
OSC开源社区  ·  使用DeepSeek拯救数据中台
昨天
程序员的那些事  ·  误杀!微软道歉了!
3 天前
全球局势战略纵横  ·  F-15要退役?!
7 年前
Clinic門诊新视野  ·  ESC 2017|ESC四大重磅临床实践指南更新
7 年前