前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >java高并发架构设计原理:java的内存模型,volatile和线程数据安全

java高并发架构设计原理:java的内存模型,volatile和线程数据安全

作者头像
望月从良
发布2021-04-21 10:29:11
3130
发布2021-04-21 10:29:11
举报
文章被收录于专栏:Coding迪斯尼Coding迪斯尼

最近工作上需要使用java完成高并发的服务器后台设计,因此对此作了一些研究,于是想把研究的心得,总结,经验写出来与大家分享,顺便巩固自己的认知。java通常用来开发大型网站,特别是用来开发应对高并发的后台服务器,例如淘宝就是依赖java后台来满足每天面临的海量数据请求。

java在应对高并发上形成了一系列成熟的设计思想以及应用框架,掌握这些知识能大大扩宽一个技术人员的择业范围和技术实力,在未来十年内,在处理海量数据请求和高并发需求上,java的统治地位不会有太大的动摇。

掌握高并发海量数据处理的技术能力会使你在市场上非常吃香,如果你找后台开发的职位,你会发现“高并发”,“海量数据处理”几乎都是这类职位的必备要求。高并发的处理本质上来说,就是把海量请求分发到足够多的服务器集群上,也就是采用分而治之的原则,“海量请求”经过足够密度的切割后,所得的每一小块数量没那么大,并且服务器的处理能力又足够强,那么应对高并发情景自然没有太大问题。

由此“并行计算”就是处理高并发的核心所在。然而并行计算本身需要处理的技术问题也足够复杂,这次我们看一个常见棘手问题,那就是信息共享问题。假设我们在服务器上有多个线程并行处理数据或请求,线程的运行逻辑受到一系列共享变量的影响,假设线程A,B同时需要读取变量C,A,B可能运行在不同的处理器上,C可能存储在另一台机器上,线程A更改了C的值后,我们如何确保线程B能读取到C最新的最新值?这个不是一个简单容易处理的问题, 我们先先看一个例子:

代码语言:javascript
复制

public class java_model implements Runnable{
    private  String str;
      void setStr(String str) {this.str = str;}

      public void run() {
        while (str == null);
        System.out.println(str);
      }

      public static void main(String[] args) throws InterruptedException {
          java_model delay = new java_model();
        new Thread(delay).start();
        Thread.sleep(1000);
        delay.setStr("Hello world!!");
      }


}

运行上面代码,你会发现程序会陷入死锁状态,原因在于while(str == null);这条语句一直在执行,问题在于在main中,我们已经使用setStr设置了str变量的值,因此语句while(str ===null)不应该一直执行下去,如果我们给private String str改成private volatile String str,那么程序就会打印出”Hello World!”后顺利终结,为何会出现这种奇怪的现象呢,这就涉及到java的内存模型:

在java虚拟机中,每个线程有自己的本地缓存,不同线程不同读取其他线程的缓存。与此同时虚拟机还有全局缓存,也就是上图对应的L3 cache,全局变量存储在全局缓存中,当线程需要读取全局变量时,它会将变量在全局缓存中的信息拷贝到本地缓存,以后读取时它会直接从本地缓存读取,由此能大大提高信息读取的效率。

这意味着变量str其实由多份拷贝,每个线程一份,同时全局内存中还有一份。这带来一个非常严重的问题,那就是数据根本不同步,线程1修改了全局变量后,线程2根本就不知道,如此程序运行就会出现严重错误。解决这个问题的办法就是迫使线程在读取数据时,每次都必须从全局内存将变量的信息拷贝到本地缓存,写入数据时必须立马将写入的数据更新到全局缓存中,如此一来全局变量被线程1修改后,线程2能尽快看到,实现这个动作就需要volatile关键字。

其次volatile关键字还涉及到字节码的重排序问题。程序在运行时,代码的执行顺序并非像我们编写的那样一条条从上到下,编译器或虚拟机为了优化执行速度,有可能会在不影响程序逻辑的情况下先执行下面的代码,然后在执行上面的代码,例如:

代码语言:javascript
复制
int h = 10; //1
int w; //2
w = 15; //3
int a = h * w; //4

通常我们会认为上面代码的执行次序是从上到下,也就是1,2,3,4.实际执行时的次序有可能是2,3,1,4,次序的改变通常不会改变逻辑结构,但是在某些特定情况下也会带来意外,意外通常来自单子模式,例子如下:

代码语言:javascript
复制

public  class Singleton{
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }

    return instance;
    }
}

这种代码在多线程条件下运行时很容易出问题,原因在于前面提到的指令重排序。原因在于语句instance = new Singleton();在顺序执行时,该语句会先分配内存,调用类的构造方法,然后将内存地址分配给变量instance。但重排序发生时语句的执行有可能变成先分配内存,然后把内存地址分配给变量instance,然后在执行初始化函数。因此在多线程时,如果有一个线程执行了该语句,并执行了第2步,此时instance变量不再为null, 这时另一个线程同时调用了getInstance()函数,于是它就会得到一个初始化函数没有被调用的实例对象。

为了避免这种重排序问题就可以使用volatile关键字,将语句变成private volatile static Singleton instance = null;就能避免上面描述的问题。然而使用volatile还有问题,那就是它不能保证操作的原子性,例如a++这类操作在多线程下即使变量用valotile修饰也同样出问题。

因为volatile修饰的关键字可以保证其信息及时刷新,但a++这种操作等价于a = a + 1,如果a被volatile修饰,那么在执行a = a + 1时,它会先把a的变量从主存读入线程的本地缓存,然后更改本地缓存的值,接着把更改后的结果重新写回到主存。在多线程情况下,线程1执行a++时会将a的值从主存读入,同一时间线程2也执行a++,同样也把a的值从主存读入,注意此时线程2读入的a值还没有被线程1更新,于是在多线程同时对volatile变量进行读写时也容易出问题,例如下面的例子:

代码语言:javascript
复制

public class VolatileForPlusPlus {
    public static volatile int a = 0;
    public static void main(String[] strs) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        a++; //这里有问题
                    }
                }
            }).start();
        }

        Thread.sleep(3000); //等待所有线程启动
        System.out.print(a); //a的值很可能不会是10000 * 100
    }
}

在我电脑上输出结果为956626,出现这个结果的原因就是因为a++操作其实蕴含了好几步指令,无法实现原子化操作。java提供了保证若干计算操作实现原子性的接口,例如AtomicInteger类能实现整形类型加法操作的原子性,于是把上面代码替换如下:

代码语言:javascript
复制
import java.util.concurrent.atomic.AtomicInteger;

public class VolatileForPlusPlus {
    public static AtomicInteger  a = new AtomicInteger(0);
    public static void main(String[] strs) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        a.incrementAndGet(); //这里有问题
                    }
                }
            }).start();
        }

        Thread.sleep(3000); //等待所有线程启动
        System.out.print(a); //a的值很可能不会是10000 * 100
    }
}

保证操作的原子性后就能得到准确结果,更多java多线程高并发模型原理我们在后续章节继续讨论。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-04-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Coding迪斯尼 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
GPU 云服务器
GPU 云服务器(Cloud GPU Service,GPU)是提供 GPU 算力的弹性计算服务,具有超强的并行计算能力,作为 IaaS 层的尖兵利器,服务于深度学习训练、科学计算、图形图像处理、视频编解码等场景。腾讯云随时提供触手可得的算力,有效缓解您的计算压力,提升业务效率与竞争力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档