前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >对java多线程里Synchronized的思考

对java多线程里Synchronized的思考

作者头像
用户1153489
发布2018-01-12 16:47:17
5030
发布2018-01-12 16:47:17
举报

    Synchronized这个关键字在多线程里经常会出现,哪怕做到架构师级别了,在考虑并发分流时,也经常会用到它。在本文里,将通过一些代码实验来验证它究竟是“锁”什么。

    在启动多个线程后,它们有可能会并发地执行某个方法或某块代码,从而可能会发生不同线程同时修改同块存储空间内容的情况,这就会造成数据错误。   

1    //需要同步的对象类
2    class SynObject {
3        // 定义两个属性
4        int i;
5        int j;
6        // 把两个属性同时加1
7        public void add() {
8            i++;
9            // 睡眠500毫秒
10            try {
11                Thread.sleep(500);
12            } catch (InterruptedException e) {
13                e.printStackTrace();
14            }
15            j++;
16        // 打印当前i,j的值
17        System.out.println("Operator:+  Data:i=" + i + ",j=" + j);
18      }
19      // 把两个属性同时减1
20      public void minus() {
21        i--;
22        // 睡眠500毫秒
23        try {
24            Thread.sleep(500);
25        } catch (InterruptedException e) {
26             e.printStackTrace();
27        }
28        j--;
29        // 打印当前i,j的值
30        System.out.println("Operator:-  Data:i=" + i + ",j=" + j);
31      }
32    }

    从上文的第2到第32行里,我们定义了一个SynObject类,在其中的第3和第4行里,我们定义了i和j两个属性。

    在第7行的add方法里,我们是把i和j两个属性的值都加1,为了提升该方法被抢占的概率,在第11行里,我们通过sleep方法让该线程睡眠500毫秒。

    同样地我们在第20行定义了minus方法,在其中我们是把i和j都减1,同样在第24行添加了sleep方法。    

33    class SynThreadAdd extends Thread {
34        // 需要同步的对象
35        SynObject o;
36        // 接受需要操作的那个对象的代参构造函数
37        public SynThreadAdd(SynObject o) {
38            this.o = o;
39        }
40        // 覆写线程对象的run方法定义真正的执行逻辑
41        public void run() {
42            for (int i = 0; i < 3; i++) {
43                o.add();
44            }
45        }
46    }

    在第33行里,我们通过extends Thread的方式创建了一个线程对象SynThreadAdd,在第37行的构造函数里,设置待操作的对象o,在第41行的run方法里,我们通过了一个for循环调用了SynObject对象的add方法,对其中的i和j属性进行加的操作。    

47    class SynThreadMinus extends Thread {
48        SynObject o;
49        public SynThreadMinus(SynObject o) {
50            this.o = o;
51        }
52        public void run() {
53            for (int i = 0; i < 3; i++) {
54                o.minus();
55            }
56        }
57    }

    第47行的SynThreadMinus对象和刚才定义的SynThreadAdd对象很相似,同样是通过extends Thread的方式创建了一个线程对象,不同的是,在第52行的run方法里,是通过一个for循环调用了SynObject对象的minus方法,对其中的i和j属性进行减操作。    

58    public class ThreadError {
59        // 测试主函数
60        public static void main(String args[]) {
61            // 实例化需要同步的对象
62            SynObject o = new SynObject();
63            // 实例化两个并行操作该同步对象的线程
64            Thread t1 = new SynThreadAdd(o);
65            Thread t2 = new SynThreadMinus(o);
66            // 启动两个线程
67            t1.start();
68            t2.start();
69        }
70    }

    在main函数里,我们在第62行里创建了一个SynObject对象,在第64和65行里分别创建了SynThreadAdd和SynThreadMinus这两个线程对象,并在67和68这两行里启动了这两个线程。

    我们来看下运行结果,如果大家多次运行,每次的结果会不相同,但不影响下文的讲解。    

1    Operator:+  Data:i=0,j=1
2    Operator:-  Data:i=1,j=0
3    Operator:+  Data:i=0,j=1
4    Operator:-  Data:i=1,j=0
5    Operator:-  Data:i=0,j=-1
6    Operator:+  Data:i=0,j=0

    在第1行里,我们看到的是执行完add方法后的输出,奇怪的是,在这个方法里,我们明明是对i和j这两个对象进行加操作,按理说应当i和j都是1,但这里的值确出乎我们意料,同样地,第2到第5行的输出里,i和j的值也不一致。

    原因出在多线程竞争上,这里的两个线程t1和t2会分别通过add和minus方法操作SynObject对象里的i和j,在多线程并发的情况下,完全有可能按如下表7.1所列的次序执行上述代码。

次序

t1的动作

t2的动作

i

j

1

通过t1.start();方法启动

0

0

2

通过t2.start();方法启动

3

t1通过run方法执行o.add操作

0

0

4

在add方法里执行i++

1

0

5

在add方法里执行sleep方法进入到阻塞状态

1

0

6

处于阻塞状态

t2通过run方法执行o.minus操作

1

0

7

处于阻塞状态

在minus方法里执行i--

0

0

8

处于阻塞状态

在minus方法里执行sleep方法进入到阻塞状态

0

0

9

sleep时间到,恢复执行

处于阻塞状态

0

0

10

执行j++并输出i和j

处于阻塞状态

0

1

    上表解释了为什么在第1行输出里i和j不一致的原因,从中我们能看到,一旦t1通过add方法操作SynObject类型的o对象后,t2线程通过minus方法,也有机会同时地操作这个对象,这样, t1的add方法没执行完(尚未完全地完成对i和j操作),t2的minus方法就插进来并发地操作同一个SynObject类型o对象,所以就导致了数据不一致的问题。这里我们解释了第1行的输出,后继输出的不一致现象是由于同样的原因造成的。

    也就是说,在多线程并发的情况下,多个线程有可能会像上例那样,通过不同的方法同时更改同一个资源(一般把它叫临界资源),这样就会造成临界资源紊乱的情况。

    为了避免这样的问题,我们可以在SyncObject类的add和minus方法前加上synchronized关键字,改写后的SynObject类代码如下所示。    

1    class SynObject {
2        // 定义两个属性,这部分代码不变
3        int i;
4        int j;
5        // 给这个方法加上了synchronized关键字,而且sleep时间是5秒
6        public synchronized  void add() {
7            i++;
8            // 睡眠5秒
9            try {
10                Thread.sleep(5000);
11            } catch (InterruptedException e) {
12                e.printStackTrace();
13           }
14         j++;
15         // 打印当前i,j的值
16         System.out.println("Operator:+  Data:i=" + i + ",j=" + j);
17        }
18        // 也加了synchronized关键字
19         public  synchronized  void minus() {
20            i--;
21            //依然是睡眠500毫秒
22            try {
23                Thread.sleep(500);
24            } catch (InterruptedException e) {
25                e.printStackTrace();
26            }
27            j--;
28            // 打印当前i,j的值
29            System.out.println("Operator:-  Data:i=" + i + ",j=" + j);
30        }

    这里我们是把synchronized关键字作用到方法上。在给出正确的讲解前,我们先列个似是而非的错误的说法,这些错误的说法看上去很有迷惑性,请大家在阅读后一定要明辨是非。

    错误说法:如果我们把synchronized作用在方法上,那么就相当于给这个方法加了锁,也就是说在一个时间段里只可能有一个线程来访问这个方法。

    反驳的依据:我们用反证法,假设上述说法是正确的,加上synchronized后,假设add和minus方法是只能同时被一个线程调用,那么有这种情况,t1调用add,t2调用minus,(这符合假设的说法)由于add里睡眠时间是5秒,而minus是0.5秒,这样minus方法还是有足够多的时间来修改j的值,从而会导致i和j不一致,但我们不论运行多少次程序,均不会再出现i和j不一致的情况,所以这种说法是错的。

    正确的说法:一旦给方法加了synchronized,就相当于给调用该方法的对象加了锁,比如这里的add方法加了synchronized,调用的写法是o.add();,也就是说是给o对象加了把锁,在o.add调用结束之前,其它线程是无法得到o对象的控制和访问权的。

    正确说法的依据:在调用add方法时,哪怕我们在其中sleep了5秒(大家甚至可以修改成睡眠10秒,效果更有说明意义),在这5秒里哪怕我们给了t2线程足够多的时间让它有机会去执行minus去造成i和j不一致,但从输出结果上来看,不会出现i和j不一致的现象。正是因为给o对象加了锁,那么在执行add时就不怕其它线程来抢占o对象了,从而也就不会有数据不一致的问题了。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-12-06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云硬盘
云硬盘(Cloud Block Storage,CBS)为您提供用于 CVM 的持久性数据块级存储服务。云硬盘中的数据自动地在可用区内以多副本冗余方式存储,避免数据的单点故障风险,提供高达99.9999999%的数据可靠性。同时提供多种类型及规格,满足稳定低延迟的存储性能要求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档