聊聊高并发(六)实现几种自旋锁(一)

聊聊高并发(五)理解缓存一致性协议以及对并发编程的影响 我们了解了处理器缓存一致性协议的原理,并且提到了它对并发编程的影响,“多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能。”

这一篇我们通过两种实现自旋锁的方式来看一下不同的编程方式带来的程序性能的变化。

先理解一下什么是自旋,所谓自旋就是线程在不满足某种条件的情况下,一直循环做某个动作。所以对于自旋锁来锁,当线程在没有获取锁的情况下,一直循环尝试获取锁,直到真正获取锁。

聊聊高并发(三)锁的一些基本概念 我们提到锁的本质就是等待,那么如何等待呢,有两种方式

1. 线程阻塞

2. 线程自旋

阻塞的缺点显而易见,线程一旦进入阻塞(Block),再被唤醒的代价比较高,性能较差。自旋的优点是线程还是Runnable的,只是在执行空代码。当然一直自旋也会白白消耗计算资源,所以常见的做法是先自旋一段时间,还没拿到锁就进入阻塞。JVM在处理synchrized实现时就是采用了这种折中的方案,并提供了调节自旋的参数。

这篇说一下两种最基本的自旋锁实现,并提供了一种优化的锁,后续会有更多的自旋锁的实现。

首先是TASLock (Test And Set Lock),测试-设置锁,它的特点是自旋时,每次尝试获取锁时,采用了CAS操作,不断的设置锁标志位,当锁标志位可用时,一个线程拿到锁,其他线程继续自旋。

缺点是CAS操作一直在修改共享变量的值,会引发缓存一致性流量风暴

package com.test.lock;

// 锁接口
public interface Lock {
    public void lock();
    
    public void unlock();
}

package com.test.lock;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 测试-设置自旋锁,使用AtomicBoolean原子变量保存状态
 * 每次都使用getAndSet原子操作来判断锁状态并尝试获取锁
 * 缺点是getAndSet底层使用CAS来实现,一直在修改共享变量的值,会引发缓存一致性流量风暴
 * **/
public class TASLock implements Lock{
	private AtomicBoolean mutex = new AtomicBoolean(false);

	@Override
	public void lock() {
		// getAndSet方法会设置mutex变量为true,并返回mutex之前的值
		// 当mutex之前是false时才返回,表示获取锁
		// getAndSet方法是原子操作,mutex原子变量的改动对所有线程可见
		while(mutex.getAndSet(true)){

		}
	}

	@Override
	public void unlock() {
		mutex.set(false);
	}

	public String toString(){
		return "TASLock";
	}
}

一种改进的算法是TTASLock(Test Test And Set Lock)测试-测试-设置锁,特点是在自旋尝试获取锁时,分为两步,第一步通过读操作来获取锁状态,当锁可获取时,第二步再通过CAS操作来尝试获取锁,减少了CAS的操作次数。并且第一步的读操作是处理器直接读取自身高速缓存,不会产生缓存一致性流量,不占用总线资源。

缺点是在锁高争用的情况下,线程很难一次就获取锁,CAS的操作会大大增加。

package com.test.lock;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 测试-测试-设置自旋锁,使用AtomicBoolean原子变量保存状态
 * 分为两步来获取锁
 * 1. 先采用读变量自旋的方式尝试获取锁
 * 2. 当有可能获取锁时,再使用getAndSet原子操作来尝试获取锁
 * 优点是第一步使用读变量的方式来获取锁,在处理器内部高速缓存操作,不会产生缓存一致性流量
 * 缺点是当锁争用激烈的时候,第一步一直获取不到锁,getAndSet底层使用CAS来实现,一直在修改共享变量的值,会引发缓存一致性流量风暴
 * **/
public class TTASLock implements Lock{

private AtomicBoolean mutex = new AtomicBoolean(false);

	@Override
	public void lock() {
		while(true){
			// 第一步使用读操作,尝试获取锁,当mutex为false时退出循环,表示可以获取锁
			while(mutex.get()){}
			// 第二部使用getAndSet方法来尝试获取锁
			if(!mutex.getAndSet(true)){
				return;
			}	

		}
	}

	@Override
	public void unlock() {
		mutex.set(false);
	}

	public String toString(){
		return "TTASLock";
	}
}

针对锁高争用的问题,可以采取回退算法,即当线程没有拿到锁时,就等待一段时间再去尝试获取锁,这样可以减少锁的争用,提高程序的性能。

package com.test.lock;

import java.util.Random;

/**
 * 回退算法,降低锁争用的几率
 * **/
public class Backoff {
	private final int minDelay, maxDelay;

	private int limit;

	final Random random;

	public Backoff(int min, int max){
		this.minDelay = min;
		this.maxDelay = max;
		limit = minDelay;
		random = new Random();
	}

	// 回退,线程等待一段时间
	public void backoff() throws InterruptedException{
		int delay = random.nextInt(limit);
		limit = Math.min(maxDelay, 2 * limit);
		Thread.sleep(delay);
	}
}

package com.test.lock;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 回退自旋锁,在测试-测试-设置自旋锁的基础上增加了线程回退,降低锁的争用
 * 优点是在锁高争用的情况下减少了锁的争用,提高了执行的性能
 * 缺点是回退的时间难以控制,需要不断测试才能找到合适的值,而且依赖底层硬件的性能,扩展性差
 * **/
public class BackoffLock implements Lock{

    private final int MIN_DELAY, MAX_DELAY;
    
    public BackoffLock(int min, int max){
        MIN_DELAY = min;
        MAX_DELAY = max;
    }
    
    private AtomicBoolean mutex = new AtomicBoolean(false);
    
    @Override
    public void lock() {
        // 增加回退对象
        Backoff backoff = new Backoff(MIN_DELAY, MAX_DELAY);
        while(true){
            // 第一步使用读操作,尝试获取锁,当mutex为false时退出循环,表示可以获取锁
            while(mutex.get()){}
            // 第二部使用getAndSet方法来尝试获取锁
            if(!mutex.getAndSet(true)){
                return;
            }else{
                //回退
                try {
                    backoff.backoff();
                } catch (InterruptedException e) {
                }
            }    
            
        }
    }

    @Override
    public void unlock() {
        mutex.set(false);
    }

    public String toString(){
        return "TTASLock";
    }
}

 

回退自旋锁的问题是回退的时间难以控制,需要不断测试才能找到合适的值,而且依赖底层硬件的性能,扩展性差。后面会有更好的自旋锁实现算法。

下面我们测试一下TASLock和TTASLock的性能。

首先写一个计时的类

package com.test.lock;

public class TimeCost implements Lock{

	private final Lock lock;

	public TimeCost(Lock lock){
		this.lock = lock;
	}

	@Override
	public void lock() {
		long start = System.nanoTime();
		lock.lock();
		long duration = System.nanoTime() - start;
		System.out.println(lock.toString() + " time cost is " + duration + " ns");
	}

	@Override
	public void unlock() {
		lock.unlock();
	}

}

然后采用多个线程来模拟对同一把锁的争用

package com.test.lock;

public class Main {
	private static TimeCost timeCost = new TimeCost(new TASLock());

	//private static TimeCost timeCost = new TimeCost(new TTASLock());

	public static void method(){
		timeCost.lock();
		//int a = 10;
		timeCost.unlock();
	}

	public static void main(String[] args) {
		for(int i = 0; i < 100; i ++){
			Thread t = new Thread(new Runnable(){

				@Override
				public void run() {
					method();
				}

			});
			t.start();
		}
	}

}

测试机器的性能如下:

CPU: 4  Intel(R) Core(TM) i3-2120 CPU @ 3.30GHz

内存: 8G

测试结果:

50个线程情况下:

TASLock平均获取锁的时间: 339715 ns

TTASLock平均获取锁的时间: 67106.2 ns

100个线程情况下:

TASLock平均获取锁的时间: 1198413 ns

TTASLock平均获取锁的时间: 1273588 ns

可以看到TTASLock的性能比TASLock的性能更好

转载请注明来源: http://blog.csdn.net/iter_zc

时间: 2024-09-29 17:38:31

聊聊高并发(六)实现几种自旋锁(一)的相关文章

聊聊高并发(十三)实现几种自旋锁(六)

聊聊高并发(十一)实现几种自旋锁(五) 给出了限时有界队列锁的lock和unlock实现.这篇给出tryLock的实现 tryLock比lock略微复杂一点.要处理超时的情况.超时有几种情况: 1. 第一步在等待队列还没有获得节点的时候超时,直接返回false就可以 2. 第二步在等待队列已经获得节点可是还没有增加工作队列时超时,把节点状态能够直接改成FREE给兴许线程使用,然后返回false就可以 3. 第三步在前一个节点的状态上自旋时超时,将节点的preNode设置成前一个节点,然后将节点状

聊聊高并发(七)实现几种自旋锁(二)

在聊聊高并发(六)实现几种自旋锁(一) 这篇中实现了两种基本的自旋锁:TASLock和TTASLock,它们的问题是会进行频繁的CAS操作,引发大量的缓存一致性流量,导致锁的性能不好. 对TTASLock的一种改进是BackoffLock,它会在锁高争用的情况下对线程进行回退,减少竞争,减少缓存一致性流量.但是BackoffLock有三个主要的问题: 1. 还是有大量的缓存一致性流量,因为所有线程在同一个共享变量上旋转,每一次成功的获取锁都会产生缓存一致性流量 2. 因为回退的存在,不能及时获取

聊聊高并发(十一)实现几种自旋锁(五)

在聊聊高并发(九)实现几种自旋锁(四)中实现的限时队列锁是一个基于链表的限时无界队列锁,它的tryLock方法支持限时操作和中断操作,无饥饿,保证了先来先服务的公平性,在多个共享状态上自旋,是低争用的.但是它的一个缺点是牺牲了空间,为了让线程可以多次使用锁,每次Lock的时候都要new QNode,并设置给线程,而不能重复使用原来的节点. 这篇说说限时有界队列锁,它采用了有界队列,并且和ArrayLock不同,它不限制线程的个数.它的特点主要有 1. 采用有界队列,减小了空间复杂度,L把锁的空间

聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则

在前几篇将Java内存模型的那些事基本上把这个域底层的概念都解释清楚了,聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障 这篇分析了在X86平台下,volatile,synchronized, CAS操作都是基于Lock前缀的汇编指令来实现的,关于Lock指令有两个要点: 1. lock会锁总线,总线是互斥的,所以lock后面的写操作会写入缓存和内存,可以理解为在lock后面的写缓存和写内存这两个动作称为了一个原子操作.当总线被锁时,其他的CPU是无法使用总线的,也就让其他的读写都等

聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类

这篇说说java.util.concurrent.atomic包里的类,总共12个.网上有非常多文章解析这几个类.这里挑些重点说说. 这12个类能够分为三组: 1. 普通类型的原子变量 2. 数组类型的原子变量 3. 域更新器 普通类型的原子变量的6个, 1. 当中AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference分别相应boolean, int,  long, object完毕主要的原子操作 2. AtomicMarkableRe

聊聊高并发(十八)理解AtomicXXX.lazySet方法

看过java.util.concurrent.atomic包里面各个AtomicXXX类实现的同学应该见过lazySet方法,比如AtomicBoolean类的lazySet方法 public final void lazySet(boolean newValue) { int v = newValue ? 1 : 0; unsafe.putOrderedInt(this, valueOffset, v); } 它的底层实现调用了Unsafe的putOrderedInt方法,来看看putOrde

聊聊高并发(二十五)解析java.util.concurrent各个组件(七) 理解Semaphore

前几篇分析了一下AQS的原理和实现.这篇拿Semaphore信号量做样例看看AQS实际是怎样使用的. Semaphore表示了一种能够同一时候有多个线程进入临界区的同步器,它维护了一个状态表示可用的票据,仅仅有拿到了票据的线程尽能够进入临界区,否则就等待.直到获得释放出的票据. Semaphore经常使用在资源池中来管理资源.当状态仅仅有1个0两个值时,它退化成了一个相互排斥的同步器.类似锁. 以下来看看Semaphore的代码. 它维护了一个内部类Sync来继承AQS,定制tryXXX方法来使

聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁

上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和主要的方法,显示了如何实现的锁降级.但是下面几个问题没说清楚,这篇补充一下 1. 释放锁时的优先级问题,是让写锁先获得还是先让读锁先获得 2. 是否允许读线程插队 3. 是否允许写线程插队,因为读写锁一般用在大量读,少量写的情况,如果写线程没有优先级,那么可能造成写线程的饥饿 关于释放锁后是让写锁先获得还是让读锁先获得,

聊聊高并发(十二)分析java.util.concurrent.atomic.AtomicStampedReference源码来看如何解决CAS的ABA问题

在聊聊高并发(十一)实现几种自旋锁(五)中使用了java.util.concurrent.atomic.AtomicStampedReference原子变量指向工作队列的队尾,为何使用AtomicStampedReference原子变量而不是使用AtomicReference是因为这个实现中等待队列的同一个节点具备不同的状态,而同一个节点会多次进出工作队列,这就有可能出现出现ABA问题. 熟悉并发编程的同学应该知道CAS操作存在ABA问题.我们先看下CAS操作. CAS(Compare and