七周七并发模型 第二章笔记
date
Jul 8, 2022
slug
七周七并发模型 第二章笔记
status
Published
tags
读书笔记
summary
七周七并发模型 第二章笔记
type
Post
第二章 线程与锁
线程与锁模型其实是对底层硬件运行过程的形式化。
不应在产品代码上直接使用Thread类等底层服务。
Java中,并发的基本单位是线程,可以将线程看作控制流。线程之间通过共享内存进行通信。
Thread.yield()作用:通知调度器,当前线程想要让出对处理器的占用。
乱序执行可能发生的位置:
- 编译器的静态优化可以打乱代码的执行顺序
- JVM的动态优化也会打乱代码的执行顺序
- 硬件可以通过乱序执行来优化其性能
Java内存模型内存可见性基本原则:如果读线程和写线程不进行同步,就不能保证可见性(一个容易被忽略的重点是:两个线程都需要进行同步)
同步的方法:
- 获取对象的内置锁
- 开启一个线程并通过join()检查线程是否已经终止
- 使用java.util.concurrent包提供的工具
一个简单的规则可以避开死锁:总是按照一个全局的固定顺序获取多把锁。一个常用的技巧是使用对象的散列值作为锁的全局顺序。
警惕对一无所知的外星方法的调用,外星方法可以做任何事,例如持有另一把锁。唯一的解决思路是避免持有锁时调用外星方法
一些避免危害的准则:
- 对共享变量的所有访问都需要同步化
- 读线程和写线程都需要同步化
- 按照约定的全局顺序来获取多把锁
- 当持有锁时避免调用外星方法
- 持有锁的时间应尽可能短
内置锁的限制:
- 一个线程因为等待内置锁而进入阻塞之后,就无法中断该线程了
- 尝试获取内置锁时,无法设置超时
- 获得内置锁,必须使用synchronized块
活锁:如果所有死锁线程同时超时,它们极有可能再次陷入死锁。为每个线程设置不同的超时时间,可以减小活锁的几率。
交替锁
在链表中插入一个节点,交替锁可以只锁链表的一部分,允许不涉及被锁部分的其他线程自由访问链表。插入新的节点时,需要将待插入位置两边的节点枷锁,首先锁住链表的前两个节点,如果这两节点不是间不是待插入位置,那么就解锁第一个节点,并锁住第三个节点,以此类推。
条件变量
建议使用以下模式使用条件变量:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while(!条件为真) {
condition.await();
}
<使用共享资源>
} finally {lock.unlock();}
一个条件变量需要与一把锁关联,线程在开始等待条件之前必须获取这把锁。获取锁后,线程检查所等待的条件是否已经成真。如果条件为真,线程将解锁并继续执行。如果条件不为真,线程会调用await(),它将原子的解锁并阻塞等待该条件。当一个线程调用了signal()或者signalAll(),意味着对应的条件可能变为真,await()将原子的恢复运行并重新加锁。这就是为什么要在循环中调用await()的原因——从await()返回时,需要重新检查等待的条件是否为真,必要的话可能再次调用await()并阻塞。
原子变量
AtomicInteger的incrementAndGet()方法功能上等价于++count,getAndIncrement()方法等价于count++。两者都是原子操作。
原子变量的好处:
- 不会忘了在正确的时候获取锁
- 因为没有锁的参与,对原子变量的操作不会引发死锁
- 原子变量是无锁非阻塞算法的基础
不足:无锁的代码比有锁的代码更为复杂
volatile:
它可以保证变量的读写不被乱序执行。给count标记成volatile并不能保证count++操作是原子的。volatile变量适用的场景也越来越少,如果考虑使用这个,也许应当在atomic包中寻找更合适的工具
公平锁和非公平锁
公平锁 表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序
非公平锁 就是一种获取锁的抢占机制,是随机获得锁的,获取不到再使用类似公平锁的机制
非公平锁可能出现“饿死”的情况
使用线程池减少线程创建和回收的开销
写入时复制
CopyOnWriteArrayList使用了保护性复制的策略。它并不是在遍历列表前进行复制,而是在列表被修改时进行,已经投入使用的迭代器会使用当时的旧副本。
线程池应该有多大:
经验法则:对于CPU密集型任务,线程池的大小应接近于可用核数;对于IO密集型任务,线程池可以设置的更大些。最佳的方法是建立一个真实环境下的压力测试来衡量性能。
生产者-消费者模式
一个线程自产自销->我们可以创建两个线程:一个生产者,一个消费者->多个生产者和多个消费者
java.util.concurrent包中的ArrayBlockingQueue是一个并发队列,非常适合实现生产者-消费者模式。当对一个空队列调用take()时,程序会阻塞直到队列变为非空;当对一个满队列调用put()时,程序会阻塞直到队列有足够空间。
为什么生产者-消费者模式要用阻塞队列?
生产者和消费者可能不会(几乎肯定不会)保持相同的速度。比如,当生产者的速度快于消费者的速度,很容易让队列大小超过内存容量。相比之下,阻塞队列只允许生产者的速度在一定程度上超过消费者的速度,但不会超过很多。
优化方法:
- 使用线程池,而不直接创建线程
- 使用CopyOnWriteArrayList让监听器相关的代码更简单高效
- 使用ArrayBlockingQueue让生产者和消费者之间高效协作
- ConcurrentHashMap提供了更好的并发访问
线程与锁模型的优缺点:
优点:
- 最大的优点是其适用面很广。
- 该模型更接近于“本质”——近似于对硬件工作方式的形式化。正确使用时,效率很高。能解决从小到大不同粒度的问题
- 这个模型可以被轻松的集成到大多数编程语言中
缺点:
- 该模型没有为并行提供直接的支持。
- 除了一些特例,比如实验性的分布式共享内存的系统,该模型仅支持共享内存模型。这也意味着该模型不适用于单个系统无力解决的问题