并发编程的艺术读书笔记(一)

并发编程的艺术读书笔记(自己记录的,有误勿怪)
1.synchrouniszed底层实现?
首先synchrouniszed的锁是存在对象头中的markword,基于monitorenter和monitorexit指令实现,
monitorenter指令在编译后插入到字节码开始位置,monitorexit插入到结束的位置,jvm保证这两个指令
是一一对应的,执行到monitorenter指令后尝试获取monitor,获取成功则获得锁,此时其他线程也来获取monitoer
就会进入到同步队列,线程阻塞直到持有monitor的线程释放。

2.偏向锁,轻量级锁,重量级锁底层实现和锁升级,撤销,自旋?
锁是存在对象头中的markword,markword中存储着对象的hashCOde,分代年龄和
锁标记位,锁标记位有4个值来标识无锁,偏向锁,轻量级锁,重量级锁

(1)偏向锁:
当线程进入同步代码块时,会在当前线程的桢栈和对象头中markword存储当前线程ID,
下次该线程再获取锁时,只需测试对象头中的线程ID是不是自己,如果是,则说明已经获取了锁,不用再竞争锁,
直接执行同步代码,如果不是则cas获取锁,将markword中的线程id设置为当前线程。
选讲:偏向锁的撤销要等到有竞争时才会释放,会先检查当前时间是否有正在执行的字节码,也就是全局安全点,
然后暂停拥有偏向锁的线程并检查是否活着,如果还活着则线程继续执行,否则设置为无锁状态,其他线程继续
获取锁

(2)轻量级锁:
当线程执行到同步代码快时,jvm会在当前线程中创建锁记录空间,拷贝markword存储到锁记录空间,使用cas将对象头的
markword更新为指向所记录空间的指针,更新成功则获取到锁,并更新锁标记位为轻量级锁。如果更新失败,则使用自旋来竞争锁,
如果仍然获取不到锁,就会升级为重量级锁,同时更改锁标记位,升级重量级锁后其他线程也会阻塞。

轻量级锁的释放则是将锁记录空间的markword拷贝回到对象头中,如果更新失败,说明存在锁竞争,升级为重量级锁。
ps:自旋其实是用循环实现的,会消耗cpu,所以为了避免无用的自旋,锁升级到重量级锁后不能降级为轻量级锁了。

3.操作系统实现原子操作的原理(选讲)?java中如何实现原子操作?cas有什么问题?
(1)(选讲)cpu通过总线基于对总线加锁和缓存加锁的方式实现多处理器之间的原子操作,其中总线锁就是使用处理器
提供一个lock#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,当前处理器可以独享内存。
当使用缓存锁定时,由缓存一致性机制保证只允许一个处理器将缓存行的数据写入内存
(2)java中通过锁和循环cas实现原子操作,jvm底层用处理器提供的cmpxchg指令实现。
(3)会有aba问题,而且只能保证一个变量的原子操作,循环消耗cpu性能。

4.happen-before原则
(1)单线程中前一个操作happen-befor于后一个操作
(2)同一个锁的解锁happen-before于它的加锁
(3)volatie变量的写happen-before于该后续对变量的读

5.volatile:
如果一个共享变量未申明为volatile,如下:
public class Volatile {

	int a = 0;
	boolean flag = false;

	public void writer() {
		a = 1; //1 
		flag = true;//2
	}

	public void reader() {
		if (flag) {//3
			int i = a * a;//4
		}
	}
}

上述代码如果在单线程下,由于as-if-serial语义支持下,不会发生线程安全问题,
在多线程下,1和2步,3和4不存在数据依赖,1和2重排序或者3和4重排序都会导致
线程不安全问题。

(1)内存语义:
a.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
b.当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
(2)volatile重排序规则:
a.当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
b.当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
c.当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。(分析下若果第一个volatile读,第二个volatile写是否可重排序)

(3)内存语义cpu层通过插入内存屏障来实现。
如果接着问:cpu有loadload,storestore,loadstore,storeload四种内存屏障,。。。
a.在每个volatile写操作的前面插入一个StoreStore屏障
b.在每个volatile写操作的后面插入一个StoreLoad屏障。
c.在每个volatile读操作的后面插入一个LoadLoad屏障。
d.在每个volatile读操作的后面插入一个LoadStore屏障。
StoreLoad 屏障是一个“全能型”的屏障,它同时具有其他3个屏障的效果,

因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中,执行该屏障开销很大。
ps:多处理器下内存屏障是通过lock前缀指令完成的,单处理器自身会维护单处理器内的顺序一致
性,不需要lock前缀。

6.Lock接口机器实现类
(1)使用规范如下,注意lock()要在try之前调用,在try中调用抛异常的话会在final中释放锁
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock();
}

(2)相比synchronized的特性,
a.非阻塞
b.超时获取锁,
c.中断时中断异常会抛出,同时锁释放

(3)独占锁

机制:
由一个同步器和一个FIFO的队列实现,同步器中的头节点对象指向队列中的头节点,尾节点对象指向
队列的尾节点,队列中只有头节点获取到锁,其他线程(节点)被构造成为节点并加入到同步队列中,自旋获取锁,
获取到了锁就退出自旋

获取锁:
由同步器和同步队列协同完成。同步器包含了头节点和尾节点,当一个线程成功地获取了同步状态(只能是头节点),其他线程(其他节点)将无法获取到同步状态。
其他线程转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此
同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Nodeupdate),
它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联,其他节点在
同步队列中保持自旋获取锁,获取到了锁就退出自旋。

释放锁:
在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点

ps:加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节
点的方法:compareAndSetTail(Node expect,Nodeupdate)

(4)重入锁reentrantLock
已经获得锁的线程再次获取锁时将volatile变量state加1,每次再进入都会再加1,
最终释放锁就要对应减1并将占用线程设置为null
ps:synchronized关键字也是隐式实现了可重入的。

(5)公平锁
公平锁是严格按照获取锁的顺序加锁的,所以等待最长的线程能获取到锁,
加锁时判断是否有前驱节点,有则加锁失败。
非公平锁的效率会比公平锁好。
reentrantLock也可通过构造函数传入参数,实现公平锁,
private static Lock fairLock = new ReentrantLock2(true);
private static Lock unfairLock = new ReentrantLock2(false);

(6)读写锁ReentrantReadWriteLock
a.reentrantLock是排他锁,而读写锁可以允许多个线程同时拥有
读锁,但写锁是独占的,排他的,读写锁维护了一个读锁和一个写锁,
b.读写锁支持公平锁与非公平锁,读锁与写锁均支持可重入,支持写锁降级为读锁(不支持读锁升级为写锁)

读写状态的设计:
因为要控制两个锁,而且还要是原子操作,所以采用在一个整形变量state(4个字节32bit)的高16位
代表读锁变量,低16位代表写锁变量,则读锁变量+1等于state + 1<<16,写锁变量+1等于state + 1,
所以如果state不等于0,且state >> 16大于0,(即右移后低16位的二进制数不是0)则获取到了读锁。

写锁降级成读锁流程(同一线程):
a.获取写锁-》b.获取读锁-》c.释放写锁-》d.释放读锁,b步骤获取读锁即时完成降级。

ps:
a.对于一个线程获取写锁后还能获取读锁,获取读锁后也能获取写锁,类比mysql对于
一个线程获取写锁后还能获取读锁,获取读锁后也能获取写锁,类比mysql
当前事务。

b.拓展:实现本地缓存为了更好的锁性能肯定会用到读写锁
c.读状态是所有线程获取锁的总和,jdk1.6后由于增加了一些新的功能,
每个线程获取锁的数量存在threadlocal中,读锁也变得复杂了

7java中的7种阻塞队列
ArrayBlockingQueue:
一个由数组结构组成的有界阻塞队列,按fifo对元素排序,默认情况下不保证线程公平的访问队列,
(类比数组可以随机访问)

LinkedBlockingQueue:
一个由链表结构组成的有界阻塞队列。·

PriorityBlockingQueue:
一个支持优先级排序的无界阻塞队列。默认情况下元素采取自然顺序升序排列

DelayQueue:
一个使用优先级队列实现的无界阻塞队列,在创建元素时可以指定多久才能从队列中获取当前元素。
只有在延迟期满时才能从队列中提取元素。
·
SynchronousQueue:
一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素,
支持公平访问队列。默认情况下线程采用非公平性策略访问队列·

LinkedTransferQueue:
一个由链表结构组成的无界阻塞队列。·有tryTransfer和transfer两个重要方法,
如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法,
时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等
待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。

tryTransfer方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,
则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,
方法立即返回,而transfer方法是必须等到消费者消费了才返回

LinkedBlockingDeque:
一个由链表结构组成的双向阻塞队列。双向队列指的是可以从队列的两端插入和移出元素,
因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争,

CountDownLatch和CyclicBarrier
CountDownLatch的构造函数接收一个int类型的参数N作为计数器,调用CountDownLatch的countDown方法时,N就会减1。可以让一组线程
同时达到一个位点。

CyclicBarrier让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,
所有被屏障拦截的线程才会继续运行

区别:
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景,
CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得Cyclic-Barrier阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断

ps:
一个线程调用countDown方法happen-before,另外一个线程调用await方法

Semaphore
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源,
Semaphore可以用于做流量控制,

Exchanger:
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,
如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,
这两个线程就可以交换数据,将本线程生产出来的数据传递给对方

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值