线程安全是一个老生长谈的话题,做开发的人人都会碰到且谈论这个话题,今天就来从内存角度上深入剖析一下什么是线程安全。

首先,我们知道jvm内存总体来讲分为:栈、堆、程序计数器、方法区。其中又分为线程私有(每个线程单独维护)和线程共享的区域,线程私有区域不会涉及多线程间通信和同步问题,所以线程安全肯定是出现在线程共享区域的。

接下来提出一个问题,上述4个内存区域中,哪些是线程私有的,哪些是线程共享的?我们来一个一个来分析:

  1. 栈(虚拟机栈和本地方法栈在这里归为同类讨论)。定义总结:每当启用一个线程时,JVM就为他分配一个JAVA栈,每当线程调用一个java方法时,JVM就会在该线程对应的栈中压入一个帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,当方法执行结束时此栈帧出栈,并将结果返回给对应的操作数栈中。由定义可以知道栈是线程私有的,每个线程拥有一个自己的栈,这个其实在我们平时使用中也可以知道,例如一个线程阻塞时,如果所有线程共用一个栈的话,当前栈帧会一直不能出栈,后续方法都不能执行,则会直接导致整个程序阻塞。
  2. 堆。定义总结:堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,我们平时所创建的对象实例绝大部分都存放在堆中。由定义可知堆是线程共享的,所以其中存储的有状态数据要保证线程安全需加密。
  3. 程序计数器。定义总结:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器
  4. 方法区。定义总结:方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区很好理解,同样的类,没必要每个线程中都存储一遍其信息,所以肯定是线程共享的。

看完了枯燥的定义,接下来我们通过代码和内存模型来生动的理解线程为什么以及什么情况下会不安全。

首先拿我们常用的i++举例,来看如下一段代码:

@Test
public void testStackSafe() {Thread thread1 = new Thread(() -> {add2Ten();});Thread thread2 = new Thread(() -> {add2Ten();});thread1.start();thread2.start();
}private void add2Ten() {int i = 0;while (i < 10) {i++;}System.out.println(i);
}

很简单的一段代码,这段代码是否是线程安全的呢?我们来分析下,代码中非原子性的有状态操作其实只有i++,那么这里的i++会否引起线程安全问题呢?这里就取决于i存放在哪里,我们这个例子中的i其实是在线程内部定义的局部变量,所以它存在与线程私有的栈中,即线程1和线程2都有一个int i,内存模型如下:

由上图可以看出,所以的i++操作其实都是在各自线程栈中计算的,并不会涉及与主线程同步问题,所以此程序是线程安全的。

接下来我们把上面程序修改如下:

@Test
public void testStackSafe() throws InterruptedException {Player player = new Player();Thread thread1 = new Thread(() -> {add2Ten(player);System.out.println(MessageFormat.format("thread1最后调用player等级:{0}",  player.getLevel()));});Thread thread2 = new Thread(() -> {add2Ten(player);System.out.println(MessageFormat.format("thread2最后调用player等级:{0}",  player.getLevel()));});thread1.start();thread2.start();Thread.sleep(1000l);
}private void add2Ten(Player player) {for (int i = 0 ; i < 10 ; i++){player.levelUp();}
}class Player {private int level = 500;public void levelUp() {level++;}public int getLevel() {return level;}
}

读者可以多次运行上面程序,会发现有时候会输出如下结果:

thread1最后调用player等级:519
thread2最后调用player等级:519

读者可能会发问了,为什么明明循环加了20次,结果只有519呢?首先我们来看类图:

通过图可以看到,线程1和线程2在执行player.levelUp()时,需要先通过player的引用取到int level,之后在栈中做++运算,再将运算结果同步回去。这里引入线程同步问题原因概念:线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存.JMM规定了jvm有主内存(Main Memory)和工作内存(Working Memory) ,主内存存放程序中所有的类实例、静态数据等变量(ps:其实就是我们说的堆、方法区),是多个线程共享的。而工作内存存放的是该线程从主内存中拷贝过来的变量以及访问方法所取得的局部变量(ps:其实就是栈),是每个线程私有的其他线程不能访问,每个线程对变量的操作都是以先从主内存将其拷贝到工作内存再对其进行操作的方式进行.我们看出,这一步时,就会出现线程不安全,假如线程1取到level为1,之后在线程1的栈中做运算++后为2此时还未同步回堆中,线程2读取的还是1,之后拷贝回自己的栈中计算出结果也是2,这时明明执行了两步计算,结果却只增加了1。这就是上面例子有时会输出小于520这个值的原因。

通过上面可以看出线程不安全就是起于主存和缓存间同步的原因,那么我们要解决安全问题可以从哪些方面着手呢?
1. 可见性。如上述例子,我们之所以出现线程不安全就是因为线程1在自己的栈中做计算时,线程2是不知道这一动作的,如果让这一系列动作变得可见则线程2可以实时看见线程1的值的话就不会出现上述问题。java内置了volatile关键字用于实现此功能:volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型变量时总会返回最新写入的值。
2. 原子性。同样上述例子中,假如我们线程1在读取-拷贝值-计算-回写值这一整个过程中,线程2并没有进入,而是在等待线程1执行完毕,即把线程1的这一系列操作当做一个原子操作来看的话,线程2就不能插入到其中步骤,它进入的时机只能是读取前或者写值后,这样的话线程1中不论做多久操作,线程2的计算结果都不会收到影响。java内置synchronized关键字来实现加锁,及可将其锁定的代码块当做具备原子性的操作处理。

此处volatile和synchronize就不用代码演示了,本篇主要为了讲述原理,不在于实际运用。

总结:

  1. 线程内定义的局部变量存在于各个线程私有栈中,对于其他线程是不可见的,其操作都是自身可见的,不会引发线程安全问题。
  2. 线程外定义的变量,被多个线程共用时,每个线程在执行运算时会先将变量从堆中同步会当前线程的栈中,运算后将结果同步回堆中,由于在线程栈中的运算别的线程不可见,所以这个过程会引发线程安全问题。

欢迎关注个人博客:blog.scarlettbai.com

【并发编程】当我们谈论线程安全时我们在谈论什么相关推荐

  1. [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ...

    [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ... 摘要 介绍 Java 并发包里的几个主要 ExecutorService . 正文 ...

  2. Java 并发编程——Executor框架和线程池原理

    Java 并发编程系列文章 Java 并发基础--线程安全性 Java 并发编程--Callable+Future+FutureTask java 并发编程--Thread 源码重新学习 java并发 ...

  3. [Java并发编程(二)] 线程池 FixedThreadPool、CachedThreadPool、ForkJoinPool?为后台任务选择合适的 Java executors...

    [Java并发编程(二)] 线程池 FixedThreadPool.CachedThreadPool.ForkJoinPool?为后台任务选择合适的 Java executors ... 摘要 Jav ...

  4. python多线程并发编程技术_同步线程 - Python并发编程教程™

    线程同步可以定义为一种方法,借助这种方法,可以确信两个或更多的并发线程不会同时访问被称为临界区的程序段. 另一方面,正如我们所知道的那样,临界区是共享资源被访问的程序的一部分. 因此,同步是通过同时访 ...

  5. 《Java并发编程的艺术》——线程(笔记)

    文章目录 四.Java并发编程基础 4.1 线程简介 4.1.1 什么是线程 4.1.2 为什么要使用多线程 4.1.3 线程优先级 4.1.4 线程的状态 4.1.5 Daemon线程 4.2 启动 ...

  6. Java并发编程(8)——常见的线程安全问题

    线程安全问题: 多个线程同时执行也能工作的代码就是线程安全的代码 如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的. 具体说明: java并发线程实战(1) 线程安全和机制原 ...

  7. 【并发编程十一】c++线程同步——future

    [并发编程十一]c++线程同步--future 一.互斥 二.条件变量 三.future 1.promise 1.1.子线程设值,主线程获取 1.2.主线程设置值,子线程获取 2.async 2.1. ...

  8. 高并发编程-自定义简易的线程池(2),体会原理

    文章目录 概述 示例 概述 高并发编程-自定义简易的线程池(1),体会原理 中只实现了任务队列,我们这里把其余的几个也补充进来 拒绝策略 关闭线程池 最小 最大 活动线程数 - 示例 比较简单,直接上 ...

  9. java 线程钩子_高级并发编程系列六(线程池钩子函数)

    1.考考你 国庆假期快要结束了,准备回到工作岗位的你,是不是已经开始撸起袖子敲代码,反正发完文章我就要准备去加班了,程序员就这样,有干劲对吧 那么来吧,让我们一起分享完高级并发编程系列中,线程池小节的 ...

  10. Java并发编程|第二篇:线程生命周期

    文章目录 系列文章 1.线程的状态 2.线程生命周期 3.状态测试代码 4.线程终止 4.1 线程执行完成 4.2 interrupt 5.线程复位 5.1interrupted 5.2抛出异常 6. ...

最新文章

  1. Vue.Draggable 实现组件拖拽
  2. SQL Server 中WITH (NOLOCK)浅析
  3. 【五线谱】五线谱的线与间 ( 五线谱中的 第N线与第N间 | 五线谱上的 上加N线与上加N间 | 五线谱下的 下加N线与下加N间 | 高音谱号下加一线 等同于 低音谱号上加一线 )
  4. c++读取txt中每行的数据到数组中
  5. ASP.NET Core应用程序容器化、持续集成与Kubernetes集群部署(一)
  6. HaProxy+Keepalived+Mycat高可用群集配置
  7. 华为gsm模块_出货量全球第一,华为阿里腾讯都是其客户,上海移远通信牛在哪?...
  8. 【Python】列表类型操作函数和方法
  9. Spring Boot学习总结(7)——SpringBoot之于Spring优势
  10. x64位windows上程序开发的注意事项
  11. python文件夹中的__init__.py的作用
  12. 产品经理 - 统一支付 、结算、清算
  13. View内容保存为图片
  14. 1017 A除以B (20 分)—PAT (Basic Level) Practice (中文)
  15. leetcode 20. 有效的括号 (python)
  16. [Android] Android 任务栈 【转载】
  17. Excel 对比两列数据大小 大于等于 高亮显示
  18. Vulnhub靶机 it is october
  19. window PCL安装编译
  20. 红蜻蜓抓图精灵抓视频播放器画面结果一片漆黑解决教程

热门文章

  1. 企业微信和钉钉的区别以及企业微信的功能
  2. Android 拦截Home键的常用方法
  3. 2008服务器远程桌面连接设置密码,WinServer 2008 远程桌面连接设置
  4. python文件seek函数,Python 文件操作seek()函数
  5. [转载]GGB0/OB28/OKC7/GGB1/OBBH/OKC9 FICO增强(转)_SAP刘梦_新浪博客
  6. 如何平衡CVR预估中的延迟反馈问题?(内含招聘)
  7. webstorm导致CPU占用率高
  8. If python is on the left-most side of the chain, that‘s the version you‘ve asked for.
  9. c/c++原子锁应用(跨平台)
  10. DropBox系列-安卓DropBox介绍