Java并发编程的笔记

1.简介

操作系统为独立的进程分配资源->内存/文件句柄/安全证书

资源利用率/公平性/便利性促使进程出现

3.对象的共享

访问共享的可变状态需要正确的管理 共享和发布对象 安全的多个线程同时访问

我们希望某个线程在使用对象状态 另一个修改状态 线程能够看到状态的变化。

1)可见性

为了确保多个线程对内存写入操作的可见性 必须使用同步机制

非volatile类型的64位数职变量的读写绘分解为两个32位的操作。

加锁的含义不仅仅局限于互斥行为,也包括内存可见性,确保所有线程都能看到共享变量的最新值。

volatile 可见性/防止重新排序

volatile常见场景:确保自身状态的可见性/确保引用对象的状态可见性/标识重要程序生命周期事件的发生。不要在构造中使this引用溢出

2)发布与溢出

this引用溢出:在构造中启动线程 无论是显式还是隐式,this都会被新创建的线程共享

使用私有构造和公共工厂方法来避免不正确的构造。

3)线程封闭

避免同步 -> 不共享数据 ->线程封闭

Ad-hoc

线程的封闭性完全由程序自己完成

栈封闭

线程内部使用或线程局部使用 需要做工作确保引用的对象不会逸出

ThreadLocal

每个使用该变量都有一份独立的副本 ThreadLocal视为包含了Map对象,保存了特定于线程的值。使用ThreadLocal中实现事务比较方便。

4)不变性

Immutable 对象 线程安全性式不可变对象的固有属性 只有一种状态

不可变对象:对象创建后就不能修改/所有域都是final类型/对象正确创建(this 没有引用溢出)

1)Final域

const 机制的受限版本 除非某个域是可变的 否则将其声明位final域

5)安全发布

多个线程间共享对象 安全的进行共享。

2)不可变对象与初始化安全性

不可变对象是非常重要的对象,Java内存模型位不可变对象提供了特殊的初始化安全性保证

任何线程都可以在不需要额外同步访问不可变对象

3)安全发布的常用模式

对象引用以及对象状态必须对其他线程可见

1)静态初始化函数中初始化

2)对象引用保存到volatile或者atomicRefrance

3) 正确构造的final

4)保存到由锁保护的域

发布静态的构造 最简单安全是使用静态初始化器

4)事实不可变对象

如果对象在发布后不会被修改,在没有额外同步安全访问对象的线程,安全发布是足够的。

在没有额外同步,任何线程都可以安全使用安全发布的事实不可变。

并发共享对象:

1.线程封闭 2.只读共享3.线程安全共享4.保护对象

4.对象的组合

1)设计线程安全的类

      找出构成对象的所有变量

找出约束状态变量的不变条件

建立对象状态的的并发访问管理策略

同步策略规定了如何将不变性,线程封闭和加锁机制结合恰里为何线程安全性。

收集同步需求

状态越小,越容易判断线程状态。final域越多,越简化对象可能状态的分析过程。许多情况下,所有权和封装性是相互关联的,状态变量所有者决定采用哪种枷锁协议维持变量状态完整性。

安全共享对象,线程安全/事实不可变/锁保护

2)实例封闭

通过封闭机制与合适的加锁策略结合,确保线程安全使用非线程安全对象。Collections.synchronizedList 非线程安全类在多线程环境中安全使用。封闭类状态,分析类线程安全无须检查程序。

Java监视器模式 Vector/HashTable 优势在于简单

如果一个类是多个独立 线程安全的状态变狼,那么可以线程安全委托给底层状态变量

扩展线程安全的类 继承和组合

5.基础构建模块

Java平台类库包含了丰富的并发基础构建模块 模块构造并发应用程序的常用模式。

1)同步容器类

Vector/HashTable->对每个公有方法进行同步 每次只有一个线程访问容器状态

可以在客户端加锁解决迭代抛出的异常 -> 会牺牲伸缩性 降低了并发性

如果在同步容器中发现被修改 -> fail fast -> 抛出ConcurrentModificationException

不在遍历加锁 替代 加锁克隆

2)并发容器

通过并发容器来代替同步容器,可以极大提高伸缩降低风险。

Queue/BlockingQueue 阻塞队列实现生产消费者模型

ConcurrentHashMap  HashTable + 拉链法/红黑树 分桶 循环+CAS 实现同步

CopyOnWriteArrayList 写入复制的List 迭代操作远远大于写入操作

3)阻塞队列和生产者-消费者模式

支持定时的offer和poll方法 阻塞的put和take

BlockingQueue简化了生产者消费者模式 - 线程池 调整生产者和消费者之间的线程比例,提高了线程的利用率。

PriorityBlockingQueue 优先级队列

SynchrnousQueue没有储存功能 直接传递消息

生产者消费者设计与阻塞队列一起促进了 串行线程的封闭

双端队列和工作密取

如果一个消费者完成了双端队列中的全部工作,可以从其他消费者双端队列末尾秘密获取工作

密取工作模式有更高的伸缩性 ->  密取机制保持了高效并行。

4)阻塞方法和中断方法

阻塞状态BLOCKED/WAITING/TIMED_WAITING Thread 提供了interrupt方法,中断线程查询线程是否中断

每个线程的boolean属性 表示线程的中断状态。中断不是强行停止的 是一种协作机制

5)同步工具类

信号量/栅栏/闭锁/FutureTask

可变是重要的 可变状态越少 越容易保证线程安全

尽量声明位final类型

不可变对象一定是线程安全

保护一个不变性条件所有变量 要使用同一个锁 同步策略文档化

6.结构化并发程序

任务是抽象且离散的工作单元 应用分解到多个任务 提供了自然事务边界来优化错误恢复过程。

1)在线程中执行任务

独立的任务可以并行执行 调度与负载均衡更高的灵活。

负荷过载 缓慢劣化

串行执行 -> 创建线程(生命周期/资源消耗/稳定性) 没有限制创建线程的数量,限制了远程用户提交速率

2)Executor 框架

串行执行在于糟糕的响应性和吞吐量 Executor接口 用Runnable表示任务

Executor基于生产者和消费者模式

线程池 管理一组同构工作线程的资源池。线程池和工作队列密切相关

在支持事件 Future.get 可以设置默认时间

Executor 任务提交与执行策略分开 支持多种不同类型的执行策略

7.取消与关闭

线程安全/快速/可靠的停下来,使用中断,线程协作机制。

sleep和wait 会检查线程何时中断 interupt只是传递了请求中断

中断时取消的最合理方式

中断策略规定线程如何解释中断请求

合理的中断线程级取消,服务级取消。

响应中断的策略传递异常/恢复中断状态。中断可以用来获取线程状态,为中断的线程

提供进一步的提示。

Future 只能通过任务的Future实现取消

SocketIO/同步IO/获取锁/Selector

线程的所有权是不可变的

传递毒丸对象

JVM shutDownHook

任务/线程/服务生命周期问题 -> 协作式取消 ->使用FutureTask/Executors构建可取消的任务和服务

8.线程池的使用

依赖性任务/响应时间敏感/线程封闭机制/使用ThreadLocal的任务->执行任务和策略的隐形耦合

依赖其他任务 可能造成死锁 需要在Executor中记录线程池大小和配置限制

设置线程池大小 避免过大过小

计算密集型一般是N+1 IO密集型的会更大 内存/文件句柄/套接字/数据库连接都会影响

线程池的参数:基本大小 corePollSize, 最大大小 maxPoolSize 基本大小线程池的目标大小

maxSize是线程的最大大小 超过了空闲时间超过了存活时间

管理队列任务 Executor允许使用BlockingQueue保存等待执行的任务:

无界队列/有界队列/同步移交 配置饱和策略

如果有界的线程池和队列可能导致线程死锁问题

饱和策略:抛弃/异常

线程工厂 扩展ThreadPoolExecutor 扩展了ThreadPoolExecutor行为

添加日志/计时/监视统计信息收集

创建线程和关闭线程/处理队列任务策略。

活跃性/性能/测试

10.避免活跃性危险

过度加锁会导致顺序死锁

1)死锁

如果持有锁调用某个外部方法 将出现活跃性问题 阻塞时间过长其他线程获得当前持有的锁。

如何分析死锁  jstack/带有定时的死锁

线程饥饿/活锁 忙等待 线程重复执行相同的操作失败

最好的解决方法使用开放调用

11.性能和可伸缩性

提升性能 更少的资源做更多的事情

多个线程 线程的协调/上下文切换/线程创建销毁/线程的调度

1.有效利用现有资源 2.CPU尽可能忙碌

衡量标准 运算时间和吞吐量

可伸缩性往往会破坏性能

避免不成熟的优化 程序正确再提高运行速度

以测试为基准 不要猜测

Amdahl :可用资源越多 问题解决速度就越快

F是必须串行的部分

speedup<= 1/F+((1-F)/N) 量化了串行化的效率开销

3)线程引入的开销

为了性能引入线成 带来的性能提升必须超过并发的开销

1)上下文切换

将上下文切换开销分摊到更多不会中断的响应时间上 提高整体的吞吐量。

频繁发生阻塞,他们将无法使用完整的调度时间片。

2)内存同步

synchronized和volatile 内存栅栏 volatile一般是非竞争 优化掉不会发生竞争的锁,减少不必要的开销。某个线程的同步可能会影响其他线程性能,同步会增加共享内存通信量 所有同步线程可能会有影响。

阻塞:

1.自旋2.线程挂起

由于锁竞争导致阻塞时,线程持有锁有一定的开销。释放锁的时候 需要通知被阻塞的性能。

4)减少锁的竞争

减少锁的竞争能提高性能和伸缩性

减少时间/减少频率/协调机制独占锁

加快锁的时间/减少锁的粒度(锁分解和锁分段)

锁分解 提高伸缩性和提高性能

锁分段:concurrenty 通过散列桶 提升写入的并发量 劣势:需要多个锁 开销大

避免热点域

使用分段锁反复请求数据 会引入热点域

使用锁分段来实现散列链

代替独占锁的方法 1.读写锁 2.原子变量

对象分配操作的开销比同步开销更低

同步容器性能变得糟糕

6)减少上下文的开销

锁的持有时间应该尽可能减少 减少可能存在的锁竞争

Amdahl定律 程序的可伸缩性取决于在所有代码中必须串行执行代码比例。

提升伸缩性:减少锁持有时间/降低锁的粒度/使用非独占锁代替阻塞锁

12. 并发程序的测试

1.安全性测试 2.活跃性测试

不发生任何错误的行为/某个良好的行为终究会发生

安全性:测试代码的不变性

活跃性:吞吐量/响应性/可伸缩性

测试并发的程序需要引入额外的同步和时序限制 静态编译语言的程序相比。

高级主题:

13.显式锁

编译器级别的锁优化方式,代码不需要加锁。逃逸分析->锁消除。

Lock提供了无条件/可轮询/定时以及可中断的锁获取操作

和synchronized 相同的互斥和内存可见性

Lock 可以中断有多个条件变量 可定时可轮询避免了死锁的发生 try-lock 完成平缓失败

提供了TimeWaiting的操作

tryLock/try-finally机制可以响应中断

java1.6之后 性能和新版的synchroized的性能相差不大

ReentranLock 实现了公平锁和非公平锁

公平锁效率低 -> 恢复一个被挂起的线程

非公平锁和公平锁的两处不同: 

1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。

2.非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

读写锁:读写锁可重入,实现了公平或者非公平锁。写锁可以降级。读锁不可以升级,会造成死锁。读写锁在以读取操作为主的数据结构,提高了程序的可伸缩性

14.构建自定义的同步工具

状态依赖的类 创建状态依赖的类 在AQS上进行改造

1)状态依赖性的管理

内置的条件队列可以一直让线程阻塞直到可以继续执行  并且当被阻塞的线程可以执行时再唤醒

有界缓存的前提条件:不能从空缓存获得元素,不能将元素放入已经满的缓存中

实现缓存得到简化 不能抵消在使用时的复杂性

要在1.容忍自旋导致浪费 2.要么容忍休眠导致低响应 休眠间隔越小

条件队列:一组线程(等待线程集合)通过某种方式来等待特定的条件变真。条件队列时一个个等待相关条件的线程。每个对象同样作为一个条件队列。

等待由状态构成的条件和维护状态一致性必须紧密的绑定在一起。

使用条件队列

条件队列让构建高效以及高可响应的状态依赖变得容易。但同时容易被不正确使用。

条件谓词:

将与条件谓词以及在这些条件谓词等待的操作都写入文档

过早唤醒:

从wait中唤醒 不一定正在等待的条件谓词已经变成真了

丢失的信号:线程必须等待一个已经为真的条件,开始等待之前没有检查条件谓词。

4)通知

在等待一个条件,一定确保条件谓词变真通过某种方式发出通知。

使用notify而不是notifyAll:1)所有线程类型都相同 2)单进单出

仅当put/take影响状态转换才发出通知 -> 条件通知

设计一个可继承的状态依赖类 -> 1.等待和通知向子类公开 2.完全禁止子类化

AQS的入口协议和出口协议 入口条件谓词 出口包括操作修改的状态变量‘

synchroized 内置条件队列 每个内置锁有一个相关联的条件队列

Lock 可以有任意数量的Condition对象

使用AQS的好处 1.减少实现的工作,不必处理多个位置上发生的竞争问题 2.不必处理多个位置上的竞争问题 3.考虑了可伸缩性。

AQS

基本操作包括各种形式获取操作和释放操作 AQS负责管理同步器的状态

state 整数状态信息,可以用整数表示任何状态。

ReentranLocak 将同步状态保存锁获取和操作的次数

Semaphore将AQS的同步状态用于保存当前可用许可的数量

实现状态依赖的类 类的方法必须阻塞 内置条件队列 构建自己的同步器

15.原子变量与非阻塞同步机制

使用底层的原子指令(CAS)代替锁 确保一致性 非阻塞算法多个线程在竞争数据时不会发生阻塞

不存在死锁和其他活跃性问题 非阻塞算法不会收到单个线程失败的影响。

AtomicXXX 原子变量 AtomicReference 原子变量 ABA问题

1)锁的劣势

挂起和恢复线程存在大的开销,较长时间中断。激烈竞争,调度开销与工作开销的比值会非常高。volatile 轻量级同步机制

目前原子操作唯一方法使用锁定方法。线程之间的竞争应该有一种粒度更细的技术,类似于volatile变量 支持原子的更新操作

2)硬件并发的支持

通过冲突检查机制来判断在更新中是否存在来自其他线程的干扰。存在将失败并且重试。

现代处理器中包含原子读-改-写指令

比较并交换/关联加载条件存储

比较并交换:

CAS CAS是乐观的技术,希望能成功执行更新操作。

线程在竞争失败 在CAS失败不会阻塞 决定是否重新尝试

竞争程度不高 基于CAS的操作性能远远超过锁 因为CAS在大多数情况下成功执行 把负责控制逻辑的开销最低。

CAS的缺点是需要调用者处理竞争问题。

3)原子变量类:

原子变量类 泛化的volatile变量

标量类/更新器类/数组类/复合变量类

在高度竞争的情况下,锁的性能超过原子变量的性能 真实情况,原子变量性能高。锁在发生竞争的时候 会挂起线程,降低了CPU使用lv和共享内存上的同步通信量。

4)非阻塞算法

一个线程的失败或挂起不会导致其他线程实拍活着挂起。Lock-Free算法

非阻塞的栈:使用CAS提供原子性又提供可见性。通过CAS来修改。

非阻塞的链表:1.在包含多个步骤的更新操作,确保数据结构总是处于一致的状态。2.如果B到达的时候发现A正在修改数据结构,数据结构中应该有足够多的信息。

原子域更新器:原子领域反射器 基于反射的视图

ABA问题 -> 引用加一个版本号 AtomicStampedReference 并发性能的提升来自于JVM内部对非阻塞算法的使用。

16.Java 内存模型

JMM 规定了JVM遵循的最小保证,规定了对变量写入操作在何时对于其他线程可见。在没有正确同步的情况下,难以预测并发程序的行为。 同步限制编译器,运行时和硬件对内存的重排序,不会破坏JMM的可见性保证。

通过程序顺序规则和volatile变量规则结合在一起 -> 防止内存重排

缺少Happens-Before 重排序 除了不可变对象以外,使用被另一个线程初始化对象通常不安全的。

安全的初始化

延迟初始化 -> 双重检查锁(性能糟糕的技巧)

Java 内存模型说明了某个线程内存在哪些情况下对其他线程是可见的

Happens-Before

你可能感兴趣的:(Java并发编程的笔记)