• java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析


    java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析

    前言:如有不正确的地方,还望指正。

    目录

    Synchronized

    原理

    • synchronized关键字是通过字节码指令来实现的
    • synchronized关键字编译后会在同步块前后形成monitorenter和monitorexit两个字节码指令
    • 执行monitorenter指令时需要先获得对象的锁(每个对象有一个监视器锁monitor),如果这个对象没被锁或者当前线程已经获得此锁(也就是重入锁),那么锁的计数器+1。如果获取失败,那么当前线程阻塞,直到锁被对另一个线程释放
    • 执行monitorexit指令时,计数器减一,当为0的时候锁释放
    class Test
    {
        public int i=1;
        public void test()
        {
    	    synchronized (this)
    	    {
    	    	i++;	
    	    }
    }
    }
    
    • 反编译后结果

    volatile

    作用

    • 保证变量对所有的线程的可见性,当一个线程修改了这个变量的值,其他线程可以立即知道这个新值(之所以有可见性的问题,是因为java的内存模型)

    原理

    • 所有变量都存在主内存,每条线程有自己的工作内存,工作内存保存了被该线程使用的变量的主内存副本拷贝
    • 线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存的变量,也就是必须先通过工作内存
    • 一个线程不能访问另一个线程的工作内存
    • volatile保证了变量更新的时候能够立即同步到主内存,使用变量的时候能立即从主内存刷新到工作内存,这样就保证了变量的可见性
    • 实际上是通过内存屏障来实现的。语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。

    Atomic

    作用

    • 当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,一直等到执行成功。

    原理

    • CAS操作(compare and swap 对比和设置),是通过一个cpu指令实现的,这个指令是一个原子指令,指令有3个操作数ABC,A为内存位置,B为预期值,C为新值,如果A符合旧预期值B,那么用V更新A的值,如果不符合就不更新,这个过程是原子操作
    • 所以我们并没有通过代码来实现同步,而是通过硬件级别的cpu指令来实现的,并不像synchronized一样阻塞线程
    //加一并返回值
    public final int incrementAndGet() {
            for (;;) {
                int current = get();
                int next = current + 1;
                if (compareAndSet(current, next))
                    return next;
            }
       }
    
    //返回CAS操作成功与否
    public final boolean compareAndSet(int expect, int update) {
            //根据变量在内存中的偏移地址valueOffset获取原值,然后和预期值except进行比,如果符合,用update值进行更新,这个过程是原子操作
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
        }
    
    
     
    
    • 如果此时有两个线程,线程A得到current值为1,线程B得到current值也为2,此时线程A执行CAS操作,成功将值改为2,而此时线程B执行CAS操作,发现此时内存中的值并不是读到current值1,所以返回false,此时线程B继续进行循环,最后成功加1

    Lock

    作用

    • 显式加锁

    原理

    • 通过同步器AQS(AbstractQueuedSynchronized类)来实现的,AQS根本上是通过一个双向队列来实现的
    • 线程构造成一个节点,一个线程先尝试获得锁,如果获取锁失败,就将该线程加到队列尾部
    • 非公平锁的lock方法,调用的sync(NonfairSync和fairSync的父类)的lock方法
     public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    
    // ReentrantLock的lock方法
    public void lock() {
            sync.lock();
        }
    
    • NonfairSync的lock方法,acquire的是Sync的父类AQS的acquire方法
    
    final void lock() {
                //如果当前同步状态为0(锁未被占有),CAS操作设置同步状态,设置成功的话当前线程获得锁(如果此时是公平锁,那么不会执行compareAndSetState方法,直接acuire排队)
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                //否则调用AQS的acquire方法
                else
                    acquire(1);
            }
    
    
     //CAS设置锁的状态
     protected final boolean compareAndSetState(int expect, int update) {
            // See below for intrinsics setup to support this
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
        }
    
    • AQS的acquire方法
    //尝试获得锁,如果获取失败,将节点加入到尾节点
    public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    
    • tryAcquire方法,尝试获得锁
    final boolean nonfairTryAcquire(int acquires) {
        
        final Thread current = Thread.currentThread();
        //获取state变量值
        int c = getState();
        if (c == 0) { //如果没有线程占用锁
            if (compareAndSetState(0, acquires)) {
                //设置当前线程获得锁
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 更新state值为新的重入次数
            setState(nextc);
            return true;
        }
        //获取锁失败
        return false;
    }
    
    
    • 如果获取锁失败,将节点加入尾节点
    private Node addWaiter(Node mode) {
             Node node = new Node(Thread.currentThread(), mode);
             // Try the fast path of enq; backup to full enq on failure
             Node pred = tail;
                //如果尾节点不为空
             if (pred != null) {
                 node.prev = pred;
                    //此时可能同时有其他线程插入,再进行判断(通过CAS),如果没有,将节点设置为尾节点
                 if (compareAndSetTail(pred, node)) {
                     pred.next = node;
                     return node;
                 }
             }
                //如果节点为空或者节点不为空并且有其他线程插入(CAS返回false),执行enq方法
             enq(node);
             return node;
         }
    
    • 如果节点为空或者节点不为空并且有其他线程插入(CAS返回false),执行enq
    //通过自旋进行设置
    private Node More enq(final Node node) {
             for (;;) {
                 Node t = tail;
                 if (t == null) { // Must initialize
                     if (compareAndSetHead(new Node()))
                         tail = head;
                 } else {
                     node.prev = t;
                     if (compareAndSetTail(t, node)) {
                         t.next = node;
                         return t;
                     }
                 }
             }
         }
    
    • 进入队列的线程尝试获得锁
    
     
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true; //是否成功获取锁
        try {
            boolean interrupted = false; //线程是否被中断过
            for (;;) {
                final Node p = node.predecessor(); //获取前驱节点
                //如果前驱是head尝试获锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node); // 获取成功,将当前节点设置为head节点
                    p.next = null; // 原head节点出队
                    failed = false; 
                    return interrupted; //返回是否被中断过
                }
                // 前节点不是头节点或者获取失败,判断是否可以挂起
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                    // 线程若被中断,设置interrupted为true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    
    
    • 线程是否可以挂起
    
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //前驱节点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 前驱节点状态为signal(此节点线程结束后唤醒下一个节点线程)
            return true;
        //如果 前驱节点状态为CANCELLED(线程已经被取消)
        if (ws > 0) {
            // 删除cancelled状态的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 将前驱节点的状态设置为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    
    • 挂起当前线程,返回线程中断状态并重置
     
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
    

    我觉得分享是一种精神,分享是我的乐趣所在,不是说我觉得我讲得一定是对的,我讲得可能很多是不对的,但是我希望我讲的东西是我人生的体验和思考,是给很多人反思,也许给你一秒钟、半秒钟,哪怕说一句话有点道理,引发自己内心的感触,这就是我最大的价值。(这是我喜欢的一句话,也是我写博客的初衷)

    作者:jiajun 出处: http://www.cnblogs.com/-new/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧。

  • 相关阅读:
    购买成熟软件产品后的二次开发的问题
    outlook2010如何导入csv的通讯录?
    导入Excel数据时对数据校验提示方法
    系统开发中存储过程使用的优势和劣势
    FCKeditor.Net_2.5的使用
    [正则表达式]如何高亮显示搜索关键字
    国外网站模板网址集锦
    _NET 下 FCKeditor_2_5_1上传图片的配置
    用属性模拟多继承机制
    FCKeditor 2.6在ASP.NET中的配置方法
  • 原文地址:https://www.cnblogs.com/-new/p/7326820.html
Copyright © 2020-2023  润新知