前言

        可能有人会觉得,只要我写代码的时候不去开启其他线程,那么就不会有多线程的问题了。
        然而事实并非如此,如果仅仅是一些简单的测试代码,确实代码都会顺序执行而不是并发执行,但是Java应用最广泛的web项目中,绝大部分(如果不是所有的话)web容器都是多线程的——以tomcat为例, 每一个进来的请求都需要一个线程,直到该请求结束。 这样一来,即使本身不打算多线程运行的代码,实际上几乎都会以多线程的方式执行。
        在 Spring 注册的 bean(默认都是单例),在设为单例的 bean 中出现的成员变量或静态变量,都必须注意是否存在多线程竞争导致的多线程不安全的问题。
        ——可见,有些时候确实都是人在江湖,身不由己。积累多线程的知识是必不可少的。

1.为什么会有多线程不安全的问题

1.1.写不安全

        上面讲到 web 容器会多线程访问 JVM,这里还有一个问题,为什么多线程时就会存在多线程不安全呢?这是因为在 JVM 中的内存管理,并不是所有内存都是线程私有的,Heap(Java堆)中的内存是线程共享的。
        而 Heap 中主要是存放对象的,这样多个线程访问同一个对象时,就会使用到同一块内存了,在这块内存中存着的成员变量就会受到多个线程的操作。
如下图所示:
  
因为是增加2和3,结果应该是15才对,但是因为多线程的原因,导致结果是12或13。

1.2.读不安全

        上面的写操作不安全是一方面,事实上 Java 中还存在更加糟糕的问题,就是读到的数据也不一致。
        因为多个线程虽然访问对象时是使用的同一块内存(这块内存可称为主内存),但是为了提高效率,每个线程有时会都会将读取到的值缓存在本线程内(具体因不同 JVM 的实现逻辑而有不同,所以缓存不是必然的),这些缓存的数据可称为副本数据。
        这样,就会出现,某个值已经被某个线程更改了,但是其他线程却不知道,也不去主内存更新数据的情况。
如下图所示:
 
上图的情况,其实线程的并发度相对要低一点,但即使是其他线程更改的数据,有的线程也不知道,因为读不安全导致了数据不一致。

2.如何让多线程安全

        既然已经知道了会发生不安全的问题,那么要怎么解决这些问题呢?

2.1.读一致性

        Java 中针对上述“读不安全”的问题提供了关键字 volatile 来解决问题,被 volatile 修饰的成员变量,在内容发生更改的时候,会通知所有线程去主内存更新最新的值,这样就解决了读不安全的问题,实现了读一致性。
        但是,读一致性是无法解决写一致性的,虽然能够使得每个线程都能及时获取到最新的值,但是1.1中的写一致性问题还是会存在。
        既然如此,Java 为啥还要提供 volatile 关键字呢?这并非多余的存在,在某些场景下只需要读一致性的话,这个关键字就能够满足需求而且性能相对还不错,因为其他的能够保证“读写”都一直的办法,多多少少存在一些牺牲。

2.2.写一致性

        Java 提供了三种方式来保证读写一致性,分别是互斥锁、自旋锁、线程隔离。

2.2.1.互斥锁

        互斥锁只是一个锁概念,在其他场景也叫做独占锁、悲观锁等,其实就是一个意思。它是指线程之间是互斥的,某一个线程获取了某个资源的锁,那么其他线程就只能睡眠等待。
        在 Java 中互斥锁的实现一般叫做同步线程锁,关键字 synchronized,它锁住的范围是它修饰的作用域,锁住的对象是: 当前对象(对象锁) 或 类的全部对象(类锁) ——锁释放前,其他线程必将阻塞,保证锁住范围内的操作是原子性的,而且读取的数据不存在一致性问题。
  • 对象锁:当它修饰方法、代码块时,将会锁住当前对象
  • 类锁:修饰类、静态方法时,则是锁住类的所有对象
注意: 锁住的永远是对象,锁住的范围永远是 synchronized 关键字后面的花括号划定的代码域。

2.2.2.自旋锁

        自旋锁也只是一个锁概念,在其他场景也叫做乐观锁等。
        自旋锁本质上是不加锁,而是通过对比旧数据来决定是否更新:
 
        如上所示,不管线程1与线程2哪个先执行,哪个后执行,结果都会是15,由此实现了读写一致性。而因为步骤3的更新失败而在步骤4中更新数据后再此尝试更新的过程,就叫做自旋——自旋只是个概念:表示 操作失败后,线程会循环进行上一步的操作,直到成功为止。
        这种方式避免了线程的上下文切换以及线程互斥等,相对于互斥锁而言,它允许并发的存在(互斥锁不存在并发,只能同步进行)。
        在 Java 的 java.util.concurrent.atomic 包 中提供了自旋的操作类,诸如 AtomicInteger、AtomicLong 等,都能够达到此目的。
 

  1. 上面代码中的18行的代码,直接对一个int变量++操作,这是多线程不安全的
  2. 其中注释掉的19、20、21行代码则是加上了同步线程锁的写法,同步的操作使得多线程安全
  3. 下面的25行代码则是基于自旋锁的操作,也是多线程安全的
        但是,如果并发度很高的话,就会导致某些线程一直都无法更新成功(因为一直有其他线程更改了值),会使得线程长时间占用CPU和线程。所以自旋锁是属于低并发的解决方案。
        另外,直接使用这些自旋的操作类还是太过原始,所以Java还在这个基础上封装了一些类,能够简单直接地接近于 synchronized 那么方便地对某段代码上锁,即是 ReentrantLock 以及 ReentrantReadWriteLock,限于篇幅,这里不详细介绍他们的使用。

2.2.3.线程隔离

        既然自旋锁只是低并发的解决方案,那么遇到高并发要如何处理呢?答案是将成员变量设成线程隔离的,也就是说每个线程都各自使用自己的变量,互相自己是不相关的。这样自然也做到了多线程安全。但是这种做法是让所有线程都互相隔离的了,所以他们之间是不存在互相操作的。
        在 Java 中提供了 ThreadLocal 类来实现这种效果:
// 声明线程隔离的变量,变量类型通过泛型决定
private static ThreadLocal<Integer> localInt = new ThreadLocal<>(); // 获取泛型类的对象
Integer integer = localInt.get(); if (integer==null){
integer = 0;
} // 将泛型对象设到变量中
localInt.set(++integer);

总结

        本文主要讲了为什么会出现多线程不安全的原因,其中涉及读不安全与写不安全。Java 使用 volatile 关键字实现了读一致性,使用同步线程锁(synchronized)、自旋操作类(AtomicInteger等 )以及线程隔离类(ThreadLocal )来实现了写一致性,这三种方法中,同步线程锁效率最低,自旋操作类在非高并发的场景可大大提高效率,但是要想实现真正的高并发,还是需要用到线程隔离类来实现。

Java下如何保证多线程安全的更多相关文章

  1. Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。

    精彩理解:  https://www.jianshu.com/p/21be831e851e ;  https://blog.csdn.net/heyutao007/article/details/19 ...

  2. java集群优化——多线程下的单例模式

    在最初学习设计模式时,我为绝佳的设计思想激动不已,在以后的project中.多次融合设计模式,而在当下的设计中.我们已经觉察出了当初设计模式的高瞻远瞩.可是也有一些不足,须要我们去改进.有人说过.世界 ...

  3. java保证多线程的执行顺序

    1. java多线程环境中,如何保证多个线程按指定的顺序执行呢? 1.1 通过thread的join方法保证多线程的顺序执行, wait是让主线程等待 比如一个main方法里面先后运行thread1, ...

  4. C#多线程下如何保证线程安全?

    多线程编程相对于单线程会出现一个特有的问题,就是线程安全的问题.所谓的线程安全,就是如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码.如果每次运行结果和单线程运行的结果是 ...

  5. 在 java 程序中怎么保证多线程的运行安全?(未完成)

    在 java 程序中怎么保证多线程的运行安全?(未完成)

  6. java 并发性和多线程 -- 读感 (一 线程的基本概念部分)

    1.目录略览      线程的基本概念:介绍线程的优点,代价,并发编程的模型.如何创建运行java 线程.      线程间通讯的机制:竞态条件与临界区,线程安全和共享资源与不可变性.java内存模型 ...

  7. Java 并发性和多线程

    一.介绍 在过去单 CPU 时代,单任务在一个时间点只能执行单一程序.之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程.虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个 ...

  8. Java 下实现锁无关数据结构--转载

    介绍 通常在一个多线程环境下,我们需要共享某些数据,但为了避免竞争条件引致数据出现不一致的情况,某些代码段需要变成原子操作去执行.这时,我们便需要利用各种同步机制如互斥(Mutex)去为这些代码段加锁 ...

  9. Java并发性和多线程

    Java并发性和多线程介绍   java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题, ...

随机推荐

  1. 聊聊推荐系统,FM模型效果好在哪里?

    本文始发于公众号:Coder梁 大家好,我们今天继续来聊聊推荐系统. 在上一回当中我们讨论了LR模型对于推荐系统的应用,以及它为什么适合推荐系统,并且对它的优点以及缺点进行了分析.最后我们得出了结论, ...

  2. 谷歌浏览器加载驱动(chromedriver)——selenium

    http://chromedriver.storage.googleapis.com/index.html 可以到该网站下载对应的谷歌驱动器(注意:需要版本和操作系统对应,其中windows统一32的 ...

  3. SprignBoot是如何访问工程目录下的静态资源?

    ​ 目录 1.牛刀小试 1.1 图片静态资源的访问 1.2 为静态资源添加访问前缀 1.3  WelCome Page 的奇妙跳转 2.那么,SpringBoot是如何做到的呢? ​​​​​​​ 1. ...

  4. JVM学习第一篇思考:一个Java代码是怎么运行起来的-上篇

    JVM学习第一篇思考:一个Java代码是怎么运行起来的-上篇 作为一个使用Java语言开发的程序员,我们都知道,要想运行Java程序至少需要安装JRE(安装JDK也没问题).我们也知道我们Java程序 ...

  5. C#WebService的创建与发布

    VS中新建项目-Web-ASP.NET Web应用程序 然后确定,选择空模版就可以了 其中CRMService.asmx是点击项目新建Web服务(asmx) 这样基本的功能就能用了,然后就是发布 点击 ...

  6. Vue.js的基础学习

    Vue.js的基础网上很多,这里不记录 开始正式页面的开发 1.页面加载时请求数据 methods: { post() { //发送post请求 this.$http.post('../../ashx ...

  7. Linux 安装及管理程序

    Linux 安装及管理程序 目录 一.Linux应用程序基础 1.1.应用程序与系统命令的关系 1.2.典型应用程序的目录结构 1.2.常见的软件包封装类型 二.RPM包管理工具 2.1.RPM软件包 ...

  8. 精尽Spring Boot源码分析 - Jar 包的启动实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  9. 快速简单的了解VLAN(VXLAN)和端口链路类型

    目录 前言 一.VLAN是什么? 1.优点 2.为什么推出VXLAN 二.VXLAN又是什么? 1.优点 三.创建VLAN 四.介绍端口链路类型 五.Access 1.特性 六.Trunk 1.特性 ...

  10. jenkins+nexus上传插件发布制品到nexus

    nexus安装 nexus安装参考:https://www.cnblogs.com/afei654138148/p/14974124.html nexus配置 创建制品库 制品库URL:http:// ...