0.前沿
之前已经讲了Synchronized的锁一些基本使用,接下来就来进一步介绍一些对于锁计相关的内容,(锁的类型、锁的升级、消除、粗化等),以及通过一些其他的方式来达到线程安全的方式(CAS ReentrantLock Semaphore CountDownLatch 等等)
1.锁的六种类型
1.1乐观锁 VS 悲观锁
这组概念是对于设计锁的一个上层概念,是一种哲学原理;是一种实现锁的逻辑
**区别:**预测访问变量时发生冲突概率的大小
对于容易发生冲突的就是用悲观锁的思路:在访问该变量前就进行加锁,这样可以确保访问的安全性,但同时也会带来较大的开销
对于那些发生冲突概率小的变量就是用乐观锁的思路:认为访问该变量是安全的,就直接进行访问和操作,到了要提交的时候才进行判定该变量是否被修改,如果被修改了在执行其他操作来处理,这样就会大幅度降低加锁带来的开销,从而提高程序效率
对于乐观锁而言,如果在提交时有报错可以触发回滚机制, 或者通过**CAS(并没有真加'锁')**来进行处理
对于悲观锁通常有重量级锁 和轻量级锁两种实现方式
1.2重量级锁 VS 轻量级锁
这两种又是对于悲观锁的一种划分
区别: 对于加锁开销的大小;本质上是重量级锁要调用内核态 层面的操作->阻塞等相关操作->开销大,轻量级锁只在用户态层面进行操作->没有进行阻塞->而是通过另一种方式实现了等价于阻塞的操作(CAS)->开销小
对于重量级锁的具体实现有对应到了挂起等待锁 ,轻量级锁的具体实现对应到自旋锁
1.3挂起等待锁 VS 自旋锁
这两种锁描述的是对于线程等待的策略
**挂起等待锁:**按照 悲观锁的思路 + 重量级锁的逻辑来进行实现的一种线程等待的策略,当一个线程想要获取锁资源的时候发现这把锁已经被占用,就直接进行阻塞
**自旋锁:**按照 悲观锁的思路 + 轻量级锁的逻辑来进行实现的一种线程等待的策略,当一个线程想要获取锁资源的时候发现这把锁已经被占用,不进行阻塞,而是通过循环等待的方式一直来申请锁资源直到这把锁被释放
**优劣:**对于自旋锁而言,一般情况某个线程对于锁占用的时间是比较短暂的,通过循环等待的方式就可以在锁被释放的第一时间获取到这把锁,但是如果这把锁长时间没有被释放一直进行申请锁,这会占用CPU的资源,而通过挂起等待的方式直接进行阻塞就不会占用CPU资源,但是再次获取到这把锁可能会过去很长时间
1.4公平锁 VS 非公平锁
这组概念描述的是多个线程竞争一把锁的情况下,究竟谁能获取到这把锁
公平锁的定义:"先到先得",认为最先进行阻塞等待的线程会获得这把锁
非公平锁:"各凭本事"
1.5可重入锁 VS 不可重入锁
这组概念描述的是对于一把锁加锁多次,是否会发生死锁。对于可重入锁就不会死锁,Java中的Synchronized就是可重入锁
1.6普通互斥锁 VS 读写锁
这里最主要的就是读写锁,普通互斥锁就是正常的锁
**读写锁:**分别对读操作和写操作进行加锁,并约定读锁和读锁之间不会阻塞,而读锁和写锁 、写锁和写锁之间会进行阻塞,对于两个读操作之间不存在线程安全问题,不进行加锁也就进一步提高了效率
2.锁的自适应
锁的自适应过程是对于锁相关操作的一种优化,从而实现效率的提高
2.1锁升级
锁升级的过程:无锁 --> 偏向锁 --> 自旋锁 --> 挂起等待锁
对于锁升级的过程是单向的,也就是只升不降
**无锁 --> 偏向锁:**就是一个线程中的一段逻辑进行了加锁,准备执行这段逻辑时就从无锁状态变为偏向锁状态,偏向锁并不是真的加锁,而是对该锁做了一个标记(表名是哪个线程拥有这把锁)
偏向锁 --> 自旋锁: 当有多个线程要使用一把锁,才将之前做了标记的线程进行加锁,此时这个锁才从偏向锁变为自旋锁
**自旋锁 --> 挂起等待锁 :**当竞争一把锁的线程数达到一定数目时,才会将其变为挂起等待所
2.2锁消除
锁消除是编译器对于锁操作的一种优化,这里的消除是真正意思上的去除这把锁,所以对于这种操作编译器只会对特别有把握的进行去除(只有一个线程用到这把锁,或者这把锁中的只有读操作等)
2.3锁粗化
锁粗化是对于一个线程中多个连续的逻辑对其进行加锁解锁,从而将这些连续的逻辑的多次加锁解锁步骤将其合并为一个大的逻辑只对这个大的逻辑进行一次加锁和一次解锁
3.CAS锁的另一种实现方式
对于CAS实现的基本原理就是比较和交换 ,对于比较和交换而言这个操作在是通过CPU以原子方式进行执行的
这里的机制是通过循环的方式来进行判定该线程获取到的变量是否和内存中的变量,不同就说明数据不匹配,通过交换的方式,知道数据匹配才执行相关操作,
这里的交换:更本质上是赋值,对于Value是一直再从内存中读取数据,可以理解为将Value赋值给oldValue
自旋锁也是通过这同方式进行实现的,如果一个锁资源没有被释放,该线程就一直循环判定该锁是否为空,当锁被释放时就可以以及获取到锁
3.1CAS中的ABA问题
ABA问题描述的是当一个变量从A->B 又从B->A这样的情况,对于CAS来说就相当于没有改变过;但是对于实际的应用场景中就是一个问题:
对于银行的存款和取款操作而言,如果一个用户在某一时间内取款x,然后又恰好有一个用户往该账户中转入了x元,而对于CAS而言该用户账户余额没有改变,此时又会再次触发扣款操作,也就是说,取了x元,扣了2x,这就是CAS中的ABA问题
对于ABA问题的解决方式:
ABA问题是由于一个参数先增后减,或先减后增导致的,如果将这个操作变为单调递增的方式就不会有问题了,就是引入一个版本号用来表示执行该操作的时机,通过判断oldValue和value是否相等,如果不相等就意味着已经进行了扣款操作,这样就解决了ABA问题
4.一些和线程安全相关的类
4.1ThreadLocal线程级变量
相当于这个变量在每个线程中又拷贝了一份,这个变量的作用域只在当前线程中有效,类似于有一个本体和多个分身这样
java
//使用ThreadLocal
public class demo29 {
static ThreadLocal<Integer> tl = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
tl.set(0);
Thread t1 = new Thread(()->{
tl.set(0);
for(int i = 0; i<10000; i++)
tl.set(i);
System.out.println(tl.get());
tl.remove();
});
Thread t2 = new Thread(()->{
tl.set(0);
for(int i = 0; i<100000; i++)
tl.set(i);
System.out.println(tl.get());
tl.remove();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(tl.get());
}
}

4.2java.util.concurrent中的Callable
对于JUC这个包存放的是和线程相关的类
对于Callable是和Runnable 类似,不同之处在于Callable中的call方法有返回值,而Runnable中的run方法没有返回值
Callable的使用:首先要重写call方法,再通过FutureTask来接受Callable这个参数,最后将FutureTask作为Thread的参数进行传递,最后通过get方法进行获取

4.3Atomic原子类
Atomic提供了一系列的原子类也原是基于CAS机制实现的,可以用来解决线程安全问题

4.4semaphore 信号量
信号量是用来表示剩余可用资源的数量,相当于一个计数器,对于二元信号量来说,就相当于一个互斥锁

4.5CountDownLatch
CountDownLatch用来表示将一个大的任务分成多个小的任务,统计这个大任务完成的结束时间,
就类似于发令枪和运动员,表示的是从发令枪响到所有远动员都完成该项目的总时间


