文章目录
1. 乐观锁、悲观锁
乐观锁
认为自己在使用数据时,不会有别的资源修改数据或资源,所以不会添加锁。
在java中是通过无锁编程来实现,只是在更新数据的时候去判断,之前有没有线程更新了这个数据。
如果没有被更新,当前线程将自己修改的数据写入
如果已经被其他线程更新了,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等。
判断是否更新的规则:
1. 版本号机制
2. CAS(Compare-and-Swap,比较交换)算法,java原子类中的递增操作就通过CAS自旋实现的(常用)
- 适合读操作多的场景,不加锁的特点就是读操作的性能大幅度提升
- 乐观锁直接去操作同步资源,是一种无锁算法
悲观锁
认为一定有其他线程来修改自己使用的资源,因此在获取数据时会先加锁,确保数据不会被其他线程修改。
synchronized关键字和Lock的实现类都是悲观锁。
- 适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 显示的锁定之后再操作同步资源
2. synchronized锁相关
- 作用于实例方法:当前实例加锁,进入同步代码前要获得当前实例的锁
对象锁
调用指令将会检查方法的ACC_AYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程将先持有monitor锁,再执行方法,最后再方 法完成后释放monitor。 - 作用于代码块,对括号内的配置对象加锁
底层使用monitorenter、monitorexit
实现锁的获取和释放。一般情况是一个enter俩个eixt(防止出现异常,保证异常时可以退出代码块) - 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
类锁
ACC_AYNCHRONIZED
和ACC_STATIC
防蚊标志区分该方法是否是静态同步方法
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法(alibaba手册)
-
俩个普通同步方法,标准访问有ab 两个线程,请问先打印邮件还是短信
-
sendEmal方法中加入暂停3 秒钟,网先打印邮件还是短信
一个对象中如果有多个synchronized方法,某一时刻,只要一个线程去调用了其中的一个synchronized方法,其他线程都只能等待。锁的对象是
this
,被锁定后,其他线程都不能进入到当前对象的其他synchronized方法。 -
添加一个普通的hello方法,请向先打印邮件还是hello
普通方法和同步锁无关。
-
有两部手机,请问先打印邮件还是短信
俩部手机是俩个类,不会有争抢。
-
有两个静态同步方法,有1部手机,请问先打印邮件还是短信
static synchronized
-
有两个静态同步方法,有2 部手机,请问先打印邮件还是短信
加了static之后,就是静态方法。
Phone phone = new Phone()
new出来的是Phone类。此时是类锁 -
有1个静态同步方法,有1个普通同步方。有部手机,请先打印邮件还是短信
-
有1个静态同步方法,有1个普通同步方法有2部手机,请问先打印邮件还是短信
- 对于普通同步方法,锁得是当前实例对象,通常是this,具体是一部部的手机,所有普通同步方法用的都是同一把锁–> 实例对象本身
- 对于静态同步方法,锁得是当前类的Class对象,如Phone.class唯一的一个模板
- 对于同步方法块,锁得是synchronized括号内的对象
3. 为什么任何一个对象都可以成为一个锁
管程(monitor)、锁、监视器是一个概念。是一种程序结构,结构内的多个子程序(对象或者模块)形成的多个工作线程互斥访问共享资源。
因为c++底层源码中,monitor
采用ObjectMonitor
实现。 每个Object都有一个ObjectMonitor
。
- 其中
_owner
关键属性是指向持有ObjectMonitor
对象的线程。 _WaitSet
存放处于wait状态的线程队列EntryList
存放处于等待锁block状态的线程队列_recursions
锁的重入次数_count
记录该线程获取锁的次数
公平锁和非公平锁
// 什么都没有默认为非公平锁
ReentrantLock lock = new ReentrantLock ();
// fair==true;公平锁
ReentrantLock lock = new ReentrantLock (true);
公平锁:多个线程按照申请锁的顺序来获取锁(排队买票,先来先得)
非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的环境下,有可能造成优先级翻转或者饥饿的状态
为什么默认为非公平锁?
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。
什么时候用公平、不公平?
如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
否则那就用公平锁,大家公平使用。
AQS
可重入锁(递归锁)
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入自己可以获取自己的内部锁。
自己可以获取自己内部的锁。
- 指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
- 如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
- 所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
分类:
- 隐式锁(synchronized 关键字 修饰的锁) 默认是可重入锁 --> 同步块和同步方法都有
- 显示锁(Lock),ReentrantLock
// 显示锁加锁几次就要解锁几次
ReentrantLock lock = new ReentrantLock ();
lock.lock();
lock.unlock();
可重入锁的实现原理
- 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
_owner
关键属性是指向持有ObjectMonitor
对象的线程。_recursions
锁的重入次数_count
记录该线程获取锁的次数
- 当执行monitorenter时,如果目标锁对象的让数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设留为当前线程,并且将其计数器加1。
- 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
- 计数器为零代表锁已被释放当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。当计数器为0时,代表锁已经释放。
死锁及排查
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种瓦相等待的现象,若无外力干涉那它们都将无法推进下去,如果
系然资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
// 死锁
public class DeadLockDemo {
public static void main(String[] args) {
final Object objectA = new Object();
final Object objectB = new Object();
new Thread(()->{
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"\t 自己持有A锁,希望获得B锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"\t 成功获得B锁");
}
}
},"t1").start();
new Thread(()->{
synchronized (objectB){
System.out.println(Thread.currentThread().getName()+"\t 自己持有B锁,希望获得A锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (objectA){
System.out.println(Thread.currentThread().getName()+"\t 成功获得A锁");
}
}
},"t2").start();
}
}
产生死锁的原因:
- 系统资源不足
- 进程进行顺序不合适
- 资源分配不当
检查死锁:
- 命令行:
jps-l
当前程序的进程编号。–> jstack 进程编号(打印出栈信息) - jconsole