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对象了,从而也就不会有数据不一致的问题了。

我的博客即将同步至腾讯云+社区,邀请大家一同入驻。

对java多线程里Synchronized的思考的更多相关文章

  1. Java多线程-同步:synchronized 和线程通信:生产者消费者模式

    大家伙周末愉快,小乐又来给大家献上技术大餐.上次是说到了Java多线程的创建和状态|乐字节,接下来,我们再来接着说Java多线程-同步:synchronized 和线程通信:生产者消费者模式. 一.同 ...

  2. Java多线程:synchronized的可重入性

    从Java多线程:线程间通信之volatile与sychronized这篇文章中我们了解了synchronized的基本特性,知道了一旦有一个线程访问某个对象的synchronized修饰的方法或代码 ...

  3. Java 多线程之 synchronized 和 volatile 的比較

    概述 在做多线程并发处理时,常常须要对资源进行可见性訪问和相互排斥同步操作.有时候,我们可能从前辈那里得知我们须要对资源进行 volatile 或是 synchronized 关键字修饰处理.但是,我 ...

  4. Java多线程同步 synchronized 关键字的使用

    代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A, ...

  5. Java多线程同步方法Synchronized和volatile

    11 同步方法  synchronized – 同时解决了有序性.可见性问题  volatile – 结果可见性问题 12 同步- synchronized synchronized可以在任意对象上加 ...

  6. java 多线程8 : synchronized锁机制 之 方法锁

    脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数 ...

  7. 四、java多线程核心技术——synchronized同步方法与synchronized同步快

    一.synchronized同步方法 论:"线程安全"与"非线程安全"是多线程的经典问题.synchronized()方法就是解决非线程安全的. 1.方法内的变 ...

  8. 关于JAVA多线程并发synchronized的测试与合理使用

    在项目开发中, 或许会碰到JAVA的多线程处理, 为保证业务数据的正常, 必须加上锁机制,  常用的处理方法一般是加上synchronized关键字, 目前JDK版本对synchronized已经做了 ...

  9. Java多线程学习——synchronized锁机制

    Java在多线程中使用同步锁机制时,一定要注意锁对对象,下面的例子就是没锁对对象(每个线程使用一个被锁住的对象时,得先看该对象的被锁住部分是否有人在使用) 例子:两个人操作同一个银行账户,丈夫在ATM ...

随机推荐

  1. fedora 19 gnome 3.8 关闭笔记本盖子的动作

    gnome-tweak-tool里没有了相关选项,但是又想让关闭盖子不挂起,后来看看才知道gnome3.8不再提供这功能,而是交给systemd来处理,所以估计用dconf-edit在gnome的po ...

  2. 如何利用Oracle VM Templates 在几分钟内部署Oracle Real Application Clusters (RAC)

    本文未经授权,禁止一切形式的转载.如果对本文有任何疑问可以通过以下方式和我交流: 邮箱: jiangxinnju@163.com 博客园地址: http://www.cnblogs.com/jiang ...

  3. velocity的基础使用

    velocity的基本使用要求:掌握jsp的jstl技术,因为velocity的用法和jstl非常相似.语法上差别不大,但是velocity的示例明显比jstl少,解释也少,所以使用velocity必 ...

  4. (function($){...})(jQuery)和$(document).ready(function(){}) 的区别

    (function($){...})(jQuery)  实际上是执行()(para)匿名函数,只不过是传递了jQuery对象.   立即执行函数:相当于先申明一个函数,声明完后直接调用: 用于存放开发 ...

  5. Knight Moves

    Problem Description A friend of you is doing research on the Traveling Knight Problem (TKP) where yo ...

  6. 初入红尘——在安联IT实习的一点感受(未完......)

    文章很短,只有800字. 从踏进安联的大门开始,我便入了红尘. 安联的迎客之道 “花径不曾缘客扫,蓬门今始为君开.”我的第一个贵人就是前台的美君姐.由于路况不熟,所以我没把握好时间,到的时候比约定的面 ...

  7. git使用教程之git基础

    1 获取Git仓库 在现有目录中初始化仓库 git init 克隆现有的仓库 git clone https://github.com/yangwang12345/node_test.git Git ...

  8. JAVA基础-JDBC(一)

    一.JDBC的简介 JDBC(Java Data Base Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,由与各种数据库都有着一套自己的规范,JAVA对其操 ...

  9. 通过jettymain启动项目

    jetty是一个比tomcat轻量级好多的服务器,通过简单的配置即可成功的跑起来,编译过程要短一点,可以一定程度上提高开发效率 首先,要下载下来jetty的包,mvn信息如下: <depende ...

  10. ssm框架下web项目,web.xml配置文件的作用

    1. web.xml中配置了CharacterEncodingFilter,配置这个是拦截所有的资源并设置好编号格式. encoding设置成utf-8就相当于request.setCharacter ...