并发编程2

多线程锁的os内核理解

os同步的方式(操作系统级别的锁)

  • 1.互斥量(mutex)

    • pthread_Mutex_t(互斥锁)
    • 发生竞争的时候如果拿不到锁则睡眠
  • 2.自旋锁(spinlock)

    • pthread_spin_t(自旋锁)—拿不到锁不会睡眠,而是一直空转,由操作系统控制

    • 大部分面试时问到的自旋锁不是指spinlock,而是jvm内部对于线程的控制,维度不同

    • while(!cas(xx,xx,xx)){
               
      // 一直自旋
      }
      return;
      
  • 3.信号量

sysnchronized

synchronized是不是自旋锁?

  • 1.编译成汇编后,解析成interpreterRuntime.cpp中的monitorenter

    • // 首先判断有没有使用偏向锁
      if (UseBiasedLocking) {
          // Retry fast entry if bias is revoked to avoid unnecessary inflation
          ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
      }
      // 会进入轻量锁,对应的方法是slow_enter
      else {
          ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
      }
      
  • 2.synchronizer.cpp中对应的slow_enter

    • 底层使用的不是自旋锁

    • jvm内部获取锁的时候也没有自旋

    • void ObjectSynchronizer::slow_entermoniterenter(Handle obj, BasicLock* lock, TRAPS) {
        markOop mark = obj->mark();
        assert(!mark->has_bias_pattern(), "should not see bias pattern here");
      
        if (mark->is_neutral()) {
          // Anticipate successful CAS -- the ST of the displaced mark must
          // be visible <= the ST performed by the CAS.
          lock->set_displaced_header(mark);
          if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
            TEVENT (slow_enter: release stacklock) ;
            return ;
          }
          // Fall through to inflate() ...
        } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) 	{
          assert(lock != mark->locker(), "must not re-lock the same lock");
          assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
          lock->set_displaced_header(NULL);
          return;
        }
      
      #if 0
        // The following optimization isn't particularly useful.
        if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
          lock->set_displaced_header (NULL) ;
          return ;
        }
      #endif
      
        // The object header will never be displaced to this lock,
        // so it does not matter what the value is, except that it
        // must be non-zero to avoid looking like a re-entrant lock,
        // and must not look locked either.
        lock->set_displaced_header(markOopDesc::unused_mark());
        ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
      }
      
    • inflate是膨胀的意思,inflate方法会膨胀成重量锁

  • 3.锁膨胀—inflate也在synchronizer.cpp中

    • 虽然有一个死循环,但是不是为了拿锁自旋,而是为了等其他线程把锁膨胀完,因为同一时间只让一个线程来膨胀锁

    • // CASE: inflation in progress - inflating over a stack-lock.
            // Some other thread is converting from stack-locked to inflated.
            // 下面一句英文的意思,只有一个可以完成锁膨胀
            // Only that thread can complete inflation -- other threads must wait.
            // The INFLATING value is transient.
            // Currently, we spin/yield/park and poll the markword, waiting for inflation to finish.
            // We could always eliminate polling by parking the thread on some auxiliary list.
            if (mark == markOopDesc::INFLATING()) {
               TEVENT (Inflate: spin while INFLATING) ;
               ReadStableMark(object) ;
               continue ;
            }
      
  • 因此synchronized不是自旋锁

    • 1.底层使用的不是自旋锁,是mutex

    • 2.jvm内部获取锁的时候也没有自旋

mutex—操作系统级别同步原语

  • man pthread_mutex_init,查看mutex的手册

  • 作用:初始化和销毁一个互斥量(一把锁)

  • 需要引入pthread.h头文件,才能使用pthread_mutex_init

  • #include 
    #include 
    #include 
    
    // 
    int sharei = 0;
    void increase_num(void);
    // add mutex
    // PTHREAD_MUTEX_INITIALIZER是c语言封装的宏,会去调用pthread_mutex_init
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
     
    int main()
    {
      int ret;
      // 创建3个线程
      pthread_t thread1,thread2,thread3;
      ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
      ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
      ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);
      // 防止主线程直接结束
      pthread_join(thread1,NULL);
      pthread_join(thread2,NULL);
      pthread_join(thread3,NULL);
     
      printf("sharei = %d\n",sharei);
     
      return 0;
    }
    //run
     
    void increase_num(void)
    {
      long i,tmp;
      for(i =0;i<=9999;++i)
      {
    	 
        //上锁
        // 相当于synchronzed(){
        pthread_mutex_lock(&mutex);
    	//有同学问为什么不加锁会小于30000
    	//比如当t1 执行到tmp=sharei的时候假设 sharei这个时候=0,那么tmp也等于0
    	//然后tmp=tmp+1;结果tmp=1(t1);这个时候如果t2进入了
    	//t2获取的sharei=0;然后重复t1的动作 tmp=1(t2)
    	//t2 sharei = tmp;  sharei = 1;
    	//然后CPU切回t1 执行 sharei = tmp;  sharei = 1;
    	//结果两个线程执行了两遍但是结果还是sharei = 1;(本来要等于2的)
        tmp=sharei;
        tmp=tmp+1;
        
        sharei = tmp;
    	//解锁
        // 相当于synchronized后面的}
        pthread_mutex_unlock(&mutex);
        
      }
    }
    
    
  • user o = new user();{
           
    
      i = 0;对象头里面的i值不同,锁的状态不同
    
    }
    
    synchronized(o)
    
  • java中的sync关键字如果是(10),它底层同步或者锁机制就是mutex,而mutex是重量锁

mutex锁机制分析

  • 1.mutex实现的锁是互斥锁,java当中对于synchronized关键字如果是重量锁(数字10–对象头),它底层同步或者锁机制用的是mutex;如果不是10,与对象头相关,所以也可能是轻量锁。

    • 为什么是重量锁?

      (1).因为mutex拿不到锁就会sleep,进入内核态,发生了一次系统调用

      (2).因为sleep方法是调用的linux的api,会去内核里面寻找,因为sleep会映射到内核态,系统保护机制,cpu会升级到core0,保存当前执行到哪行代码,执行完后再返回到用户态

    • java定义的线程是用户态还是内核态?

      (1).java中的thread通过jvm映射到内核上的pthread

      (2).所以jvm定义多少个线程,内核就会创建相应数量的线程,从而会有大量的线程切换(上下文切换),很耗性能

      (3).所以协程比普通的多线程在线程切换上要更优

      (4).所以线程是跟cpu相关的,当前时刻既有可能是用户态,也有可能是内核态,是未知的。

    • 为什么分用户态和内核态?

      因为有些指令非常危险,操作系统不让用户直接调用,当这个指令没有时,会通过虚拟内存映射进入到内核态来执行。

  • 2.什么时候变成重量级锁,是因为锁膨胀,是可逆的,当一个线程持有锁,当另一个也来获取锁时,此时锁会膨胀,变为重量级锁。

  • 3.线上很多代码,很多时候没有并发,所有直接用synchronized是性能很差的,此时只是偏向锁,也就是说高并发情况下才会变成重量级锁,所以高并发情况下,尽量避免用锁,通过mq、copyonwrite、解决,对于超卖这种特殊情况则需要使用synchronized,

  • 4.mutex为什么是重量锁?mutex互斥特点是拿不到锁就会sleep,调用sleep()会进入内核态,sleep是一种内核操作,因为发生了一次系统调用,jdk1.6之前是重量级锁,所以睡眠线程进入了内核态,当线程唤醒,它被唤醒,又从内核态切换为用户态。ReentrantLock中没有调用sleep,只用了cas,而cas不是内核操作,所以ReentrantLock是轻量级锁。

内核态

什么是内核态?线程的本质 ->jvm ->内核线程(一对一模型)

  • 优点:jvm不需要做什么操作,通过各种native方法区实现了线程

  • 缺点 由于线程的底层需要内核线程来实现,所以java多线程切换时,需要频繁切换内核态和用户态,造成资源浪费

  • 内核态?用户态?跟线程无关,跟cpu有关,需要看当前cpu是什么状态,执行自己的代码是用户态,执行系统代码,需要进行系统的内核升级,此时就变成了内核态,

    • mmu mmap,虚拟地址映射,每一个核都有一个单独的mmu,电脑中的内存地址是虚拟地址,只是先分配一个虚拟地址,真实调用时才会去映射到真实的地址,这样cpu只有4g,所以也可以运行大于这个容量的程序。

      (1)用户程序 -> d磁盘 -> 内存 -> cache(cpu级别的缓存) ->寄存器(最小缓存单位)

    • 假设有4g,进程共享操作系统的内核,但是有自己的独立代码

      (1)如果有两个进程,每个进程最多可以分配4g内存,则总共需要分8g内存,这样内存就不够了?

      (2)实际分配给它的是虚拟地址6,7,8,9,而6,7,8,9会映射代真实的地址上去,而这个过程就是通过mmu完成的(虚拟地址映射)

    • 上下文切换

      1.进程之间的上下文切换(跨进程)

      2.进程内部的用户和内核切换

      3.线程和线程之间的切换(进程内部跨线程)— 代价最小,java层面main方法和t1、t2的切换

      4.线程内部与外部另一个进程的线程的切换

自旋锁

代码

  • #include 
    #include 
    #include 
     
    int sharei = 0;
    void increase_num(void);
    // 定义一把自旋锁
    pthread_spinlock_t a_lock;
     
    int main()
    {
      //初始化自旋锁
      pthread_spin_init(&a_lock, 0);
      int ret;
      // 启动3个线程
      pthread_t thread1,thread2,thread3;
      ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
      ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
      ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);
     
      pthread_join(thread1,NULL);
      pthread_join(thread2,NULL);
      pthread_join(thread3,NULL);
     
      printf("sharei = %d\n",sharei);
     
      return 0;
    }
     
    void increase_num(void)
    {
      long i,tmp;
      for(i =0;i<=9999;++i)
      {
        // lock spin 不停自旋获取锁
        pthread_spin_lock(&a_lock);
        tmp=sharei;
        tmp=tmp+1;
        
        sharei = tmp;
        pthread_spin_unlock(&a_lock);
        
      }
    }
    
    

解释一下自旋锁

  • 操作系统,os自旋
  • 平台级别,reentrantLock.lock()(有个死循环一直tryAcquire()),实现了java层面的自旋,但不是操作系统级别的spin自旋锁,但是spin性能不高,一直是操作系统的空转
  • park就是sleep,
  • reentrantLock第一次拿锁失败,不会立即睡眠,不想立刻进入队列,不想睡眠,不想进入内核态,假设另一个线程释放了锁,所以再去拿锁,就可以拿到
  • java层面没有直接调用spinLock的代码,因为是操作系统的空转,性能低,它的原理是去模仿spin的空转

简单理解synchronized的锁升级过程

  • 首先是偏向锁,第一个线程来了变成偏向锁
  • 第二个线程来了,第一个线程没有释放锁,升级成重量锁

书籍

  • 哈工大—计算机组成原理

你可能感兴趣的:(并发编程,多线程)