多线程
锁策略
乐/悲观锁
乐观锁:出现锁竞争的概率较低(线程少,不太涉及锁竞争)
悲观锁:出现锁竞争的概率较高(线程多,很可能涉及锁竞争)
在操作系统中提供的锁接口,Mutex(互斥量,操作系统的锁),就是一个典型的悲观锁,认为竞争很大,一旦竞争,那么就有线程阻塞,进入等待,而什么时候被唤醒,就要看调度器的实现了
在应用程序里面,还可以通过一些其它的方式实现锁(如:CAS机制),相当于仅仅一个 用户态的锁,不太涉及到内核和操作系统之间的切换,也就更高效一点
而Java中的synchronized即是悲观锁,也是乐观锁,可以针对当前锁冲突的情况,自动的切换模式,而这种类型的锁,也叫做 自适应 的锁
读写锁
普通锁提供两个操作:加锁、释放锁
读写锁提供三个操作:读加锁、写加锁、释放锁
读加锁与读加锁,不进行竞争。
读加锁和写加锁,发生竞争。
写加锁和写加锁,发生竞争。
也就是读的时候可以进行读操作,写的时候不可以进行其它操作
作用就是能进一步的降低锁冲突的概率。
synchronized 不是读写锁
重/轻量级锁
和 乐观锁、悲观锁 比较相似
一般来说:乐观,工作量少。悲观,工作量大。这主要是站在锁冲突概率的角度来看待的
一般来说,乐观锁就是轻量级锁,悲观锁就是重量级锁,但是并不绝对。这主要是站在工作量与消耗资源的消毒来看待的
轻量级锁:工作量小,消耗资源少,锁更快。
重量级锁:工作量大,消耗资源多,锁更慢。
mutex (操作系统的锁)就是一个重量级锁,也是悲观锁,这个锁在加锁的时候遇到冲突,就会产生内核态与用户态的切换,以及线程的阻塞和调度。
而基于 CAS机制 的轻量级锁,在加锁的时候遇到冲突,不会涉及到内核态与用户态的切换,直接尝试重新获取锁,直到获取成功,这个过程没有放弃CPU,不涉及线程调度(这种一直循环获取锁的锁叫自旋锁)
如果当前锁的竞争压力很大,采用自旋锁的策略也不是很好用,如果竞争压力小,那么采用自旋锁就比较舒服
synchronized 是自适应锁,开始的时候是轻量级锁,如果竞争激烈,就会编程重量级锁
(非)公平锁
符合先来后到的规则的锁就叫公平锁,有线程会进行插队就叫非公平锁,synchronized 就是非公平锁
系统中对于线程的调度就是一个随机的过程(先来的和后到的线程获取锁的机会是均等的),而这种就属于非公平锁,默认不做任何处理,就是非公平锁
自旋锁也是非公平锁
(不)可重入锁
可重入锁:一个线程,连续对同一把锁加锁两次(针对a加锁,锁内还有一个锁,也是针对a的锁)如果第二个锁能获取到锁,就是可重入锁。
不可重入锁:如果第二个锁不能获取到锁,就是不可重入锁。
synchronized 就是可重入锁(通过计数器来确定是否释放所有的锁)
而如果是不可重入锁,就会进入一个死锁,线程无法继续工作,一直处于阻塞状态,就叫做进入了死锁状态
死锁
产生死锁就表示线程进入了无限等待(阻塞)的状态,无法继续工作(很严重给的BUG)
死锁的几个典型场景
一个线程1把锁:一把锁连续加两次,就进入了无限阻塞状态(可重入锁不涉及这种情况)
两个线程2把锁:线程1获取到锁1,同时线程2获取到锁2,线程1再获取锁2时就需要等线程2释放锁2,但是线程2此时也要获取锁1,那么两个线程都会进入到无限等待的情况,谁也不让谁,这也是死锁的常见情况之一。而这种代码是很容易(1%的可能)出现死锁的
N个线程M把锁:本质上与两个线程2把锁是一样的,都是进入了锁后再获取锁时进入循环等待的状态,从而无限阻塞
死锁产生的原因:环路等待(核心原因)、不可抢占、互斥使用、请求和保持
死锁的危害:导致严重的BUG(对应的线程无法继续工作)
死锁的解决方案;规避死锁
1、不要在加锁代码中尝试获取其它锁(同一时刻只获取一把锁)
2、约定一种固定的顺序来加锁(如:获取锁1的情况下才能获取锁2,同理)
CAS机制
CAS机制 全称 compare and swap 比较并交换 是一个原子操作(有原子性)
在Java代码中,比较、赋值、等操作都不是原子操作,这样的代码翻译成机器指令后不是单一的机器指令,而是多条指令,多条指令就可能会受到线程调度的影响。
CPU会支持类似SWAP/EXCHANGE这样的指令,能够做到一条指令完成比较和交换的操作
CAS的用法:
boolean CAS(address,expectedValue,newValue){
if(*address == expectedValue){
// 实际上是把这两个变量的内存上的值进行了交换
*address = newValue;
return true;
}
return false;
}
CAS一条指令就可以完成这样的操作
基于 CAS 可以实现一个自旋锁/轻量级锁
class Lock{
Thread owner = null;
void lock(){
while(!CAS(&owner,null,Thread.currentThread())){
// 先看owner中记录的值是不是为null,如果为null,为null就表示当前锁没有被其它线程获取,如果没有其它线程获取当前锁,就把当前线程设置到owner里,如果部位null,那么就继续循环。
// 也就是说这是一个自旋锁,这里只写了加锁方法并没有写解锁方法。
}
}
}
CAS还可以用来实现原子类(如:AtomicInteger)
原子类:int long 类型的数据在进行操作的时候,很可能不是原子的,原子类能够保证,针对这些数据进行操作时,也都是原子的
ABA问题
在CAS机制中进行比较时,只能看到内存中的当前值,不能直到这个值是否被修改过(修改后又修改回来)
如:
送快递,有一个用户A的快递要去送,但是你中午去吃饭,你朋友去送这个快递,但是被用户A被退回,然后你下午去了之后又看到这个快递,于是你再去送。(快递员表示线程,送快递、退快递表示修改数据)
预期送一次,实际送了两次,不符合预期。
解决方案:
在进行修改数据时,添加一个 上次修改时间 ,CAS在进行数据比较的时候,不仅仅比较数据,也比较 修改时间 ,如果都一致,就表示数据没有被修改过。(不一定是时间,也可以是别的数据,如:版本号)
补充
synchronized:自适应锁(乐观锁或悲观锁,轻量锁或重量锁)、轻量级锁是基于自旋的方式实现的、非公平锁、可重入锁、不是读写锁
mutex:悲观锁、重量级锁、不是自旋锁、非公平锁、不可重入锁、不是读写锁
GPU:显卡,用来计算。在之前都是CPU中包含显卡,也就是CPU和GPU不分家,以前就称为 集成显卡 核心显卡。
虽然都是用来计算的,但是GPU的硬件特性和CPU差异极大,也就导致GPU编程和CPU编程差别也很大。对于Java来说,代码主要是围绕CPU来写的。
而一些游戏开发/人工智能就要用GPU编程来进行开发,编程的思路就是截然不同的了
CAS也可以理解为:进入CAS机制后,其它线程不工作,本线程把要使用的数据先与内存中做一个比较,然后再对数据进行操作,存入内存,退出机制。