锁相关知识
约 3333 字大约 11 分钟
2024-11-17
https://cloud.tencent.com/developer/article/1953236
Java中锁分为以下几种:
- 乐观锁、悲观锁
- 自旋锁、适应性自旋锁
- 锁升级(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁)
- 公平锁、非公平锁
- 可重入锁
- 独享锁、共享锁
- 互斥锁、读写锁
乐观锁 & 悲观锁
两种锁只是一种概念。
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
乐观锁:
概念:乐观锁认为一个线程去拿数据的时候不会有其他线程对数据进行更改,所以不会上锁。实现:CAS机制、版本号机制。以Atomic开头的包装类,例如AtomicBoolean,AtomicInteger,AtomicLong。
悲观锁:
概念:悲观锁认为一个线程去拿数据时一定会有其他线程对数据进行更改。所以一个线程在拿数据的时候都会顺便加锁,这样别的线程此时想拿这个数据就会阻塞。
实现:加锁。Java中,synchronized关键字和Lock的实现类都是悲观锁。
Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
行锁与表锁
当执行 select ... for update
时,将会把数据锁住,因此,我们需要注意一下锁的级别。MySQL InnoDB 默认为行级锁。当查询语句指定了主键时,MySQL会执行「行级锁」,否则MySQL会执行「表锁」。
常见情况如下:
- 若明确指明主键,且结果集有数据,行锁;
- 若明确指明主键,结果集无数据,则无锁;
- 若无主键,且非主键字段无索引,则表锁;
- 若使用主键但主键不明确,则使用表锁;
select * from tbl_user where id<>1 for update;
小结: innoDB的行锁是通过给索引上的索引项加锁实现的,因此,只有通过索引检索数据,才会采用行锁,否则使用的是表锁。
线程嵌套造成死锁
一个死锁问题的分析
现象:当同时送入线程池的任务大于配置core-size时,会引起程序死锁。 测试代码如下:
/**
* 创建i个nestedCall任务,送入线程池
*/
public static void main(String[] args) throws InterruptedException {
ThreadPoolTaskExecutor taskExecutor = taskExecutor();
for (int i = 0; i < 3; i++) {
int finalI = i;
taskExecutor.submit(() -> outerCall(taskExecutor, finalI));
}
System.out.println("task push over");
}
/**
* 先睡2s,再将一个任务送入线程池,再等待其完成
*/
public static void outerCall(Executor taskExecutor, int i){
try {
System.out.println("outer " + i);
TimeUnit.SECONDS.sleep(2);
CountDownLatch latch = new CountDownLatch(1);
taskExecutor.execute(() -> {
nestedCall(latch, i);
});
latch.await();
}catch (Exception e){
e.printStackTrace();
}
}
//这个里层的
public static void nestedCall(CountDownLatch latch, int i){
System.out.println("nested " + i);
latch.countDown();
}
public static ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(6);
executor.setQueueCapacity(12);
executor.setKeepAliveSeconds(30);
executor.setRejectedExecutionHandler(rejectedExecutionHandler());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
public static RejectedExecutionHandler rejectedExecutionHandler() {
return (r, executor) -> {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}
如果运行main方法会产生什么结果呢,如下:
task push over
outer 0
outer 1
outer 2
不会再有新的输出了,程序已经死锁。 为什么呢? 我们来看线程池里边发生的事情(数字的顺序可能是乱的,这里只是其中一种情况):
- 执行outerCall i=0
- 执行outerCall i=1
- 执行outerCall i=2
- 送入等待队列 nestedCall i=0
- 送入等待队列 nestedCall i=1
- 送入等待队列 nestedCall i=2
outerCall的执行占据了所有的活动线程(3个),等待nestedCall完成。然而nestedCall在当前线程池的等待队列中如果outerCall不退出,他永远也得不到执行的机会,死锁了。
结论
在使用java线程池的时候要特别注意最好不要有这种嵌套的任务。如果有这种嵌套使用线程池的情况的话一定要保证外层的线程不会等待内层线程完成再返回,或者为等待添加超时时间
锁分类
https://blog.csdn.net/weixin_60272582/article/details/123265596
是不是吓一跳,下面我们来一项一项说明各种锁的概念以及使用
1.乐观锁
乐观锁顾名思义就是一种乐观的思想,认为读数据时没有别的线程进行过修改,所以不会上锁,写数据时判断当前与期望的值是否相同,如果相同进行更新(更新期间是要枷锁的,为了保证原子性)
举例:java中的乐观锁---CAS
CAS的使用以及CAS原子操作面临的问题,以及解决方案
CAS的详细内容请参考
多线程常见面试题总结(简单版)_Mr.米斯特儿赵的博客-CSDN博客 第17个回答
2.悲观锁
悲观锁顾名思义就是一种悲观的思想,每次拿数据都会悲观的认为其他线程修改了数据,所以每次读写时都会上锁,其他线程想要读写这个数据时,就会被该线程阻塞 ,直到这个线程释放锁.
举例: java中的悲观锁 synchronized修饰的方法和方法块 比如我们尝试用的hashtable,以及StringBuffer他们的方法都被synchronized修饰,ReentrantLock不仅悲观还重入(也属于重入锁)
3.自旋锁
自旋锁就是在获取锁的时候,如果锁被其他线程获取,该线程就会一直循环等待,一直尝试着去获取锁,直到目标达成。而不像普通的锁那样,如果获取不到锁就进入阻塞
自旋锁的优点: 避免了线程切换的开销,不会使线程进入阻塞的状态,减少了不必要的上下文的切换,执行速度块
自旋锁的缺点: 长时间占用处理器,会白白消耗 处理器资源,却没有干任何事情,性能浪费,所以自旋等待的时间必须有一定的限度 超过限度就挂起 线程
自旋默认的次数: 10次
4.可重入锁(递归锁)
可重入锁使一种技术,任意线程在获取到 锁之后能够再次 获取 该锁而不会被锁阻塞
原理 : 通过组合自定义同步器来实现锁的获取和释放
再次获取锁: 识别获取 锁的线程是否为当前占据锁的线程,如果是,则再次成功获取,获取锁后,进行计数自增
释放锁: 释放锁 进行计数自减
java中的可重入锁:
ReentrantLock、synchronized修饰的方法或代码段
5.读写锁
读写锁使一种技术,通过ReentrantReadWriteLock类来实现的,为了提高性能,Java提供了读写锁,读的地方使用 读锁,写的地方使用写锁,在没有写锁的情况下,读锁是无阻塞的,多个读锁不互斥,读锁与写锁互斥,这是由jvm来控制的
读锁: 允许线程获取读锁,同时访问一个资源
写锁: 允许一个线程获取 写锁,不允许 同时访问一个资源
如何使用:
1.创建一个读写锁 ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
2.获取读锁和释放锁 // 获取读锁rwLock.readLock().lock();// 释放读锁rwLock.readLock().unlock();
3.获取写锁和释放锁 创建一个写锁rwLock.writeLock().lock();// 写锁 释rwLock.writeLock().unlock()
6.公平锁
公平锁使一种思想,多个线程按照顺序来获取锁 ,并发环境中,每个线程会去查看锁的维护队列,如果队列为空,就占有锁,如果队列不为空,就加入等待队列的末尾,按照FIFO原则获取锁
7.非公平锁
非公平锁也是一种思想,线程尝试获取锁,如果获取不到,按照公平锁的方式,多个线程获取锁不是按照 先到先得的顺序.是无序的,有可能后到了先获取到锁
优点: 比公平锁性能高
缺点: 线程 饥饿(某个线程很长一段时间获取不到锁)
举例: synchronized是非公平锁
ReentrantLock通过构造函数指定该锁是公平的还是非公平的,默认是非公平的。
8.共享锁
共享锁是一种思想,可以多个线程获取读锁,以共享的方式持有锁,和乐观锁还有读写锁同义
9.独占锁
独占锁是一种思想,只能有一个线程获取锁,以独有的方式持有锁,悲观锁和互斥锁同义
synchronized,ReentrantLock
10.重量级锁
synchronized 就是重量级锁,为了优化重量级锁,引入了轻量级锁和偏向锁
11.轻量级锁
jdk6是加入的一种锁的优化机制,轻量级锁是在没有多线程竞争的情况下使用的CAS操作去消除同步使用的互斥量
上面理解起来很吃力,解析一下,首先是没有竞争,也就是说是单线程 ,两条以上线程,轻量级锁不会生效
12.偏向锁
偏向锁 是JDK6时加入的一种锁优化机制: 在无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。偏是指偏心,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作.
13.分段锁
是一种机制,是不是想到了ConcurrentHashMap了,默认情况下ConcurrentHashMap被细分为16个段(Segment)每次上锁只是锁的每个segment. segment通过继承ReentrntLock来进行加锁,只要保证每个segment是线程安全的,是不是就保证了全局的线程安全.
14.互斥锁
互斥锁和悲观锁还有独占锁同义,某个资源,只能被一个线程访问,其他线程不能访问.
例如上文提到的读写锁中的写锁,写与写之间是互斥的,写与读之间也是互斥的
15.同步锁
与互斥锁同义,字并发执行多个线程时,在同一时间只允许一个线程访问共享数据 synchronized
Java锁之阻塞锁介绍和代码实例pdf0星超过10%的资源47KB
下载
16.死锁
死锁是一种现象: 如线程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获
取不到对方的资源,就会造成死锁。
Java中的死锁不能自行打破,所以线程死锁后,线程不能进行响应。所以一定要注意
程序的并发场景,避免造成死锁。
17.synchronized(简单总结)
synchronized是java中的关键字,用来修饰方法,对象实例,属于独占锁,悲观锁,可重入锁,非公平锁
用于实例方法时,锁住的是对象的实例也就是this
用于静态方法上,锁主的是Class类
18.Lock和synchronized的区别
lock 是java中的接口,是可重入锁,悲观锁,独占锁,互斥锁,同步锁
lock 需要手动获取锁和释放锁
lock 是一个接口,synchronized是关键字
synchronized发生异常会自动释放锁,不会导致死锁现象,而lock发生异常如果没有unlock()释放锁,就有可能产生死锁,一般使用lock锁的时候,需要在finally中进行释放锁
lock锁可以使等待的线程响应中断,而synchronized不会,会一直等待下去
19.ReentrantLock 和synchronized的区别
Reentrantlock是java中的类,继承了lock类,是可重入锁,悲观锁,独占锁,互斥锁,同步锁
相同点: 主要解决共享变量如何访问的问题
都是可重入锁,同一线程可以多次获取锁
保证了线程安全的两大特性 可见性 原子性
不同点:
- ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法,synchronized 隐式获得释放锁。
- ReentrantLock 可响应中断, synchronized 是不可以响应中断的ReentrantLock 为处理锁的不可用性提供了更高的灵活性
- ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
- ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
- ReentrantLock 通过 Condition 可以绑定多个条件