`
javaeyetodj
  • 浏览: 431576 次
  • 性别: 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中的多线程实例详解(自己去运行看结果)

    最后,附带的教学PPT可能会包含线程创建、同步机制、并发工具类的实例分析,以及解决多线程问题的策略等内容,对深入理解和掌握Java多线程编程非常有帮助。通过实践这些实例,读者可以更好地运用多线程技术,提升...

    多线程编程详解.rar

    在IT领域,多线程编程是一项关键技能,尤其是在开发高效能和实时响应的应用程序时。本文将深入探讨多线程编程的概念、原理及其在VC++环境中的应用。 首先,我们来理解什么是“多线程”。在单线程环境中,一个程序...

    基于Java回顾之多线程同步的使用详解

    在Java编程中,多线程同步是一个关键的概念,用于解决并发执行中的数据一致性问题。线程同步确保了多个线程在访问共享资源时按照预定的顺序或规则进行,防止数据竞争和死锁等异常情况发生。本文将深入探讨线程同步的...

    C#用了多线程界面卡死

    ### C#中多线程与界面卡顿问题详解 #### 一、问题概述 在C#应用程序开发中,特别是Windows Forms应用中,界面卡顿是一个常见的问题。这往往发生在使用了多线程的情况下,尽管多线程技术可以有效提高程序性能,但...

    多线程小游戏

    《多线程小游戏详解》 在编程领域,游戏开发是一项技术含量高且充满挑战的工作,尤其是在实现复杂的交互和流畅的用户体验时。本文将探讨一款名为“多线程小游戏”的项目,该程序通过利用多线程技术来提升游戏性能,...

    所有线程同步的方法VC++

    #### 线程同步方法详解 在多线程编程中,线程同步是确保多个线程之间正确、安全地共享数据的关键技术。以下是一些常用的线程同步方法: 1. **wait()**:使一个线程进入等待状态并释放对象的锁。当调用此方法时,...

    JAVA高质量并发详解,多线程并发深入讲解

    - **线程安全问题:** 如何避免共享资源访问冲突,确保多线程环境下的数据一致性。 - **核心API:** - **synchronized关键字:** 实现对象或代码块级别的独占锁,用于保证线程安全。 - **Lock接口:** 更灵活的...

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

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

    人工智能-项目实践-多线程-Paxos算法的多线程实现.zip

    - **线程同步**:通过synchronized关键字、Lock接口(如ReentrantLock)等机制防止数据竞争,保证同一时刻只有一个线程访问共享资源。 - **线程通信**:使用wait()、notify()和notifyAll()方法,或者条件变量...

    Linux内核同步操作详解

    在多线程或多处理器环境中,多个执行单元可能同时访问共享资源,这就需要有效的同步机制来避免数据竞争和不一致性问题。 《Linux内核同步操作详解》是一篇由Paul Rusty Russell撰写的关于Linux内核锁机制的详细指南...

    Java 多线程与并发-Java并发知识体系详解.pdf

    总的来说,Java并发编程是一个复杂但重要的主题,需要深入理解线程基础、同步机制、并发工具类等知识,才能编写出高效、稳定的多线程程序。在实际开发中,应根据具体场景选择合适的并发控制手段,合理利用Java提供的...

    嵌入式多线程实验报告原创

    ### 嵌入式多线程实验报告知识点详解 #### 实验背景与目的 本次实验旨在通过实际操作加深对嵌入式系统中多线程编程的理解。实验的主要目的是让参与者熟悉Linux环境下多线程程序的设计与调试过程。具体而言,需要...

    Java多线程和并发知识整理

    Java多线程和并发知识是Java开发中的重要组成部分,它涉及到如何高效地利用系统资源,尤其是在多核CPU环境下,合理地使用多线程可以显著提升应用程序的性能。 **1. 理论基础** 1.1 为什么需要多线程 多线程的引入...

    Delphi多线程生命模拟程序

    《Delphi多线程生命模拟程序详解》 生命模拟程序是一种计算机模拟实验,它尝试通过简单的规则来模拟生物系统的复杂行为。在编程领域,利用 Delphi 这样的高级编程语言来实现多线程的生命模拟程序,可以极大地提高...

    实验二、嵌入式Linux多线程编程实

    通过本次实验,学生不仅能够深入了解嵌入式Linux环境下多线程编程的关键技术,还能熟练掌握线程同步与互斥的方法,以及Makefile的编写技巧。这对于进一步研究复杂多线程应用程序的设计与开发具有重要意义。

    C#写的坦克大战 多线程编程

    《C#实现的坦克大战:多线程编程详解》 在计算机编程的世界中,游戏开发是一种极具挑战性和趣味性的实践。本文将深入探讨一个由C#语言编写的坦克大战项目,其中涉及到了多线程编程的关键概念和技术。通过分析这个...

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

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

Global site tag (gtag.js) - Google Analytics