Java中的锁
一、乐观锁和悲观锁
1、悲观锁
悲观锁: 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改,synchronized和Lock的实现类都是悲观锁,适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源-----狼性锁
2、乐观锁
乐观锁: 认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁,Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据,如果这个数据没有被更新,当前线程将自己修改的数据成功写入,如果已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等。判断规则有:版本号机制Version,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。-----适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我
🌟概念区别
特性 | 乐观锁(Optimistic Lock) | 悲观锁(Pessimistic Lock) |
---|---|---|
思想 | 认为并发冲突很少,操作时先不加锁,提交时再检测是否有冲突。 | 认为并发冲突很常见,操作前先加锁,确保别人无法同时修改。 |
实现方式 | 版本号机制 或 时间戳机制:更新前先比较版本,不一致则更新失败。 | 数据库锁机制:如 SELECT...FOR UPDATE ,或者 synchronized / ReentrantLock 。 |
性能影响 | 冲突少时,性能很好;冲突多时,频繁重试会影响性能。 | 并发高时,线程等待多,吞吐量较低。 |
应用场景 | 读多写少,冲突概率低的场景。 | 写多,冲突概率高的场景。 |
🔥实际案例
乐观锁:
-
数据库表有一个
version
字段。 -
取数据时,连同
version
一起读出。 -
更新时,使用:
UPDATE table_name SET value = ?, version = version + 1 WHERE id = ? AND version = ?;
-
如果
version
不一致,说明有别的线程修改了,更新失败,可以重试。
悲观锁:
-
数据库级别:
SELECT * FROM table_name WHERE id = ? FOR UPDATE;
查询时就加锁,其他事务不能修改,等当前事务提交/回滚后才能解锁。
-
Java并发:
synchronized (obj) { // 临界区代码 }
Lock lock = new ReentrantLock(); lock.lock();try { // 临界区代码 } finally { lock.unlock(); }
✅总结一句话:
-
乐观锁:适合冲突很少的业务,提升并发性能,失败时重试。
-
悲观锁:适合冲突很频繁的业务,直接锁住资源,确保安全但牺牲并发。
二、synchronized关键字分析
1、阿里Java规范:
高并发时,同步调用应该去考置锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体﹔能用对象锁,就不要用类锁。
说明︰尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。
2、案例分析
/*** @author Guanghao Wei* @create 2023-04-10 14:57*/class Phone {public synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("------sendEmail");}public synchronized void sendSMS() {System.out.println("------sendSMS");}public void hello() {System.out.println("------hello");}
}/*** 现象描述:* 1 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信 共用一个对象锁* 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?---------先邮件,后短信 共用一个对象锁* 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件* 4. 有两部手机,请问先打印邮件还是短信? ----先短信后邮件 资源没有争抢,不是同一个对象锁* 5. 有两个静态同步方法,一步手机, 请问先打印邮件还是短信?---------先邮件后短信 共用一个类锁* 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? ----------先邮件后短信 共用一个类锁* 7. 有一个静态同步方法 一个普通同步方法,请问先打印邮件还是短信? ---------先短信后邮件 一个用类锁一个用对象锁* 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? -------先短信后邮件 一个类锁一个对象锁*/public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}}
第1 第2中案例说明,只要我的一个类中有方法加了synchronized 关键字,这个synchronized 锁的并不是当前该方法,而是整个资源类,也就是说可能该类有多个方法都加了synchronized 关键字,但是多线程的环境中,只有一个线程能够进入众多加了synchronized 方法中的一个方法。然后依次排队。换句话说,某一个时间内,只能有唯一的一个线程去访问这些synchronized 方法,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized 方法。
第3个案例说明,普通方法不需要竞争锁,直接执行。
第4个案例说明,对于普通的方法加上了synchronized关键字,我们锁的是当前实例对象(this),如果有两个不同的实例对象,等于说不同的资源,所以也不会竞争锁。
第5 第6种案例说明,如果在静态方法上加锁,那么锁的是类,也就是说不管你有多少个实例,但其实都是同一个类,所以不同的实例也会被锁住。
第7 第8种案列说明,静态方法上加synchronized代表类锁,普通方法加锁是实例锁,这两种锁不产生冲突,不会互相竞争。
3、synchronized的三种应用方式:
1)作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
2)作用于代码块,对括号里配置的对象加锁。
3)作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
第一 第三种跟上述案例分析的是一样的,着重说一下第二中静态代码块:
对于代码块:
代码块的锁也有两种,一种是synchronized中加一个实例对象,一种是加类.class.这两种锁也分别代表实例锁跟类锁。
⚙️重点区别:
场景 | 锁对象 | 影响范围 |
---|---|---|
synchronized(obj) | 普通对象实例 obj | 同一个对象的线程互斥,不同对象互不影响。 |
synchronized(类名.class) | 类的Class对象(全局唯一) | 所有线程,不管用哪个对象实例,都会互斥。 |
📌形象解释:
1️⃣ synchronized(obj):
假如你 new 出很多个对象,每个对象自己用synchronized(this)
,那每个对象都像是自己的小屋,互不干扰。
2️⃣ synchronized(类名.class):
锁住的是整个类,哪怕不同的对象实例,线程也必须抢同一个锁,类似一个“工厂总门”,只有拿到钥匙的人才能进去,别人都得等。
🌰举个例子:
class MyClass {public void instanceMethod() {synchronized(this) {System.out.println("对象锁:锁住的是当前实例");}}public static void staticMethod() {synchronized(MyClass.class) {System.out.println("类锁:锁住的是整个类");}}
}
-
synchronized(this)
:只会锁住这个对象实例,不同实例之间互不干扰。 -
synchronized(MyClass.class)
:锁住整个类,无论用哪个对象调用这个代码,只要一个线程进了,其他线程必须等!
4、解释分析
在java虚拟机种,class loader类加载器把 Car.class文件读进来,Car class就是类锁,这个就是模板,由一份模板可以生成 car1、car2、car3 三个实例对象,这是三个不同的对象但是均来自于一个模板。所以类锁对应的就是Car Class,在方法区中有且仅有一份,但是对于我们的对象锁,new出来的实例对象,在jvm的堆中。所以类锁跟对象锁,加锁的对象跟地方都不一样,自然就会产生不同的效果。
5、从字节码角度分析synchronized实现
从字节码角度分析,需要借助两个命令:
javap -c ***.class 对代码进行反编译
javap -v ***.calss 对文件进行反编译,但是会输出更多附加信息(包括行号、本地变量表、反汇编等详细信息)
1)使用javap -c 反编译一个同步代码块的class文件: