`
javaeyetodj
  • 浏览: 427060 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

详解多线程同步规则【一】

阅读更多
转自http://earthrelic.blog.sohu.com/157151118.html
熟悉 Java 的多线程的一般都知道会有数据不一致的情况发生,比如两个线程在操作同一个类变量时,而保护数据不至于错乱的办法就是让方法同步或者代码块同步。同步时非原子操作就得同步,比如一个简单的 1.2+1 运算也该同步,以保证一个代码块或方法成为一个原子操作。

简单点说就是给在多线程环境中可能会造成数据破坏的方法,做法有两种,以及一些疑问:

1. 不论是静态的或非静态的方法都加上 synchronized 关键字,那静态的方法和非静态的方法前加上 synchronized 关键字有区别吗?

2. 或者在可疑的代码块两旁用 synchronized(this) 或 synchronized(someObject) 包裹起来,而选用 this 还是某一个对象--someObject,又有什么不同呢?

3. 对方法加了 synchronized 关键字或用 synchronized(xxx) 包裹了代码,就一定能避免多线程环境下的数据破坏吗?

4. 对方法加 synchronized 关键字与用 synchronized(xxx) 同步代码块两种规避方法又有什么分别和联系呢?

为了理解上面的问题,我们还得从 Java 对线程同步的原理上说起。我们知道 Java 直接在语言级上支持多线程的。在多线程环境中我们要小心的数据是:

1) 保存在堆中的实例变量

2) 保存在方法区中的类变量。

现实点说呢就是某个方法会触及到的同一个变量,如类变量或单态实例的实例变量。避免冲突的最容易想到的办法就是同一时刻只让一个线程去执行某段代码块或方法,于是我们就要给一段代码块或整个方法体标记出来,被保护的代码块或方法体在 Java 里叫做监视区域(Monitor Region),类似的东西在 C++ 中叫做临界区(Critical Section)。

比如说一段代码:




01.public void operate() {
02.    flag ++;
03.    try {
04.        //休眠一个随机时间,让不同线程能在此交替执行
05.        Thread.sleep(new Random().nextInt(10));
06.    } catch (InterruptedException e) {
07.        e.printStackTrace();
08.    }
09.    flag --;
10.    System.out.println("Current flag: " + flag);
11.}



用 synchronized 标记起来的话,可以写成:




01.public void operate() {
02.    synchronized(this){//只需要把可能造成麻烦的代码标记起来
03.        flag ++;
04.        try {
05.            //休眠一个随机时间,让不同线程能在此交替执行
06.            Thread.sleep(new Random().nextInt(5));
07.        } catch (InterruptedException e) {
08.            e.printStackTrace();
09.        }
10.        flag --;
11.         
12.        System.out.println("Current flag: " + flag);
13.    }
14.     
15.    //some code out of the monitor region
16.    System.out.println("线程安全的代码放外面就行啦");
17.     
18.}

那如果我们悲观,或许是偷点懒,直接给方法加个 synchronized 关键字就行,就是这样:




01.public synchronized void operate() {
02.    flag ++;
03.    try {
04.        //休眠一个随机时间,让不同线程能在此交替执行
05.        Thread.sleep(new Random().nextInt(10));
06.    } catch (InterruptedException e) {
07.        e.printStackTrace();
08.    }
09.    flag --;
10.    System.out.println("Current flag: " + flag);
11.}

给方法加个关键字 synchronized 其实就是相当于把方法中的所有代码行框到了 synchronized(xxx) 块中。同步肯定会影响到效率,这也是大家知道的,因为它会造成方法调用的等待。方法中有些代码可能是线程安全的,所以可不用包裹在 synchronized(xxx) 中。

那么只要给方法加上关键字 synchronized,或者 synchronized(this) 括起一段代码一定就是线程安全的吗?现在来看个例子,比如类 TestMultiThread:




01.package com.unmi;
02. 
03.import java.util.Random;
04. 
05./**
06. * 多线程测试程序
07. * 
08. * @author Unmi
09. */
10.public class TestMultiThread {
11. 
12.    // 一个标志值
13.    private static int flag = 1;
14. 
15.    /**
16.     * @param args
17.     */
18.    public static void main(String[] args) {
19.        new Thread("Thread-01") {
20.            public void run() {
21.                new TestMultiThread().operate();
22.            }
23.        }.start(); // 启动第一个线程
24. 
25.        new Thread("Thread-02") {
26.            public void run() {
27.                new TestMultiThread().operate();
28.            }
29.        }.start(); // 启动第二个线程
30.    }
31. 
32.    /**
33.     * 对 flag 进行一个自增,然后自减的操作,正常情况下 flag 还应是 1
34.     */
35.    public void operate() {
36.        flag++;
37.        try {
38.            // 增加随机性,让不同线程能在此交替执行
39.            Thread.sleep(new Random().nextInt(5));
40.        } catch (InterruptedException e) {
41.            e.printStackTrace();
42.        }
43.        flag--;
44. 
45.        System.out.println("Thread: " + Thread.currentThread().getName()
46.                + " /Current flag: " + flag);
47.    }
48.}

有一个静态变量 flag = 1,还有一个实例方法 operate() 方法,对 flag 进行 flag ++,然后 flag -- 操作,最后输出当前的 flag 值,理想情况下,输出的 flag 应该仍然是 1。可实际上是两个线程执行行的输出很大的机会得到:

Thread: Thread-01 /Current flag: 2
Thread: Thread-02 /Current flag: 1

好,我们也知道那是因为线程在对 flag 操作不同步引起的,对照代码来理解就是:

当线程 Thread-01 执行到 flag ++ 后,此时 flag 等于 2,有个 sleep,能使得 Thread-01 稍事休息
此时线程 Thread-02 进入方法 operate,并相执行 flag ++,即当前的 2 ++,flag 为 3 了,碰到 sleep 也停顿一下
Thread-01 又再执行剩下的 flag --,在当前的 flag 为 3 基础上进行 flag --,最后输出 Thread: Thread-01 /Current flag: 2
Thread-02 接着执行 flag --,当前 flag 为 2,flag -- 后输出就是 Thread: Thread-02 /Current flag: 1

注:在 flag++ 与 flag -- 之前加个随机的 sleep 是为了模拟有些环境,比如某个线程执行快,另一个线程执行慢的可能性,多执行几遍,你也能看到另外几种输出:

Thread: Thread-02 /Current flag: 2
Thread: Thread-01 /Current flag: 1



Thread: Thread-02 /Current flag: 1
Thread: Thread-01 /Current flag: 1



Thread: Thread-01 /Current flag: 1
Thread: Thread-02 /Current flag: 1

出现不同状况的可能性都好理解。为确保 flag 的完整性,于是加上 synchronized(this) 把代码 flag ++ 和 flag -- 代码块同步了,最后的 operate() 方法的代码如下:




01.public void operate() {
02.    synchronized(this){//只需要把可能制造麻烦的代码标记起来
03.        flag ++;
04.        try {
05.            //增加随机性,让不同线程能在此交替执行
06.            Thread.sleep(new Random().nextInt(5));
07.        } catch (InterruptedException e) {
08.            e.printStackTrace();
09.        }
10.        flag --;
11.         
12.        System.out.println("Thread: "+ Thread.currentThread().getName() + 
13.                " /Current flag: " + flag);
14.    }
15.     
16.    //some code out of the monitor region
17.    System.out.print("");
18.}

再次执行上面的测试代码,仍然会看到如下的输出:

Thread: Thread-01 /Current flag: 2
Thread: Thread-02 /Current flag: 1

而不是我们所期盼的两次输出 flag 值都应为 1 的结果。难道 synchronized 也灵验了,非也,玄机就在 synchronized() 中的那个对象的选取上,我们用 this 在这里不可行。

现在来解析跟在 synchronized 后面的那个对象参数。在 JVM 中,每个对象和类(其实是类本身的实例) 在逻辑上都是和一个监视器相关联的,监视器指的就是被同步的方法或代码块。这句话不好理解,主谓调换一下再加上另外几条规则:

1) Java 程序中每一个监视区域都和一个对象引用相关联,譬如 synchronized(this)  中的 this 对象。

2) 线程在进入监视区域前必须对相关联的对象进行加锁,退出监视区域后释放该锁。

3) 不同线程在进入同一监视区域不能对关联对象加锁多次。意即 A 线程在进入 M 监视区域时,获得了关联对象 O 的锁,在未释放该锁之前,另一线程 B 无法获得 M 监视区域的对象锁,此时就要等待 A 线程释放锁。但是 A 线程可能对 O 加锁多次(递归调用就可能出现这种情况)。

4) 线程只能获得了监视区域相关联的对象锁,才能执行监视区域内的代码,否则等待。JVM 维护了一个监视区域相关联的对象锁的计数,比如 A 线程对监视区域 M 相关联的 O 对象加锁了 N 次,计数则为一,要等锁全部释放了,计数即为零,此时另一线程 B 才能获得该对象锁。

好了,明白了线程,监视区域,相关联对象,对象锁的关系之后,我们就可以理解上面的程序为何加了 synchronized(this)  后还是未能如我们所愿呢?

监视区域与 this 对象相关联的
线程 Thread-01 进入监视区域时,对此时的 this 对象加锁,也就是获得了 this 对象锁。因为代码中有意加了个 sleep 语句,所以还不会立即释放该锁
这时候线程 Thread-02 要求进入同一监视区域,也试图获得此时的 this 对象锁,并执行其中的代码

从执行的结果,或者可进行断点调试,你会发现,尽管 Thread-01 获得了 this 对象锁后,还未释放该锁时,另一线程 Thread-02 也可轻而易举的获得 this 对象锁,并同时执行监视区域中的代码。

前面不是说过,某一线程对监视区域相关联对象加锁上后,另一线程将不能同时对该对象加锁,必须等待其他线程释放该对象锁才行吗?这句话千真万确,原因就在于此 this 非彼 this,也就是 this 指代的对象一直在变。Thread-01 进入监视区域是对 this 代表的 new TestMultiThread() 对象,即使你没有释放该锁,Thread-02 在进入同一监视区域时当然还能对 this 代表的另一 new TestMultiThread() 对象加锁的。

所以说这里机械的框上 synchronized(this) 其实起不到任何效果,正确的做法,可以写成

synchronized(TestMultiThread.class){...};  //TestMultiThread 类实例在同一个 JVM 中指的就是同一个对象(不同 ClassLoader 时不考虑)

或者预先在 TestMultiThread 中声明一个静态变量,如 private static Object object = new Ojbect();,然后 synchronized 部分写成

synchronized(object){...}

然后再执行前面的测试代码,保管每回执行后,输出的两次 flag 的值都为 1。

又有人会有疑问了,难道就不能用 synchronized(this) 这样的写法了吗?这种写法也没少见啊,不能说人家总是错的吧。在有些时候,能确保每一次 this 会指向到与前面相同的对象时都不会有问题的,如单态类的 this。

到这里,前面的第二个疑问也同时得到解决了,答案是不一定,看关联对象是否同一个,有时候应分析实际的运行环境。

分享到:
评论

相关推荐

    Java多线程和并发知识整理

    1.1为什么需要多线程 1.2不安全示例 1.3并发问题的根源 1.4JMM 1.5线程安全的分类 1.6线程安全的方法 二、线程基础 2.1状态 2.2使用方式 2.3基础机制 2.4中断 2.5互斥同步 2.6线程合作 三、...

    汪文君高并发编程实战视频资源下载.txt

    │ 高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号.mp4 │ 高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制中抽取出来.mp4 │ 高并发编程第一阶段07讲、策略模式在Thread和Runnable...

    Linux多线程服务端编程:使用muduo C++网络库

    《Linux多线程服务端编程:使用muduo C++网络库》主要讲述采用现代C++在x86-64 Linux上编写多线程TCP网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即one loop per thread。...

    汪文君高并发编程实战视频资源全集

    │ 高并发编程第一阶段05讲、采用多线程方式模拟银行排队叫号.mp4 │ 高并发编程第一阶段06讲、用Runnable接口将线程的逻辑执行单元从控制中抽取出来.mp4 │ 高并发编程第一阶段07讲、策略模式在Thread和Runnable...

    java基础案例与开发详解案例源码全

    12.4.1 线程同步的方法317 12.4.2 对象锁319 12.4.3 wait和notify方法320 12.4.4 死锁322 12.5 集合类的同步问题323 12.5.1 使用synchronized同步块324 12.5.2 使用集合工具类同步化集合类对象324 12.5.3 使用JDK5.0...

    Java基础知识点总结.docx

    线程同步 49 线程通信 52 线程池 58 死锁 64 线程相关类 65 十三、 同步★★★★★ 67 十四、 Lock接口 70 十五、 API 71 < java.lang >String字符串:★★★☆ 71 < java.lang >StringBuffer字符串缓冲区:★★★☆...

    宋劲彬的嵌入式C语言一站式编程

    3.4. 其它线程间同步机制 4. 编程练习 36. TCP/IP协议基础 1. TCP/IP协议栈与数据包封装 2. 以太网(RFC 894)帧格式 3. ARP数据报格式 4. IP数据报格式 5. IP地址与路由 6. UDP段格式 7. TCP协议 7.1. 段格式 7.2. ...

    疯狂JAVA讲义

    7.8.3 同步控制 288 7.8.4 设置不可变集合 288 7.9 烦琐的接口:Enumeration 289 7.10 本章小结 290 本章练习 290 第8章 泛型 291 8.1 泛型入门 292 8.1.1 编译时不检查类型的异常 292 8.1.2 手动实现编译时...

    [14本经典Android开发教程]-8-Linux内核阅读心得体会

    读核感悟 伪装现场 内核线程: 17 读核感悟 伪装现场 信号通信 19 读核感悟 kbuild系统 内核模块的编译 22 读核感悟 kbuild系统 编译到内核和编译成模块的区别 24 读核感悟 kbuild系统 make bzImage的过程 26 读核...

    嵌入式Linux应用程序开发标准教程(第2版全)

    第9章 多线程编程 9.1 Linux线程概述 9.1.1 线程概述 9.1.2 线程机制的分类和特性 9.2 Linux线程编程 9.2.1 线程基本编程 9.2.2 线程之间的同步与互斥 9.2.3 线程属性 9.3 实验内容——“生产者消费者”实验 9.4 本...

    asp.net知识库

    应用系统的多语言支持 (一) 应用系统的多语言支持 (二) 自动返回上次请求页面(小技巧) ASP.NET 2.0 控件 ASP.NET 2.0 验证控件新的功能 DataGridView中如何在textbox列中限制输入。 ASP.NET 2.0构建动态导航的...

    Java开发技术大全 电子版

    第8章Java的多线程机制266 8.1线程的概念266 8.1.1多线程的特点266 8.1.2线程的状态267 8.2Thread类268 8.2.1Thread类的构造方法268 8.2.2Thread类的常用方法268 8.3多线程程序的编写269 8.3.1利用Thread的...

    亮剑.NET深入体验与实战精要2

    4.11 创建多线程应用程序 191 4.12 WinForm开发常见问题 194 4.12.1 如何设置运行时窗体的起始位置 194 4.12.2 如何使一个窗体在屏幕的最顶端 194 4.12.3 实现窗体渐显效果 194 4.12.4 设置窗口背景为渐变色 195 ...

    亮剑.NET深入体验与实战精要3

    4.11 创建多线程应用程序 191 4.12 WinForm开发常见问题 194 4.12.1 如何设置运行时窗体的起始位置 194 4.12.2 如何使一个窗体在屏幕的最顶端 194 4.12.3 实现窗体渐显效果 194 4.12.4 设置窗口背景为渐变色 195 ...

Global site tag (gtag.js) - Google Analytics