Java高并发程序设计(八)锁的优化和注意事项

1.锁优化的思路和方法

减少锁持有时间

方法一,方法二的区别:方法二对锁持有时间比价短,因为在操作共享数据时才需要加锁;而ohtercode1(),othercode2()方法并不涉及对共享数据的操作,所以不需要阻塞其它线程。

减小锁粒度

将大对象,拆成小对象,大大增加并行度,降低锁竞争。
偏向锁,轻量级锁成功率提高。
HashMap的同步实现
– Collections.synchronizedMap(Map<K,V> m)
– 返回SynchronizedMap对象

//并没有整个对象加锁,而只是对一小块变量加锁
public V get(Object key) {
    synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
    synchronized (mutex) {return m.put(key, value);}
}

ConcurrentHashMap
– 若干个Segment :Segment<K,V>[] segments
– Segment中维护HashEntry<K,V>
– put操作时
• 先定位到Segment,锁定一个Segment,执行put
在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入。

锁分离

根据功能进行锁分离
ReadWriteLock
读多写少的情况,可以提高性能

读写分离思想可以延伸,只要操作互不影响,锁就可以分离
LinkedBlockingQueue

锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完
公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行
任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗
系统宝贵的资源,反而不利于性能的优化。

锁消除

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。


2.虚拟机内的锁优化

对象头Mark

Mark Word,对象头的标记,32位
描述对象的hash、锁信息,垃圾回收标记,年龄
– 指向锁记录的指针
– 指向monitor的指针
– GC标记
– 偏向锁线程ID


偏向锁

大部分情况是没有竞争的,所以可以通过偏向来提高性能
所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步
当其他线程请求相同的锁时,偏向模式结束
-XX:+UseBiasedLocking
– 默认启用
在竞争激烈的场合,偏向锁会增加系统负担

轻量级锁

普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。
如果对象没有被锁定
– 将对象头的Mark指针保存到锁对象中
– 将对象头设置为指向锁的指针(在线程栈空间中)

lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)){
    TEVENT (slow_enter: release stacklock) ;
    return ;
}

如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)。
在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗。
在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降。

自旋锁

当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(
自旋)
JDK1.6中-XX:+UseSpinning开启
JDK1.7中,去掉此参数,改为内置实现
如果同步块很长,自旋失败,会降低系统性能
如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能

偏向锁,轻量级锁,自旋锁总结
不是Java语言层面的锁优化方法
内置于JVM中的获取锁的优化方法和获取锁的步骤
– 偏向锁可用会先尝试偏向锁
– 轻量级锁可用会先尝试轻量级锁
– 以上都失败,尝试自旋锁
– 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起

3.一个错误使用锁的案例

public class IntegerLock {
    static Integer i=0;
    public static class AddThread extends Thread{
        public void run(){
        for(int k=0;k<100000;k++){
                //此处对每次循环都加锁,错误。应在循环体外加锁。
                synchronized(i){
                    i++;
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AddThread t1=new AddThread();
        AddThread t2=new AddThread();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

4.ThreadLocal及其源码分析

使用场景:
Synchronized,锁,并发控制用于多个线程间的数据共享。

ThreadLocal则用于线程间的数据隔离。

每个线程会维护自己的ThreadLocalMap。

比如:多个用户都在同时登陆,会给每个用户分配一个Token,Token是每个用户独立的,相互隔离。

package com.thread.chapter07.threadlocal;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 使用ThreadLocal,每个线程都有自己的SimpleDateFormat实例
 * Created by chenbin on 2019\8\22 0022.
 */
public class ThreadLocalDemo {
    private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>();

    public static class ParseDate implements Runnable {
        int i = 0;
        public ParseDate(int i) {
            this.i = i;
        }

        public void run() {
            try {
                if (threadLocal.get() == null) {
                    //为每个线程分配一个实例
                    threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                }
                Date t = threadLocal.get().parse("2019-08-22 17:29:"+i%60);
                System.out.println(i+":"+t);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(10);
        for (int i = 0;i < 1000;i++) {
            es.execute(new ParseDate(i));
        }
    }
}

为了解释ThreadLocal类的工作原理,必须同时介绍与其工作甚密的其他几个类

ThreadLocalMap(内部类)

Thread

首先,在Thread类中有一行:

   /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

其中ThreadLocalMap类的定义是在ThreadLocal类中,真正的引用却是在Thread类中。同时,ThreadLocalMap中用于存储数据的entry定义:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

从中我们可以发现这个Map的key是ThreadLocal类的实例对象,value为用户的值。

ThreadLocal的set和get方法代码:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, 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();
    }

其中的getMap方法:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

给当前Thread类对象初始化ThreadlocalMap属性:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

到这里,我们就可以理解ThreadLocal究竟是如何工作的了

  1. Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象。
  2. 当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value。get值时则类似。
  3. ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
  4. 由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的。

备注:本文为JAVA高并发程序设计(葛一鸣著)读书笔记,以及自身的整理和实践。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值