线程安全知多少

线程是把双刃剑:多线程会导致性能问题(线程引入的开销和上下文切换)

不管业务中遇到怎样的多线程的访问某个对象或者某个方法的情况,而在编程这个业务逻辑的时候,都不需要额外做任何的处理(也就是可以像单线程编程一样),程序也可以正常的运行(不会因为多线程而出错),就可以称为线程安全, 相反,如果在编程的时候,需要考虑这些线程在运行时的调度和交替(例如在get调用到的期间不能调用set),或者需要额外的同步(比如使用synchronized关键字等),那么就是线程安全的

线程不安全分类:

1.运行结果错误:a++多线程下出现消失的请求现象
2.活跃性问题:死锁、活锁、饥饿
3.对象发布和初始化的时候的安全问题
什么是发布:对象可以超出本类的地方使用,或者作为一个参数或retrue
1):返回一个private对象(private本意是不让外访问)
返回副本
2):还为完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如:
a.在构造函数中未初始化完就this赋值
b.隐式溢出—注册监听事件
c.构造函数中运行线程

那些场景需要额外注意线程安全问题?

1.访问共享变量或资源,会由并发危险,比如对象的属性、静态变量、共享缓存、数据库等
2.所有依赖时序的操作:即使每一步都是线程安全的,还是会存在并发问题:
一个线程读取了一个共享数据,并在此基础上做了修改例如:index++
3.不同的数据之间存在绑定关系的时候
多个线程对多个共享数据进行更新:如果这些共享数据之间存在关联关系,那么为了保障操作的原子性我们可以考虑使用锁。例如关于服务器的配置信息可能包括主机ip地址、端口号等。一个线程如果要对这些数据进行更新,则必须要保证更新操作的原子性,即主机ip地址和端口号是一起被更新的,否则其他线程可能看到一个并不真实存在的主机ip地址和端口号组合所代表的服务器
4.我们使用其他类的时候,如果对方没有申明自己是线程安全的,那么大概率存在在并发问题的隐患

多线程会导致的性能问题:

性能问题有哪些体现、什么是性能问题:
处理速度慢,资源消耗大、吞吐量降低等。
为什么多线程会带来性能问题:
体现在两个方面:线程的调度和协作,这两方面通常相辅相成,也就是说,由于线程需要协作,所以会引起调度:

调度:上下文切换

什么时候会需要线程调度呢?当可运行的线程超过cpu核心数,那么操作系统就要调度线程,以便于让每个线程都有运行的机会。

调度会引起上下文切换。

例如当某个运行Thread.sleep(1000)的时候,线程调度器就会让当前这个线程阻塞,然后往往会让另一个正在等待的CPU资源的线程进入可运行状态,这里会产生上下文切换,这是一种比较大的开销,有时上下文切换到的开销甚至比线程执行的时间都要长。

什么是上下文?
上下文是指某一时刻cpu寄存器和程序计数器的内容。寄存器占CPU内部的数量较少但是速度很快的内存。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算器程序的运算速度。程序计数器是一个专用的寄存器,由于表明指令序列中CPU正在执行的位置,存的值未正在执行的指令的位置或则下一个将要执行的指令的位置,具体依赖于特定的操作系统。
上下文切换可以认为是内核(操作系统的核心数)在cpu上对于进程(包括线程)进行一下活动:(1)挂机去一个线程,将这个线程在cpu的状态(上下文)存储于内存中某处,(2)在内存中检索下一个进程的上下文并将其在cpu的寄存器恢复,(3)跳转到程序计数所指向的位置(即跳转到进程被中断时的位置),以恢复该进程。

缓存开销
除了刚才提到了上下文切换带来的直接开销外,还需要考虑到间接带来的缓存失效的问题。我们知道程序会会有很大的概率会访问刚才访问过的数据。所以cpu为了加快执行速度,会根据不同的算法,把常用到的数据缓存到cpu内,这样以后再用到该数据时,可以很快使用。
但是现在上下文切换了,也就是说,cpu即将执行不同的代码,那么原本缓存的内容有极大的概率也没有价值了。这就需要cpu重新缓存,者导致线程再被调度运行后,一开始启动速度会变慢。

何时会导致密集的上下文切换
如果程序频繁的竞争锁,或者由于IO读写等原因导致的频繁阻塞,那么这个程序就可能需要跟多的上下文切换,这也导致了更大的开销

协作:内存同步

线程之间如果使用共享数据,那么为了避免数据混乱,肯定使用同步手段,为了数据的正确性,同步手段往往会使用禁止编译器优化、、使CPU内的缓存失效等手段,这显然带来额外的开销,因为减少了原本可以进行的优化。

Java内存模型------底层原理:

三兄弟:JVM内存结构VS java内存模型VS java对象模型
容易混淆:三个截然不同的概念,但是很多人容易混淆
**JVM内存结构,**和java虚拟机的运行时区域有关
在这里插入图片描述
堆:占用内存最多,存放引用数据类型,对象实例和数组
虚拟机栈:基本数据类型和对象的引用,编译的时候确定了大小而且不会被改变
方法区:静态变量,类信息,常量信息,永久引用(static)
本地方法栈:本地方法相关
程序计数器:占最小的内存,保存字节码的行号数

Java内存模型,和java的并发编程有关

Java对象模型,和java对象在虚拟机中的表现形式有关,需要一个标准,让多线程运行结果可预期
Volatile、synchronized、lock等的底层原理都是JMM

重排序:
实际执行顺序和代码在java文件中的顺序不一致
在这里插入图片描述
优点:提高处理速度,减少指令

可见性:

在这里插入图片描述
Cpu有多级缓存,导致读的数据过期:1.高速缓存的容量比内存小,但是速度仅次于寄存器,所以在cpu和主内存就多了Cache层
2.线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的
3.如个多个核心都只用一个缓存,那么也就不存在可见性问题了
4.每个核心都会将自己需要的数据读到独占缓存中,数据修改后也要写入缓存,然后等待刷入到主存中。所以会导致有些核心读取的是一个过期值
在这里插入图片描述
什么是主内存和工作内存

1.所有变量都存在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存的拷贝
2.线程不能直接读写主内存中的变量,而是只能操作自己工作内存的变量,然后同步到主内存中
3.主内存是多个线程共享的,但是线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转完成,所有的共享变量存在于主内存中,而且线程读写共享数据也是通过本地内存交换的,所以才会导致可见性问题,

解决可见性:Happens-before::在时间上,动作a发生在动作b之前,b保证能看见a,这就是happens-before

Happens-before规则有哪些?
1.单线程原则:
单线程中的每一个操作都happens-before该程序顺序中稍后出现的该线程中的每一个操作。如果操作x和操作y是同一个线程两个操作,并且在代码执行上x先于y出现,那么hb(x,y)在这里插入图片描述
并不是受x操作一定要在y操作之前被执行,而是说x的执行结果对于y是可见的。只要满足可见性
2.锁操作(Lock/synchronized)
在这里插入图片描述
3**.Volatile变量**

4.线程启动

5**.线程join**
我们知道join可以让线程之间等待,假设线程A通过调用thread.start()生成一个新的线程B,然后在调用thread.join()。线程A在Join期间会等待,知道线程B的run方法执行完成,在join方法返回后,线程A中的所有后续操作将看到线程Brun方法执行的操作

在这里插入图片描述

6.传递性

7.中断
一个线程被其他线程interrupt时,那么检查中断(isInterrupted)或者抛出InterrupedException一定能看到

8.构造方法
对象构造方法的最后一行指令happens-before于finalize()方法的第一行指令

9.工具类的happens-before:
a.线程安全的容器get一定能看到在此之前的put等存入动作
b.CountDownLatch
c.Semapjore
d.Future
e.线程池
f. Cyclicbarrier

原子性:
Volatile:

volatile是一种同步机制,比synchronized或者Lock相关类更加轻量,因为使用volatile并不会发生上下文切换等开销很大的行为

如果一个变量被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改

但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在有限的场景下能发挥作用

不适用于:a++这样的不具有原子操作

使用场合:
1.Booblean flag,如果一个共享变量自始自终只被各个线程赋值,而没有其他操作,呢么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是具有原子性的,而volatile又保证可见性,所以线程安全
2.作为刷新之前的触发器:用了volatile int x,可以保证读取x后,之前的所有变量可见

volatile的作用:可见性–读一个volatile变量,需要首先让本地缓存失效,这样就必须到主内存中读取最新值,写一个volatile属性会立即写入到主内存中去
禁止指令重排序优化-–解决单例双重锁乱序问题

Volatile和synchronized的关系:volatile可以做是synchronized的轻量版:如果一个共享变量自始自终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值具有原子性,而volatile又保证可见性,所以保证线程安全
小结:1) .volatile修饰符适用于一下场景:某个属性被多个线程共享,其中有一个线程修改了额此属性,其他线程立即能获得修改后的值,比如boolean flag
2). volatile属性的读写操作是无锁的,他不能代替synchronized,因为没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以成本低
3**)Volatile只能用作于属性**,我们用volatile修饰属性,compilers就不会对这个属性做指令重排序
4)Volatile提供可见性,任何一个线程对其的需改将立即对其他线程可见。Volatile属性不会被线程缓存,始终从主存中读
5)Volatile提供了happens-before保证,对volatile变量v的写入happens-before其他线程后续对v的操作
6)Volatile可以使得long和double的赋值具有原子性
能保证可见性的措施:除了volatile可以然变量保证可见习性外、synchronized、lock、并发集合、Thread.join()和Thread.start等都可以保证一定的可见性,具体见happens-before

升华:synchronized可见性的正确理解:synchronized也能保证happens-before效果
特别注意的是,synchronized不仅防止了一个线程在操作某个对象时受到其他线程干扰,同时还保证了修改后,可以立即被其他线程所看见

原子性:
一系列操作,要么全部执行成功,要么全部不执行,不会出现执行一般的情况,是不可分割的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章