Java多线程初学者指南(9):为什么要进行数据同步

简介:
Java 中的变量分为两类:局部变量和类变量。局部变量是指在方法内定义的变量,如在run 方法中定义的变量。对于这些变量来说,并不存在线程之间共享的问题。因此,它们不需要进行数据同步。类变量是在类中定义的变量,作用域是整个类。这类变量可以被多个线程共享。因此,我们需要对这类变量进行数据同步。
数据同步就是指在同一时间,只能由一个线程来访问被同步的类变量,当前线程访问完这些变量后,其他线程才能继续访问。这里说的访问是指有写操作的访问,如果所有访问类变量的线程都是读操作,一般是不需要数据同步的。
那么如果不对共享的类变量进行数据同步,会发生什么情况呢?让我们先看看下面的代码会发生什么样的事情:
package  test;

public   class  MyThread  extends  Thread
{
    
public   static   int  n  =   0 ;

    
public   void  run()
    {
        
int  m  =  n;
        yield();
        m
++ ;
        n 
=  m;
    }
    
public   static   void  main(String[] args)  throws  Exception
    {
        MyThread myThread 
=   new  MyThread ();
        Thread threads[] 
=   new  Thread[ 100 ];
        
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
            threads[i] 
=   new  Thread(myThread);
        
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
            threads[i].start();
        
for  ( int  i  =   0 ; i  <  threads.length; i ++ )
            threads[i].join();
        System.out.println(
" n =  "   +  MyThread.n);
    }
}

     在执行上面代码的可能结果如下:
     n  =   59
看到这个结果,可能很多读者会感到奇怪。这个程序明明是启动了100 个线程,然后每个线程将静态变量n 1 。最后使用join 方法使这100 个线程都运行完后,再输出这个n 值。按正常来讲,结果应该是n = 100 。可偏偏结果小于100
其实产生这种结果的罪魁祸首就是我们经常提到的“脏数据”。而run方法中的yield() 语句就是产生“脏数据”的始作俑者(不加yield语句也可能会产生“脏数据”,但不会这么明显,只有将100改成更大的数,才会经常产生“脏数据”,在本例中调用yield就是为了放大“脏数据”的效果)。yield 方法的作用是使线程暂停,也就是使调用yield 方法的线程暂时放弃CPU 资源,使CPU 有机会来执行其他的线程。为了说明这个程序如何产生“脏数据”,我们假设只创建了两个线程:thread1 thread2 。由于先调用了thread1 start 方法,因此,thread1 run 方法一般会先运行。当thread1 run 方法运行到第一行(int m = n; )时,将n 的值赋给m 。当执行到第二行的yield 方法后,thread1 就会暂时停止执行,而当thread1 暂停时,thread2 获得了CPU 资源后开始运行(之前thread2 一直处于就绪状态),当thread2 执行到第一行(int m = n;) 时,由于thread1 在执行到yield n 仍然是0 ,因此,thread2 中的m 获得的值也是0 。这样就造成了thread1 thread2 m 获得的都是0 。在它们执行完yield 方法后,都是从0 开始加1 ,因此,无论谁先执行完,最后n 的值都是1 ,只是这个n thread1 thread2 各赋了一遍值。这个过程如下图如示:

也许有人会问,如果只有n++,会产生“脏数据”吗?答案是肯定的。那么n++ 只是一条语句,又如何在执行过程中将CPU 交给其他的线程呢?其实这只是表面现象,n++ 在被Java 编译器编译成中间语言(也叫做字节码)后,并不是一条语言。让我们看看下面的Java 代码将会被编译成什么样的Java 中间语言。
Java 源代码
public   void  run()
{
    n
++ ;
}
被编译后的中间语言代码
   001    public   void  run()
  
002   {
  
003       aload_0         
  
004       dup             
  
005       getfield
  
006       iconst_1        
  
007       iadd            
  
008       putfield       
  
009        return           
  
010   }
大家可以看到在run 方法中只有n++ 一条语句,而在编译后,却有7 条中间语言语句。我们并不需要知道这些语句的功能是什么,只看一下第005 007 008 行语句。在005 行是getfield ,根据它的英文含义可知是要得到某个值,因为这里只有一个n ,所以毫无疑问,是要得到n 的值。而在007 行的iadd 也不难猜测是将这个得到的n 值加1 。在008 行的putfield 的含义我想大家可能已经猜出来了,它负责将这个加1 后的n 再更新回类变量n 。说到这,可能大家还有一个疑惑,执行n++ 时直接将n 1 不就行了,为什么要如此费周折。其实这里涉及到一个Java 内存模型的问题。
Java 的内存模型分为主存储区和工作存储区。主存储区保存了Java 中所有的实例。也就是说,在我们使用new 来建立一个对象后,这个对象及它内部的方法、变量等都保存在这一区域,在MyThread类中 n 就保存在这个区域。主存储区可以被所有线程共享。而工作存储区就是我们前面所讲的线程栈,在这个区域里保存了在run 方法以及run 方法所调用的方法中定义的变量,也就是方法变量。在线程要修改主存储区中的变量时,并不是直接修改这些变量,而是将它们先复制到当前线程的工作存储区,在修改完后,再将这个变量值覆盖主存储区的相应的变量值。
在了解了Java 的内存模型后,就不难理解为什么n++ 也不是原子操作了。它必须经过一个拷贝、加1 和覆盖的过程。这个过程和在MyThread类 中模拟的过程类似。大家可以想象,如果在执行到getfield 时,thread1 由于某种原因被中断,那么就会发生和MyThread类的执行结果 类似的情况。要想彻底解决这个问题,就必须使用某种方法对n进行同步,也就是在同一时间只能有一个线程操作n,这也称为对n的原子操作。





 本文转自 androidguy 51CTO博客,原文链接:http://blog.51cto.com/androidguy/214828,如需转载请自行联系原作者

相关文章
|
29天前
|
自然语言处理 Java 关系型数据库
Java|小数据量场景的模糊搜索体验优化
在小数据量场景下,如何优化模糊搜索体验?本文分享一个简单实用的方案,虽然有点“土”,但效果还不错。
31 0
|
3月前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
244 60
【Java并发】【线程池】带你从0-1入门线程池
|
1月前
|
Java 中间件 调度
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
74 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
|
16天前
|
Java
java 多线程异常处理
本文介绍了Java中ThreadGroup的异常处理机制,重点讲解UncaughtExceptionHandler的使用。通过示例代码展示了当线程的run()方法抛出未捕获异常时,JVM如何依次查找并调用线程的异常处理器、线程组的uncaughtException方法或默认异常处理器。文章还提供了具体代码和输出结果,帮助理解不同处理器的优先级与执行逻辑。
|
2天前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
14 0
|
2月前
|
前端开发 Cloud Native Java
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
|
2月前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
120 23
|
1月前
|
数据采集 存储 网络协议
Java HttpClient 多线程爬虫优化方案
Java HttpClient 多线程爬虫优化方案
|
2月前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
187 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
3月前
|
Java API 数据处理
深潜数据海洋:Java文件读写全面解析与实战指南
通过本文的详细解析与实战示例,您可以系统地掌握Java中各种文件读写操作,从基本的读写到高效的NIO操作,再到文件复制、移动和删除。希望这些内容能够帮助您在实际项目中处理文件数据,提高开发效率和代码质量。
77 4