`

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

 
阅读更多
在这篇文章里,我们关注线程同步的话题。这是比多线程更复杂,稍不留意,我们就会“掉到坑里”,而且和单线程程序不同,多线程的错误是否每次都出现,也是不固定的,这给调试也带来了很大的挑战
-
首先阐述什么是同步,不同步有什么问题,然后讨论可以采取哪些措施控制同步,接下来我们会仿照回顾网络通信时那样,构建一个服务器端的“线程池”,JDK为我们提供了一个很大的concurrent工具包,最后我们会对里面的内容进行探索。

为什么要线程同步?

说到线程同步,大部分情况下, 我们是在针对“单对象多线程”的情况进行讨论,一般会将其分成两部分,一部分是关于“共享变量”,一部分关于“执行步骤”。

共享变量

当我们在线程对象(Runnable)中定义了全局变量,run方法会修改该变量时,如果有多个线程同时使用该线程对象,那么就会造成全局变量的值被同时修改,造成错误。我们来看下面的代码:


复制代码 代码如下:
共享变量造成同步问题
class MyRunner implements Runnable
{
     public int sum = 0;

     public void run()
     {
         System.out.println(Thread.currentThread().getName() + " Start.");
         for (int i = 1; i <= 100; i++)
         {
             sum += i;
         }
         try {
             Thread.sleep(500);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName() + " --- The value of sum is " + sum);
         System.out.println(Thread.currentThread().getName() + " End.");
     }
}


private static void sharedVaribleTest() throws InterruptedException
{
     MyRunner runner = new MyRunner();
     Thread thread1 = new Thread(runner);
     Thread thread2 = new Thread(runner);
     thread1.setDaemon(true);
     thread2.setDaemon(true);
     thread1.start();
     thread2.start();
     thread1.join();
     thread2.join();
}

这个示例中,线程用来计算1到100的和是多少,我们知道正确结果是5050(好像是高斯小时候玩过这个?),但是上述程序返回的结果是10100,原因是两个线程同时对sum进行操作。

执行步骤

我们在多个线程运行时,可能需要某些操作合在一起作为“原子操作”,即在这些操作可以看做是“单线程”的,例如我们可能希望输出结果的样子是这样的:


复制代码 代码如下:
线程1:步骤1
线程1:步骤2
线程1:步骤3
线程2:步骤1
线程2:步骤2
线程2:步骤3

如果同步控制不好,出来的样子可能是这样的:
复制代码 代码如下:
线程1:步骤1
线程2:步骤1
线程1:步骤2
线程2:步骤2
线程1:步骤3
线程2:步骤3

这里我们也给出一个示例代码:
复制代码 代码如下:
执行步骤混乱带来的同步问题
class MyNonSyncRunner implements Runnable
{
     public void run() {
         System.out.println(Thread.currentThread().getName() + " Start.");
         for(int i = 1; i <= 5; i++)
         {
             System.out.println(Thread.currentThread().getName() + " Running step " + i);
             try
             {
                 Thread.sleep(50);
             }
             catch(InterruptedException ex)
             {
                 ex.printStackTrace();
             }
         }
         System.out.println(Thread.currentThread().getName() + " End.");
     }
}


private static void syncTest() throws InterruptedException
{
     MyNonSyncRunner runner = new MyNonSyncRunner();
     Thread thread1 = new Thread(runner);
     Thread thread2 = new Thread(runner);
     thread1.setDaemon(true);
     thread2.setDaemon(true);
     thread1.start();
     thread2.start();
     thread1.join();
     thread2.join();
}

如何控制线程同步

既然线程同步有上述问题,那么我们应该如何去解决呢?针对不同原因造成的同步问题,我们可以采取不同的策略。

控制共享变量

我们可以采取3种方式来控制共享变量。

将“单对象多线程”修改成“多对象多线程”

上文提及,同步问题一般发生在“单对象多线程”的场景中,那么最简单的处理方式就是将运行模型修改成“多对象多线程”的样子,针对上面示例中的同步问题,修改后的代码如下:


复制代码 代码如下:
解决共享变量问题方案一
private static void sharedVaribleTest2() throws InterruptedException
{
     Thread thread1 = new Thread(new MyRunner());
     Thread thread2 = new Thread(new MyRunner());
     thread1.setDaemon(true);
     thread2.setDaemon(true);
     thread1.start();
     thread2.start();
     thread1.join();
     thread2.join();
}

我们可以看到,上述代码中两个线程使用了两个不同的Runnable实例,它们在运行过程中,就不会去访问同一个全局变量。
将“全局变量”降级为“局部变量”

既然是共享变量造成的问题,那么我们可以将共享变量改为“不共享”,即将其修改为局部变量。这样也可以解决问题,同样针对上面的示例,这种解决方式的代码如下:


复制代码 代码如下:
解决共享变量问题方案二
class MyRunner2 implements Runnable
{
     public void run()
     {
         System.out.println(Thread.currentThread().getName() + " Start.");
         int sum = 0;
         for (int i = 1; i <= 100; i++)
         {
             sum += i;
         }
         try {
             Thread.sleep(500);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println(Thread.currentThread().getName() + " --- The value of sum is " + sum);
         System.out.println(Thread.currentThread().getName() + " End.");
     }
}


private static void sharedVaribleTest3() throws InterruptedException
{
     MyRunner2 runner = new MyRunner2();
     Thread thread1 = new Thread(runner);
     Thread thread2 = new Thread(runner);
     thread1.setDaemon(true);
     thread2.setDaemon(true);
     thread1.start();
     thread2.start();
     thread1.join();
     thread2.join();
}

我们可以看出,sum变量已经由全局变量变为run方法内部的局部变量了。
使用ThreadLocal机制

ThreadLocal是JDK引入的一种机制,它用于解决线程间共享变量,使用ThreadLocal声明的变量,即使在线程中属于全局变量,针对每个线程来讲,这个变量也是独立的。

我们可以用这种方式来改造上面的代码,如下所示:


复制代码 代码如下:
解决共享变量问题方案三
class MyRunner3 implements Runnable
{
     public ThreadLocal<Integer> tl = new ThreadLocal<Integer>();

     public void run()
     {
         System.out.println(Thread.currentThread().getName() + " Start.");
         for (int i = 0; i <= 100; i++)
         {
             if (tl.get() == null)
             {
                 tl.set(new Integer(0));
             }
             int sum = ((Integer)tl.get()).intValue();
             sum+= i;
             tl.set(new Integer(sum));
             try {
                 Thread.sleep(10);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }

         System.out.println(Thread.currentThread().getName() + " --- The value of sum is " + ((Integer)tl.get()).intValue());
         System.out.println(Thread.currentThread().getName() + " End.");
     }
}


private static void sharedVaribleTest4() throws InterruptedException
{
     MyRunner3 runner = new MyRunner3();
     Thread thread1 = new Thread(runner);
     Thread thread2 = new Thread(runner);
     thread1.setDaemon(true);
     thread2.setDaemon(true);
     thread1.start();
     thread2.start();
     thread1.join();
     thread2.join();
}

综上三种方案,第一种方案会降低多线程执行的效率,因此,我们推荐使用第二种或者第三种方案。

控制执行步骤

说到执行步骤,我们可以使用synchronized关键字来解决它。


复制代码 代码如下:
执行步骤问题解决方案
class MySyncRunner implements Runnable
{
     public void run() {
         synchronized(this)
         {
             System.out.println(Thread.currentThread().getName() + " Start.");
             for(int i = 1; i <= 5; i++)
             {
                 System.out.println(Thread.currentThread().getName() + " Running step " + i);
                 try
                 {
                     Thread.sleep(50);
                 }
                 catch(InterruptedException ex)
                 {
                     ex.printStackTrace();
                 }
             }
             System.out.println(Thread.currentThread().getName() + " End.");
         }
     }
}


private static void syncTest2() throws InterruptedException
{
     MySyncRunner runner = new MySyncRunner();
     Thread thread1 = new Thread(runner);
     Thread thread2 = new Thread(runner);
     thread1.setDaemon(true);
     thread2.setDaemon(true);
     thread1.start();
     thread2.start();
     thread1.join();
     thread2.join();
}

在线程同步的话题上,synchronized是一个非常重要的关键字。它的原理和数据库中事务锁的原理类似。我们在使用过程中,应该尽量缩减synchronized覆盖的范围,原因有二:1)被它覆盖的范围是串行的,效率低;2)容易产生死锁。我们来看下面的示例:
复制代码 代码如下:
synchronized示例
private static void syncTest3() throws InterruptedException
{
     final List<Integer> list = new ArrayList<Integer>();

     Thread thread1 = new Thread()
     {
         public void run()
         {
             System.out.println(Thread.currentThread().getName() + " Start.");
             Random r = new Random(100);
             synchronized(list)
             {
                 for (int i = 0; i < 5; i++)
                 {
                     list.add(new Integer(r.nextInt()));
                 }
                 System.out.println("The size of list is " + list.size());
             }
             try
             {
                 Thread.sleep(500);
             }
             catch(InterruptedException ex)
             {
                 ex.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName() + " End.");
         }
     };

     Thread thread2 = new Thread()
     {
         public void run()
         {
             System.out.println(Thread.currentThread().getName() + " Start.");
             Random r = new Random(100);
             synchronized(list)
             {
                 for (int i = 0; i < 5; i++)
                 {
                     list.add(new Integer(r.nextInt()));
                 }
                 System.out.println("The size of list is " + list.size());
             }
             try
             {
                 Thread.sleep(500);
             }
             catch(InterruptedException ex)
             {
                 ex.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName() + " End.");
         }
     };

     thread1.start();
     thread2.start();
     thread1.join();
     thread2.join();
}

我们应该把需要同步的内容集中在一起,尽量不包含其他不相关的、消耗大量资源的操作,示例中线程休眠的操作显然不应该包括在里面。

转载:http://www.jb51.net/article/36553.htm
分享到:
评论

相关推荐

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

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

    基于Java回顾之多线程详解

    在Java编程中,多线程是一个至关重要的概念,它允许程序同时执行多个任务,从而提高系统的效率和响应速度。本文将深入探讨Java中的多线程,包括线程的基本属性、创建线程的方式、线程状态的转换以及线程间的通信。 ...

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

     高并发编程第三阶段30讲 使用Condition实现一个多线程下的Producer-Consumer_.mp4  高并发编程第三阶段31讲 JDK8-StampedLock详细介绍-上_.mp4  高并发编程第三阶段32讲 JDK8-StampedLock详细介绍-下.mp4  高...

    Java软件开发实战 Java基础与案例开发详解 19-3 lnetAddress类 共6页.pdf

    - **多线程**:探讨Java中的线程生命周期、线程同步、线程调度等高级主题。 - **IO操作**:包括文件流、缓冲流、转换流等多种输入输出流的使用方法。 - **图形用户界面设计**:介绍如何使用Java的AWT和Swing库来创建...

    基于Java的远程视频会议系统(系统+论文).rar

    - **多线程**:视频会议系统需要处理多个并发连接,多线程技术使得程序能够同时处理多个任务,提高系统性能。 - **IO流**:Java的IO流用于读写文件和网络数据,包括输入流和输出流,实现音频、视频数据的实时传输...

    SUN Java 考试大纲

    - **课程内容总览**:课程内容涵盖Java基础知识、面向对象编程、异常处理、多线程编程、I/O流操作、网络编程等多个方面。 - **课程进度安排**:按照模块化的方式组织教学,确保每个主题都能得到充分讲解与实践。 ##...

    java外国留学课件

    - **多线程编程**:讲解线程的概念,探讨线程同步和并发控制的方法。 - **网络编程**:了解Socket编程的基础知识,包括TCP/IP协议和客户端/服务器模型。 #### 十三、软件工程实践 - **需求分析**:教授如何进行需求...

    JAVA入门1.2.3:一个老鸟的JAVA学习心得 PART1(共3个)

    对Java语言的每个语法都提供了一个或多个例程讲解 大量使用流程图表示程序的执行过程,使用结构图表示程序的内部状态 每章最后都给出了典型的练习题,让读者及时练习,巩固提高,并提供了参考答案 目录 第1篇 ...

    JAVA实验指导

    - **知识点**:理解多线程的概念;掌握Thread类和Runnable接口的使用;了解线程间的通信和同步问题。 - **操作步骤**:创建多个线程并演示线程的启动、暂停、停止等操作。 - **注意事项**:注意线程安全问题,避免...

    疯狂java pdf 影印版

    由于链接无法在此解读或验证,我们将基于标题、描述及标签中的信息来提炼和扩展相关的Java知识点。 ### 《疯狂Java》第二版概览 #### 一、Java基础知识回顾 1. **Java概述**: - Java是一种广泛使用的高级编程...

    Java入门1·2·3:一个老鸟的Java学习心得.PART3(共3个)

    对Java语言的每个语法都提供了一个或多个例程讲解 大量使用流程图表示程序的执行过程,使用结构图表示程序的内部状态 每章最后都给出了典型的练习题,让读者及时练习,巩固提高,并提供了参考答案 目录 第1篇 ...

    Java语言程序设计进阶篇

    - **线程同步**:synchronized关键字、volatile变量、Lock接口等。 - **线程池**:Executor框架、ThreadPoolExecutor类等。 - **泛型**: - **泛型类和泛型方法**:使用泛型可以编写类型安全的代码。 - **...

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

     高并发编程第三阶段30讲 使用Condition实现一个多线程下的Producer-Consumer_.mp4  高并发编程第三阶段31讲 JDK8-StampedLock详细介绍-上_.mp4  高并发编程第三阶段32讲 JDK8-StampedLock详细介绍-下.mp4  高...

    2018年java学习计划-范文模板 (16页).docx

    2. **多线程**:掌握并发编程,了解线程的创建、同步和通信。 3. **绘图**:通过Java进行图形绘制,提升可视化编程能力。 **实践项目**:开发一个资源管理器,实践文件系统操作和图形界面的结合。 **第三部分:...

    the-caiquan-game-based-on-java.rar_java五子棋_五子棋java

    【Java五子棋游戏开发详解】 Java五子棋游戏是一种基于Java编程语言开发的网络对战游戏,允许玩家在线上进行实时对弈。这款游戏的实现涉及了多方面的Java技术,包括图形用户界面(GUI)、网络通信、并发处理以及...

    java版基于UDP协议网上聊天程序课程设计报告书.doc

    【Java版基于UDP协议网上聊天程序】的课程设计旨在让学生理解和掌握使用UDP协议进行网络通信的基本原理和方法。UDP(User Datagram Protocol)是一种无连接的传输层协议,它不保证数据包的顺序、可靠性和错误恢复,...

    SWT线程教程

    本篇文章将详细介绍SWT中的线程管理机制,并通过具体的示例来阐述如何有效地在SWT项目中使用多线程。 #### 二、SWT线程的基本概念 ##### 2.1 SWT线程限制 SWT的设计原则之一是为了提高程序的安全性和响应速度,它...

    [Java语言程序设计-进阶篇(原书第8版)](高清)

    - **线程同步**:synchronized关键字和Lock接口的使用,避免并发访问时的数据不一致性问题。 - **线程池**:Executor框架的使用,提高线程管理的效率。 #### 5. 泛型与反射 - **泛型**:深入理解泛型的工作原理,...

    李兴华javase全部笔记

    7. **多线程**:讲解并发编程的基础,包括线程的创建、同步机制(synchronized、wait、notify等)、线程池和死锁问题。 8. **反射机制**:通过反射来动态访问类的信息,创建对象,调用方法,以及处理泛型。 9. **...

    Java面试题

    Java作为一门广泛使用的编程语言,其面试题涵盖了众多的知识领域,包括基础语法、面向对象、集合框架、多线程、异常处理、IO流、网络编程、设计模式、JVM优化、数据库操作等。以下是一些Java面试中常被问到的知识点...

Global site tag (gtag.js) - Google Analytics