zoukankan      html  css  js  c++  java
  • Java并发编程的艺术(一)

    看《java并发编程的艺术》这本书,想着看的时候做个简单的总结,方便以后直接看重点。

    一.并发编程的挑战

    1.上下文切换

    Cpu时间片通过给每个线程分配CPU时间片来实现多线程机制,时间片一般是几十毫秒。任务从保存到再加载的过程就是一次上下文切换。

    如何减少上下文切换?

    • 无锁并发编程:多线程处理数据时,避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
    • CAS算法:Compare and Swap,即比较再交换。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
    public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }
    
    • 使用最少线程:避免创建不需要的线程
    • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

    2.死锁

    避免死锁的几个常见方法:

    • 避免一个线程同时获取多个锁
    • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
    • 尝试使用定时锁,使用lock.trylock(timeout)来替代使用内部锁机制
    • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

    二.java并发机制的底层实现原理

      java使用的并发机制依赖于JVM的实现和CPU的指令

    1.volatile的应用

      volatile是轻量级的synchronized,它保证了共享变量的“可见性”,就是说当一个线程修改一个共享变量的时候,另外一个线程能读到这个修改的值。它不会引起线程上下文的切换和调度。

    那么volatile如何保证可见性的呢?

    volatile变量修饰的共享变量在进行写操作的时候会多出第二行汇编代码,有一个lock前缀指令:

    • 将当前处理器缓存行的数据写回到系统内存
    • 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

    2.synchronized的实现原理与应用

    synchronized实现同步的基础:java中的每一个对象都可以作为锁

    • 对于普通同步方法,锁是当前实例对象
    • 对于静态同步方法,锁是当前类的class对象
    • 对于同步方法块,锁是Synchronized括号里配置的对象

    当一个线程视图访问同步代码块时,首先必须得到锁,退出或抛出异常时必须释放锁。

    synchronized在jvm里的实现原理:JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,通过两个指令,monitorenter和monitorexit

    monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit是插入到方法结束处和异常处,jvm要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

    synchronized用的锁是存在java对象头里的

    3.原子操作的实现原理

    处理器如何实现原子操作?

    1. 使用总线锁保证原子性:总线锁就是使用处理器提供的一个Lock#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,那么该处理器就可以独占共享内存。
    2. 使用缓存锁保证原子性:同一时刻,只需保证对某个内存地址的操作是原子性即可,但总线锁把CPU和内存之间的通信锁住了,使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。采用缓存锁定的方式来实现复杂的原子性。

    java如何实现原子操作?

    java中可以通过锁和循环CAS的方式来实现原子操作

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;
    
    
    /**
     * java使用循环CAS实现原子操作
     */
    public class Counter {
        private AtomicInteger  atomic1 = new AtomicInteger(1);
        private  int i=1;
    
        public static void main(String[] args) {
            final  Counter cas = new Counter();
            List<Thread> ts = new ArrayList<>(600);
            long start = System.currentTimeMillis();
            for(int j=0;j<100;j++){
                Thread t = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i=0;i< 10000;i++){
                            cas.count();
                            cas.safeCount();
                        }
                    }
                });
                ts.add(t);
            }
            for (Thread t:ts){
                t.start();
            }
            for (Thread t:ts){
                try {
                    t.join();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
            System.out.println(cas.i);
            System.out.println(cas.atomic1.get());
            System.out.println(System.currentTimeMillis()-start);
        }
    
        /**
         * 使用CAS实现线程安全计数器
         */
        private  void safeCount(){
            for(;;){
                int i = atomic1.get();
                boolean suc = atomic1.compareAndSet(i,++i);
                if(suc){
                    break;
                }
            }
        }
    
        /**
         * 非线程安全计数器
         */
        private  void count(){
            i++;
        }
    }
    

     运行结果如下:

     可见线程安全计数器正确显示了结果。

    CAS实现原子操作的三大问题:

    • ABA问题:CAS需要在操作值的时候检查值有没有发生变化,如果没有发生变化就更新。但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时就会发现它的值没有发生变化,但是实际上变化了。ABA问题的解决思路就是使用版本号,A->B->A变成了1A->2B->3A,  java中提供了一个类来解决这个ABA问题:AtomicStampedReference
     
     /**
         * Atomically sets the value of both the reference and stamp
         * to the given update values if the
         * current reference is {@code ==} to the expected reference
         * and the current stamp is equal to the expected stamp.
         *
         * @param expectedReference the expected value of the reference
         * @param newReference the new value for the reference
         * @param expectedStamp the expected value of the stamp
         * @param newStamp the new value for the stamp
         * @return {@code true} if successful
         */
    public boolean compareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));
        }
    

    这个方法的作用就是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    • 循环时间长开销大。
    • 只能保证一个共享变量的原子操作。解决办法是把多个共享变量合并成一个共享变量来操作·,比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。java中提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

    使用锁机制实现原子操作:锁机制保证了只有获得锁的线程才能操作锁定的内存区域,JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁,但是除了偏向锁,JVM实现锁的方式都用了循环CAS

  • 相关阅读:
    mybatis入门截图三
    centos 6.X 关闭selinux
    安装完最小化 RHEL/CentOS 7 后需要做的 30 件事情(二)
    安装完最小化 RHEL/CentOS 7 后需要做的 30 件事情(一)
    Linux定时任务Crontab命令详解
    CentOS 更换yum源为aliyun yum源
    CentOS7 FTP服务搭建(虚拟用户访问FTP服务)
    CentOS 7 安装Oracle 11gR2
    印度项目质量管理经验
    项目管理系列之质量管理
  • 原文地址:https://www.cnblogs.com/Dream-chasingGirl/p/11458341.html
Copyright © 2011-2022 走看看