JUC并发编程

多线程技术可以使程序的响应更加的快,可以在进行其他工作的时候一直处于工作状态。性能得到提升,但是多线程会给程序带来多线性并发安全问题。并发的安全问题发生的原因是多个线程对同一个资源的操作而造成的不安全问题。

首先需要了解JMM(内存模型),JMM是指java内存模型,和JVM不同,它是不存在的,是一个规范模型,是一种约定。

在JMM的规定中,所有的变量都存放在主内存中,当线程调用主内存中的变量时,会拷贝一份数据到该线程的独享的工作内存中,来在此线程中来对该变量副本进行操作,操作完成后会将修改后的变量重新写会到主内存中。

JUC并发编程_第1张图片

JUC并发编程_第2张图片

主内存与工作内存八种操作指令:

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态;
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作;
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用;
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;

如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作,如果要把变量从工作内存同步回主内存,就要按顺序执行store和write操作。注意Java内存模型只要求上述两个操作必须按顺序执行,但不要求是连续执行。也就是说read与load之间、store与write之间是可插入其他指令的,如对主内存中的变量a、b进行访问时,一种可能出现的顺序是read a、read b、load b、load a。除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现;
  • 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存;
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中;
  • 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作;
  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执 行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁;
  • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值;
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量;
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作);

关于JMM的一些同步约定:

1.线程解锁前,必须把共享变量立刻刷新到主内存中。

2.线程加锁前,必须读取主内存中的最新值到工作内存中。

3.加锁和解锁是同一把锁。

并发编程问题的核心问题---可见性,原子性,有序性

可见性:当一个线程对共享的主内存变量进行修改后,其他的线程能否立马发现,并及时更新自己的缓存的值。因为可见性的存在,所以主内存中的数据容易和线程工作内存中的数据不一致。

原子性:线程A在执行任务时,不能被打扰,也不能被分割。要么同时成功,要么同时失效。

有序性:有序性指的是程序按照代码的先后顺序执行。为了优化性能,有时候会改变程序中语句的先后顺序。 cpu 的读等待同时指令执行是 cpu 乱序执行的根源。 读指令的同时可以同时执行不影响的其他指令。

并发问题总结:

缓存导致的可见性问题编译优化带来的有序性问题线程切换带来的原子性问题 其实缓存、线程、编译优化的目的和我们写并发程序的目的是相同的, 都是提高程序安全性和性能。但是技术在解决一个问题的同时,必然会带来另外 一个问题,所以在采用一项技术的同时,一定要清楚它带来的问题是什么,以及 如何规避这些问题。
volatile 关键字
1.保证可见性
2.原子性无法保证
3.禁止进行指令重排
volatile 底层实现原理
使用 Memory Barrier ( 内存屏障 ) 。内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指 令重排做出一定的限制,比如,一条内存屏障指令可以禁止编译器和处理器将其 后面的指令移到内存屏障指令之前。Volatile 变量编译为汇编指令会多出#Lock 前缀.
有序性实现: 主要通过对 volatile 修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性的。
可见性实现: 主要是通过 Lock 前缀指令 + MESI 缓存一致性协议来实现的。对 volatiile 修饰的变量执行写操作时, JVM 会发送一个 Lock 前缀指令给 CPU,CPU 在执行完写操作后,会立即将新值刷新到内存,同时因为 MESI 缓 存一致性协议,其他各个 CPU 都会对总线嗅探,看自己本地缓存中的数据是否 被别人修改,如果发现修改了,会把自己本地缓存的数据过期掉。然后这个 CPU
里的线程在读取改变量时,就会从主内存里加载最新的值了,这样就保证了可见性。
如何保证原子性
“同一时刻只有一个线程执行”我们称之为互斥。如果我们能够保证对共享变量的修改是互斥的那么就都能保证原子性了。使用原子类来解决原子性问题,Atomic类,这些类的底层都与操作系统挂钩,在内存中修改值,Unsafe类是一个很特殊的类。
锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。
JUC并发编程_第3张图片
synchronized 是独占锁/排他锁(就是有你没我的意思),但是注意!
synchronized 并不能改变 CPU 时间片切换的特点,只是当其他线程要访问这个资源时,发现锁还未释放,所以只能在外面等待。 synchronized 一定能保证原子性,因为被 synchronized 修饰某段代码 后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一 定能保证原子操作。 synchronized 也能够保证可见性和有序性。
原子变量
现在我们已经知道互斥锁可以保证原子性,也知道了如何使用
synchronized 来保证原子性。但 synchronized 并不是 JAVA 中唯一能保证原子性的方案。
如果你粗略的看一下 JUC(java.util.concurrent 包),那么你可以很显眼的 发现它俩:
JUC并发编程_第4张图片
一个是 locks 包,一个是 atomic 包,它们可以解决原子性问题。加锁是一种阻塞式方式实现, 原子变量是非阻塞式方式实现
原子类
原子类原理(AtomicInteger 为例)
原子类的原子性是通过 volatile + CAS 实现原子操作的。 AtomicInteger 类中的 value 是有 volatile 关键字修饰的,这就保证了 value 的内存可见性,这为后续的 CAS 实现提供了基础。 低并发情况下:使用 AtomicInteger。
CAS
CAS(Compare-And-Swap) :比较并交换,该算法是硬件对于并发操作的支持。
CAS 是乐观锁的一种实现方式,他采用的是自旋锁的思想,是一种轻量级的锁机制。 即每次判断我的预期值和内存中的值是不是相同,如果不相同则说明该内存值已经被其他线程更新过了,因此需要拿到该最新值作为预期值,重新判断。而该 线程不断的循环判断是否该内存值已经被其他线程更新过了,这就是自旋的思 想。 底层是通过 Unsafe 类中的 compareAndSwapInt 等方法实现.
CAS 包含了三个操作数:
①内存值 V
②预估值 A (比较时,从内存中再次读到的值)
③更新值 B (更新后的值)
当且仅当预期值 A==V,将内存值 V=B,否则什么都不做。
这种做法的效率高于加锁,当判断不成功不能更新值时,不会阻塞,继续获得 cpu执行权,继续判断执行。 JUC并发编程_第5张图片

CAS 的缺点
CAS 使用自旋锁的方式,由于该锁会不断循环判断,因此不会类似 synchronize 线程阻塞导致线程切换。但是不断的自旋,会导致 CPU 的消耗,在并发量大的时候容易导致 CPU 跑满。
ABA 问题(添加一个版本号)
JUC并发编程_第6张图片

ABA 问题,即某个线程将内存值由 A 改为了 B,再由 B 改为了 A。当另外一个线程使用预期值去判断时,预期值与内存值相同, 当前线程的 CAS 操作无法分辨 当前 V 值是否发生过变化。 解决 ABA 问题的主要方式,通过使用类添加版本号,来避免 ABA 问题。如原先 的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改 为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较, 只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。
Java 中的锁分类
java中有许多的锁名词,这些并不是全指锁,有的指 锁的特性,有的指 锁的设计,有的指 锁的状态。
(1) 乐观锁/悲观锁
       乐观锁:不会对共享数据进行加锁限制,认为多线程不会对共享数据进行修改。允许多个线程来对共享数据进行操作,当一个线程将修改后的数据需要从工作内存写会主内存时,一般通过 版本号或者 时间戳来判断数据是否已经被修改,在该线程将数据写回主内存前,是否已经有别的线程已经操作过来。如果数据已经被其他线程操作过了,那么就需要处理当前冲突,使用 回滚操作,或者 合并操作。优点是它不会立即阻塞其他线程,从而提高了并发性。
       悲观锁:是一种并发控制机制,用于处理多个并发线程对同一资源的读写操作。认为每一个线程都会去修改共享数据,所以给共享数据加锁,使得每次只能有一个得到锁的并发线程来对共享数据进行操作。其他线程进入阻塞状态,等待释放锁。它适合对于数据一致性要求高的场景。尤其是写操作。但会导致并发性能下降。
(2) 可重入锁
      可重入锁又名递归锁,是指在同一个线程在最外层方法获取锁的时候,在进入内层方法会自动获取锁。对于Java ReentrantLock而言,它的名字就可以看出是一个可重入锁。但是它不会自动的解锁,在进入的每一层都需要单独的加锁和解锁,否则会出现死锁。Synchronized也就是一个可重入锁。它和ReentrantLock不同的地方是,它是自动加锁和解锁的。
(3) 读写锁(更加细化粒度的控制)
      读写锁:对资源类的读加上读锁,对资源类的写加上写锁。可以避免在写时被其他的线程插队。
       ReadwriteLock类,写锁(独占锁) :一次只能一个线程持有。读锁(共享锁):多个线程可以同时持有。读读可以共存,读写,写写不可以共存。
JUC并发编程_第7张图片

 (4)分段锁

       分段锁并非一种实际的锁,而是一种思想,用于将数据分段,并在每个分段上都会单独加锁,把锁进一步细粒度化,以提高并发效率。
 (5) 自旋锁
       自旋锁: 所谓自旋其实指的就是自己重试,当线程抢锁失败后,重试几次,要是抢到锁了 就继续,要是抢不到就阻塞线程。说白了还是为了尽量不要阻塞线程。 由此可见,自旋锁是是比较消耗 CPU 的,因为要不断的循环重试,不会释放 CPU 资源。另外,加锁时间普遍较短的场景非常适合自旋锁,可以极大提高锁的效率。
(6)共享锁/独占锁
共享锁 :是指该锁可被多个线程所持有,并发访问共享资源。
独占锁 :也叫互斥锁,是指该锁一次只能被一个线程所持有。 对于 Java ReentrantLock
,Synchronized 而言,都是独享锁。但是对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
(7)公平锁/非公平锁
公平锁(Fair Lock) 是指按照请求锁的顺序分配,拥有稳定获得锁的机。
非公平锁(Nonfair Lock) 是指不按照请求锁的顺序分配,不一定拥有获得锁的机会。对synchronized 而言,是一种非公平锁。ReentrantLock 默认是非公平锁,但是底层可以通过 AQS(AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个抽象类,用于实现同步器的基础框架。 ) 的来实现线程调度,所以可以使其变成公平锁。
JUC并发编程_第8张图片
(7)偏向锁/轻量级锁/重量级锁
锁的状态 无锁状态  偏向锁状态  轻量级锁状态    重量级锁状态
锁的状态是通过对象监视器在对象头中的字段来表明的。
四种状态会随着竞争的情况逐渐升级。
这四种状态都不是 Java 语言中的锁 ,而是 JVM 为了提高锁的获取与释放效率 而做的优化( 使用 synchronized 时 )。
偏向锁: 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。 降低获取锁的代价。
轻量级: 轻量级锁是指当锁是偏向锁的时候,此时又有一个线程访问,偏向锁就会升级 为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁 : 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一 直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁 膨胀为重量级锁。在高并发情况下,出现大量线程自旋获得锁,对 cpu 销毁较大, 升级为重量级锁后,获取不到锁的线程将阻塞,等待操作系统的调度.
轻量级锁 自旋
重量级锁 需要操作系统调度

你可能感兴趣的:(jvm,java,开发语言)