• Java高并发程序设计学习笔记(九):锁的优化和注意事项


    转自:https://blog.csdn.net/dataiyangu/article/details/87612028

    锁优化的思路和方法
    减少锁持有时间
    减小锁粒度
    锁分离
    锁粗化
    举个栗子
    举个栗子
    锁消除
    虚拟机内部的锁优化(当使用synchronize关键字的时候里面会做那些事情)
    对象头Mark
    偏向锁
    举个栗子
    轻量级锁
    自旋锁
    举个栗子
    偏向锁,轻量级锁,自旋锁总结
    一个错误使用锁的案例
    ThreadLocal及其源码分析
    举个栗子
    为每一个线程分配一个实例
    如果使用共享实例,起不到效果
    源码分析

    注意:只要是持有锁的,性能就会比无锁要差,不论如何优化。
    锁优化的思路和方法
    注意tryLock是无锁的,只是去尝试下,拿不到就会接着做其他的事情,Lock是有锁的操作。

    减少锁持有时间
    public synchronized void syncMethod(){
    othercode1();
    mutextMethod();
    othercode2();
    }
    1
    2
    3
    4
    5
    转化为:

    public void syncMethod2(){
    othercode1();
    synchronized(this){
    mutextMethod();
    }
    othercode2();
    }
    1
    2
    3
    4
    5
    6
    7
    值同步相关的代码,无关的代码就不要同步。

    减小锁粒度
    将大对象,拆成小对象,大大增加并行度,降低锁竞争 ,原来可能是对一个很大的对象加锁。
    偏向锁,轻量级锁成功率提高
    ConcurrentHashMap(就是将大对象拆分成小对象)
    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);}
    }
    1
    2
    3
    4
    5
    6
    ConcurrentHashMap
    – 若干个Segment :Segment<K,V>[] segments
    – Segment中维护HashEntry<K,V>
    – put操作时
    • 先定位到Segment,锁定一个Segment,执行put
    在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入
    就是讲hashmap拆分成多个hashMap,即拆分成多个对象。

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

    读锁 写锁
    读锁 可访问 不可访问
    写锁 不可访问 不可访问
    读操作和读操作是不需要阻塞的,将阻塞的并发变成了无等待的并发。

    读写分离思想可以延伸,只要操作互不影响,锁就可以分离
    LinkedBlockingQueue
    – 队列
    – 链表
    就像队列put和take操作,一个在头部一个在尾部互不影响

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

    举个栗子
    public void demoMethod(){
    synchronized(lock){
    //do sth.
    }
    //做其他不需要的同步的工作,但能很快执行完毕
    synchronized(lock){
    //do sth.
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    应该转化成

    public void demoMethod(){ //整合成一次锁请求
    synchronized(lock){
    //do sth.
    //做其他不需要的同步的工作,但能很快执行完毕
    }
    }
    1
    2
    3
    4
    5
    6
    不断的加锁释放锁也是会消耗很大的性能的,这个时候就应该锁粗化,但是前提上面的代码中的其他不需要同步的工作是能够很快的执行完毕的,否则不应该所粗化。

    举个栗子
    for(int i=0;i<CIRCLE;i++){
    synchronized(lock){
    }
    }
    1
    2
    3
    4
    synchronized(lock){
    for(int i=0;i<CIRCLE;i++){
    }
    }
    1
    2
    3
    4
    锁消除
    在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作(jdk自身的优化)

    public static void main(String args[]) throws InterruptedException {
    long start = System.currentTimeMillis();
    for (int i = 0; i < CIRCLE; i++) {
    craeteStringBuffer("JVM", "Diagnosis");
    }
    long bufferCost = System.currentTimeMillis() - start;
    System.out.println("craeteStringBuffer: " + bufferCost + " ms");
    }
    public static String craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    观察上面的代码,在jdk中有许多类似StringBuffer这样的类,本身就是在锁的基础上封装的,自己是线程安全的,我用了它的某些方法(append等)后,发现sb这个变量是局部变量,不会被其他的线程访问到,并不需要为了线程安全,而进行锁的操作。

    解决办法:
    CIRCLE= 2000000

    -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
    craeteStringBuffer: 187 ms
    -server -XX:+DoEscapeAnalysis -XX:-EliminateLocks
    craeteStringBuffer: 254 ms
    1
    2
    3
    4
    自私server模式下能有更多的操作,-XX:+DoEscapeAnalysis :逃逸分析,分析sb这个变量会不会被其他的线程访问到,如果会怎样,如果会不怎样,+EliminateLocks:经过逃逸分析,看是否打开锁消除。
    通过上面的时间显示,进行了锁消除,确实性能有很大的提升。

    虚拟机内部的锁优化(当使用synchronize关键字的时候里面会做那些事情)
    对象头Mark
    每个对象都有一个对象头
    Mark Word,对象头的标记,32位(32位操作系统中)
    描述对象的hash、锁信息,垃圾回收标记,年龄
    – 指向锁记录的指针
    – 指向monitor的指针
    – GC标记
    – 偏向锁线程ID

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

    举个栗子
    public static List<Integer> numberList =new Vector<Integer>(); public static void main(String[] args) throws InterruptedException {
    long begin=System.currentTimeMillis();
    int count=0;
    int startnum=0; while(count<10000000){
    numberList.add(startnum); startnum+=2;
    count++;
    }
    long end=System.currentTimeMillis();
    System.out.println(end-begin);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //本例中,使用偏 向锁,可以获得 5%以上的性能 提升
    -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    -XX:-UseBiasedLocking
    1
    2
    3
    BiasedLockingStartupDelay是系统启动的几秒之内金童偏向锁,因为在系统刚刚启动的时候确实会有许多的锁竞争,虚拟机会提供一个默认的时间,这里设置为0,是因为需要写的代码很少,能够在很短的时间内执行完毕,所以将BiasedLockingStartupDelay设置为0。

    轻量级锁
    BasicObjectLock
    – 嵌入在线程栈中的对象
    如果偏向锁失败,就会去执行轻量级锁的操作。
    普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。
    操作系统层面的锁性能是比较差的,jvm相当于操作系统上的一个应用,jvm级别的锁能够提高性能。
    如果对象没有被锁定
    – 将对象头的Mark指针保存到锁对象中
    – 将对象头设置为指向锁的指针(在线程栈空间中)

    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark))
    {
    TEVENT (slow_enter: release stacklock) ;
    return ;
    }
    1
    2
    3
    4
    5
    6
    lock位于线程栈中
    注意上面说的,对象头mark保存在锁中,锁位于线程栈中,对象头设置为锁的指针,所以如果对象头指向了线程栈中,则表示持有这把锁,上面的指令同样是cas操作。

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

    自旋锁
    举个栗子
    concurrentHashMao中的put就是线程的锁被其他人拿走之后,不急着挂起自己,而是执行几次trylocak操作,因为一旦挂起会消耗八万个时光周期。不断的循环trylock,当多次循环之后还是没有拿到,就挂起。

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

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

    一个错误使用锁的案例
    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);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    注意这句话:synchronized(i)
    其中的i是Integer类型的,i++操作一般是在int类型上,Integer类型会有自动的拆箱装箱的操作,并不是将i++的值直接赋值给了i,而是new了一个新的对象,然后将i指向它,所以这里synchronized(i)中的i不能确定是代码中的i还是new的一个新的Integer对象,从而不能保证拿到的是同一个对象,所以这段代码并不是线程安全的。

    ThreadLocal及其源码分析
    把锁去掉,为每一个线程都提供一个对象实例,不同的线程,都去访问自己的对象,而不去访问别人的对象,这个时候锁就完全没有必要等待。

    举个栗子
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static class ParseDate implements Runnable{
    int i=0;
    public ParseDate(int i){this.i=i;} public void run() {
    try {
    Date t=sdf.parse("2015-03-29 19: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));
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    SimpleDateFormat不是线程安全的,被多线程访问是容易抛出一些异常,不能正常的工作。

    为每一个线程分配一个实例
    static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();
    public static class ParseDate implements Runnable{
    int i=0;
    public ParseDate(int i){this.i=i;} public void run() {
    try {
    if(tl.get()==null){
    tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    }
    Date t=tl.get().parse("2015-03-29 19: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));
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    如果使用共享实例,起不到效果
    static ThreadLocal<SimpleDateFormat> tl=new ThreadLocal<SimpleDateFormat>();
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static class ParseDate implements Runnable{
    int i=0;
    public ParseDate(int i){this.i=i;}
    public void run() {
    try {
    if(tl.get()==null){
    tl.set(sdf );
    }
    Date t=tl.get().parse("2015-03-29 19: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));
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    注意上述代码中tl.set(sdf ); 正确的应该是tl.set(new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”));
    每次都new一个新的实例,只有这样才能起到作用,否则锁一个的threadlocal里面存的还是一个对象,还是会出现线程不安全的现象。

    源码分析
    public void set(T value) {
    Thread t = Thread.currentThread();
    //拿到ThreadlocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
    //可以看到Threadlocal本身就隶属于这个线程
    return t.threadLocals;
    }
    //在当前的线程中,也就是每个线程中都有自己的map
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //key是ThreadLocal本身,value就是设进去的值
    private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
    e != null;
    e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();

    if (k == key) {
    e.value = value;
    return;
    }

    if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
    }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
    }
    //nextIndex其实是i++的操作,因为get出来,发现如果有了会有hash冲突,这个时候
    //下标加一,再赋值。
    private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
    }
    //执行清理的工作,可以看到并不是全部清理,只是n >>>= 1右移操作,清理部分。
    //前面知道Entry是弱引用的,只有当entry!=null并且e.get()--》ThreadLocal是null的时候
    //通过expungeStaleEntry方法将tab[staleSlot].value = null;
    //tab[staleSlot] = null;这两个置为null
    private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
    i = nextIndex(i, len);
    Entry e = tab[i];
    if (e != null && e.get() == null) {
    n = len;
    removed = true;
    i = expungeStaleEntry(i);
    }
    } while ( (n >>>= 1) != 0);
    return removed;
    }
    //可以看到 ThreadLocalMap.Entry e = map.getEntry(this);
    //也是传的ThreadLocal本身
    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();
    }
    //这里注意Entry是继承于弱引用的,也就是没有数据引用到这个对象,这个对象就会被系统释放掉。
    static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
    }
    ---------------------
    作者:Leesin Dong
    来源:CSDN
    原文:https://blog.csdn.net/dataiyangu/article/details/87612028
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    成为一个会思考的学习者
    我的第4篇博客
    我的第3篇博客
    第2次作业
    第一次作业:大学,人生的另一个新的开始
    第四次作业
    第三次作业
    第二次作业
    作为大一新生的感悟
    第四次作业
  • 原文地址:https://www.cnblogs.com/sharpest/p/10850990.html
Copyright © 2020-2023  润新知