靓仔,你会synchronized吗?
悲观锁 & 乐观锁
在介绍synchronized之前,需要知道悲观锁&乐观锁。
悲观锁与乐观锁是一种广义上的概念,体现了看待线程同步的不同角度。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定会有其他的线程来修改数据,因此在获取数据的时候会先加锁,以此确保数据不会被其他线程修改。JAVA中,synchronized
关键字和Lock
实现类都是悲观锁。
悲观锁分析图:
乐观锁恰恰与之相反,乐观锁认为自己在使用数据时不会有其他线程修改数据,所以不会加锁,只是在更新数据的时候去判断之前有没有其他线程更新了这个数据。如果这个数据没有被更新,则当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新了,则根据不同实现方式执行不同操作,比如报错或者自动重试。
其实,悲观锁&乐观锁说白了,就是判断线程要不要锁住同步资源,如需要锁住,则是悲观锁,不需要锁住,就是乐观锁。
synchronized
synchronized关键字主要有两种用法,分别是同步方法和同步代码块,被synchronized修饰的方法或者代码块,在同一时间,只能被一个线程访问。
举个栗子:
售票处现在只剩下2张票,但是有10个人来抢票,到底谁可以买到票呢?
传统写法:
public class TicketMgt {
private int ticket = 2; //还剩最后2张票
public void booking() {
if (ticket > 0) {
ticket--;
System.out.print("这张票我买了!");
}
System.out.println(" 还剩:" + ticket + "张。");
}
}
10个壮汉来抢票:
public class Main {
public static void main(String[] args) {
TicketMgt ticketMgt = new TicketMgt();
for (int i = 0; i < 10; i++) {
new Thread(ticketMgt::booking).start();
}
}
}
抢票结果:
这张票我买了! 还剩:0张。
还剩:0张。
这张票我买了! 还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
What? 弄啥嘞,为什么会出现这个结果呢?这不乱套了吗?
嗯哼。是时候体现在出synchronized
的重要性了,只需要用synchronized
修饰booking()
方法就可以啦:
public synchronized void booking() {
......
}
10个壮汉再抢一次,抢票结果:
这张票我买了! 还剩:1张。
这张票我买了! 还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
还剩:0张。
对于这个抢票结果,基本可以达到我们预期的效果了。它的实现原理:synchronized
给TicketMgt
对象进行加锁了,同一时间只允许一个线程对ticket
进行修改。就好比壮汉去售票处买票,synchronized
相当于一把门锁,1#壮汉进去就把门锁上,2#、3#、4#等其他壮汉就在门外等着,1#壮汉买完开锁出来,其他壮汉才能接着进去买,保证每次只能进去一个买票的壮汉。
针对上面的小栗子,稍微讲讲synchronized的两种用法吧:
- 用在方法上,又分实例方法和类方法:
/**
* ①synchronized修饰实例方法
* 锁的是对象,同一时间只能有一个线程访问方法。
*/
public synchronized void doStd() {
// TODO: 2020/7/28
}
/**
* ②synchronized修饰类方法
* 锁住的是类,同一时间只能有一个线程访问这个类
*/
public synchronized static void doStaticStd() {
// TODO: 2020/7/28
}
- 用在方法块上,也分实例方法和类方法:
/**
* ①实例方法
* 当在某个线程中执行这段代码块,该线程会获取this对象的锁,从而使得其他线程无法同时访问该代码块
*/
public void doSynchronizedStd() {
synchronized (this) {
// TODO: 2020/7/28
}
/**
* 当一个线程访问对象的一个synchronized(this)同步代码块时
* 另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
*/
// TODO: 2020/7/28 可被其他线程访问到
}
/**
* ②类方法
* 锁住的是类,这个类的所有对象是同一把锁,效果与synchronized修饰类方法一样
*/
public static void doSynchronizedStaticStd() {
synchronized (MultiThreads.class) {
// TODO: 2020/7/28
}
}
两者区别:
synchronized代码块
的锁粒度要比synchronized方法
小一些,因为synchronized代码块
所在的方法里还可以有其他代码。
synchronized 是非公平锁,也是可重入锁。
- 公平锁:获取不到锁的时候,会自动加入队列,等待线程释放后,队列的第一个线程获取锁
- 非公平锁:获取不到锁的时候,会自动加入队列,等待线程释放锁后所有等待的线程同时去竞争
-
可重入锁:也叫递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞
代码示例:
public synchronized void booking() {
System.out.println("提交订单");
WeChatPay();
}
public synchronized void WeChatPay() {
System.out.println("微信支付");
}
在上面的代码中,类中的两个方法都是被内置锁synchronized
修饰的,booking()
方法中调用WeChatPay()
方法。因为内置锁是可重入的,所以同一个线程在调用booking()
时可以直接获得当前对象的锁,进入WeChatPay()
进行操作。
看到这里,是不是对synchronized并没有那么陌生了呢。非常感谢你能看到最后,如果能帮助到你,是我的荣幸。后期可能还会写一篇关于synchronized实现原理的文章,还有synchronized是如何保证的原子性、顺序性和可见性。
本文地址:https://blog.csdn.net/qq_36270361/article/details/107393278
上一篇: 吃莲子要去莲心吗?如何吃莲子?
下一篇: 过夜饭可以吃吗?隔餐剩米饭的吃法有哪些?