精华内容
下载资源
问答
  • 目录状态对象synchronized修饰代码块synchronized修饰方法synchronized修饰静态方法Monitor设计概念互斥与同步定义synchronized底层原理用户态和内核态资源调度存在问题解决方案互斥锁属性MonitorMonitor对象是啥...

    状态对象

    如果一个对象有被修改的成员变量 被称为有状态的对象相反如果没有可被修改的成员变量 称为无状态的对象

    示例

    
    public class MyThreadTest {
    
        public static void main(String[] args) {
            Runnable r = new MyThread();
    
            Thread t1 = new Thread(r);
            Thread t2 = new Thread(r);
    
            t1.start();
            t2.start();
        }
    }
    
    class MyThread implements Runnable {
        /**
         * 如果一个对象有被修改的成员变量 被称为有状态的对象
         * 相反如果没有可被修改的成员变量 称为无状态的对象
         *
         * 由于两个线程同时访问有状态的对象 当一个线程x++完
         * 此时另外一个线程又将x变成0 此时就会输出两次0
         */
        int x;
    
        @SneakyThrows
        @Override
        public void run() {
            x = 0;
            while (true) {
                System.out.println("result: " + x++);
    
                Thread.sleep((long) Math.random() * 1000);
                if (x == 30) {
                    break;
                }
            }
        }
    }
    /*
    result: 0
    result: 0
    result: 1
    result: 2
    result: 3
    result: 4
    result: 5
    result: 6
    result: 7
    result: 8
    result: 9
    ....
     */
    

    示例2

    /**
     * 类锁和对象锁互相不干扰 线程可以获取对象锁不影响其他的线程获取类锁 反之亦然
     */
    public class MyThreadTest2 {
        public static void main(String[] args) {
            MyClass myClass = new MyClass();
            MyClass myClass2 = new MyClass();
    
            Thread t1 = new Thread1(myClass);
            Thread t2 = new Thread2(myClass);
    
            t1.start();
    
            try {
                System.out.println("name: "+Thread.currentThread().getName());
                Thread.sleep(700);//睡眠main线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            t2.start();
    
        }
    }
    
    class MyClass {
    
        public synchronized void hello() {
    
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hello");
    
        }
    
        public synchronized void world() {
            System.out.println("world");
        }
    }
    
    class Thread1 extends Thread {
        private MyClass myClass;
    
        public Thread1(MyClass myClass) {
            this.myClass = myClass;
        }
    
        @Override
        public void run() {
            myClass.hello();
        }
    }
    
    class Thread2 extends Thread {
        private MyClass myClass;
    
        public Thread2(MyClass myClass) {
            this.myClass = myClass;
        }
    
        @Override
        public void run() {
            myClass.world();
        }
    }
    
    

    输出

    name: main
    hello
    world
    

    将代码修为

            Thread t1 = new Thread1(myClass);
    //        Thread t2 = new Thread2(myClass);
            Thread t2 = new Thread2(myClass2);
    

    输出

    name: main
    world
    hello
    

    结论:每个实例对象都有一个唯一的Monitor(锁)

    synchronized修饰代码块

    当我们用synchronized修饰代码块时字节码层面上是通过monitorenter和monitorexit指令来实现的锁的获取与释放动作。

    /**
     * 当我们使用synchronized关键字来修饰代码块时,
     * 字节码层面上是通过monitorenter和monitorexit指令来实现的锁的获取与释放动作。
     * monitorenter跟monitorexit 是一对多的关系
     *
     * 当线程进入到monitorenter指令后,线程将会持有Monitor对象,执行monitorexit指令,线程将会释放Monitor对象
     */
    public class MyTest1 {
    
        private Object object = new Object();
    
        public void method() {
            int i = 1;
            /*
             此处的不是只能锁object 所有的都可以
              此处synchronized 它会尝试去获取该对象的锁 有执行无则阻塞
             */
            synchronized (object) {
                System.out.println("hello world!");
                //当应用主动抛出异常此时字节码 会直接执行并且直接执行monitorexit解锁
                throw new RuntimeException();
            }
        }
    
        public void method2() {
            synchronized (object) {
                System.out.println("welcome");
            }
        }
    }
    
    
    /*
     0 iconst_1
     1 istore_1
     2 aload_0
     3 getfield #3 <com/example/demo/com/concurrecy/concurrency3/MyTest1.object>
     6 dup
     7 astore_2
     8 monitorenter
     9 getstatic #4 <java/lang/System.out>
    12 ldc #5 <hello world!>
    14 invokevirtual #6 <java/io/PrintStream.println>
    17 aload_2
    18 monitorexit
    19 goto 27 (+8)
    
    22 astore_3
    23 aload_2
    24 monitorexit //22-26为了保证抛出异常也能释放锁
    25 aload_3
    26 athrow //IO流输出 存在这种异常情况发生
    27 return
    
     */
    
    /*
        当加了throw new RuntimeException();
        code 字节码生成的助记符 只生成一个monitorexit
    
     0 iconst_1
     1 istore_1
     2 aload_0
     3 getfield #3 <com/example/demo/com/concurrecy/concurrency3/MyTest1.object>
     6 dup
     7 astore_2
     8 monitorenter
     9 getstatic #4 <java/lang/System.out>
    12 ldc #5 <hello world!>
    14 invokevirtual #6 <java/io/PrintStream.println>
    17 new #7 <java/lang/RuntimeException>
    20 dup
    21 invokespecial #8 <java/lang/RuntimeException.<init>>
    24 athrow
    25 astore_3
    26 aload_2
    27 monitorexit//此时在方法体中抛出了异常 异常结束 直接释放了锁
    28 aload_3
    29 athrow //表示该方法一定会以异常结束
     */
    

    synchronized修饰方法

    对于synchronized关键字修饰方法说,并没有出现monitorenter与monitorexit指令,而是出现一个ACC_SYNCHRONIZED标志。

    /**
     * 对于synchronized关键字修饰方法说,并没有出现monitorenter与monitorexit指令,而是出现一个ACC_SYNCHRONIZED标志。
     * <p>
     * JVM使用了ACC_SYNCHRONIZED访问标志来区分一个方法是否为同步方法;当方法被调用时,调用指令会检查该方法是否拥有ACC——SYNCHRONIZED标志
     * 如果有,那么执行线程将会先持有方法所在对象的Monitor对象,然后再去执行方法体;在改方法执行期间,其他任何线程均无法在获取到这个
     * monitor,当线程执行完该方法后,它会释放掉这个Monitor对象。
     */
    public class MyTest2 {
    
        public synchronized void method() {
            System.out.println("hello world");
        }
    }
    /*
    0 getstatic #2 <java/lang/System.out>
    3 ldc #3 <hello world>
    5 invokevirtual #4 <java/io/PrintStream.println>
    8 return
    
     */
    

    在这里插入图片描述
    而其对应的标识符如下
    在这里插入图片描述

    此时就没有通过monitorebter和moniterexit 来获取锁而是通过ACC_SYNCHRONIZED标识符来尝试获取锁

    synchronized修饰静态方法

    当synchronized修饰静态方法其实跟修饰成员方法一样 只不过方法标识符多了个ACC_STATIC,并且其锁的是类锁

    /**
     * 当synchronized修饰静态方法其实跟修饰成员方法一样 只不过方法标识符多了个ACC_STATIC
     * 其次锁的是 类锁
     */
    public class MyTest3 {
        /**
         * static静态方法不存在this局部变量
         * 原因直接类名.就能调用
         */
        public static synchronized void method() {
            System.out.println("hello world!");
        }
    
    }
    /*
    0 getstatic #2 <java/lang/System.out>
    3 ldc #3 <hello world!>
    5 invokevirtual #4 <java/io/PrintStream.println>
    8 return
    
     */
    

    在这里插入图片描述

    Monitor设计的概念

    互斥与同步定义

    关于“互斥”和“同步”的概念

    • 答案很清楚了,互斥就是线程A访问了一组数据,线程BCD就不能同时访问这些数据,直到A停止访问了
    • 同步就是ABCD这些线程要约定一个执行的协调顺序,比如D要执行,B和C必须都得做完,而B和C要开始,A必须先得做完。

    synchronized底层原理

    JVM中的同步是基于进入与退出监视器对象(管程对象)(Monitor)来实现的,每个对象实例都会有一个Monitor对象(每个Class生成时都会有且只有一个Monitor对象(锁), Monitor对象会和Java对象一同创建并销毁。Monitor对象是由C++来实现的。

    当多个线程同时访问一段同步代码时,这些线程会被放到一个EntryList集合当中,处于阻塞状态(未获取对象锁 要区别WaitSet)的线程都会被放到该列表当中。 接下来,当线程获取到对象的Monito时,Monitor是依赖于底层操作系统的mutex lock(互斥锁)来实现互斥的,线程获取mutex成功。则会持有该mutex,这时其他线程就无法获取到该mutex.。

    如果线程调用了wait方法(意思的调用wait方法才会进入WaitSet 竞争monitor时是和entryList 公平竞争),那么该线程就会释放掉所持有的mutex, 并且该线程会进入到WaitSet集合(等待集合)中,等待下一次被其他该对象锁线程调用notify/notifyAll唤醒(此处注意如果在WaitSet中被唤醒的线程没有竞争到锁该线程会进入entryList阻塞集合)。如果当前线程顺利执行完毕方法。那么它也会释放掉所持有的mutex.

    用户态和内核态资源调度

    总结一下:同步锁在这种实现方式当中,因为Monitor是依赖底层的操作系统实现,这样就存在用户态(如程序执行业务代码在用户端)与内核态(Monitor是依赖于底层操作系统 此时阻塞就是内核执行)之间的切换,所以会增加性能开销。 采用自旋作为回退机制当线程自旋时还是用户态占用的是CPU资源==(自旋太久也会造成CUP资源的浪费) 当自旋时间超过预期值还是会进入内核态。

    通过对象互斥锁的概念来保证共享数据操作的完整性。每个对象都对应与一个可称为【互斥锁】的标记,这个标记用于保证在任何时刻,只能有一个线程访问该对象。

    存在问题

    那些处于EntryList与WaitSet中的线程均处于阻塞状态(两个集合都属于Monitor对象的成员变量,阻塞操作是由操作系统来完成的,在Linux下是通过pthread_mutex_lock函数实现的。 线程被阻塞后便会进入到内核调度状态,这会导致系统在用户态与内核态之间来回切换,严重影响锁的性能

    解决方案

    解决上述问题的办法便是自旋(Spin)。其原理是:当发生对Monitor的争用时,若Owner(拥有线程或BasicLock指针)能够在很短的时间内释放掉锁,则哪些正在争用的线程就可以稍微等待一下(即所谓的自旋),在Owner线程释放锁之后,争用线程可能会立刻获取到锁,从而避免了系统阻塞(内核态).不过,当Owner运行的时间超过了临界值后。争用线程自旋一段时间后依然无法获取到锁,这时争用线程则会停止自旋而进入到阻塞状态(内核态)。所有总体的思想:先自旋,不成功再进行阻塞,尽量降低阻塞的可能性,这对哪些执行时间很短的代码块来时有极大的性能提升。显然自旋在多处理器(多核心)上才有意义。
    *

    互斥锁属性

    1. PTHREAD_MUTEX_TIMED_NP: 这是省缺值,也就是普通锁,当一个线程加锁以后,其余请求锁的线程将会形成一个等待队列,并且在解锁后按照优先级获取到锁,这种策略可以确保资源分配的公平性。
    2. PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁.允许一个线程对同一个锁成功获取多次,并通过unlock解锁。如果是不同线程请求,则在加锁线程解锁时重写进行竞争。
    3. PTHREAD_MUTEX_ERRORCHECK_NP:检错锁。如果一个线程请求同一把锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMEDNP类型动作相同,这样就能保证了当不允许多次加锁时不会出现最简单的死锁。
    4. PTHREAD_MUTEX_ADAPTIVE_NP:适应锁.动作最简单的锁类型,仅仅等待解锁后重新竞争。

    Monitor

    JVM中的同步是基于进入与退出监视器对象(管程对象)(Monitor)来实现的,每个对象实例都会有一个Monitor对象(每个Class生成对象时都会有且只有一个Monitor对象(锁)伴生, Monitor对象会和Java对象一同创建并销毁。Monitor对象是由C++来实现的。

    Monitor对象是啥?

    OpenJDK地址
    通过OpenJDK翻看JVM底层的一些C++代码
    在这里插入图片描述
    点击进入hpp后缀文件找到如下的方法
    ObjectWaiter对当前线程的封装 底层通过链表来记录

    //截取如下
    class ObjectWaiter : public StackObj {
     public:
      enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ } ;
      enum Sorted  { PREPEND, APPEND, SORTED } ;
      ObjectWaiter * volatile _next;//指向下一个ObjectWaiter 
      ObjectWaiter * volatile _prev;//指向上游的ObjectWaiter 
      Thread*       _thread; 
    

    这么做的好处
    在这里插入图片描述
    我们可以从一个ObjectWaiter 知道其他ObjectWaiter 的位置可以根据对应的策略选择性的唤醒对应的ObjectWaiter 如首位 中间指定等。
    在这里插入图片描述
    当waitset中唤醒的线程没有获取到monitor 就会将唤醒的线程放到entryList(也是链表格式)当中当entryList当中拿到锁就将对应线程从entryList中移除

    没有遇到wait()方法时直接进入EntryList集合当中
    在这里插入图片描述

    注意:WaitSet线程只是那些调用了wait()的线程,而EntryList是用来存储阻塞线程

    Wait JVM底层核心代码解析

    class ObjectMonitor {
     public:
      enum {
        OM_OK,                    // no error 没有错误
        OM_SYSTEM_ERROR,          // operating system error 操作系统错误
        OM_ILLEGAL_MONITOR_STATE, // IllegalMonitorStateException  异常
        OM_INTERRUPTED,           // Thread.interrupt()
        OM_TIMED_OUT              // Object.wait() timed out  超时
      };
    

    对应成员变量

      // initialize the monitor, exception the semaphore, all other fields
      // 初始化monitor,  
      // are simple integers or pointers
      ObjectMonitor() {
        _header       = NULL;
        _count        = 0;
        _waiters      = 0, 
        _recursions   = 0;//嵌套锁 递归嵌套
        _object       = NULL;
        _owner        = NULL;//拥有线程或BasicLock指针
        _WaitSet      = NULL;//wait等待集合
        _WaitSetLock  = 0 ; //自旋锁标识字段 保护等待队列
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //阻塞集合
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
        _previous_owner_tid = 0;
      }
    
    

    如下文档注释

     protected:
      ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor
      									//Monitor上的所有线程等待()集合
    
     protected:
      ObjectWaiter * volatile _EntryList ;     // Threads blocked on entry or reentry.	
      											//线程在进入或返回时被阻塞。
    
     protected:                         // protected for jvmtiRawMonitor
      void *  volatile _owner;          // pointer to owning thread OR BasicLock
      									//指向拥有线程或BasicLock的指针
    

    由JVM底层C++代码和文档注释我们可知_WaitSet和_EntryList 其实是其Monitor对应的的成员变量 初始值都为NULL

    在objectMonitor.cpp文件当中
    如wait方法实际对应与java Object基类当中的wait(0)所对应的方法

    // Wait/Notify/NotifyAll
    //
    // Note: a subset of changes to ObjectMonitor::wait()
    // will need to be replicated in complete_exit above
    void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
    	  .....
    	ObjectWaiter node(Self);//被包装的线程节点
       node.TState = ObjectWaiter::TS_WAIT ; 
       Self->_ParkEvent->reset() ;
       OrderAccess::fence();          // ST into Event; membar ; LD interrupted-flag
    
     // Enter the waiting queue, which is a circular doubly linked list in this case
    //输入等待队列,在本例中是一个循环的双链接列表
    // but it could be a priority queue or any data structure.
    //但它可以是优先级队列或任何数据结构。(链表优势)
    // _WaitSetLock protects the wait queue. Normally the wait queue is accessed only
    //_WaitSetLock保护等待队列。通常只访问等待队列
    // by the the owner of the monitor *except* in the case where park()
    //由监视器的所有者*except*在park()的情况下
    // returns because of a timeout of interrupt. Contention is exceptionally rare
    //由于中断超时而返回。争论异常罕见
    // so we use a simple spin-lock instead of a heavier-weight blocking lock.
    //所以我们使用了一个简单的自旋锁,而不是一个更重的重量级锁。
       Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;//自旋捕获 锁
       AddWaiter (&node) ;//用来更换指针引用
       ......
       exit (true, Self) ;      // exit the monitor 退出monitor
    

    更换内容如下

    inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
      assert(node != NULL, "should not dequeue NULL node");
      assert(node->_prev == NULL, "node already in list");
      assert(node->_next == NULL, "node already in list");
      // put node at end of queue (circular doubly linked list)
      if (_WaitSet == NULL) {
        _WaitSet = node;
        node->_prev = node;
        node->_next = node;
      } else {
        ObjectWaiter* head = _WaitSet ;
        ObjectWaiter* tail = head->_prev;
        assert(tail->_next == head, "invariant check");
        tail->_next = node;
        head->_prev = node;
        node->_next = head;
        node->_prev = tail;
      }
    }
    

    在这我们看出当调用了waitSet方法时底层C++时,先进行SpinAcquire (自旋捕获)尝试获取锁,没获取到则将对应线程添加到waitSet当中以链表的形式,当完成上述操作时exit monitor

    notify底层核心代码解析

    void ObjectMonitor::notify(TRAPS) {
      CHECK_OWNER();
      if (_WaitSet == NULL) {//_WaitSet 为null 直接返回
         TEVENT (Empty-Notify) ;
         return ;
      }
      ....
       Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ;
      //DequeueWaiter 根据不同的调度策略获取waitSet集合链表中目标线程
      ObjectWaiter * iterator = DequeueWaiter() ;
      .....
       if (Policy == 0) {       // prepend to EntryList
           if (List == NULL) {
               iterator->_next = iterator->_prev = NULL ;
               _EntryList = iterator ;
           } else {
               List->_prev = iterator ;
               iterator->_next = List ;
               iterator->_prev = NULL ; 
               _EntryList = iterator ; //此时如果目标线程未获取到monitor则放入ENtryList当中
          }
    

    总结:notify底层会先根据不同调度策略获取waitSet集合链表中目标线程,此时如果目标线程未获取到monitor则放入ENtryList当中

    notifyAll底层核心代码解析

    
    void ObjectMonitor::notifyAll(TRAPS) {
      CHECK_OWNER();
      ObjectWaiter* iterator;
      if (_WaitSet == NULL) { //WaitSet null 直接返回
          TEVENT (Empty-NotifyAll) ;
          return ;
      }
      DTRACE_MONITOR_PROBE(notifyAll, this, object(), THREAD);
    
      int Policy = Knob_MoveNotifyee ;
      int Tally = 0 ;
      Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notifyall") ;
    
      for (;;) { //遍历所有
         iterator = DequeueWaiter () ; //拿到对应的WaitSet 全部唤醒
         if (iterator == NULL) break ;
         TEVENT (NotifyAll - Transfer1) ;
         ++Tally ;
         ....
    

    总结:notifyAll底层通过死循环唤醒WaitSet 所有的ObjectWaiter 目标线程,

    展开全文
  • 二、Redisson实现Redis分布式锁的底层原理 (1)加锁机制 (2)锁互斥机制 (3)watch dog自动延期机制 (4)可重入加锁机制 (5)锁释放机制 (6)此种方案Redis分布式锁的缺陷 三、未完待续 一、写...

    一、写在前面

    二、Redisson实现Redis分布式锁的底层原理

          (1)加锁机制

          (2)锁互斥机制

          (3)watch dog自动延期机制

          (4)可重入加锁机制

          (5)锁释放机制

          (6)此种方案Redis分布式锁的缺陷

    三、未完待续

    一、写在前面

    现在面试,一般都会聊聊分布式系统这块的东西。通常面试官都会从服务框架(Spring Cloud、Dubbo)聊起,一路聊到分布式事务、分布式锁、ZooKeeper等知识。所以咱们这篇文章就来聊聊分布式锁这块知识,具体的来看看Redis分布式锁的实现原理。

    说实话,如果在公司里落地生产环境用分布式锁的时候,一定是会用开源类库的,比如Redis分布式锁,一般就是用Redisson框架就好了,非常的简便易用。

    大家如果有兴趣,可以去看看Redisson的官网,看看如何在项目中引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁。下面给大家看一段简单的使用代码片段,先直观的感受一下:

    怎么样,上面那段代码,是不是感觉简单的不行!此外,人家还支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构,都可以给你完美实现。

    二、Redisson实现Redis分布式锁的底层原理

    好的,接下来就通过一张手绘图,给大家说说Redisson这个开源框架对Redis分布式锁的实现原理。

    (1)加锁机制

    咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。这里注意,仅仅只是选择一台机器!这点很关键!紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:

    为啥要用lua脚本呢?因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性

    那么,这段lua脚本是什么意思呢?这里**KEYS[1]**代表的是你加锁的那个key,比如说:RLock lock = redisson.getLock("myLock");这里你自己设置了加锁的那个锁key就是“myLock”。

    **ARGV[1]**代表的就是锁key的默认生存时间,默认30秒。**ARGV[2]**代表的是加锁的客户端的ID,类似于下面这样:8743c9c0-0795-4907-87fd-6c719a6b4586:1

    给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。如何加锁呢?很简单,用下面的命令:hset myLock

    8743c9c0-0795-4907-87fd-6c719a6b4586:1 1,通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:

    上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。好了,到此为止,ok,加锁完成了。

    (2)锁互斥机制

    那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。

    所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的**剩余生存时间。**比如还剩15000毫秒的生存时间。此时客户端2会进入一个while循环,不停的尝试加锁。

    (3)watch dog自动延期机制

    客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

    简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

    (4)可重入加锁机制

    那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?比如下面这种代码:

    这时我们来分析一下上面那段lua脚本。第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”

    此时就会执行可重入加锁的逻辑,他会用:

    incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1  ,通过这个命令,对客户端1的加锁次数,累加1。此时myLock数据结构变为下面这样:

    大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数

    (5)释放锁机制

    如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:“del myLock”命令,从redis里删除这个key。然后呢,另外的客户端2就可以尝试完成加锁了。这就是所谓的分布式锁的开源Redisson框架的实现机制。

    一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。

    (6)上述Redis分布式锁的缺点

    其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。

    接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。此时就会导致多个客户端对一个分布式锁完成了加锁。这时系统在业务语义上一定会出现问题,导致各种脏数据的产生

    所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁

    三、未完待续

    下一篇文章,给大家分享一下电商系统中,大促销的活动场景下,每秒上千订单的时候如何对Redis分布式锁进行高并发的优化。

    敬请关注:

    《每秒上千订单的高并发场景下如何完成分布式锁的性能优化?》

    END

    如有收获,请帮忙转发,您的鼓励是作者最大的动力,谢谢!

    一大波微服务、分布式、高并发、高可用的****原创系列

    文章正在路上,欢迎扫描下方二维码,持续关注:

    石杉的架构笔记(id:shishan100)

    十余年BAT架构经验倾囊相授


    作者:石杉的架构笔记
    链接:https://juejin.cn/post/6844903717641142285
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    展开全文
  • 虽然我们可以用到CAS,互斥锁,消息队列,甚至分布式锁来解决,但是对于锁的底层实现,这次分享,我们想更深入的来分析和探讨锁的底层原理,以便更好地理解和掌握并发编程。 大纲:1.并发编程与锁2.缓存和一致性协议...

    并发编程,多核、多线程的情况下,线程安全性问题都是一个无法回避的难题。虽然我们可以用到CAS,互斥锁,消息队列,甚至分布式锁来解决,但是对于锁的底层实现,这次分享,我们想更深入的来分析和探讨锁的底层原理,以便更好地理解和掌握并发编程。

    大纲:1.并发编程与锁2.缓存和一致性协议MESI3.CPU/缓存与锁4.常见锁总结1 并发编程与锁

    我们写的各种应用系统,像网络编程,基本上都是并发编程,不论是多进程还是多线程,亦或是协程、队列的方式,也都是并发编程的范畴。并发编程中,在多核操作系统中,多线程的时候,就会出现 线程安全性 问题,有的也说 并发安全性 问题。这种问题,都是因为对共享变量的并发读写引起的数据不一致问题。所以,在并发编程中,就会经常用到锁,当然也可能使用队列或者单线程的方式来处理共享数据。

    我们先来还原一下具体的问题,然后再用不同的方法来处理它们。

    线程安全性问题1

     

     

     

     

     

    代码中共享变量num是一个简单的计数器,main主线程启动了两个协程,分别循环一万次对num进行递增操作。正常情况下,预期的结果应该是1w+1w=2w,但是,在并发执行的情况下,最终的结果只有10891,离2w差的好多。

    典型应用场景:

    1 库存数量扣减

    2 投票数量递增

    并发安全性问题:

    num+ +是三个操作(读、改、写),不满足原子性

    并发读写全局变量,线程不安全

    线程安全性问题2

     

     

     

     

     

    代码中共享变量list作为一个数据集合,由两个协程并发的循环append数据进去。同样是每个协程执行一万次,正常情况下,预期的list长度应该是2w,但是,在并发执行下,结果却可能连1w都不到。

    具体的原因,大家可以思考下,为什么并发执行的情况下,2个协程,竟然list长度还小于1w呢?

    典型应用场景:

    1 发放优惠券

    2 在线用户列表

    并发安全性问题:

    append(list, i) 内部是一个复杂的数组操作函数

    并发读写全局变量,线程不安全

    问题修复

     

     

     

     

     

    方法一: 通过WaitGroup将两个协程分开执行,第一个执行完成再执行第二个,避免并发执行,串行化两个任务。

     

     

     

     

     

    方法二: 通过互斥锁,在数字递增的前后加上锁的处理,数值递增操作时互斥。

     

     

     

     

     

    方法三: 针对int64的数字指针递增操作,可以利用atomic.AddInt64原子递增方法来处理。

    当然还会有更多的实现方法,但是内部的实现原理也都类似,原子操作,避免对共享数据的并发读写。

    并发编程的几个基础概念

    概念1:并发执行不一定是并行执行。

    概念2:单核CPU可以并发执行,不能并行执行。

    概念3:单进程、单线程,也可以并发执行。

    并行是 同一时刻 的多任务处理,并发是一个 时间段 内(1秒、1毫秒)的多任务处理。

    区别并发和并行,多核的并行处理涉及到多核同时读写一个缓存行,所以很容易出现数据的脏读和脏写;单核的并发处理中因为任务内部的中间变量,所以有可能存在脏写的情况。

    锁的作用

    • 避免并行运算中,共享数据读写的安全性问题。
    • 并行执行中,在锁的位置,同时只能有一个程序可以获得锁,其他程序不能获得锁。
    • 锁的出现,使得并行执行的程序在锁的位置串行化执行。
    • 多核、分布式运算、并发执行,才会需要锁。

    不用锁,也可以实现同样效果?

    单线程串行化执行,队列式,CAS。

    ——不要通过共享内存来通信,而应该通过通信来共享内存

    锁的底层实现类型

    锁内存总线 ,针对内存的读写操作,在总线上控制,限制程序的内存访问

    锁缓存行 ,同一个缓存行的内容读写操作,CPU内部的高速缓存保证一致性

    锁 ,作用在一个对象或者变量上。现代CPU会优先在高速缓存查找,如果存在这个对象、变量的缓存行数据,会使用锁缓存行的方式。否则,才使用锁总线的方式。

    速度 ,加锁、解锁的速度,理论上就是高速缓存、内存总线的读写速度,它的效率是非常高的。而出现效率问题,是在产生冲突时的串行化等待时间,再加上线程的上下文切换,让多核的并发能力直线下降。

    2 缓存和一致性协议MESI

    英文首字母缩写,也就是英文环境下的术语、俚语、成语,新人理解和学习有难度,但是,掌握好了既可以省事,又可以缩小文化差距。

    另外就是对英文的异形化,也类似汉字的变形体,“表酱紫”,“蓝瘦香菇”,老外是很难懂得,反之一样。

    MESI“生老病死”缓存行的四种状态

    • M: modify 被修改,数据有效,cache和内存不一致
    • E: exclusive 独享,数据有效,cache与内存一致
    • S: shared 共享,数据有效,cache与内存一致,多核同时存在
    • I: invalid 数据无效
    • F: forward 向前(intel),特殊的共享状态,多个S状态,只有一个F状态,从F高速缓存接受副本

    当内核需要某份数据时,而其它核有这份数据的备份时,本cache既可以从内存中导入数据,也可以从其它cache中导入数据(Forward状态的cache)。

    四种状态的更新路线图

     

     

     

     

     

    高效的状态: E, S

    低效的状态: I, M

    这四种状态,保证CPU内部的缓存数据是一致的,但是,并不能保证是强一致性。

    每个cache的控制器不仅知道自己的读写操作,而且也要监听其它cache的读写操作。

    缓存的意义

     

     

     

     

     

    1 时间局部性:如果某个数据被访问,那么不久还会被访问

    2 空间局部性:如果某个数据被访问,那么相邻的数据也很快可能被访问

    局限性:空间、速度、成本

    更大的缓存容量,需要更大的成本。更快的速度,需要更大的成本。均衡缓存的空间、速度、成本,才能更有市场竞争力,也是现在我们看到的情况。当然,随着技术的升级,成本下降,空间、速度也就能继续稳步提高了。

    缓存行,64Byte的内容

     

     

     

     

     

    缓存行的存储空间是64Byte(字节),也就是可以放64个英文字母,或者8个int64变量。

    注意伪共享的情况——56Byte共享数据不变化,但是8Byte的数据频繁更新,导致56Byte的共享数据也会频繁失效。

    解决方法:缓存行的数据对齐,更新频繁的变量独占一个缓存行,只读的变量共享一个缓存行。

    3 CPU/缓存与锁

    锁的底层实现原理,与CPU、高速缓存有着密切的关系,接下来一起看看CPU的内部结构。

    CPU与计算机结构

     

     

     

     

     

     

     

     

     

    内核独享寄存器、L1/L2,共享L3。在早先时候只有单核CPU,那时只有L1和L2,后来有了多核CPU,为了效率和性能,就增加了共享的L3缓存。

    多颗CPU通过QPI连接。再后来,同一个主板上面也可以支持多颗CPU,多颗CPU也需要有通信和控制,才有了QPI。

    内存读写都要通过内存总线。CPU与内存、磁盘、网络、外设等通信,都需要通过各种系统提供的系统总线。

    CPU流水线

     

     

     

     

     

     

     

     

     

    CPU流水线,里面还有异步的LoadBuffer,

    Store Buffer, Invalidate Queue。这些缓冲队列的出现,更多的异步处理数据,提高了CPU的数据读写性能。

    CPU为了保证性能,默认是宽松的数据一致性。

    编译器、CPU优化 

     

     

     

     

     

     

     

     

     

    • 编译器优化:重排代码顺序,优先读操作(读有更好的性能,因为cache中有共享数据,而写操作,会让共享数据失效)
    • CPU优化:指令执行乱序(多核心协同处理,自动优化和重排指令顺序)

    编译器、CPU屏蔽

    • 优化屏蔽:禁止编译器优化。按照代码逻辑顺序生成二进制代码,volatile关键词
    • 内存屏蔽:禁止CPU优化。防止指令之间的重排序,保证数据的可见性,store barrier, load barrier, full barrier
    • 写屏障:阻塞直到把Store Buffer中的数据刷到Cache中
    • 读屏障:阻塞直到Invalid Queue中的消息执行完毕
    • 全屏障:包括读写屏障,以保证各核的数据一致性

    Go语言中的Lock指令就是一个内存全屏障同时禁止了编译器优化。

     

     

     

     

     

    x86的架构在CPU优化方面做的相对少一些,只是针对“写读”的顺序才可能调序。

    加锁,加了些什么?

    • 禁止编译器做优化(加了优化屏蔽)
    • 禁止CPU对指令重排(加了内存屏蔽)
    • 针对缓存行、内存总线上的控制
    • 冲突时的任务等待队列

    4 常见锁总结

    最后,我们一起来看看常见的自旋锁、互斥锁、条件锁、读写锁的实现逻辑,以及在Go源码中,是如何来实现的CAS/atomic.AddInt64和Mutext.Lock方法的。

    自旋锁

     

     

     

     

     

    只要没有锁上,就不断重试。

    如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用功。

    优点:不切换上下文;

    不足:烧CPU;

    适用场景:冲突不多,等待时间不长的情况下,或者少次数的尝试自旋。

    互斥锁

     

     

     

     

     

    操作系统负责线程调度,为了实现「锁的状态发生改变时再唤醒」就需要把锁也交给操作系统管理。

    所以互斥器的加锁操作通常都需要涉及到上下文切换,操作花销也就会比自旋锁要大。

    优点:简单高效;

    不足:冲突等待时的上下文切换;

    适用场景:绝大部分情况下都可以直接使用互斥锁。

    条件锁

     

     

     

     

     

    它解决的问题不是「互斥」,而是「等待」。

    消息队列的消费者程序,在队列为空的时候休息,数据不为空的时候(条件改变)启动消费任务。

    条件锁的业务针对性更强。

    读写锁

     

     

     

     

     

    内部有两个锁,一个是读的锁,一个是写的锁。

    如果只有一个读者、一个写者,那么等价于直接使用互斥锁。

    不过由于读写锁需要额外记录读者数量,花销要大一点。

    也可以认为读写锁是针对某种特定情景(读多写少)的「优化」。

    但个人还是建议忘掉读写锁,直接用互斥锁。

    适用场景:读多写少,而且读的过程时间较长,可以通过读写锁,减少读冲突时的等待。

    无锁操作CAS

    Compare And Swap 比较并交换,类似于将 num+ + 的三个指令合并成一个指令 CMPXCHG,保证了操作的原子性。

    为了保证顺序一致性和数据强一致性,还需要有一个LOCK指令。 

    源码,参见 runtime/internal/atomic/asm_amd64.s

     

     

     

     

     

    LOCK指令的作用就是禁止编译器优化,同时加上内存全屏障,可以保证 LOCK指令 之后的一个指令执行时的数据强一致性和可见性。

    数字的原子递增操作 atomic.AddInt64

    在原始指针数字的基础上,原子性递增 delta 数值,并且返回递增后的结果值。

    源码1,参见sync/atomic/asm.s

     

     

     

     

     

    XADDQ 数据交换,数值相加,写入目标数据

    ADDQ 数值相加,写入目标数据

    在XADDQ之前加上LOCK指令,保证这个指令执行时的数据强一致性和可见性。

    源码2,参见runtime/internal/atomic/asm_amd64.s

     

     

     

     

     

    互斥锁操作 sync.Mutex.Lock

     

     

     

     

     

     

     

     

     

    源码,参见 sync/mutex.go

    大概的源码处理逻辑如下:

    1 通过CAS操作来竞争锁的状态 &m.state;

    2 没有竞争到锁,先主动自旋尝试获取锁runtime_canSpin 和 runtime_doSpin (原地烧CPU);

    3 自旋尝试失败,再次CAS尝试获取锁;

    4 runtime_SemacquireMutex 锁请求失败,进入休眠状态,等待信号唤醒后重新开始循环;

    5 m.state等待队列长度(复用的int32位数字,第一位是锁的状态,后31位是锁的等待队列长度计数器)。

    以上便是这次分享的全部内容,有不足和纰漏的地方,还请指教,谢谢 ~

     

    开发十年,留下最完整的Java架构学习路线,学完年薪65W

    展开全文
  • 虽然我们可以用到CAS,互斥锁,消息队列,甚至分布式锁来解决,但是对于锁的底层实现,这次课程,我们想更深入的来分析和探讨锁的底层原理,以便更好地理解和掌握并发编程。大纲:1.并发编程与锁2.缓存和一致性协议...

    背景

    并发编程,多核、多线程的情况下,线程安全性问题都是一个无法回避的难题。虽然我们可以用到CAS,互斥锁,消息队列,甚至分布式锁来解决,但是对于锁的底层实现,这次课程,我们想更深入的来分析和探讨锁的底层原理,以便更好地理解和掌握并发编程。

    大纲:

    1.并发编程与锁

    2.缓存和一致性协议MESI

    3.CPU/缓存与锁

    4.常见锁总结

    1 并发编程与锁

    我们写的各种应用系统,像网络编程,基本上都是并发编程,不论是多进程还是多线程,亦或是协程、队列的方式,也都是并发编程的范畴。并发编程中,在多核操作系统中,多线程的时候,就会出现**线程安全性**问题,有的也说**并发安全性**问题。这种问题,都是因为对共享变量的并发读写引起的数据不一致问题。所以,在并发编程中,就会经常用到锁,当然也可能使用队列或者单线程的方式来处理共享数据。

    我们先来还原一下具体的问题,然后再用不同的方法来处理它们。

    线程安全性问题1

    9869fa57494365e0989c0fa8850b66ca.png

    推荐c/c++编程学习交流群,741818652,进去可以领取学习资料

    代码中共享变量num是一个简单的计数器,main主线程启动了两个协程,分别循环一万次对num进行递增操作。正常情况下,预期的结果应该是1w+1w=2w,但是,在并发执行的情况下,最终的结果只有10891,离2w差的好多。

    典型应用场景:

    1 库存数量扣减

    2 投票数量递增

    并发安全性问题:

    num+ +是三个操作(读、改、写),不满足原子性

    并发读写全局变量,线程不安全

    线程安全性问题2

    3054e397b284c5d3f6ef8d8a512bc1d3.png

    代码中共享变量list作为一个数据集合,由两个协程并发的循环append数据进去。同样是每个协程执行一万次,正常情况下,预期的list长度应该是2w,但是,在并发执行下,结果却可能连1w都不到。

    具体的原因,大家可以思考下,为什么并发执行的情况下,2个协程,竟然list长度还小于1w呢?

    典型应用场景:

    1 发放优惠券

    2 在线用户列表

    并发安全性问题:

    append(list, i) 内部是一个复杂的数组操作函数

    并发读写全局变量,线程不安全

    问题修复

    8c7fbcdeca77efb6d0cb6f96753a6610.png

    通过WaitGroup将两个协程分开执行,第一个执行完成再执行第二个,避免并发执行,串行化两个任务。

    7ca028d6f41c90d2293a4569af7fb7be.png

    通过互斥锁,在数字递增的前后加上锁的处理,数值递增操作时互斥。

    99fcbbba8d52f88ecbba512894b22941.png

    推荐c/c++编程学习交流群,741818652,进去可以领取学习资料

    针对int64的数字指针递增操作,可以利用atomic.AddInt64原子递增方法来处理。

    当然还会有更多的实现方法,但是内部的实现原理也都类似,原子操作,避免对共享数据的并发读写。

    并发编程的几个基础概念

    概念1:并发执行不一定是并行执行。

    概念2:单核CPU可以并发执行,不能并行执行。

    概念3:单进程、单线程,也可以并发执行。

    并行是同一时刻的多任务处理,并发是一个时间段内(1秒、1毫秒)的多任务处理。

    区别并发和并行,多核的并行处理涉及到多核同时读写一个缓存行,所以很容易出现数据的脏读和脏写;单核的并发处理中因为任务内部的中间变量,所以有可能存在脏写的情况。

    锁的作用

    避免并行运算中,共享数据读写的安全性问题。

    并行执行中,在锁的位置,同时只能有一个程序可以获得锁,其他程序不能获得锁。

    锁的出现,使得并行执行的程序在锁的位置串行化执行。

    多核、分布式运算、并发执行,才会需要锁。

    不用锁,也可以实现同样效果?

    单线程串行化执行,队列式,CAS。

    ——不要通过共享内存来通信,而应该通过通信来共享内存

    锁的底层实现类型

    1 锁内存总线,针对内存的读写操作,在总线上控制,限制程序的内存访问

    2 锁缓存行,同一个缓存行的内容读写操作,CPU内部的高速缓存保证一致性

    ,作用在一个对象或者变量上。现代CPU会优先在高速缓存查找,如果存在这个对象、变量的缓存行数据,会使用锁缓存行的方式。否则,才使用锁总线的方式。

    速度,加锁、解锁的速度,理论上就是高速缓存、内存总线的读写速度,它的效率是非常高的。而出现效率问题,是在产生冲突时的串行化等待时间,再加上线程的上下文切换,让多核的并发能力直线下降。

    2 缓存和一致性协议MESI

    英文首字母缩写,也就是英文环境下的术语、俚语、成语,新人理解和学习有难度,但是,掌握好了既可以省事,又可以缩小文化差距。

    另外就是对英文的异形化,也类似汉字的变形体,“表酱紫”,“蓝瘦香菇”,老外是很难懂得,反之一样。

    MESI“生老病死”缓存行的四种状态

    M: modify 被修改,数据有效,cache和内存不一致

    E: exclusive 独享,数据有效,cache与内存一致

    S: shared 共享,数据有效,cache与内存一致,多核同时存在

    I: invalid 数据无效

    F: forward 向前(intel),特殊的共享状态,多个S状态,只有一个F状态,从F高速缓存接受副本

    当内核需要某份数据时,而其它核有这份数据的备份时,本cache既可以从内存中导入数据,也可以从其它cache中导入数据(Forward状态的cache)。

    四种状态的更新路线图

    01e38ecec9c60348622809848eea5812.png

    高效的状态: E, S

    低效的状态: I, M

    这四种状态,保证CPU内部的缓存数据是一致的,但是,并不能保证是强一致性。

    每个cache的控制器不仅知道自己的读写操作,而且也要监听其它cache的读写操作。

    缓存的意义

    27eeec363512c494b6368e3c16eaa273.png

    1 时间局部性:如果某个数据被访问,那么不久还会被访问

    2 空间局部性:如果某个数据被访问,那么相邻的数据也很快可能被访问

    局限性:空间、速度、成本

    更大的缓存容量,需要更大的成本。更快的速度,需要更大的成本。均衡缓存的空间、速度、成本,才能更有市场竞争力,也是现在我们看到的情况。当然,随着技术的升级,成本下降,空间、速度也就能继续稳步提高了。

    缓存行,64Byte的内容

    3bf6a1855151fb8b97f0e834af32826b.png

    缓存行的存储空间是64Byte(字节),也就是可以放64个英文字母,或者8个int64变量。

    注意伪共享的情况——56Byte共享数据不变化,但是8Byte的数据频繁更新,导致56Byte的共享数据也会频繁失效。

    解决方法:缓存行的数据对齐,更新频繁的变量独占一个缓存行,只读的变量共享一个缓存行。

    3 CPU/缓存与锁

    锁的底层实现原理,与CPU、高速缓存有着密切的关系,接下来一起看看CPU的内部结构。

    CPU与计算机结构

    b72fa790750cb1213c1dcb333b1d4afc.png
    c33d412bd3253f8b2babbc356d8ccbd1.png

    内核独享寄存器、L1/L2,共享L3。在早先时候只有单核CPU,那时只有L1和L2,后来有了多核CPU,为了效率和性能,就增加了共享的L3缓存。

    多颗CPU通过QPI连接。再后来,同一个主板上面也可以支持多颗CPU,多颗CPU也需要有通信和控制,才有了QPI。

    内存读写都要通过内存总线。CPU与内存、磁盘、网络、外设等通信,都需要通过各种系统提供的系统总线。

    CPU流水线

    e3707759d2edd02f22a5e72f171e4a4c.png
    2f44969045410a69d20ac0c0d0125233.png

    推荐c/c++编程学习交流群,741818652,进去可以领取学习资料

    CPU流水线,里面还有异步的LoadBuffer,

    Store Buffer, Invalidate Queue。这些缓冲队列的出现,更多的异步处理数据,提高了CPU的数据读写性能。

    CPU为了保证性能,默认是宽松的数据一致性。

    编译器、CPU优化

    d4bcfb56dcedbd78029e1b2990ade187.png
    83782272aeb869001a2cde189f0cc281.png

    编译器优化:重排代码顺序,优先读操作(读有更好的性能,因为cache中有共享数据,而写操作,会让共享数据失效)

    CPU优化:指令执行乱序(多核心协同处理,自动优化和重排指令顺序)

    编译器、CPU屏蔽

    优化屏蔽:禁止编译器优化。按照代码逻辑顺序生成二进制代码,volatile关键词

    内存屏蔽:禁止CPU优化。防止指令之间的重排序,保证数据的可见性,store barrier, load barrier, full barrier

    写屏障:阻塞直到把Store Buffer中的数据刷到Cache中

    读屏障:阻塞直到Invalid Queue中的消息执行完毕

    全屏蔽:包括读写屏障,以保证各核的数据一致性

    Go语言中的Lock指令就是一个内存全屏蔽同时禁止了编译器优化。

    58f31b3b6471c79ee4a7d3ad3c9dd2c8.png

    x86的架构在CPU优化方面做的相对少一些,只是针对“写读”的顺序才可能调序。

    加锁,加了些什么?

    禁止编译器做优化(加了优化屏蔽)

    禁止CPU对指令重排(加了内存屏蔽)

    针对缓存行、内存总线上的控制

    冲突时的任务等待队列

    4 常见锁总结

    最后,我们一起来看看常见的自旋锁、互斥锁、条件锁、读写锁的实现逻辑,以及在Go源码中,是如何来实现的CAS/atomic.AddInt64和Mutext.Lock方法的。

    自旋锁

    f7b0fd9ecd555550bd8884e04c381058.png

    只要没有锁上,就不断重试。

    如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用功。

    优点:不切换上下文;

    不足:烧CPU;

    适用场景:冲突不多,等待时间不长的情况下,或者少次数的尝试自旋。

    互斥锁

    c6d24c2890eb40e9036b03ec9c5ff2c1.png

    操作系统负责线程调度,为了实现「锁的状态发生改变时再唤醒」就需要把锁也交给操作系统管理。

    所以互斥器的加锁操作通常都需要涉及到上下文切换,操作花销也就会比自旋锁要大。

    优点:简单高效;

    不足:冲突等待时的上下文切换;

    适用场景:绝大部分情况下都可以直接使用互斥锁。

    条件锁

    52e61d915b7514afcf34a18e1cc000f4.png

    它解决的问题不是「互斥」,而是「等待」。

    消息队列的消费者程序,在队列为空的时候休息,数据不为空的时候(条件改变)启动消费任务。

    条件锁的业务针对性更强。

    读写锁

    02475e62f6e7e52f5b53e7c3928a26aa.png

    内部有两个锁,一个是读的锁,一个是写的锁。

    如果只有一个读者、一个写者,那么等价于直接使用互斥锁。

    不过由于读写锁需要额外记录读者数量,花销要大一点。

    也可以认为读写锁是针对某种特定情景(读多写少)的「优化」。

    但个人还是建议忘掉读写锁,直接用互斥器。

    试用场景:读多写少,而且读的过程时间较长,可以通过读写锁,减少读冲突时的等待。

    无锁操作CAS

    Compare And Swap 比较并交换,类似于将 num+ + 的三个指令合并成一个指令 CMPXCHG,保证了操作的原子性。

    为了保证顺序一致性和数据强一致性,还需要有一个LOCK指令。

    源码,参见 runtime/internal/atomic/asm_amd64.s

    3c308b8f4aa0035feab90a8e118152a6.png

    LOCK指令的作用就是禁止编译器优化,同时加上内存全屏障,可以保证LOCK指令之后的一个指令执行时的数据强一致性和可见性。

    数字的原子递增操作 atomic.AddInt64

    在原始指针数字的基础上,原子性递增 delta 数值,并且返回递增后的结果值。

    源码1,参见 sync/atomic/asm.s

    59bd5b5da596776e705e8cf9860f0c43.png

    XADDQ 数据交换,数值相加,写入目标数据

    ADDQ 数值相加,写入目标数据

    在XADDQ之前加上LOCK指令,保证这个指令执行时的数据强一致性和可见性。

    源码2,参见 runtime/internal/atomic/asm_amd64.s

    5983ad6873bff9145333c81c88579a64.png

    互斥锁操作 sync.Mutex.Lock

    9b7a3b70c474e842cf53ba8090163a2f.png
    eccc7afd2f6339e6ccd9b0224be0cc7c.png

    推荐c/c++编程学习交流群,741818652,进去可以领取学习资料

    源码,参见 sync/mutex.go

    大概的源码处理逻辑如下:

    1 通过CAS操作来竞争锁的状态 &m.state;

    2 没有竞争到锁,先主动自旋尝试获取锁 runtime_canSpin 和 runtime_doSpin (原地烧CPU);

    3 自旋尝试失败,再次CAS尝试获取锁;

    4 runtime_SemacquireMutex 锁请求失败,进入休眠状态,等待信号唤醒后重新开始循环;

    5 m.state等待队列长度(复用的int32位数字,第一位是锁的状态,后31位是锁的等待队列长度计数器);

    以上便是这次分享的全部内容,有不足和纰漏的地方,还请指教,谢谢~想要一起学习的同学可以进我推荐的交流Q群哦!

    展开全文
  • 虽然我们可以用到CAS,互斥锁,消息队列,甚至分布式锁来解决,但是对于锁的底层实现,这次课程,我们想更深入的来分析和探讨锁的底层原理,以便更好地理解和掌握并发编程。大纲:1.并发编程与锁2.缓存...
  • 系列文章iOS汇编入门教程(一)ARM64汇编基础iOS汇编入门教程(二)在Xcode工程中嵌入汇编代码iOS汇编入门教程(三)汇编中 Section 与数据存取iOS汇编教程(四)基于 LLDB 动态调试...我们常常使用互斥锁来保证全局变量...
  • 文章目录互斥互斥锁的原理 ...1.互斥锁的底层是一个互斥量,而互斥量的本质是一个计数器,计数器的取值只有两种,一种是1,一种是0。1表示当前临界资源可以被访问,0表示当前临界资源不可以被访问。 ...
  • 什么是 synchronized 及互斥锁怎么理解 synchronized 的基本使用方式有哪几种 使用 synchronized 加锁时,锁的到底是什么 synchronized 死锁是怎么造成的,该如何避免 synchronized 锁被忽视的点:可见性和有序性 ...
  • 一、Redisson实现Redis分布式锁的底层原理 (1)加锁机制 (2)锁互斥机制 (3)watch dog自动延期机制 (4)可重入加锁机制 (5)锁释放机制 (6)此种方案Redis分布式锁的缺陷 (1)加锁机制 咱们来看上面...
  • 互斥的实现目录:一.概念二.原理三....对于互斥的实现其实底层就是对于互斥锁的使用 三.接口 1.定义互斥锁变量 2.初始化互斥锁变量 3.在访问临界资源的之前加锁 4.在访问临界资源之后解锁 5.销毁互.
  • 原子操作适用于简单单个操作,无锁算法适用于相对简单一连串操作,而线程适用于复杂一连串操作 原子操作 修改状态要么成功且状态改变,要么失败且状态不变,并且外部只能观察到修改前或者修改后状态,...
  • 要搞清楚Synchronize的底层原理,先了解一下对象头 每个对象都会有一个对象头 如果这个对象是是数组类型,那么jvm会用3个字宽(32位虚拟机中,1个字宽4个字节)存储对象头,如果是非数组类型,jvm会用2个字宽存储对象...
  • 在多线程环境中,多个线程可能会同时访问同一个资源,为了避免访问发生冲突,可以根据访问复杂程度采取不同措施原子操作适用于简单单个操作,无锁算法适用于...
  • 在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统 Mutex Lock 来实现,Java 线程是映射到操作系统原生线程之上的互斥锁来实现。如果要挂起或者...
  • C++11中定义的互斥量,实现互斥锁的功能,即同一时刻只能有一个线程获取该锁。底层的实现原理是包装了pthread_mutex_t结构体,并调用pthread_mutex_lock和pthread_mutex_unlock完成加锁和解锁的功能。其内存布局: t
  • 自旋锁是专为防止多处理器并发而引入一种锁,它在内核...其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时...
  • * 看操作系统原理说软件层面也可以实现多线程互斥,但是看java一些,以及 cas,synchronized 等用户都是底层硬件层面相关指令 * 来实现,所以今天特地研究一下可否只用变量实现多线程互斥效果,但是效率很低,...
  • synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。加锁的方式: 同步实例方法,锁是当前实例对象 同步类方法,锁是当前类对象 同步代码块...
  • 底层实现原理主要是利用通过继承AQS来实现,也是利用通过对volatile stateCAS操作+CLH队列来实现; 支持公平和非公平。 CAS:Compare and Swap 比较并交换。CAS思想很简单:3个参数,一个当前内存值V、...
  • synchronized的底层原理

    2020-08-29 23:13:23
    synchronized是由一对monitorenter和monitorexit指令来实现同步,在JDK6之前,monitor实现是依靠操作系统内部的互斥锁来实现,所以需要进行用户态和内核态切换,所以此时同步操作是一个重量级操作,性能...
  • 常见案例有java atomic原子类,底层原理的cas机制(比较并交换)就是在更新时传入预期值、目标值,仅当其值等于预期值时才会更新;数据库write_condition机制大同小异,小异指是数据库会给记录添加version(版本)...
  • (1) synchronized的底层实现 synchronized是用来保证线程同步,用的锁存在java对象头中,利用monitorenter和monitorexit指令实现,...而底层,synchrnoized是利用操作系统的Mutex Lock(互斥锁)来实现的,
  • synchronized底层原理

    2021-03-11 15:15:45
    监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁...
  • 智能指针shared_ptr本身(底层实现原理是引用计数)是线程安全 智能指针引用计数在手段上使用了atomic原子操作,只要shared_ptr在拷贝或赋值时增加引用,析构时减少引用就可以了。首先原子是线程安全,所有...
  • 自旋锁与互斥锁比较类似,它们都是为了解决对某项资源互斥使用。 无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。 调度机制上略有不同: 互斥锁:...
  • 在计算机科学中,(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程环境中强制对资源访问限制。旨在强制实施互斥排他、并发控制策略。 通常需要硬件支持才能有效实施。这种支持通常采取一个或多...
  • Redis分布式锁的底层原理 ​ Redisson这个框架对Redis分布式锁的实现原理图如下: 加锁机制 ​ 某个客户端要加锁。如果该客户端面对的是一个Redis Cluster集群,它首先会根据hash节点选择一台机器,这里注意,...
  • 图解Gochannel底层原理

    万次阅读 2019-04-14 22:26:34
    废话不多说,直奔主题。 channel整体结构图 简单说明: buf是有缓冲channel所特有...lock是个互斥锁。 recvq和sendq分别是接收(<-channel)或者发送(channel <- xxx)goroutine抽象出来结构体(sudog)...

空空如也

空空如也

1 2 3 4 5 ... 12
收藏数 224
精华内容 89
关键字:

互斥锁的底层原理