一、synchronized使用
多个线程并发时,经常面临的问题就是会操作相同的资源。比如商品下单,库存量修改,转帐等。
举个例子:
家里有一口炒菜的锅,老大要做辣椒炒肉(制作步骤:a1.放肉,a2.放盐,a3.放辣椒),老二要做番茄炒蛋(制作步骤,b1.放蛋,b2.放盐,b3.放番茄)。两人把食材准备好后,都要抢着炒自己拿手的菜。老大往锅里放肉(a1),老二这时挤过来,放蛋(b1),放盐(b2);老大又挤过来,放盐(a2)…….后面自己脑补,这菜还能吃吗?在这里老大是线程A,老二是线程B,锅是资源。
下面我们模拟一下这种场景:
package JConcurrence.Study;
/*定义烹饪外部类*/
class Pan {
/*烹饪方法,该方法输出步骤*/
public void Cook(String[] steps) {
for (int i = 0; i < steps.length; i++) {
/*模拟竞争造成的线程等待,这样效果明显些*/
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(steps[i]);
}
System.out.println("");
}
/*青椒炒肉制作步骤:a1.放肉,a2.放盐,a3.放辣椒 a4 a5....*/
String[] steps_LaJiaoChaoRou={"a1.","a2.","a3.","a4.","a5.","a6.","a7.","a8.","a9.","a10.","a11.","a12.","a13.","a14."
,"a15.","a16.","a17.","a18.","OK:辣椒炒肉"};
/*番茄炒蛋制作步骤:b1.放蛋,b2.放盐,b3.放番茄......*/
String[] steps_FanQieChaoDan={"b1.","b2.","b3.","b4.","b5.","b6.","OK:番茄炒蛋"};
}
public class chapter2 {
public static void main(String[] args) {
final Pan pan=new Pan();
/*线程1:老大炒青椒炒肉。*/
new Thread(){
public void run() {
/*为了看出错乱效果,这里用死循环,一段时间后手工点击停止运行按钮*/
while (true) {
try {
/*模拟青椒炒肉需要5秒;*/
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pan.Cook(pan.steps_LaJiaoChaoRou);
}
}
}.start();
/*线程2:老二炒番茄炒蛋。*/
new Thread(){
public void run() {
/*为了看出错乱效果,这里用死循环,一段时间后手工点击停止运行按钮*/
while (true) {
try {
/*模拟番茄炒蛋需要2秒;*/
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pan.Cook(pan.steps_FanQieChaoDan);
}
}
}.start();
}
}
输出结果:
这是我的电脑输出的结果,红框标出来的番茄炒蛋和辣椒炒肉这两盘菜肯定不好吃,因为里面步骤和放入的材料都不对。
我们希望的是炒菜是一个连贯的事务工作,中间环节不可断开。每个人在做菜的时侯,要求是独占这口锅完成炒菜的动作,谁也不能中途打断。
为了解决这个问题,可以使用synchronized同步代码块来对公共部分进行同步操作。
在上面这个场景中,公共部分都就是Cook方法,每个人在调用Cook方法时,都不允许别人同时也执行Cook方法。因此对于线程来说,它希望的是自己独占Cook方法,直到Cook方法运行结束。
所以,我们只需要修改Cook方法就行了。下面我们借助Synchronzied关键字来实现。
Synchronzied关键字有两种用法:synchronized 方法和 synchronized 块:
1、第一种方法:在Cook方法上加关键字synchronized
/*烹饪方法,该方法输出步骤
加入synchronized关键字*/
public synchronized void Cook(String[] steps) {
for (int i = 0; i < steps.length; i++) {
/*模拟竞争造成的线程等待,这样效果明显些*/
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(steps[i]);
}
System.out.println("");
}
可以正常输出:
2、第二种方法:定义锁,使用synchronized{}块
/*定义一个锁,锁可以是任意类型,如:Object,Int,String等,值也可以任意,但需要保证所有访问该值的线程得到同样的结果*/
private String lock = "anything";
/*烹饪方法,该方法输出步骤*/
public void Cook(String[] steps) {
/*用synchronized块包含公共执行部分*/
synchronized(lock){
for (int i = 0; i < steps.length; i++) {
/*模拟竞争造成的线程等待,这样效果明显些*/
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(steps[i]);
}
System.out.println("");
}
}
可以正常输出:
总结:Synchronzied可以用来修改对象中的方法,将对象加锁。不管哪个线程,每次运行到这个方法或者语句块时,都要检查这个方法或语句块有没有其它线程占用,有的话要等这个线程运行完成后。如果没有的话,直接运行。
最后补充一点:把synchronized关键字放在类的前面,这个类中的所有方法都是同步方法。
二、synchronized锁的对象
在上例中,synchronized方法在执行中并没有像synchronized同步代码块 一样,声明一个自定义的锁对像。那么它锁住的是什么呢?
先看代码:
package JConcurrence.Study;
/*定义外部测试类SyncLockTest*/
class SyncLockTest{
/*使用synchronized同步方法 */
public synchronized void testSyncMethod() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":testSyncMethod:" + i);
}
}
/*使用synchronized同步代码块 */
public void testSyncBlock() {
synchronized ("lock") {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":testSyncBlock:" + i);
}
}
}
}
public class ExecuteDemo {
public static void main(String[] args) {
final SyncLockTest LockTest=new SyncLockTest();
/*使用synchronized方法 */
new Thread(new Runnable() {
public void run() {
/*休眠三秒可以更好的观测错乱效果*/
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*执行*/
LockTest.testSyncMethod();
}
}).start();
/*使用synchronized代码块 */
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*执行*/
LockTest.testSyncBlock();
}
}).start();
}
}
输出结果摘取一段:
Thread-1:testSyncBlock:0
Thread-0:testSyncMethod:0
Thread-1:testSyncBlock:1
Thread-0:testSyncMethod:1
Thread-1:testSyncBlock:2
Thread-0:testSyncMethod:2
Thread-1:testSyncBlock:3
Thread-1:testSyncBlock:4
Thread-1:testSyncBlock:5
Thread-0:testSyncMethod:3
Thread-1:testSyncBlock:6
结果显示两个线程并发执行,交替输出。说明两个方法之间不存在同步。
因为它们锁的对像不同,也就是使用的监视器对象不同。
修改synchronized同步代码块的代码,将synchronized ("lock")
换成synchronized (this)
重新运行结果:
Thread-0:testSyncMethod:97
Thread-0:testSyncMethod:98
Thread-0:testSyncMethod:99
Thread-1:testSyncBlock:0
Thread-1:testSyncBlock:1
Thread-1:testSyncBlock:2
Thread-1:testSyncBlock:3
Thread-1:testSyncBlock:4
Thread-1:testSyncBlock:5
Thread-1:testSyncBlock:6
Thread-1:testSyncBlock:7
可见一个线程完成0-99打印后,另一个线程才从0开始,并非之前的交替进行打印。
由此可知,synchronized 同步方法采用的锁对像,就是实例化对像本身,即this。
现在看看静态方法。我们在testSyncMethod方法前加上static
/*使用synchronized静态方法 */
public static synchronized void testSyncMethod() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":testSyncMethod:" + i);
}
}
Main方法中原来的LockTest.testSyncMethod()改为静态调用SyncLockTest.testSyncMethod()
执行结果如下:
Thread-1:testSyncBlock:15
Thread-1:testSyncBlock:16
Thread-0:testSyncMethod:0
Thread-1:testSyncBlock:17
Thread-0:testSyncMethod:1
Thread-0:testSyncMethod:2
Thread-0:testSyncMethod:3
Thread-0:testSyncMethod:4
Thread-0:testSyncMethod:5
结果显示两个线程并发执行,交替输出。说明两个方法之间不存在同步。
修改synchronized同步代码块的代码,将synchronized (this)
换成synchronized (this.getClass())
或者synchronized (SyncLockTest.class)
(个人建议用this.getClass(),与类名解耦)
重新运行结果摘取:
Thread-0:testSyncMethod:97
Thread-0:testSyncMethod:98
Thread-0:testSyncMethod:99
Thread-1:testSyncBlock:0
Thread-1:testSyncBlock:1
Thread-1:testSyncBlock:2
Thread-1:testSyncBlock:3
Thread-1:testSyncBlock:4
Thread-1:testSyncBlock:5
可见一个线程完成0-99打印后,另一个线程才从0开始,并非之前的交替进行打印。
由此可知,synchronized 静态同步方法采用的锁对象就是类本身(字节码文件对象)。
归纳起来:
1、synchronized代码块的锁可以是任意类型的变量,如string,int,object等,但需要保证任何需要同步执行的线程访问时都是同一个值。
2、synchronized 同步方法的锁是对象(Instance)自己,即this。
3、synchronized 静态(static)同步方法的锁是类本身(字节码文件对象),即this.getClass()或 SyncLockTest.class
再归纳线程争夺锁的规则:
首先,我们把synchronized同步方法(包括static方法)理解成指定了特殊锁(如this,this.getClass)的synchronized{}代码块。
这样理解后,总结起来有三点:
1、多个线程访问同一个对象(如上面的LockTest)内部的synchronized(lockA){}块中代码时,同一时间内,只可能有一个线程能够访问lockA锁住的代码块。这个很好懂,不然还叫什么同步。
2、在对像中有多个拥有同样锁的synchronized( lockA ){}代码块时(如上面的testSyncMethod()和testSyncMethod()都拥有this锁),当一个线程访问任意其中一个synchronized(lockA){}代码块时,其他线程不可以访问所有拥有lockA锁的其它synchronized(lockA){}代码块内容。
3、在对像中有多个拥有不同锁的代码块时,如:synchronized(lockA){},synchronized(lockB){},synchronized(this){},当一个线程访问object中的synchronized(lockA){}代码块时,其它线程仍可以访问synchronized(lockB){},synchronized(this){}。