并发,我需要告诉你这些

Posted yifeixiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发,我需要告诉你这些相关的知识,希望对你有一定的参考价值。

  如果你是个java并发的入门的学习者,这一系列文章是你入门的好帮手,快速理解并发,全面了解你所需的并发工具类,获取需要注意的事项,它可以作为你的入门指南,如果你是为老鸟,可以路过无视啦,或者倘若您愿意,请将不经推敲之处指出,不胜感谢!!!

 一. 线程安全

首先来给大家展示一个多线程最基本的写法:thread/runnable

public class SharedParam                                                             //共享变量
   private static int count = 0; 
   public static void selfAdd() 
       count++ ;
   
public class RunnableDemo implements Runnable                                  
  public void run()    
          SharedParam.selfAdd()
  
  public static void main(String[] args)
          Runnable runnableDemo = new RunnableDemo();   
          Thread ThreadDemo1 = new Thread(runnableDemo);   
          Thread ThreadDemo2 = new Thread(runnableDemo);   
          ThreadDemo1.start();                                                  //启动线程
          ThreadDemo2.start(); 
  

  很明显上述代码中,count自增在多线程中,由于count取值、赋值交叉执行,多次执行很可能会看到不同的执行结果,这显然不是我们想看到的,那如何保证多次执行结果一致、线程安全呢?

二. 内置锁/同步

1. 内置锁的目的,达到冯诺依曼计算机模型串行执行一致性的目的,即每次执行结果一致,实现线程安全。
重要特征: 原子性、可见性、有序性

  锁是什么,在一个房间,加上你的锁之后,这个房间的空间为你私人所有,房间外部的人在没有取下房间锁的情况下是无法对房间内部的事务进行操作。 这是我们对于锁的初识,在开始学习并发编程时,几乎每个人都是从锁的原子性开始认识锁的,似乎它是解决不同线程执行时序的问题(竞态条件)一把万能的钥匙。

Tips:什么是竞态条件?
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件 。

  为保证各个线程对变量状态的一致性,锁的原子性采用的策略是代码块互斥的方式并发执行程序,实际意义上的程序串行来解决问题,避免再发生交替执行顺序而产生不同的结果。
然而需要知道的是由于现代CPU多级缓存和JVM中线程栈内存的复杂性,原子性只是内置锁对于线程安全的第一层保障,在代码的执行过程中我们依然存在不能保证各个线程变量的一致性的风险,原因每个线程在共享内存时基于的Java内存模型(JMM)中,这一部分的变量值并不对其他线程实时可见(见系列2多线程的内存模型),也就是说串行执行的A、B两个线程在并不能保证,A线程修改变量后在B线程立即可见,(同理可以类比分布式系统数据一致性,解决的办法总是有相通之处),因此上锁的对象就需要具备对于其他线程的可见性,即实时同步主内存的变量。以Synchronized为例,说明来同步锁我们需要掌握的3大特性。

2. 原子性(同步代码块)—— 可能并没有想的干了那么多
  原子性,每次只有一个线程在内置锁的保护下的执行对应代码块,其后都只能阻塞等待。这里需要注意的问题是在代码块互斥区时,其他线程对该对象或类没有读写权限呢?NO,锁所保护的是代码块而非( )中的对象或者类,因此其他线程是能读到的它的值交替执行,除非其他线程也加了该对象或者类的锁,这样各个同步块都需要获取到那个对象的唯一锁,才能保证串行执行(加如图)。 注意的是理解对象或类的锁时,加锁或者说代码块互斥的时机,指的是使用到这个类或者对象时,代码块互斥生效,这才是我们所说的该对象或者类的锁,而并非误解为简单的变量读写时的线程独占,理解互斥的时机十分关键呀。同步锁里有读写锁的扩展类。
同步锁示例:

Synchronized(object1)                                                              //object1为object/class[加锁粒度]——锁的拥有者
        object1.add();                                                                     //代码块1

Synchronized(object2)                                                              //object2为object/class[加锁粒度]——锁的拥有者
        object1.remove();                                                               //代码块2

  在上述代码中锁拥有者相同时,这种代码块互斥只会有:
(1)代码块1和代码2分别在两个线程执行时互斥;
(2)代码块1在两个线程执行是互斥;并不存在其他加锁阻塞情况。(其中,对象锁:某个对象调用的代码块互斥;类锁:该类调用处代码块互斥)。

Tips:需要注意的是,形式上的代码块互斥,并不是说他处无法进行读写,考虑到对象发布和escape的情况,
原子性保证的只有同一把锁的同步代码块不同线程执行时串行,无法同时进行读写,为此所有执行读操作
或者写操作的线程都必须在同一个锁上同步

  

注意:对于类锁的写法有两种:
public Synchronized static void write()...与
public static void write()
      Synchronized(thisobject.class…
)等价

  

3. 可见性 —— 请不要视而不见

  大部分初学者对于可见性可能了解甚少,理解可见性之前,务必要先补充多线程的内存模型结构内容:

  技术图片

  上图每个线程栈对于共享变量的引用会保留共享变量中成员变量的一份私有拷贝或者说是副本,以此为依据做修改,(这也是由于CPU寄存器和缓存决定的),这导致单单依赖内置锁的原子性并不能完全解决多线程发生获取无效值保证一致性的问题,内置锁中可见性的重要性在此时显而易见。可见性作为做线程安全的第二重保障也不难理解了。

Tips:不仅仅是同步块方式,volatile 关键字,也能保证,共享变量直接从主内存区读写。

  多线程可能获取到共享变量无效值的原因在于,对于共享变量,各个线程都是从各自的副本读写,在没有充分同步主内存的情况下,各个线程就无法保证共享变量的一致性,然而内置锁的第二个特性:可见性,就解决了这一问题,在加内置锁之后,该变量在读写的过程里实时的主内存区同步(底层的内存栅栏就不展开了),保证一致性,然需要注意的,那也只是在读取的时刻保证一致性,并没有额外对对象读写加锁,按此推断,两不同的同步代码块中,都有共享变量的竞态检查,是否会导致之前交替执行的是否依然有问题,会,解决办法是两个过程是由同一个对象的同步锁保护,如此才能保证串行的一致性,准确的拿到主内存的值,也就衍生出时刻注意加锁充分的问题。
同步,为何称之为同步,是不是可以理解为在于进入代码块时,将同步获取主内存的变量值,出代码块时,将代码块中修改的值同步回主内存,总的来说,Synchronized以一个同步代码块的形式存在,我们是对this,object(指定对象),*.class(类)进行不同粒度的加锁,完成控制它在线程上的可见性的目的。

Tips:同时需要注意的是并非只有对象写入时需要加锁,在对象读取时需要考虑加锁,原则上,只要发布的变量有写入的可能,
那么读写都需要加锁,才能保证加锁的充分,此时不应该忽略使用volatile原子锁的作用。

4. 有序性 —— 重排序&& happens-before原则

  由于java是JIT的,不同的编译器会存在不同编译优化(比如生产常见的IBM J9),在为了加速执行JVM编译器帮助我们对于热点代码做了执行顺序层次的优化,这会导致读取数据不一致的问题,比如存在将未完成构造的对象的地址赋给共享对象,为此同步锁还需要帮助我们解决了JVM重排序可能导致的执行错误。
  执行的有序性是线程安全的第三重保障,保证了volatile禁止重排序和Synchronized对于变量的独占性。
Java内存模型同样通过happens-before原则定义了多线程情况下,如何保证先行发生的,下列出的原则都是A操作先于B操作,并且操作A的结果能被操作B感知的情况,Java内存模型通过一下这一原则解决了竞争冲突的情况,保证执行的过程的正确性,但java内存模型的原则并不一定能保证多线程代码符合程序开发预期有序性执行。

Tips:A操作能被B操作感知的情况,才是happens-before原则需要保证先行发生的情况。
程序顺序规则 :如果程序中操作 A 在操作 B 之前,那么在线程中操作 A 将在操作 B 之前执行,这里是指流程控制并非代码,比如判断、循环。

监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行,比如,Synchronized代码块的加锁解锁。
volatile 变量规则 :对 volatile 变量的写入操作必须在对该变量的读操作之前执行。
线程启动规则 :在线程上对 Thread.start 的调用必须在该线程中执行任何操作之前执行。
线程结束规则 :线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从 Thread.join 中成功返回,或者在调用 Thread.isAlive 时返回 false。
中断规则 :当一个线程在另一个线程上调用 interrupt 时,必须在被中断线程检测到 interrupt 调用之前执行(通过抛出 InterruptException,或者调用 isInterrupted 和 interrupted)。
终结器规则 :对象的构造函数必须在启动该对象的终结器之前执行完成。
传递性 :如果操作 A 在操作 B 之前执行,并且操作 B 在操作 C 之前执行,那么操作 A 必须在操作 C 之前执行,这里的程序顺序同理程序顺序原则。

  其实在java在处理数据一致性,并发线程安全时,原子性,可见性,有序性是java考虑并发处理线程安全的基本三条原则,java的多线程并发实践中,java内存模型是JVM在现代物理内存模型的基础上抽象而来,是原子性,可见性,有序性能落地的基础,深入研究java内存模型及内存交互、通讯,对于线程安全的掌握有着莫大帮助。

以上是关于并发,我需要告诉你这些的主要内容,如果未能解决你的问题,请参考以下文章

并发编程并发编程中你需要知道的基础概念

『图解Java并发编程系列』10张图告诉你Java并发多线程那些破事

高并发先操作数据库,还是先操作缓存?5 个方案告诉你!

设计高可用高并发的微服务架构,你至少学会这些?

高并发先操作数据库,还是先操作缓存?5 个方案告诉你!

假期余额不足,这些并发知识你还记得吗?