Java多线程--锁的优化

提高锁的性能

减少锁的持有时间

一个线程如果持有锁太长时间,其他线程就必须等待相应的时间,如果有多个线程都在等待该资源,整体性能必然下降。所有有必要减少单个线程持有锁的时间。比如下面的代码:

public synchronized void someMethods() {
    fun1();
    fun2();
    // other code
    funNeedToSync();
    // other code
    fun3();
    fun4();
}

如果fun1~fun4都是耗时任务的话,对someMethods()进行同步将耗费大量时间,但实际上只有funNeedToSync()需要同步,所以只需要对部分代码进行同步。优化后如下:

public void someMethods() {
    fun1();
    fun2();
    // other code
    synchronized {
        funNeedToSync();
    }
    // other code
    fun3();
    fun4();
}

这样就减少了锁占有的时间。

降低锁粒度

如何要对HashMap的put和get方法进行同步,可以对整个HashMap加锁,这样做锁的粒度就太大了。在JDK1.7中,ConcurrentHashMap的内部进一步细分了若干个小的HashMap,称为段(Segment),默认ConcurrentHashMap被分为16个段。在进行put操作时,根据hashcode得到要存入的值应该被放置到哪个段中,只需对该段加锁即可。如果要存入的值被分配到了不同的段中,则在多线程中可以真正并行进行put操作。注意,ConcurrentHashMap在JDK8中的实现和上述略不同。在这里只是举个锁粒度优化的例子。所谓降低锁粒度,就是指缩小锁定对象的范围,从而减少锁冲突的可能性。

读写分离替代独占锁

因为读取操作并不改变值,所以应该允许多个线程同时读,即读-读不阻塞,比如ReadWriteLock读写锁,在读多写少的情况下能大大提升性能。

锁分离

LinkedBlockingQueue是基于链表实现的,take和put操作分别对队列头和队列尾操作,这两者并不冲突。如果使用独占锁,则需要获得队列的锁,那么在take的时候就不能put,put的时候也不能take;如果锁分离了,如下,正是LinkedBlockingQueue使用的额策略:

/** Lock held by take, poll, etc */
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();

take操作使用一把锁,put操作也有自己的一把锁,则实现了take和put的操作互相不阻塞。只有在多个take和多个put之间才会有锁竞争的,采用这种策略降低了锁竞争的可能性。

锁粗化

虚拟机在遇到连续对同一个锁不断进行请求和释放的操作时,会把所有的锁操作整合对锁的一次请求,从而减少对锁的请求同步次数。比如

for (int i = 0;i < 100; i++) {
    synchronized (lock) {
        fun();
    }
}

synchronized (lock) {
    for (int i = 0;i < 100; i++) {
        fun();
    }
}

上述的第一段代码对锁lock连续请求、释放了100次...其实只需要在外层申请一次即可。

Java虚拟机对锁的优化

  • 锁偏向:如果一个线程获得了锁,锁就进入了偏向模式。当这个线程再次请求锁时,无需再做任何同步操作。
  • 轻量级锁:如果偏向锁失败,虚拟机不会立即挂起线程,会使用一种称为 轻量级锁 的优化手段,如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁夹加锁失败,表示其他线程抢先拿到了锁,当前线程的锁就会膨胀为 重量级锁
  • 自旋锁:锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会做最后的努力--自旋锁。虚拟机会让当前线程做几个空循环,若干次循环后如果得到了锁,就顺利进入临界区;如果还是没得到,这才真是地将线程在操作系统层面挂起。
  • 锁消除:Java虚拟机在JIT编译时,通过扫描上下文,去除不可能存在共享资源竞争的锁。线程中的局部变量时线程的私有数据,不会跑到其他线程中去,因而不存在“共享”,锁消除的这一项关键技术称为 逃逸分析 ,即观察某一个变量是否会逃出某一个作用域,如果不会逃出,则将线程内的加锁操作去除。

ThreadLocal

使用锁是因为多个线程要访问同一个共享资源。换种思路,如果资源不是共享的,而是每个线程都有一个属于自己的资源呢?ThreadLocal就是这个思路,顾名思义这是一个线程的局部变量。只有当前线程可以访问到,自然是线程安全的。

public static ThreadLocal<SimpleDateFormat> t = new ThreadLocal<>();

// class XXX implements Runnable
@Override
public void run() {
    try {
        if (t.get() == null) {
            t.set(new SimpleDateFormat("yyyy-MM-dd"));
        } else {
            Date d = t.get().parse("2018-05-10");
        }
    } catch (ParseException e) {
        e.printStackTrace();
    }
}

上面举了个ThreadLocal的例子,从代码中可以看到,如果当前线程没有持有一个SimpleDateFormat就为其新建一个,如果有了就直接取出来用,在这里ThreadLocal为没个线程都准备了一个局部变量,这个局部变量在这里就是SimpleDateFormat。注意这里为没个线程设置新的对象t.set(new SimpleDateFormat("yyyy-MM-dd"));保证了线程安全,如果设置的对象是同一个,那不能保证线程安全

set和get是怎么实现设置为每一个线程分配一个局部变量的呢?get和set用到了Map,将当前线程对象和这个局部变量绑定在一起。

get和set的核心实现就是

public void set(T value) {
    // other code
    map.set(this, value); // 以当前线程对象为key,局部对象为value存入map中
    // other code
}

public T get() {
    // other code
    ThreadLocalMap.Entry e = map.getEntry(this); // 以当前线程对象为key,取得当前线程的局部变量
    // other code
}

ThreadLocal的实现用到了ThreadLocalMap,可以理解成一个Map,这个Map中就存放了各个线程的所有“局部变量”。

无锁

无锁使用CAS(Compare And Swap)

CAS包含三个参数(V, E, N)分别是当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。当多个线程同时使用CAS操作同一个变量时,只有一个线程能成功更新,其余线程均会操作失败。失败的线程不会被挂起,仅被告知失败,还允许再次尝试。CAS操作中这个期望值通俗点说就是当前线程认为这个变量现在的值应该是多少,如果变量的值V并不是期望的那样,说明该变量被其他线程修改过了。当前线程可以再次尝试修改。

锁的使用是悲观的,它总是假设每一次临界区操作都会产生冲突,所以只有一个线程能进入临界区而其他线程只好在临界区外等待;无锁的CAS是乐观的,它假设对资源的访问不存在冲突,那么所有的线程都不用等待,一刻不停地执行,如果真的遇到了冲突,再进行CAS操作,不断重新尝试直到没有冲突。

Java中的无锁类

JDK中有个atomic包,里面有一些直接使用了CAS操作的线程安全的类型。

AtomicInteger和Integer都表示整数,但是AtomicInteger是可变且线程安全的,它的内部使用而来CAS操作。类似的还有AtomicLong和AtomicBloolean。

AtomicReference和AtomicInteger类似,前者是对整数的封装,后者是它是对普通对象的封装。之前有说CAS操作会判断当前内存值和期望值是否一致,一致就用新值更新当前值。注意,仅仅是判断了值一致,值变化了多次又变回了原来的样子,CAS操作就无法判断这个对象是否被修改过。也就是说CAS操作只比较最终结果,当前线程无法得知该对象的状态变化过程,如果要获得对象被修改过程的状态变化AtomicReference就不适用了,此时可以使用带时间戳的AtomicStampedReference,不仅维护了对象值,还维护了一个时间戳(就是一个状态值,其实可以不用时间戳),当对象的值被修改时,除了更新数据本身,还要更新时间戳;当设置对象值时,必须满足对象值和时间戳都和期望值一致,写入才会成功。因此即使对象值被反复修改,最后回到了原来的值吗,只要时间戳变了,就能防止不恰当的写入。

Java中也读数组进行了封装,可以使用的原子数组有AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray,分别表示整数数组、Long型数组和对象数组。

普通变量也可以使用原子操作,有AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater,分别对int型ling型和对象的的普通变量进行CAS操作。这几个类可以对对象中的属性字段进行CAS操作而不用担心线程安全的问题,举个例子

public class Demo {
    public static class Student {
        public int id;
        public volatile score;
    }
    public final static AtomicIntegerFielUpdater<Student> scoreUpdater = AtomicIntegerFielUpdater.newUpdater(Sdudent.class, "score");
}

像上面的例子就实现了对Sdudent类的score属性进行CAS操作以保证其线程安全。

AtomicIntegerFieldupdater很好用,但是有几点要注意:

  • Updater使用反射得到这个变量,所以如果变量不可见就会出错。如果上面Student类中score是private的就不可以;
  • 为了保证变量对正确读取,它必须是volatile类型的;
  • CAS操作会通过对象实例中的偏移量直接进行赋值,所以不支持static字段,以为内Unsafe.objectFieldOffset()不支持静态变量。

死锁

死锁就是两个或者多个线程,相互占用着对方需要的资源,都不释放,导致彼此之间相互等待对方释放资源,产生了无限制的等待。举个简单的例子

public class DeadLock implements Runnable {

    public static Object fork1 = new Object();
    public static Object fork2 = new Object();

    private String name;
    private Object tool;

    public DeadLock(Object o) {
        this.tool = o;
        if (tool == fork1) {
            this.name = "哲学家A";
        }
        if (tool == fork2) {
            this.name = "哲学家B";
        }
    }

    @Override
    public void run() {
        if (tool == fork1) {
            synchronized (fork1) {
                try {
                    System.out.println(name+"拿到了一个叉子");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (fork2) {
                    System.out.println(name+"拿到两个叉子了");
                }
            }
        }

        if (tool == fork2) {
            synchronized (fork2) {
                try {
                    System.out.println(name+"拿到了一个叉子");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (fork1) {
                    System.out.println(name+"拿到两个叉子了");
                }
            }
        }
    }

    public static void main(String[] args) {
        DeadLock a = new DeadLock(fork1);
        DeadLock b = new DeadLock(fork2);

        Thread t1 = new Thread(a);
        Thread t2 = new Thread(b);
        t1.start();
        t2.start();

    }
}

运行上面这段程序,会输出

哲学家B拿到了一个叉子
哲学家A拿到了一个叉子

然后程序就进入了死循环,因为哲学家A在等B手里的叉子,哲学家B也在等A手上的叉子,但是他俩谁都不肯释放。

为了规避死锁,除了使用无锁操作外,还可以使用重入锁。


by @sunhaiyu

2018.5.13

Java多线程--锁的优化的更多相关文章

  1. synchronized与static synchronized 的差别、synchronized在JVM底层的实现原理及Java多线程锁理解

    本Blog分为例如以下部分: 第一部分:synchronized与static synchronized 的差别 第二部分:JVM底层又是怎样实现synchronized的 第三部分:Java多线程锁 ...

  2. Java多线程——锁概念与锁优化

    为了性能与使用的场景,Java实现锁的方式有非常多.而关于锁主要的实现包含synchronized关键字.AQS框架下的锁,其中的实现都离不开以下的策略. 悲观锁与乐观锁 乐观锁.乐观的想法,认为并发 ...

  3. Java 多线程 锁 存款 取款

    http://jameswxx.iteye.com/blog/806968 最近想将java基础的一些东西都整理整理,写下来,这是对知识的总结,也是一种乐趣.已经拟好了提纲,大概分为这几个主题: ja ...

  4. Java多线程——锁

    Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线 ...

  5. Java 多线程 - 锁优化

    http://www.cnblogs.com/pureEve/p/6421273.html https://www.cnblogs.com/mingyao123/p/7424911.html

  6. 详解Java多线程锁之synchronized

    synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法. synchronized的四种使用方式 修饰代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括 ...

  7. [java多线程] - 锁机制&同步代码块&信号量

    在美眉图片下载demo中,我们可以看到多个线程在公用一些变量,这个时候难免会发生冲突.冲突并不可怕,可怕的是当多线程的情况下,你没法控制冲突.按照我的理解在java中实现同步的方式分为三种,分别是:同 ...

  8. Java——多线程锁的那些事

    引入 Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率. 下面先带大家来总体预览一下锁的分类图 1.乐观锁 VS 悲观锁 乐观锁与悲观锁是一种广义上的概念,体现了 ...

  9. java多线程-锁

    自 Java 5 开始,java.util.concurrent.locks 包中包含了一些锁的实现,因此你不用去实现自己的锁了.但是你仍然需要去了解怎样使用这些锁. 一个简单的锁 让我们从 java ...

随机推荐

  1. C++动态(显式)调用 C++ dll

    1.创建DLL新项目Dll1,Dll1.cpp: extern "C" __declspec(dllexport) const char* myfunc() { return &q ...

  2. 3.翻译:EF基础系列--EF怎么工作的?

    原文链接:http://www.entityframeworktutorial.net/basics/how-entity-framework-works.aspx 这里,你将会大概了解到EF是怎么工 ...

  3. EF学习笔记-2 EF之支持复杂类型的实现

    使用过.NET的小伙伴们知道,在我们的实体模型中,除了一些简单模型外,还有一些复杂类型,如几个简单的类型组合而成的类型:而EF除了在实现基本的增删改查之外,也支持复杂类型的实现. 那么如何手动构造复杂 ...

  4. 【javascript】您好, 您要的ECMAScript6速记套餐到了 (一)

    [前言]本文“严重参考” 自阮一峰老师写的ES6文档,在此我郑重感谢他沉默无声的帮助 总结一下ES6为 javascript中的 对象/数组/函数 这JS三巨头所提供的更简洁优雅的书写方式,以及扩展的 ...

  5. MariaDB 插入&更新&删除数据(8)

    MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可MariaDB的目的是完全兼容MySQL,包括API和命令行,MySQL由于现在闭源了,而能轻松成为MySQ ...

  6. [JavaScript] 的异步编程之手写一个Gernerator的例子

    <html> <head> <meta charset="UTF-8"> <title>Generator Demo</tit ...

  7. day 72 crm(9) 客户关系系统,整体实现,以及自定制路由内容,客户关系梳理

    前情提要: crm(9)  ,利用stark 组件和,rbac组键的使用,   明确观点: 一:客户关系需求明确 1:客户关系系统,主要是,本业务逻辑主要是 销售就客户关系的业务逻辑, 二: 创建ap ...

  8. 使用bash echo 输出回车转义

    输出回车 [root@~]# echo -e 'hello\n'hello 回车去掉 [root@~]# echo -n hello hello[root@~]#

  9. Selenium3 + Python3自动化测试系列四——鼠标事件和键盘事件

    一.鼠标事件 在 WebDriver 中, 将这些关于鼠标操作的方法封装在 ActionChains 类提供. ActionChains 类提供了鼠标操作的常用方法. ActionChains 类的成 ...

  10. zookeeper+kafka集群的安装部署

    准备工作 上传 zookeeper-3.4.6.tar.gz.scala-2.11.4.tgz.kafka_2.9.2-0.8.1.1.tgz.slf4j-1.7.6.zip 至/usr/local目 ...