• jvm-多线程


    多线程的目的

    为什么要使用多线程?可以简单的分两个方面来说:

    • 在多个cpu核心下,多线程的好处是显而易见的,不然多个cpu核心只跑一个线程其他的核心就都浪费了;
    • 即便不考虑多核心,在单核下,多线程也是有意义的,因为在一些操作,比如IO操作阻塞的时候,是不需要cpu参与的,这时候cpu就可以另开一个线程去做别的事情,等待IO操作完成再回到之前的线程继续执行即可。

    多线程带来的问题其实多线程根本的问题只有一个:线程间变量的共享

    java里的变量可以分3类:

             1类变量(类里面static修饰的变量)

             2实例变量(类里面的普通变量)

             3局部变量(方法里声明的变量)

    根据各个区域的定义,我们可以知道:

    1. 类变量 保存在“方法区”
    2. 实例变量 保存在“堆”
    3. 局部变量 保存在 “虚拟机栈”

    “方法区”和“堆”都属于线程共享数据区,“虚拟机栈”属于线程私有数据区。

    因此,局部变量是不能多个线程共享的,而类变量和实例变量是可以多个线程共享的。事实上,在java中,多线程间进行通信的唯一途径就是通过类变量和实例变量。

    也就是说,如果一段多线程程序中如果没有类变量和实例变量,那么这段多线程程序就一定是线程安全的。

    以Web开发的Servlet为例,一般我们开发的时候,自己的类继承HttpServlet之后,重写doPost()、doGet()处理请求,不管我们在这两个方法里写什么代码,只要没有操作类变量或实例变量,最后写出来的代码就是线程安全的。如果在Servlet类里面加了实例变量,就很可能出现线程安全性问题,解决方法就是把实例变量改为ThreadLocal变量,而ThreadLocal实现的含义就是让实例变量变成了“线程私有”的,即给每一个线程分配一个自己的值。

    现在我们知道: 其实多线程根本的问题只有一个:线程间变量的共享, 这里的变量,指的就是类变量和实例变量,后续的一切,都是为了解决类变量和实例变量共享的安全问题。

    如何安全的共享变量

    现在唯一的问题就是要让多个线程安全的共享变量(下文中的变量一般特指类变量和实例变量),上文提到了一种ThreadLocal的方式,其实这种方式并不是真正的共享,而是为每个线程分配一个自己的值。

    比如现在有一个特别简单的需求,有一个类变量a=0,现在启动5个线程,每个线程执行a++;如果用ThreadLocal的方式,最后的结果就是5个线程都拥有一份自己的a值,最终结果都是1,这显然不符合我们的预期。

    那么如果不使用ThreadLocal呢?直接声明一个类变量a=0,然后让5个线程分别去执行a++;这样结果依旧不对,而且结果是不确定的,可能是1,2,3,4,5中的任一个。这种情况叫做竞态条件(Race Condition),要理解竞态条件先要理解Java内存模型:

    要理解java的内存模型,可以类比计算机硬件访问内存的模型。由于计算机的cpu运算速度和内存io速度有几个数量级的差距,因此现代计算机都不得不加入一层尽可能接近处理器运算速度的高速缓存来做缓冲:将内存中运算需要使用的数据先复制到缓存中,当运算结束后再同步回内存。如下图:

    因为jvm要实现跨硬件平台,因此jvm定义了自己的内存模型,但是因为jvm的内存模型最终还是要映射到硬件上,因此jvm内存模型几乎与硬件的模型一样:

    每个java线程都有一份自己的工作内存,线程访问变量的时候,不能直接访问主内存中的变量,而是先把主内存的变量复制到自己的工作内存,然后操作自己工作内存里的变量,最后再同步给主内存。

    现在就可以解释为什么5个线程执行a++最后结果不一定是5了,因为a++可以分解为3步操作:

    1. 把主内存里的a复制到线程的工作内存
    2. 线程对工作内存里的a执行a=a+1
    3. 把线程工作内存里的a同步回主内存

    而5个线程并发执行的时候完全有可能5个线程都先执行了第一步,这样5个线程的工作内存里a的初始值都是0,然后执行a=a+1后在工作内存里的运算结果都是1,最后同步回主内存的值肯定也是1。

    而避免这种情况的方法就是:在多个线程并发访问a的时候,保证a在同一个时刻只被一个线程使用。

    同步(synchronized)就是:在多个线程并发访问共享数据的时候,保证共享数据在同一个时刻只被一个线程使用。

    同步基本思想

    为了保证共享数据在同一时刻只被一个线程使用,我们有一种很简单的实现思想,就是 在共享数据里保存一个锁 ,当没有线程访问时,锁是空的,当有第一个线程访问时,就 在锁里保存这个线程的标识 并允许这个线程访问共享数据。在当前线程释放共享数据之前,如果再有其他线程想要访问共享数据,就要 等待锁释放 。

    我们把这种思想的三个关键点抽出来:

    1. 在共享数据里保存一个锁
    2. 在锁里保存这个线程的标识
    3. 其他线程访问已加锁共享数据要等待锁释放
  • 相关阅读:
    详解ASP.NET的内置对象
    如何架设FTP服务器
    输出JSON问题
    new , virtual , override
    Javascript 操作select控件大全(新增、修改、删除、选中、清空、判断存在等)
    Java回顾之I/O
    数据结构之栈和队列
    Java回顾之多线程同步
    设计模式之行为型模式
    Java回顾之网络通信
  • 原文地址:https://www.cnblogs.com/clovejava/p/7669114.html
Copyright © 2020-2023  润新知