第四章 锁的优化

1. 提高锁性能的几点建议

1.1 减小锁持有时间

// 减小锁持有时间可以降低锁冲突的可能性,提升系统并发能力。
public synchronized void syncMethod(){
    otherMethod1(); //不需要同步
    mutextMethod(); //需要同步
    otherMethod2() //不需要同步
}

public void syncMethod(){
    otherMethod1(); //不需要同步
    sychronized(this){
        mutextMethod(); //需要同步
    }
    otherMethod2() //不需要同步
}

1.2 减小锁粒度

  • 例如ConcurrentHashMap,将map划分为了很多段SEGMENT(默认16),需要put时,计算出属于哪个段,然后只对这个段加锁。其他段不受同步影响,幸运的话,可以同时接受16个线程的插入。
  • 但是当获取一些全局信息,如size()时,可能需要获取全部16个锁。ConcurrentHashMap会尝试无锁方法求size(),失败再请求全局锁。

1.3 锁分离

  • 读写锁
  • LinkedBlockingQueue:put和take方法分别作用于链表的两端,所以采用了两把锁takeLock和putLock。
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

1.4 锁粗化

  • 线程请求、同步、释放锁,本身也会消耗一定的资源。当锁细化到一定程度时,反而不利于系统性能。
//减少锁请求的次数
public void demoMethod(){
    sychronized(lock){
        //do sth
    }
    //other thing which need no lock
    sychronized(lock){
        //do sth
    }
}

public void demoMethod(){
    sychronized(lock){
        //do sth
        //other thing which need no lock
        //do sth
    }
}
//更极端
for(100次循环){
    sychronized(lock){

    }
}
sychronized(lock){
    for(100次循环){
        
    }
}

2. 虚拟机对锁优化所做的努力

2.1 锁偏向

  • 针对加锁操作的优化手段,核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无需做任何同步操作,节省了大量有关锁的申请操作。
  • 对于几乎没有锁竞争的场合,偏向锁有比较好的优化效果,因为连续多次有可能时同一线程请求相同的锁。
  • 对于锁竞争比较激烈的场合,其效果不佳。

2.2 轻量级锁

  • 如果偏向锁失败,虚拟机不会立即挂起线程,而是使用轻量级锁。
  • 轻量级锁只是简单的将对象头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁。如果获得轻量级锁,则可以进入临界区,如果失败,当前线程的锁请求就会膨胀为重量级锁。

2.3 自旋锁

  • 锁膨胀后,为了避免线程真实的在操作系统层面挂起,虚拟机会尝试自旋锁。
  • 简单粗暴的挂起锁消耗较大,因此假设该线程可以在不久的将来获得锁,虚拟机将该线程做几个空循环等待,如果可以获得锁,就进入临界区,还不行就GG,在操作系统层面挂起。

2.4 锁消除

  • 因为一些编码原因,可能会在产生一些根本不可能存在竞争资源的锁。虚拟机在JIT编译时会消除这些锁。
//vector内部含有锁,但是此处的vector是局部变量,分配在虚拟机栈空间,线程私有,不存在竞争。虚拟机会消除这个锁。
//此处涉及逃逸分析。
public String[] createStrings() {
    Vector<String> v = new Vector<>();
    for (int i = 0; i < 100; i++) {
        v.add(Integer.toString(i));
    }
    return v.toArray(new String[] {});
}

3. ThreadLocal

  • ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
  • 线程本地变量是和线程相关的变量,一个线程则一份数据。我们通过ThreadLocal保存的数据最终是保存在Thread类的ThreadLocalMap threadLocals变量中。ThreadlocalMap是一个Map结构,其中key为我们声明的ThreadLocal对象,value即为我们使用ThreadLocal保存的线程本地变量。
  • ThreadLocal本身并不存储value值,只是作为key在ThreadLocalMap中索引value值。
  • 当我们调用ThreadLocal变量set方法时,那么为将TheadLocal作为key,set方法的参数做为value保存在当前线程的threadLocals中.调用get方法时类似,调用get方法时,会去Thread的threadLocals中去寻找key为ThreadLocal 变量的值。
  • 值得注意的是,ThreadLocal随着当前线程的销毁而销毁,如果程序中采用线程池,在上一次任务运行完之后,记得清掉之前ThreadLocal数据。
  • 作用:提供一个线程内公共变量(比如本次请求的用户信息)
//如果get()方法先于set()方法之前调用,则调用setInitialValue()方法返回初始值。
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);// getMap方法即去获取当前线程的ThreadLocalMap变量。
    if (map != null)
        map.set(this, value);//以this(ThreadLocal本身)为Key,参数value为值进行保存
    else
        createMap(t, value);
}

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

【ThreadLocal是否会造成内存泄露?】

  • 每个Thread含有的ThreadLocalMap中的Key为ThreadLocal变量的弱引用,如果一个ThreadLocal变量没有外部强引用来引用它,那么它在JVM下一次GC的时候会被垃圾回收掉,这时候,Map中就存在了key为NULL的value,这个value无法被访问。如果当前线程再迟迟不结束的话(例如当前线程在一个线程池中),那么value所指向的对象可能永远无法释放,也即不能被回收,造成内存泄露。
  • ThreadLocalMap的设计者很显然也想到了这个问题,所以其在每一次对ThreadLocalMap的set,get,remove等操作中,都会清除Map中key为null的Entry。因此,ThreadLocal一般是不会存在内存泄露风险的。
  • 但是,将清除NULL对象的工作交给别人,并不是一个明智的选择,所以聪明的你,在Thread中使用完ThreadLocal对象后,一定要记得调用ThreadLocal的remove方法,进行手动清除。

4. 无锁

  • 锁:悲观策略,假设每一次对临界区操作都会产生冲突,宁愿牺牲性能让线程等待,也要阻塞线程。
  • 无锁:乐观策略,假设每一次对临界区操作都不会产生冲突,采用CAS(Compare and Swap)来鉴别线程冲突,一旦检测到冲突,就重试当前操作直到没有冲突为止。它不会阻塞线程。

4.1 CAS算法

  • 它包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V==E时,才会将V的值设为N,如果V!=E,说明其它线程已经做了更新,当前线程什么都不做。最后返回V的真实值。
  • 多个线程操作时,只有一个会胜出,并且成功更新,其余均会失败。失败的线程不会被挂起,仅仅是被告知失败,并且允许再次尝试,也可以放弃操作。

4.2 无锁的线程安全整数:AtomicInteger

  • 与Integer相比,AtomicInteger是可变的,线程安全的。
AtomicInteger4

就内部实现而言,AtomicInteger保存着

private volatile int value;//当前实际取值
private static final long valueOffset;//value在AtomicInteger对象中的偏移量
public class AtomicIntegerDemo {
    static AtomicInteger i = new AtomicInteger(0);  
    public static void main(String[] args) throws InterruptedException {
        Runnable r = ()->{
            for (int k = 0; k < 10000; k++) {
                i.incrementAndGet();
            }
        };
                Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(r);
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].join();
        }
        System.out.println(i);
    }
}
//输出100000,说明线程安全的。

incrementAndGet()的实现:无限循环就是CAS

public final int incrementAndGet() {
    for(;;) {
        int current = get();
        int next = current + 1;
        if(compareAndSet(current, next)) {
            return next;
        }
    }
}

4.3 无锁的对象引用:AtomicReference

class Person {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

// 普通引用,多线程下会导致数据不一致,name和age不是一个人
private static Person person;

// 原子性引用
private static AtomicReference<Person> aRperson;

4.4 带时间戳的对象引用:AtomicStampedReference

  • 当你获得对象的数据后,在准备修改为新值前,对象的值被其它线程连续修改了两次,对象的值又恢复到了旧值。如果修改的对象没有过程的状态信息,那还是ok的。如果对象的过程变化很重要,那么就需要AtomicStampedReference了。
  • AtomicStampedReference内部维护了一个时间戳。
AtomicStampedReference

4.5 数组无锁:AtomicIntegerArray

AtomicIntegerArray

4.6 让普通变量享受原子操作:AtomicIntegerFieldUpdater

  • 可以将普通变量提升为线程安全的。
  • 有三个:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
class Candidate{
    int id;
    volatile int score;
}

AtomicIntegerFieldUpdater<Candidate> updater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

【注意】

  • Updater只能修改它可见范围内的变量(因为updater使用反射得到这个变量)。如果score为private,就不可行。
  • 变量必须是volatile的。
  • CAS操作通过对象实例中的偏移量直接进行赋值,因此,不支持static。

4.7 SynchronousQueue

http://blog.csdn.net/yanyan19880509/article/details/52562039

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

推荐阅读更多精彩内容