此文是对《Java并发编程的艺术》的笔记
第一章&第二章
如何减少上下文切换:
无锁并发编程:多线程竞争锁时,会引起上下文切换,可以将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
CAS算法:Java的Atomic包使用CAS算法来更新数据,不需要加锁
使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程处理,造成大量线程都处于等待状态。
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
避免死锁的几个常见方法
避免一个线程同时获取多个锁
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
尝试使用定时锁,使用lock.tryLock(timeout)替代使用内部锁机制
对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失效的情况。
volatile
volatile保证了共享变量的可见性,一个线程修改这个变量修饰的值时,另一个线程能读到这个修改的值
volatile修饰的变量,Java内存模型能保证所有的线程看到这个变量的值是一致的。
volatile的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将会从主内存中读取共享变量。
volatile保证可见性的原理
出现汇编代码Lock前缀的指令
在多核处理器中,发生:
1)将当前处理器缓存行的数据写回到系统内存
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效
为什么会无效?是因为下面的缓存一致性协议在起作用
缓存一致性协议
在多处理器下,为了保证各个处理器的缓存是一致的,会实现 缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。
synchronized
synchronized锁的是一个对象
对于普通的同步方法,锁的是当前实例对象
对于静态的同步方法,锁的是当前类的Class对象
对于同步方法块,锁的是Synchronized括号里面配置的对象
实现原理
JVM基于进入和退出Monitor对象来实现方法和代码块的同步
使用monitorenter、monitorexit指令进入和退出
原子操作
实现原理
1)使用总线锁保证原子性
2)通过缓存锁定来保证原子性
“缓存锁定”:是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作写回内存时,处理器不在总线上声明Lock#信号,而是修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
第三章
Java内存模型
线程之间的通信机制:共享内存和消息传递
Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象角度看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
锁的内存语义
和volatile的内存语义相似
获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
释放锁时,JMM会把该线程对应的本地内存中的值刷新到主内存中。
第四章Java并发编程
线程间的通信方式
- volatile和synchronized关键字
- 等待wait()/通知notify()机制
- 管道输入/输出流 PipedOutputStream、PipedInputStream、PipedReader、PipedWriter
- ThreadLocal
第五章Java中的锁
第六章 Java并发容器和框架
ConcurrentHashMap
为什么要使用ConcurrentHashMap?
因为在并发编程中使用HashMap可能导致死循环。而使用线程安全的HashTable效率非常低下,因此使用ConcurrentHashMap
HashMap的死循环问题
在并发执行put操作时,会引起死循环,是因为多线程会导致HashMap的Entry链表结构形成环形数据结构,死循环获取Entry
HashTable
HashTable使用synchronized保证线程安全,在线程竞争激烈时效率非常低下。
ConcurrentHashMap的锁分段技术
HashTable容器在竞争激烈的并发环境下表现出的效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假设容器中有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程就不会存在锁竞争,从而可以有效提高并发效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap的结构
ConcurrentHashMap是由Segment数据结构和HashEntry数组结构组成。Segment是一种可重入锁,在ConcurrentHashMap里面锁的角色,HashEntry用于存储键值对数据
一个ConcurrentHashMap里有一个Segment数组。他的结构和HashMap类似,是一种数组和链表结构。
Segment中包含着一个HashEntry数组,也是数组加链表形式的
ConcurrentHashMap : put方法的逻辑
1、判断Node[]数组是否初始化,没有进行初始化操作
2、通过hash定位数组的索引坐标,是否有node节点,如果没有则使用CAS进行添加(链表的头结点),添加失败则进入下次循环
3、检查到内部正在扩容,就帮助他一块扩容
4、如果f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)
4.1 如果是Node链表结构则执行链表的添加操作
4.2 如果是TreeNode则执行树的添加操作
5、判断链表长度已经到达临界值8,就把链表转为树结构。
简单来说:
- 先判断是否初始化,没有则进行初始化操作
- 通过hash定位到Segment,然后在Segment里面进行插入操作。
- 插入操作有两步:首先使用synchronized锁住(链表/红黑二叉树的头元素)
- 第一步判断是否需要对Segment里的HashEntry数组进行扩容
- 第二步定位添加元素的位置,然后放在HashEntry数组里
ConcurrentHashMap的size操作
统计整个ConcurrentHashMap的元素大小,必须统计Segment里元素的大小后求和。但是在多线程环境下,Segment里的元素数量可能发生变化,最安全的做法是统计大小的时候,把所有的Segment的put、remove、clean方法全部锁住,但是非常低效。
ConcurrentHashMap的做法是先尝试两次通过不锁住Segment的方法来统计各个Segment的大小,在统计的过程中,容器的count发生了变化,则采用加锁的方式统计所有的Segment的大小。
如何判断统计过程中元素是否发生了变化,有个modCount变量,在put、remove、clean方法执行前,都会将modCount进行加1,。那么在size前后比较modCount是否发生变化,就知道了。