前言

        可能有人会觉得,只要我写代码的时候不去开启其他线程,那么就不会有多线程的问题了。
        然而事实并非如此,如果仅仅是一些简单的测试代码,确实代码都会顺序执行而不是并发执行,但是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. 如何使用 Python 统计分析 access 日志?

    一.前言 性能场景中的业务模型建立是性能测试工作中非常重要的一部分.而在我们真实的项目中,业务模型跟线上的业务模型不一样的情况实在是太多了.原因可能多种多样,这些原因大大降低了性能测试的价值. 今天的 ...

  2. WordPress简介

    WordPress是什么? WordPress是一款免费开源的内容管理系统(CMS),目前已经成为全球使用最多的CMS建站程序.根据 W3techs 的最新统计(截至2021年4月),在全球的所有网站 ...

  3. 大型情感类技术连续剧-徒手撸一个 uTools(二)

    前言 上篇手把手教你实现一个支持插件化的 uTools 工具箱我们介绍过了如何通过 electron 实现 utools 的插件功能体系,并按照 utools 的交互和设计做出了一套可以支持插件化的桌 ...

  4. 使用pdb进行Python调试

    调试应用有时是一个不受欢迎的工作,当你长期编码之后,只希望写的代码顺利运行.但是,很多情况下,我们需要学习一个新的语言功能或者实验检测新的方法,从而去理解其中运行的机制原理. 即使不考虑这样的场景,调 ...

  5. hdu1232 并查集总结

    前言 在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中. 这一类问题其特点是看似并 ...

  6. js笔记17

    BOM浏览器对象模型 1.window.open(url,ways) url 是打开的网页地址 ways 打开的方式  _self 2.window.close() 3.浏览器用户的信息 window ...

  7. gRPC(2):四种基本通信模式

    在 gRPC(1):入门及简单使用(go) 中,我们实现了一个简单的 gRPC 应用程序,其中双方通信是简单的请求-响应模式,没发出一个请求都会得到一个响应,然而,借助 gRPC 可以实现不同的通信模 ...

  8. 用python的matplotlib根据文件里面的数字画图像折线图

    思路:用open打开文件,再用a=filename.readlines()提取每行的数据作为列表的值,然后传递列表给matplotlib并引入对应库画出图像 代码实现:import matplotli ...

  9. Docker+Vagrant+Gitlab 构建自动化的 CI/CD

    如果你的开发流程是下面这个样子的, 那么你一定很好奇, 为什么我提交到仓库的代码可以自动部署并访问到最新的提交内容 这就是近年来兴起的 DevOps 文化, 很方便的解决了开发人员和运维人员每次发布版 ...

  10. MyBatis框架的使用解析!数据库相关API的基本介绍

    动态SQL if 根据条件包含where子句的一部分 <select id="findActiveBlogLike" resultType="Blog"& ...