乐观锁 and 悲观锁
乐观锁:
乐观锁总是抱着乐观的态度来看待线程之间锁冲突的问题(锁冲突:多个线程竞争一个锁对象,没有得到锁对象的线程就需要进行等待或者相应地处理),认为多个线程之间的操作并不会产生锁冲突,只有当更新数据的时候(写操作),才会做冲突检测处理。
其常见的实现方式为 :使用版本号或时间戳来标识数据的版本,只有当版本号都一致的时候,修改操作才生效 。故乐观锁一般适用于多读少写的场景中
悲观锁:
与之相反,悲观锁总是抱着悲观的态度,认为线程之间总会发生锁冲突。故线程在读操作或者写操作的时候都会加锁,加上悲观锁后,其他线程需要等改线程释放锁后才能继续执行。
其实现方式为:互斥锁(synchronized),读写锁(ReeentrantReadWriteLock)
轻量级锁 and 重量级锁
轻量级锁:
轻量级锁用于解决线程在竞争资源时所引发的性能问题从而引入的一种优化手段,当一个线程在尝试获得锁时,如果此时锁没有被其他线程占用,那么线程会将锁的对象头 中的部分数据复制到栈帧中,且将锁的对象头中的Mark Word替换成指向当前线程的指针。如果再有线程想获取锁时,就会发现此时锁已经被占用了,并且可以通过Mark Word中的指针来判断当前是否是同一线程占用锁
这里可能就会有同学不太清楚对象头和Mark Word是个啥东东
我们平常创建的实例化对象,其实是有三个部分组成的:对象头,实例数据,对其填充
其中Mark Word就存在于对象头中,主要记录的是线程的锁状态
重量级锁:
当多个线程竞争同一个锁时,轻量级锁可能会膨胀为重量级锁。重量级锁使用操作系统的互斥量来实现线程同步,它会导致线程的阻塞和唤醒,从而增加了线程切换的开销。重量级锁适用于多个线程长时间竞争同一个锁的情况。
自旋锁 and 挂起等待锁
自旋锁:
当一个线程获取锁失败后,并不会进入阻塞队列中,而是隔一段时间又来重新检测,如果锁释放,就竞争锁,如果还是释放,就再过会又来重新检测,
挂起等待锁:
当线程获取锁失败后,线程阻塞进入阻塞队列,直到锁对象释放后,才有机会重新获得锁对象
自旋锁是轻量级锁,挂起等待锁是重量级锁
使用场景:根据上面分析我们可知,临界区运行时间较短时(不允许多个并发进程交叉执行的一段程序称为临界部分),我们使用自旋锁,反之使用挂起等待锁
ex:我和朋友出去玩,约的下午7点,此时才12点,于是我可以在家先打游戏打到18.30再过去,这时候就可以使用挂起等待锁。而如果此时是自旋锁会怎样呢?我每隔五分钟就给我的朋友说一下:"下午7点集合呢,别忘了"。但距离下午7点还有很长一段时间,故这就是一个无意义的举措,是一种忙等的情况
互斥锁 and 读写锁
互斥锁
对于互斥锁,我们应该较为熟悉,因为我们一直以来最常用的synchronized 就是互斥锁。
其保证同一时间内只有唯一线程可以访问资源 ,其余的线程都得阻塞等待,直到锁被释放,才会竞争锁
读写锁:
读写锁提供了三种情况:针对读加锁/针对写加锁/解锁
我们知道,多个线程针对统一变量读时,是没有线程安全问题的,此时也不需要加锁控制
故读写锁就针对两种情况:
1.有读也有写:读操作加读锁,写操作加写锁
2.只有写:加写锁
公平锁 and 非公平锁
公平锁:
多个线程竞争锁对象,没有竞争到锁对象的线程根据申请锁资源的先后顺序 进入一个队列去排队,之后每次都是队列的第一个获得锁资源
非公平锁:
多个线程竞争锁对象,获取锁对象的线程没有什么先后顺序规则,全是抢占式调度执行,你抢到了锁资源,你就执行,你没有抢到,你就给我一边去阻塞
优点:
公平锁:保证了每一个线程都可以获取到锁资源,不会饿死在线程的抢占中
非公平:不必唤醒全部的线程,降低开销,增大吞吐量,减少唤醒线程的数量
缺点:
公平锁:吞吐量下降
非公平锁:抢占式调度,一些线程可能从头到尾都获取不到资源,导致饿死现象
重入锁 and 不可重入锁
重入锁:
当一个线程获取锁对象时,还可以获取其他的锁对象
public static void main(String[] args) { Object loker1 = new Object(); Object loker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (loker1){ synchronized (loker2){ for(int i = 0; i < 3; i++){ System.out.println("1:synchronized是可重入锁:可以获取两个不同的锁对象"); } } } }); t1.start();
不可重入锁:
当一个线程获取锁对象之后,必须等待该线程释放锁才可以获取其他对象的锁