Java多线程/并发04、synchronized同步

来源:互联网 发布:怎么看淘宝卖家信誉 编辑:程序博客网 时间:2024/05/20 06:52

一、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){}。

0 0
原创粉丝点击