1. 锁
为什么要用锁: 线程进行的过程中随时都有可能产生线程切换, 如果操作同一个变量,可能就会发生问题: 例如两个线程对同一个变量i 各自进行10次自增操作, 得到的结果可能就不是20 , 原因是i++不是原子性的, 它是执行了三条语句 Load i i+1 Store i, 在执行Load i之后如果发生了线程切换, 下一个线程执行完了 Load i i++ Store i 这时候 再切换回来, Load 到的i 还是下一个进程Store 之前的i
锁的实现,
1.屏蔽硬件中断,
缺点: 1. 需要禁用硬件中断,如果临界区执行时间很长,可能会导致其他硬件的任务无法正常执行.比如网络包,磁盘块读写等
缺点: 2. 两个CPU的情况下 ,处理起来比较麻烦, 因为CPU自己只能屏蔽自己的响应中断能力, 要去屏蔽其他CPU的硬件中断,可能这时候其他CPU已经执行了临界区代码
2.软件方法
P算法,D算法 ,解决两个线程互斥问题, E M B(bakery) 算法,解决多个线程互斥问题
优点: 只需要软件就能解决
缺点: 忙等占用CPU资源,实现复杂
3.原子操作
系统提供原子操作CAS Exchange
int CAS(*p, except, update) 如果指针所指的位置与except相同, 则更新为update, 并返回指针所指的位置的值,
如果指针所指的位置与except不同, 则不更新, 并返回 指针所指的位置的值
2.信号量
基于上面3所说的原子操作实现
信号量是可以实现锁的功能的
3. 管程
锁 :
条件变量:
进入管程是互斥的
进入之后有很多条件变量, 每个变量都有一个等待队列, 条件变量是可以释放锁的,
锁的实现在1中已经讲过了
条件变量怎么实现:
有wartingCount,和waitingQueue(注意waitingCount 和信号量是有区别的, waitingCount不需要)
wait方法, 和signal方法
wait方法: 释放锁 waitingCount++,TCB加入waitingQueue 通知操作系统发起线程调度
signal方法.if waitingCount > 0 { remove Thread t frome waitingQueue , weakup t }
4锁的种类
乐观锁
认为一般情况下没有其他线程来抢占资源,只是在要更新的时候去判断一下是否有其他线程修改了资源,
Java中: Atomic系列 CAS机制就是乐观锁
悲观锁
认为一般情况下总是有其他线程来抢占资源,所以提前加锁,
JAVA中, synchronize Lock 都是悲观锁
独占锁
加了锁之后,只能由一个线程读取和修改其中的共享资源,synchronize, ReentraindLock 都是独占锁
共享锁
一个线程对一个数据加了共享锁,其他线程也只能对它加共享锁不能加独占锁,获得共享锁的线程只能读数据,不能修改数据
JAva中 ReentraintReadWriteLock就是共享锁
互斥锁
互斥锁是独占锁的一种常规实现,某一资源同时只允许一个线程对它访问,有唯一性和排他性
读写锁
读写锁是共享锁的一种实现
独占写锁,共享读锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人在队尾排着,这是公平的。
/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。
在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。
/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(false);
可重入锁
可重入锁
又称之为递归锁
,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言,也是一个可重入锁。
敲黑板:可重入锁的一个好处是可一定程度避免死锁。
自旋锁
旋锁是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋。自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。
如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。
在 Java 中,AtomicInteger 类有自旋的操作,我们看一下代码:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
另外自适应自旋锁也需要了解一下。
在JDK1.6又引入了自适应自旋,这个就比较智能了,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会自旋较多的时间,如果自旋很少成功,那以后可能就直接省略掉自旋过程,避免浪费处理器资源。
分段锁
分段锁 是一种锁的设计,并不是具体的一种锁。
分段锁设计目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
在 Java 语言中 CurrentHashMap 底层就用了分段锁,使用Segment,就可以进行并发使用了。
5锁升级(无锁|偏向锁|轻量级锁|重量级锁)
无锁
无锁状态其实就是上面讲的乐观锁,这里不再赘述。
偏向锁
Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。
偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。
轻量级锁
当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。
重量级锁
如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。
在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁
6 锁优化
锁粗化, 锁住的范围增大,
锁消除 编译过程中发现不需要加锁的就直接消除同步锁