7张图带你轻松理解Java 线程安全

640?wx_fmt=jpeg


/   今日科技快讯   /


近日,工信部最新公布的信息显示,华为第二款在国内上市的5G手机已经拿到了入网许可证,为折叠屏手机Mate X,目前该机的量产工作已经开始。据相关报道称,华为将在8月9日的开发者大会上公布Mate X在国内的发售时间,而预计上市时间会在8月中旬或者8月底。Mate X在国内的起始售价预计会低于万元(极有可能是9999元)。


/   作者简介   /


大家周一好,酷夏来临,工作之余注意防暑哦~


本篇文章转自七彩祥云至尊宝的博客,文章主要通过图解来讲解线程相关知识,相信会对大家有所帮助!


原文地址:

https://juejin.im/post/5d2c97bff265da1bc552954b


/   什么是线程   /


按操作系统中的描述,线程是 CPU 调度的最小单元,直观来说线程就是代码按顺序执行下来,执行完毕就结束的一条线。


举个 ?,富土康的一个组装车间相当于 CPU ,而线程就是当前车间里的一条条作业流水线。为了提高产能和效率,车间里一般都会有多条流水线同时作业。同样在我们 Android 开发中多线程可以说是随处可见了,如执行耗时操作,网络请求、文件读写、数据库读写等等都会开单独的子线程来执行。


那么你的线程是安全的吗?线程安全的原理又是什么呢?(本文内容是个人学习总结浅见,如有错误的地方,望大佬们轻拍指正)


/   线程安全   /


了解线程安全的之前先来了解一下 Java 的内存模型,先搞清楚线程是怎么工作的。


 Java 内存模型 - JMM


什么是 JMM


JMM(Java Memory Model),是一种基于计算机内存模型(定义了共享内存系统中多线程程序读写操作行为的规范),屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。保证共享内存的原子性、可见性、有序性。


能用图的地方尽量不废话,先来看一张图:


640?wx_fmt=png


上图描述了一个多线程执行场景。线程 A 和线程 B 分别对主内存的变量进行读写操作。其中主内存中的变量为共享变量,也就是说此变量只此一份,多个线程间共享。但是线程不能直接读写主内存的共享变量,每个线程都有自己的工作内存,线程需要读写主内存的共享变量时需要先将该变量拷贝一份副本到自己的工作内存,然后在自己的工作内存中对该变量进行所有操作,线程工作内存对变量副本完成操作之后需要将结果同步至主内存。


线程的工作内存是线程私有内存,线程间无法互相访问对方的工作内存。


为了便于理解,用图来描述一下线程对变量赋值的流程。


640?wx_fmt=png


那么问题来了,线程工作内存怎么知道什么时候又是怎样将数据同步到主内存呢?这里就轮到 JMM 出场了。JMM 规定了何时以及如何做线程工作内存与主内存之间的数据同步。


对 JMM 有了初步的了解,简单总结一下原子性、可见性、有序性。


原子性:对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行。


可见性:多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。


有序性:程序的执行顺序按照代码顺序执行,在单线程环境下,程序的执行都是有序的,但是在多线程环境下,JMM 为了性能优化,编译器和处理器会对指令进行重排,程序的执行会变成无序。


到这里,我们可以引出本文的主题了 --【线程安全】。


线程安全的本质


其实第一张图的例子是有问题的,主内存中的变量是共享的,所有线程都可以访问读写,而线程工作内存又是线程私有的,线程间不可互相访问。那在多线程场景下,图上的线程 A 和线程 B 同时来操做共享内存里的同一个变量,那么主内存内的此变量数据就会被破坏。也就是说主内存内的此变量不是线程安全的。我们来看个代码小例子帮助理解。


 
 


示例代码中 runTest 方法2个线程分别执行 1_000_000 次 count() 方法, count() 方法中只执行简单的 x++ 操作,理论上每次执行 runTest 方法应该有一个线程输出的 x 结果应该是2_000_000。但实际的运行结果并非我们所想:


 
 


我运行了10次,其中一个线程输出 x 的值为 2_000_000 只出现了2次。


 
 


出现这样的结果的原因也就是我们上面所说的,在多线程环境下,我们主内存的 x 变量的数据被破坏了。我们都知道完成一次 i++ 相当于执行了:


 
 


在多线程环境下就会出现在执行完 int tmp = x + 1; 这行代码时就发生了线程切换,当线程再次切回来的时候,x 就会被重复赋值,导致出现上面的运行结果,2个线程都无法输出 2_000_000。


下图描述了示例代码的执行时序:


640?wx_fmt=png


那么 Java 是如何来解决上述问题来保证线程安全,保证共享内存的原子性、可见性、有序性的呢?


/   线程同步   /


Java 提供了一系列的关键字和类来保证线程安全。


Synchronized 关键字


Synchronized 作用


保证方法或代码块操作的原子性


Synchronized 保证⽅法内部或代码块内部资源(数据)的互斥访问。即同⼀时间、由同⼀个 Monitor(监视锁) 监视的代码,最多只能有⼀个线程在访问。


话不多说来张动图描述一下 Monitor 工作机制:


640?wx_fmt=gif


640?wx_fmt=gif


被 Synchronized 关键字描述的方法或代码块在多线程环境下同一时间只能由一个线程进行访问,在持有当前 Monitor 的线程执行完成之前,其他线程想要调用相关方法就必须进行排队,知道持有持有当前 Monitor 的线程执行结束,释放 Monitor ,下一个线程才可获取 Monitor 执行。


如果存在多个 Monitor 的情况时,多个 Monitor 之间是不互斥的。


多个 Monitor 的情况出现在自定义多个锁分别来描述不同的方法或代码块,Synchronized 在描述代码块时可以指定自定义 Monitor ,默认为 this 即当前类。


640?wx_fmt=gif


640?wx_fmt=gif


保证监视资源的可见性


保证多线程环境下对监视资源的数据同步。即任何线程在获取到 Monitor 后的第⼀时 间,会先将共享内存中的数据复制到⾃⼰的缓存中;任何线程在释放 Monitor 的第⼀ 时间,会先将缓存中的数据复制到共享内存中。


保证线程间操作的有序性


Synchronized 的原子性保证了由其描述的方法或代码操作具有有序性,同一时间只能由最多只能有一个线程访问,不会触发 JMM 指令重排机制。


Volatile 关键字


Volatile 作用


保证被 Volatile 关键字描述变量的操作具有可见性和有序性(禁止指令重排)。


注意:


  1. Volatile 只对基本类型 (byte、char、short、int、long、float、double、boolean) 的赋值 操作和对象的引⽤赋值操作有效。

  2. 对于 i++ 此类复合操作, Volatile 无法保证其有序性和原子性。

  3. 相对 Synchronized 来说 Volatile 更加轻量一些。


java.util.concurrent.atomic


包提供了一系列的 AtomicBoolean、AtomicInteger、AtomicLong 等类。使用这些类来声明变量可以保证对其操作具有原子性来保证线程安全。


实现原理上与 Synchronized 使用 Monitor(监视锁)保证资源在多线程环境下阻塞互斥访问不同,java.util.concurrent.atomic 包下的各原子类基于 CAS(CompareAndSwap) 操作原理实现。 


CAS 又称无锁操作,一种乐观锁策略,原理就是多线程环境下各线程访问共享变量不会加锁阻塞排队,线程不会被挂起。通俗来讲就是一直循环对比,如果有访问冲突则重试,直到没有冲突为止。


Lock


Lock 也是 java.util.concurrent 包下的一个接口,定义了一系列的锁操作方法。Lock 接口主要有 ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock 实现类。与 Synchronized 不同是 Lock 提供了获取锁和释放锁等相关接口,使得使用上更加灵活,同时也可以做更加复杂的操作,如:



/   总结   /


出现线程安全问题的原因:


在多个线程并发环境下,多个线程共同访问同一共享内存资源时,其中一个线程对资源进行写操作的中途(写⼊入已经开始,但还没 结束),其他线程对这个写了一半的资源进⾏了读操作,或者对这个写了一半的资源进⾏了写操作,导致此资源出现数据错误。


如何避免线程安全问题?


  • 保证共享资源在同一时间只能由一个线程进行操作(原子性,有序性)。

  • 将线程操作的结果及时刷新,保证其他线程可以立即获取到修改后的最新数据(可见性)。


推荐阅读:

动画代码太丑,用Kotlin DSL来拯救!

Android Q新特性,一起来学习折叠屏应该如何适配

由Android官方团队带你学习Android Studio的布局编辑器


欢迎关注我的公众号

学习技术或投稿


640.png?


640?wx_fmt=jpeg

长按上图,识别图中二维码即可关注


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Docker是一种流行的容器化技术,通过轻量级、隔离性强的容器来运行应用程序。下面我将通过十张图你深入理解Docker容器和镜像。 1. 第一张图展示了Docker容器和镜像的关系。镜像是Docker的基础组件,它是一个只读的模板,包含了运行应用程序所需的所有文件和配置。容器是从镜像创建的实例,它具有自己的文件系统、网络和进程空间。 2. 第二张图展示了Docker容器的隔离性。每个容器都有自己的文件系统,这意味着容器之间的文件互不干扰。此外,每个容器还有自己的网络和进程空间,使得容器之间的网络和进程相互隔离。 3. 第三张图展示了Docker镜像和容器的可移植性。镜像可以在不同的主机上运行,只需在目标主机上安装Docker引擎即可。容器也可以很容易地在不同的主机上迁移,只需将镜像传输到目标主机并在其上创建容器。 4. 第四张图展示了Docker容器的快速启动。由于Docker容器与主机共享操作系统内核,启动容器只需几秒钟的时间。这使得快速部署和扩展应用程序成为可能。 5. 第五张图展示了Docker容器的可重复性。通过使用Dockerfile定义镜像构建规则,可以确保每次构建的镜像都是相同的。这样,可以消除由于环境差异导致的应用程序运行问题。 6. 第六张图展示了Docker容器的资源隔离性。Docker引擎可以为每个容器分配一定数量的CPU、内存和磁盘空间,确保容器之间的资源不会互相干扰。 7. 第七张图展示了Docker容器的可扩展性。通过使用Docker Swarm或Kubernetes等容器编排工具,可以在多个主机上运行和管理大规模的容器群集。 8. 第八张图展示了Docker镜像的分层结构。镜像由多个只读层组成,每个层都包含一个或多个文件。这种分层结构使得镜像的存储和传输变得高效。 9. 第九张图展示了Docker容器的生命周期。容器可以通过创建、启动、停止和销毁等命令来管理。这使得容器的维护和管理变得简单。 10. 第十张图展示了Docker容器的应用场景。Docker容器广泛应用于开发、测试、部署和运维等领域。它可以提供一致的开发和运行环境,简化了应用程序的管理和交付过程。 通过这十张图,希望能让大家更深入地理解Docker容器和镜像的概念、特性和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值