13 多个线程间锁的并发控制
多个线程间锁的并发控制,对象锁多个线程、每个线程持有该方法所属对象的锁以及类锁。synchronized, wait, notify 是任何对象都具有的同步工具
对象锁的同步和异步
- 同步:synchronized,同步的概念就是共享,只需要针对共享的资源,才需要考虑同步。
- 异步:asynchronized,异步的概念就是独立,相互之间不受到任何制约。
同步的目的就是为线程安全,其实对于线程安全来说,需要满足两个特性:原子性(同步)、可见性。
14 Volatile关键字
Volatile作用,实现变量在多个线程间可见,保证内存可见性和禁止指令重排
多线程的内存模型:main memory(主存)、working
memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。
15 ThreadLocal
线程局部变量,以空间换时间的手段,为每个线程提供变量的独立副本,以无锁的情况下保障线程安全。主要解决的就是让每个线程执行完成之后再结束,这个时候就要用到join()方法。
适用场景:
- 在并发不是很高的时候,加锁的性能会更好
- 在高并发量场景下,使用ThreadLocal可以在一定程度上减少锁竞争。
16 多线程同步和互斥实现方法
1). 线程同步,是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程间的同步方法,大体可分为两类:用户模式和内核模式。顾名思义:
内核模式,就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态。内核模式下的方法有:
- 事件
- 信号量
- 互斥量
用户模式,就是不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:
- 原子操作(例如一个单一的全局变量)
- 临界区
2). 线程互斥,是指对于共享的进程系统资源,在各单个线程访问时的排它性。
当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。 线程互斥可以看成是一种特殊的线程同步。
17 线程之间通信
线程是操作系统中独立的个体,但这些个体之间如果不经过特殊的协作就不能成为一个整体,线程间的通信就成为整体的必用方式之一。
线程间通信的几种方式?
线程之间的通信方式:
- 共享内存
- 消息传递
共享内存:在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。
消息传递:在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。在 Java 中典型的消息传递方式,就是 wait()
和 notify()
,或者 BlockingQueue 。
18 什么是 Java Lock 接口?
java.util.concurrent.locks.Lock 接口,比 synchronized 提供更具拓展行的锁操作。它允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。它的优势有:
- 可以使锁更公平。
- 可以使线程在等待锁的时候响应中断。
- 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。
- 可以在不同的范围,以不同的顺序获取和释放锁。
19 Java AQS
AQS ,AbstractQueuedSynchronizer ,即队列同步器。它是构建锁或者其他同步组件的基础框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等),J.U.C 并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是 J.U.C 并发包中的核心基础组件。
优势:
AQS 解决了在实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO 同步队列。基于 AQS 来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
在基于 AQS 构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计 AQS 时充分考虑了可伸缩性,因此 J.U.C 中,所有基于 AQS 构建的同步器均可以获得这个优势。
20 同步类容器
何为同步容器?可以简单地理解为通过synchronized来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。
特点:
- 是线程安全的
- 某些场景下可能需要加锁来保护复合操作
常见同步类容器:
- 如Vector、HashTable
- 使用JDK的Collections.synchronized等工厂方法去创建实现的。
- 底层是用传统的synchronized关键字对方法进行同步。
- 无法满足高并发场景下的性能需求
21 并发类容器
jdk5.0以后提供了多种并发类容器来替代同步类容器从而改善性能。
同步类容器局限性:
- 都是串行化的。
- 虽实现了线程安全,却降低了并发性
- 在多线程环境时,严重降低了应用程序的吞吐量。
常用的并发类容器:
- ConcurrentHashMap
- Copy-On-Write容器
ConcurrentHashMap原理
- 内部使用段(Segment)来表示这些不同的部分
- 每个段相当于一个小的HashTable,它们有自己的锁。
- 把一个整体分成了16个段(Segment)。也就是最高支持16个线程的并发修改操作。
- 这也是在多线程场景时通过减小锁的粒度从而降低锁竞争的一种方案。
Copy-On-Write容器
Copy-On-Write简称COW,是一种用于程序设计中的优化策略。
- 读写分离,读和写分开
- 最终一致性
- 使用另外开辟空间的思路,来解决并发冲突
JDK里的COW容器有两种:
- CopyOnWriteArrayList:适用于读操作远远多于写操作的场景,例如,缓存.
- CopyOnWriteArraySet:线程安全的无序的集合,可以将它理解成线程安全的HashSet,适用于Set 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突
22 并发Queue
并发Queue:
- ConcurrentLinkedQueue,高性能队列,当许多线程共享访问一个公共集合时,ConcurrentLinkedQueue 是一个恰当的选择。
- BlockingQueue,阻塞队列,是一个支持两个附加操作的队列,常用于生产者和消费者的场景。
ConcurrentLinkedQueue
- 适用于高并发场景
- 使用无锁的方式,实现了高并发状态下的高性能
- 其性能好于BlockingQueue
- 遵循先进先出的原则
常用方法:
- Add()和offer()都是加入元素的方法
- Poll()和peek()都是取头元素节点,区别在于前者会删除元素,后者不会。
BlockingQueue接口实现:
- ArrayBlockingQueue:基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长的数组,以便缓存队列中的数据对象,其内部没实现读写分离,也就意味着生产和消费者不能完全并行,适用很多场景。
- LinkedBlockingQueue:基于链表的阻塞队列,同ArrayBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),LinkedBlockingQueue之所以能够高效地处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作完全并行运行。
- PriorityBlockingQueue:基于优先级别的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁
- DelayQueue:带有延迟时间的Queue,其中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue中的元素必须先实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除、任务超时处理、空闲连接的关闭等等。
- SynchronousQueue:一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并且立刻消费
23 多线程的设计模式
- 基于并行设计模式演变而来
- 属于设计优化的一部分
- 是对一些常用的多线程结构的总结和抽象
- 常见的多线程设计模式有哪些?
24 Concurrent.util常用类
CountDownLatch: 用于监听某些初始化操作,等初始化执行完毕后,通知主线程继续工作。
CycilcBarrier: 所有线程都准备好后,才一起出发,只要有一个人没有准备好,大家都等待。
Concurrent.util常用类定义:实现异步回调,jdk针对该场景提供了一个实现的封装,简化了调用 适合场景:处理耗时的业务逻辑时,可有效的减少系统的响应时间,提高系统的吞吐量。
Concurrent.util常用类Semaphore:信号量,适合高并发访问, 用于进行访问流量的控制
ReentrantLock(重入锁)重入锁,在需要进行同步的代码部分加上锁定,但不要忘记最后一定要释放锁定,不然会造成锁永远无法释放,其他线程永远也进不来的结果。
锁与等待/通知
- 多线程间进行协作工作则需要Object的wait()和notify()、notifyAll()方法进行配合工作
- 在使用锁的时候,可以使用一个新的等待/通知的类,它就是Condition
- 这个Condition是针对具体某一把锁的
多Condition
- 可通过一个Lock对象产生多个Condition进行多线程间的交互
- 使得部分需要唤醒的线程唤醒,其他线程则继续等待通知。
ReentrantReadWriteLock(读写锁)
- 其核心就是实现读写分离的锁。尤其适应在在高并发访问下读多写少的情况下,性能要远高于重入锁。
- 将读写锁分离为读锁和写锁
- 在读锁下,多个线程可以并发进行访问
- 在写锁下,只能一个一个的顺序访问
- 锁优化
25 线程池
使用 Executor 框架的原因:
- 每次执行任务创建线程 new Thread() 比较消耗性能,创建一个线程是比较耗时、耗资源的。
- 调用 new Thread() 创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
- 接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现
线程池的创建方式:
普通任务线程池
- newFixedThreadPool(int nThreads) 方法,创建一个固定长度的线程池。
- 每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化。
- 当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
- newCachedThreadPool() 方法,创建一个可缓存的线程池。
- 如果线程池的规模超过了处理需求,将自动回收空闲线程。
- 当需求增加时,则可以自动添加新线程。线程池的规模不存在任何限制。
- newSingleThreadExecutor() 方法,创建一个单线程的线程池。
- 它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它。
- 它的特点是,能确保依照任务在队列中的顺序来串行执行。
定时任务线程池
- newScheduledThreadPool(int corePoolSize) 方法,创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。
- newSingleThreadExecutor() 方法,创建了一个固定长度为 1 的线程池,而且以延迟或定时的方式来执行任务,类似 Timer 。
线程池的关闭方式
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是:
- shutdown() 方法,不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
- shutdownNow() 方法,立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
26 总结
由于java多线程并发涉及到的知识点太多了,这边不可能一一列全,不过我会在后续的更新中一一去补充完善,而且会涉及原理以及源码层次的解析。谢谢观看,有错误欢迎指出更改!!
各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!