多线程-锁
锁的种类
锁 分为两种, 1:阻塞锁 2:自旋锁
- 阻塞锁 当一个线程已经持有了锁,那么其他线程则必须等待。在持有锁的线程,释放锁之前,其他线程都要处于空闲状态。当持有锁的线程,释放锁后,其他线程才能获得锁。
- 自旋锁 当第一次看到这个名字的时候,直接被它吓到��。说白了,当一个线程已经持有了锁,那么其他线程则不断的在循环判断一个标志,该标志标识锁是否被释放。如果循环判断的过程中,发现标志位变成“被释放”,那么其他线程就有获取锁的机会。
两种锁的区别
阻塞锁 阻塞这个操作要通过JVM去实现,没有抢到锁的线程是处于挂起状态。而自旋锁,即使锁已经被抢占,这些没抢到锁的线程,也一直在运行,一直在循环判断标志位。
自己实现锁
手动实现自旋锁
为什么是自旋锁,而不是阻塞锁。因为自旋锁根据上面的描述好实现啊。要想实现阻塞锁,就不是这么简单了,这个以后我们再说。
通过上面的自旋锁介绍。要实现自旋锁,需要有一个标志位,这个标志位的作用就是标识当前锁的状态(释放、已被抢占),如果是释放,则代表锁可以被使用,否则锁已被占用,无法被抢占。
��想一想,有一个问题,假如锁被释放的一瞬间,锁的状态被标识为“释放”。有几个线程同时发现锁的状态为“释放”,那就出现,同一时刻,有多个线程同时抢占到了锁,这不是我们想看到的。所以,锁的标志位必须具有原子性,这里我们使用AtomicBoolean.
首先我们先看一个不使用锁的DEMO
public class Test{
public int value;
public void add(){
try {
int temp = value;
TimeUnit.MILLISECONDS.sleep(1000);
value = temp + 1;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Test test = new Test();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
test.add();
System.out.println(test.value);
}
}).start();
}
}
}
运行结果为上面的代码比较简单,做一个简单说明,开启10个线程,每个线程都对value加1操作。我们想看到结果是,每次线程执行后,value都会加1,但是现在没有。
现在我们实现一个自旋锁。
public class CasMutex {
private AtomicBoolean mutexFlag = new AtomicBoolean(true);// true 代表锁被释放 false 代表所被占用
/**
* 抢占锁.
*/
public void lock(){
while(!mutexFlag.compareAndSet(true, false)) {//锁被抢到,则跳出while,继续向下执行。否则,一直在While中自旋.
}
}
/**
* 释放锁.
*/
public void unLock(){
mutexFlag.set(true);
}
class Test{
}
}
修改我们Test类的Main方法
public static void main(String[] args) {
Test test = new Test();
CasMutex casMutex = new CasMutex();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try{
casMutex.lock();//加锁
test.add();
System.out.println(test.value);
} finally {
casMutex.unLock();//释放锁,释放锁在finally里面,保证锁一定会被释放
}
}
}).start();
}
}
运行结果:
上面的自旋锁有什么缺点
自旋锁会一直占着CPU,等待获取锁,如果自旋的时间过长,就会影响整体的性能。如果自旋的时间正好是持有锁的时间,那将多好,可是做不到。HotSpot任务最佳的的自旋周期是一个线程上下文切换的时间,但目前并没有做到。
当所有的线程共享一个变量,当这个变量一旦变动,就会导致,所有线程中这个变量的副本失效,所以有可能因为缓存一致性导致流量风暴。缓存一致性模型
所有的CPU缓存通过Bus(总线进行通讯),Bus和主存(内存)通信。当一个变量被多个CPU共享,其中一个CPU的缓存改变,通知其他CPU中缓存的该变量都失效,而通知机制就是通过Bus这个总线来实现。所以当有很多线程共享一个变量就有可能导致总线流量变大(流量风暴)。
CLHLock
为了避免所有线程共享一个变量,又要求能实现锁的功能,这个时候出现了CLHLock,CLHLock有一个头结点和一个尾节点。当有一个线程来竞争锁,就创建一个节点,然后将线程和节点绑定,最后将节点插入到CLHLock的尾节点位置,从而形成一个虚拟队列。线程何时能获取锁的标志是判断前一个节点的状态,而并非判断自身节点的状态。
每一个节点都在循环判断上一个节点的状态,此时如果头结点释放锁,设置自己的状态为释放状态,此时头结点下一个节点的线程就会处于激活状态。
CLHLock解决了,所有的线程共享一个变量的问题,但是它仍然无法实现真正的阻塞,也无法实现线程中断等操作,此时AbstractQueuedSynchronizer出厂。
AbstractQueuedSynchronizer
AbstractQueuedSynchronizer(简称AQS)是一种类似CLHLock的结构,不过它的内部实现了更加丰富的语义,包括线程阻塞、共享、中断、定时等。
AQS和CLHLock相比,AQS多了向后节点指向,而不是只有前指向。node节点属性也更多,而不仅仅只有一个status状态。
node节点有五个属性
1. waitStatus: node的状态。该属性有5个值:CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、0
2. prev: 上一个节点.
3. next: 下一个几点。
4. thread: 绑定的线程
5. nextWaiter: 标识节点的运行模式,该属性有两个值SHARED和EXCLUSIVE