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

聊聊高并发(五)理解缓存一致性协议以及对并发编程的影响 我们了解了处理器缓存一致性协议的原理,并且提到了它对并发编程的影响,“多个线程对同一个变量一直使用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容