多线程【进阶版】(上)

简介: 多线程【进阶版】

一. 常见的锁策略


1.1 乐观锁和悲观锁

说到乐观和悲观这两个概念,大家都不陌生,生活中我们也要常常面对一些乐观和悲观的时候,但是这是站在自身的角度去看待的,有的人看待一件事他认为是乐观的,而有的人认为他是悲观的;这里的 "乐观" 和 "悲观" 和我们说的乐观锁和悲观锁也是很相似的;


乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据;


举个例子:假设有A 和 B 两个线程,他们要去获取数据,因为他们是乐观的,所以双方不会认为他们去修改数据,所以他们就拿到数据后执行各自的事情去了,还有一个特点就是线程A和B在更新共享数据之前,他们要去判断这个共享数据是否被其他线程修改,如果没有修改的话,那么就直接更新内存中共享变量的值,那如果被修改了,就会报错或者去执行其他相关的操作了


悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁;


举个例子:还是有A 和 B两个线程,A 和 B要去拿数据,因为它是悲观的,所以在拿数据时需要进行加锁,假设A拿到了锁,那么B就会进入阻塞等待的状态,知道A释放锁,CPU会唤醒等待的线程B,B才能拿到这个锁,从而对数据进行操作;


总体来说,悲观锁一般要做的工作多一点,效率会更低一些;而乐观锁要做的事少一点,效率更高一点;


1.2 轻量级锁和重量级锁

轻量级锁:加锁和解锁的过程中更快更高效;


重量级锁:加锁和解锁的过程中更慢更低效;


这里看来,轻量级,重量级虽然和乐观,悲观不是一回事,但是有那么一定的相似,可以认为一个乐观锁可能是一个轻量级锁,但不是绝对的;关于这块后面还是会细说;


1.3 自旋锁和挂起等待锁

自旋锁是轻量级锁的一种代表实现,当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock);


优点:自旋锁一旦被释放,就能第一时间拿到锁,自旋锁是纯用户态操作,所以速度很快;


缺点:要一直等待,会消耗CPU资源


49528eaa9bc547d4aff2ff8a118dc527.png


挂起等待锁是重量级锁的一直代表实现,当某个线程没有申请到锁的时候,此时该线程会被挂起,即加入到等待队列等待。当锁被释放的时候,就会被唤醒,重新竞争锁;


优点:不需要盲等,在等待的过程中可以参与别的事情,充分利用了CPU的资源;


缺点:如果锁被释放,不能第一时间拿到锁,挂起等待锁是通过内核的机制来实现,所以时间会更长,效率会更低;


1.4 互斥锁和读写锁

互斥锁:互斥锁是一个非常霸道的存在,比如有线程A,B,当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞,


我们学过的Synchronized就是互斥锁;


对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。如下图:


b16f1747b3d647aba4d5bc4a2bbf2c7c.png

注:互斥锁和自旋锁最大的区别:


互斥锁加锁失败后,线程会释放 CPU ,给其他线程;

自旋锁加锁失败后,线程会忙等待,直到它拿到锁;

读写锁:它由 读锁 和 写锁 两部分构成,如果只读取共享资源用 读锁 加锁,如果要修改共享资源则用 写锁 加锁。


读写锁一般有 3 种情况:


1.给读加锁

2.给写加锁

3.解锁


读写锁中的约定:

  • 读锁和读锁之间,不会有锁竞争,不会产生阻塞等待
  • 写锁和写锁之间,有锁竞争
  • 读锁和写锁之间,有锁竞争


1.5 可重入锁和不可重入锁

针对一个线程,针对一把锁,连续加锁两次,如果出现死锁了,那就是不可重入锁,如果不死锁,那就是可重入锁;

Object locker = new Object();
synchronized(locker){
     synchronized(locker){
   }
}

像上述这样的代码就是加锁两次的情况,第二次加锁需要等待第一个锁释放,第一个锁释放,需要等待第二个锁加锁成功,所以这种情况就矛盾了,但是并不会真正的死锁,因为synchronized是可重入锁,加锁的时候会先判定一下,看当前尝试申请锁的线程是不是已经拥有锁了,如果是的话,就不会矛盾;


synchronized 和   ReentrantLock 都是可重入锁,可重入锁最大的意思就是为了防止死锁;


1.6 公平锁和非公平锁

首先从字面意思理解,先到的线程会优先获取资源,后到的会进行排队等待,这种是公平的,而非公平锁是不遵循这个原则的,其实也很好理解,看下图:


055efc6b6e634700894effca298b5808.png


这种情况就是公平的,遵循先到先得的规矩;


de5d3d4518c948f0a6abbfd2fc8f1e8a.png


而像这种情况,就是非公平的,存在 "插队" 的现象;


1.7 关于锁策略的相关面试题

1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?


悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁;


乐观锁认为多个线程访问同一个共享变量冲突的概率不大,并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突;


悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待;


2. 介绍下读写锁?


读写锁就是把读操作和写操作分别进行加锁;


读锁和读锁之间不互斥;


写锁和写锁之间互斥;


写锁和读锁之间互斥;


读写锁最主要用在 "频繁读,不频繁写" 的场景中;


3. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?


如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.


优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.

缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源



4. synchronized 是可重入锁么?

是可重入锁.

可重入锁指的就是连续两次加锁不会导致死锁


5. synchronized的特点:


  1. 既是乐观锁,也是悲观锁
  2. 既是轻量级锁,也是重量级锁
  3. 轻量级锁是基于自选锁实现的,重量级锁是基于挂起等待锁实现的
  4. 不是读写锁
  5. 是可重入锁
  6. 是非公平锁
目录
相关文章
|
8月前
|
Linux API C++
|
8月前
|
关系型数据库 MySQL 编译器
C++进阶 多线程相关(下)
C++进阶 多线程相关(下)
41 0
|
8月前
|
安全 Java 调度
多线程【进阶版】(下)
多线程【进阶版】
43 0
|
8月前
|
安全
多线程【进阶版】(中)
多线程【进阶版】
35 0
|
1月前
|
安全 调度
多线程入门
多线程入门
119 1
|
1月前
|
安全 算法 Java
多线程知识点总结
多线程知识点总结
44 3
|
8月前
|
算法 Ubuntu C++
[总结] C++ 知识点 《四》多线程相关
[总结] C++ 知识点 《四》多线程相关
|
8月前
|
算法 编译器 程序员
[笔记]C++并发编程实战 《一》你好,C++的并发世界(二)
[笔记]C++并发编程实战 《一》你好,C++的并发世界(二)
|
10月前
|
存储 安全 算法
3.多线程(进阶)(三)
3.多线程(进阶)(三)
36 0
|
10月前
|
安全 Java Linux
3.多线程(进阶)(一)
3.多线程(进阶)
39 0