synchronized关键字采用对代码块/方法体加锁的方式解决Java中多线程访问同一个资源时,引起的资源冲突问题。
synchronized 同步锁可分为两种类型,四种表现形式
-
对象锁: 对单个实例对象的独享内存的部分区域加锁
- 修饰非静态方法
- 修饰代码块
-
类锁: 对整个类的共享内存的部分区域加锁
- 修饰静态方法
- 修饰代码块
Find Two Object
沃兹吉索得: 面向对象编程的精髓在于贴合实际,更容易建立易于理解的对象模型。比如设计模式用于面向对象的编程明显更加易于理解。 :)
- 首先我们定义一个Human类:
public class Human {
//所有的Human都有吃晚饭这个动作
public void eatDinner() {
pickupByLeftHand(); //用左手拿起食物
eatFoodByMouth(); //用嘴吃食物
};
//所有的Human都有喝水这个动作
public void drinkWater() {
pickupByRightHand(); //用右手拿起水
drinkWaterByMouth(); //用嘴喝水
}
//看电视
public void watchTV() {
}
}
- 然后我们实例化两个对象:
Human XiaoMing = new Human(); //小明
Human XiaoMingTaBa = new Human(); //小明他爸
Let's have dinner!
小明要吃晚饭了 XiaoMing.eatDinner()
, 忽然他又想喝水了 XiaoMing.drinkWater()
。好吧,他只有一张嘴,同时又喝水又吃饭会被呛到吧?这时我们想了个解决办法:
//使用 synchronized 关键字修饰 eatDinner()
public synchronized void eatDinner() {
pickupByHand(); //用手拿起食物
eatFoodByMouth(); //用嘴吃食物
}
//使用 synchronized 关键字修饰 drinkWater
public synchronized void eatDinner() {
pickupByHand(); //用手拿起水
drinkWaterByMouth(); //用嘴喝水
}
现在当小明在吃饭的时候,他想喝水了,由于我们使用了synchronized修饰这两个动作。小明在喝水之前会先把饭吃完再执行喝水的动作。这样就解决了冲突的问题。
再来分析下watchTV()这个动作:由于这个动作和吃晚饭喝水并没有冲突可以同时进行,所以我们并不需要synchronized关键字来对它进行修饰。
当我们调用synchronized修饰的非静态方法时,所有采用了synchronized修饰的同步方法都会被锁定,所有没有采用synchronized修饰的非同步方法仍然可以执行。
现在我们来探讨一下小明他爸吃饭喝水的问题:虽然我们对小明的eatDinner()方法和drinkWater()方法进行了同步锁定,但是显而易见的是。小明喝水吃饭的动作并不影响小明他爸喝水吃饭的动作。所以我们可以对上面的结论进行完善:
Conclusion1: 使用 synchronized修饰非静态方法
当我们通过某个实例对象A(XiaoMing)访问非静态同步方法(eat or drink)时,所有通过对象A访问非静态同步方法(eat or drink)的操作都会被阻塞直至另一个操作执行完毕,而所有通过对象A访问非静态同步方法的操作(watchTV)仍然可以执行。而当我们通过对象A访问某个同步方法时,仍然可以通过对象B访问任意一个同步方法。(即小明吃饭喝水不影响小明他爸吃饭喝水)
Let's have dinner 再来一次!
现在我们来分析另一种吃饭喝水的情况。小明和小明他爸一起在吃饭,两个人都想喝水了,但是桌子上这时只有1瓶水了,这瓶水两个人谁先抢到谁就能喝。小明的经验告诉他吃饭的同时如果喝水的话会被呛到,所以他只好先把饭吃完再去喝水。而小明他爸吃过的盐比小明吃过的米还多, 经验丰富的他知道吃饭时喝水会被呛到是因为我们只有一张嘴,可是我们有两只手啊。所以他的吃饭和喝水的动作是这样实现的:
public void eatDinner() {
pickupByLeftHand(); //用手拿起食物
synchronized (mouth) {
eatFoodByMouth(); //用嘴吃食物
}
};
public void drinkWater() {
pickupByRightHand(); //用手拿起水
synchronized (mouth) {
drinkWaterByMouth(); //用嘴喝水
}
}
所以他在吃饭的同时,用手拿起了水。好吧,虽然小明和小明他爸吃饭速度差不多,但是当大家都吃完饭的时候水已经在小明他爸手里了。。。。(小明目瞪口呆.jpg)
总的吃饭时间计时是这样的:(如果不存在竞争关系)
小明:timeOf(用右手拿起食物) + timeOf(用嘴吃食物) + timeOf(用左手拿起水) + timeOf(喝水)
小明他爸:imeOf(用右手拿起食物) + timeOf(用嘴吃食物,同时可以用左手拿起水,而不增加吃食物的时间) + timeOf(喝水)
所以又得到了一个结论
**Conclusion2: **对象锁的两种形式的区别
这两种形式没有本质上的不同。区别在于修饰代码块的形式尽可能的精简了需要锁住的同步代码,使的我们的系统在高并发、资源竞争激烈的情况下更加高效。
关于类锁
好吧,我想不出类比的例子了。直接说结论
Conclusion3: 类锁(synchronized修饰静态方法,synchronized(Human.class)修饰代码块)
访问这两种方式修饰的操作时,所有其他的采用synchronized修饰的静态方法和采用synchronized(Human.this)修饰的代码块都会被锁定(静态的非同步方法不会被锁定)。但是不会影响到对象锁,文章开头有说明两种锁锁住的内存区域是不同的!
Conclusion4: 类锁的两种形式的区别
参考对象锁两种形式的区别
The Last Supper
关于synchronized的两种类型,四种形式我们可以得出以下结论
- 当一个线程通过A对象访问一个非静态的同步方法(或同步代码块)时,其他线程通过A对象访问非静态同步方法(或同步代码块)的操作都会被锁定。但是其他线程通过A对象访问非静态非同步方法的操作不会被锁定,而且通过B对象访问非静态同步方法的操作也不会被锁。(因为各个对象的锁是独立的所以叫做对象锁)
- 当一个线程访问静态同步方法或者由类锁修饰的同步代码块时,所有该类的静态方法或者被同一个类锁修饰的同步代码块都会被锁定,其他线程无法访问。
- 类锁和对象锁互不影响。
- 修饰方法和修饰代码块在本质上是一样的,区别只在于修饰代码块的方式在竞争激烈的情况下更加高效。
- 注意区别以下代码的对象锁的持有情况
public class Human {
//XiaoMing.eatDinner();
public synchronized void eatDinner() { //这里的锁是XiaoMing
pickupByLeftHand();
eatFoodByMouth();
};
//XiaoMing.eatDinner2();
public void eatDinner() {
synchronized(this) { //这里的锁是XiaoMing
pickupByLeftHand();
eatFoodByMouth();
}
};
//XiaoMing.drinkWater();
public void drinkWater() {
pickupByRightHand();
synchronized (mouth) { //这里的锁是mouth
drinkWaterByMouth();
}
}
}
附:四种形式的锁
-
对象锁: 对单个实例对象的独享内存的部分区域加锁
- 修饰非静态方法
public synchronized void doSth() {}
- 修饰代码块
public void doSth() {
synchronized (obj) {
}
}
-
类锁: 对整个类的共享内存的部分区域加锁
- 修饰静态方法
public static synchronized void doSth() {}
- 修饰代码块
public void doSth() {
synchronized (ClassA.class) {
}
}