美文网首页
《实战java高并发程序设计》笔记(四)

《实战java高并发程序设计》笔记(四)

作者: MikeShine | 来源:发表于2020-02-21 15:23 被阅读0次

    写在前面

    之前花了好几天看完了 JUC 部分的第3章笔记。还是挺多的。
    接下来看一下 第4章 关于锁优化部分的内容


    第四章 锁的优化及其注意事项

    第四章知识框架

    锁性能的几点建议

    1. 减小锁持有时间
    • 系统有锁时间越长,锁竞争越激烈,我们只对需要同步的方法加锁,来减小锁持有的时间从而提高锁性能
    • 减小锁持有时间可以降低锁发生冲突的可能,进而提高锁的并发能力。
    1. 减小锁粒度
    • 减小锁粒度就是 缩小锁定对象的范围,从而减小锁冲突的可能,提高并发能力
    1. 锁分离(读写分离锁代替独占锁)
    • 使用读写锁可以减少操作之间互相等待,提高性能。ConcurrentLinkedQueue (上一章最后提到的并发性能最好的 队列)中的 take 和 put 方法分别使用了两个锁避免了锁竞争,提高了性能。
    1. 锁粗化
    • 一个锁被频繁的请求、释放,也会耗费大量资源
    • JVM 在遇到一连串对同一个锁的请求、释放等操作时,将这些操作整合成为一次,这样就是锁粗化。

    Java 虚拟机对锁优化做的努力

    1. 锁偏向
    • 一个线程获得锁之后,这个锁就进入了偏向模式,当这个线程再次请求锁时,无需同步操作
    • JVM参数 -XX:UseBiasedLocking 开启偏向锁
    1. 轻量级锁
    • 偏向锁失败,JVM 不会立即挂起线程,而是使用轻量级锁进行操作
    • 轻量级锁只是 将对象头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁
    • 线程获得轻量级锁成功,则可以成功进入临界区。轻量级锁加锁失败,则其他线程抢到了锁,当前线程的轻量级锁变成重量级锁
    1. 自旋锁
    • 无法获得锁的时候,会让线程做几个空循环
    • 长时间还无法获得锁进入临界区,则会真正挂起线程
    1. 锁消除
    • JVM会去除不可能存在共享资源竞争的锁。

    ThreadLocal:人手一支笔

    书上给了这么一个例子:100个人都要填写个人信息表,如果只有一个笔,那么我们就通过加锁的方式,让大家合理利用这个笔,从而不抢夺。
    但是,从另外一个角度来说,我们可以给他们100个笔,也就是说,人手一支笔。

    Sync 和 ThreadLocal 对比:
    二者都用于解决多线程并发访问的问题。但是 sync 是通过加锁的方式,使得某个时间段只有一个线程能够访问资源;而 ThreadLocal 是为每个线程都提供了对象的副本,每个线程在同一个时间访问的并不是同一个对象。总结,Sync 用于线程间的数据共享,ThreadLocal 用于线程间的数据隔离。

    1. ThreadLocal 的使用:
    • ThreadLocal 是线程的局部变量,只有当前线程可以访问,因此是线程安全的。
    • ThreadLocal 只是起到容器的作用,为每一个线程分配不同对象需要应用层面保证。
    public class ThreadLocalExample {
    
        public static class MyRunnable implements Runnable {
            private ThreadLocal threadLocal = new ThreadLocal();
    
            @Override
            public void run() {
                //一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值
                threadLocal.set((int) (Math.random() * 100D));
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                }
                //可以通过下面方法读取保存在ThreadLocal变量中的值
                System.out.println("-------threadLocal value-------" + threadLocal.get());
            }
        }
    ​
    
        public static void main(String[] args) {
            MyRunnable sharedRunnableInstance = new MyRunnable();
            Thread thread1 = new Thread(sharedRunnableInstance);
            Thread thread2 = new Thread(sharedRunnableInstance);
            thread1.start();
            thread2.start();
        }
    }
    

    关于ThreadLocal的详细解释

    1. ThreadLocal 的原理:
      原理就牵扯到 set() 和 get() 方法。简单理解为每一个线程都对应一个 map(其实是 ThreadLocalMap),这个map就存的是 <当前线程,value>。我们 set 和 get 时候都是在取这个 value,所以才会做到每一个线程对应一个 value,做到为每一个线程分配一个对象副本。
    • 在 set 时,首先通过 getMap() 获得当前线程的 ThreadLocalMap,写入 key=currnetThread,value = set()的value

    • 在 get 时,首先当前线程的 ThreadLocalMap,然后将自己(currentThread)作为key,获得 value

    • 线程不退出的情况下,其内部维护的ThreadLocalMap 就不会清除。佐伊在线程池这种场景下,需要使用 ThreadLocal 的 remove 方法来清楚数据。


    无锁

    之前说过,并发策略都是悲观的策略;与之相对的无锁,就是一种乐观的策略。
    无锁的好处:

    • 没死锁问题
    • 没锁调度消耗
    • 没锁竞争消耗

    CAS算法
    无锁使用的一种技术叫做 比较交换(Compare and Swap),用来鉴别冲突,一旦检测到并发冲突,就重试到当前操作没有冲突为止。

    • 包含3个参数(V,E,N),V表示要被更新的变量,E表示期望值,N表示要设置的值
    • 仅当 V = E,即被更新的变量的值等于期望值,才会将 V 设置为 N。如果不相等表示V的值已经被其他线程修改了,当前线程不做任何操作
    • 当多个线程访问的时候,只有一个线程成功。其他的线程会被告知失败,其他线程可以选择再次执行或者是放弃

    无锁线程安全的整数

    • AtomicInteger:是最常用的类,它是可变并且线程安全的。
      看一个使用的例子
    public class AtomicIntegerTest {
    
        static AtomicInteger i = new AtomicInteger();
        public static class AtomicIntegerThread implements Runnable {
            @Override
            public void run() {
                for (int k = 0; k < 10000; k++) {
                    i.incrementAndGet();
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            AtomicIntegerThread atomicIntegerThread = new AtomicIntegerThread();
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for (int k = 0; k < 10; k++) {
                executorService.submit(atomicIntegerThread);
            }
            Thread.sleep(500);
            System.out.println(i);
        }
    }
    

    程序执行结果会输出 10000。说明其运行正确了。
    看到最重要的一行就是 AtomicInteger.incrementAndGet(),这个方法会使用 CAS 操作将自己加1,同时返回当前值。
    下面看一下这个方法的的具体实现

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

    第3行的 get 取得当前值,加1后得到 next。这里, 就得到了 CAS 的两个参数:期望值E 和 新值V。使用 compareAndSet 方法将新值 next 写入,成功的条件时在写入的时刻,当前的值要等于刚刚取得的 current(说明没有被别的线程修改)

    总的来说,就是在一个无限循环内,不断重复将比自己大1的新值赋给自己。如果失败,就说明在 “获取-设置” 时候被其他线程修改了,然后做下一次尝试。


    Java中的指针 Unsafe 类

    • 指针是不安全的,因此 Java 中去除了指针,但是通过 Unsafe 类封装了一些不安全的操作。我们是无法使用这个类的。

    无锁对象引用 AtomicReference

    • AtomicReference 的作用是对 “对象” 进行原子操作,但是会丢失状态信息
    • 如果需要保存状态信息则可以使用 AtomicStampedReference,其维护了一个时间戳
    • AtomicIntegArray 是线程安全的数组

    原子操作工具类AtomicIntegerFieldUpdater

    • 让普通变量,在不改变代码的基础上,也可以享受CAS操作带来的原子性线程安全,就是 原子操作工具类要做的事情
    package com.ecut.atomic;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
    
    public class AtomicIntegerFieldUpdaterTest {
    
        public static AtomicInteger allScore = new AtomicInteger();
    
        public final static AtomicIntegerFieldUpdater<Candidate> socoreUpdater =
                AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
    
        public static class Candidate {
            int id;
            volatile int score;
        }
    
        public static class Vote implements Runnable {
    
            Candidate candidate;
    
            public Vote(Candidate candidate) {
                this.candidate = candidate;
            }
    
            @Override
            public void run() {
                if (Math.random() > 0.4) {
                    socoreUpdater.incrementAndGet(candidate);
                    allScore.incrementAndGet();
                }
            }
        }
    
    
        public static void main(String[] args) throws InterruptedException {
            Candidate candidate = new Candidate();
            Vote vote = new Vote(candidate);
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            for(int i = 0 ; i < 10000 ; i++){
                executorService.execute(vote);
            }
            Thread.sleep(5000);
            System.out.println(allScore+":"+candidate.score);
        }
    }
    

    运行结果中输出的两个数字是相等的。说明通过 原子操作工具类 维护了线程安全 。
    使用时要注意:

    1. Updater只修改范围可见的变量,因为是通过反射得到的
    2. 这个变量必须是 volatile 的,因为要有可见性
    3. 不支持 static 字段的变量

    相关文章

      网友评论

          本文标题:《实战java高并发程序设计》笔记(四)

          本文链接:https://www.haomeiwen.com/subject/nnvcqhtx.html