> 文章列表 > JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

如何保证原子性?

    • 如何保证原子性?
      • 4.1 锁
      • 4.2 JUC--原子变量
    • 原子类
    • CAS
    • JUC 常用类
      • 7.1 ConcurrentHashMap
      • 7.2 CopyOnWriteArrayList和CopyOnWriteSet
      • 7.3 辅助类 CountDownLatch
      • 7.4 辅助类 CyclicBarrier

如何保证原子性?

4.1 锁

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的 一种实现

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

synchronized 是独占锁/排他锁(就是有你没我的意思),但是注意! synchronized 并不能改变 CPU 时间片切换的特点,只是当其他线程要访问这个 资源时,发现锁还未释放,所以只能在外面等待。

synchronized 一定能保证原子性,因为被 synchronized 修饰某段代码 后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一 定能保证原子操作.

synchronized 也能够保证可见性和有序性

4.2 JUC–原子变量

​ 现在我们已经知道互斥锁可以保证原子性,也知道了如何使用 synchronized 来保证原子性。但synchronized 并不是 JAVA 中唯一能保证原 子性的方案。

​ 如果你粗略的看一下 J.U.C(java.util.concurrent 包),那么你可以很显眼的 发现它俩:
JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

一个是 locks 包,一个是 atomic 包,它们可以解决原子性问题。

加锁是一种阻塞式方式实现

原子变量是非阻塞式方式实现

原子类

原子类原理(AtomicInteger 为例)

原子类的原子性是通过 volatile + CAS 实现原子操作的。

java.util.concurrent 包下AtomicInteger 类中的 value 是有 volatile 关键字修饰的,这就保证了 value的内存可见性,这为后续的 CAS 实现提供了基础。

低并发情况下:使用 AtomicInteger。

getAndIncrement(); 代替 i++ 是安全的

代码实例:

package concurrentProgrammer.automicdemo;import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;public class ThreadDemo implements Runnable{//private  int num = 0;//共享变量// private volatile int num = 0;private AtomicInteger num = new AtomicInteger(0);@Overridepublic void run() {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+":::"+getNum());}public int getNum() {// return num++;return num.getAndIncrement();}}

测试:

package concurrentProgrammer.automicdemo;import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;public class Test {public static void main(String[] args) {ThreadDemo td = new ThreadDemo();for (int i = 0; i <10 ; i++) {//循环创建10个线程Thread t = new Thread(td);t.start();}}
}

结果:

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

没有重复证明了原子类的原子性

不用AtomicInteger的结果:

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

有重复没有遵从原子性

CAS

CAS(Compare-And-Swap) :比较并交换,该算法是硬件对于并发操作的支持

CAS 是乐观锁的一种实现方式,他采用的是自旋锁的思想,是一种轻量级的锁机制

即每次判断我的预期值和内存中的值是不是相同,如果不相同则说明该内存值 已经被其他线程更新过了,因此需要拿到该最新值作为预期值,重新判断。而该 线程不断的循环判断是否该内存值已经被其他线程更新过了,这就是==自旋的思想(在其他程序编写中也可以运用这一思想解决问题)==

CAS 包含了三个操作数:

①内存值 V

②预估值 A (比较时,从内存中再次读到的值)

③更新值 B (更新后的值)

当且仅当预期值 A==V,将内存值 V=B,否则什么都不做。

这种做法的效率高于加锁,当判断不成功不能更新值时,不会阻塞,继续获得 cpu 执行权,继续判断执行

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

CAS 的缺点

​ **1.高并发占用内存太高:**CAS 使用自旋锁的方式,由于该锁会不断循环判断,因此不会类似 synchronize

线程阻塞导致线程切换。但是不断的自旋,会导致 CPU 的消耗,在并发量大的 时候容易导致 CPU 跑满。

2.ABA问题

​ ABA 问题,即某个线程将内存值由 A 改为了 B,再由 B 改为了 A。当另外一个 线程使用预期值去判断时,预期值与内存值相同,误以为该变量没有被修改过而导致的问题

解决 ABA 问题的主要方式,通过使用类似添加版本号的方式,来避免 ABA 问题。 如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2) 修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行 比较,只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了

JUC 常用类

Java 5.0 在 java.utilconcurrent 包中提供了多种并发容器类来改进同步容 器的性能

7.1 ConcurrentHashMap

​ ConcurrentHashMap 同步容器类是 Java 5 增加的一个线程安全的哈希 表对与多线程的操作,介于 HashMap 与 Hashtable 之间内部采用“锁分段”机制(jdk8 弃用了分段锁,使用 cas+synchronized)替代 Hashtable 的独 占锁。进而提高性能

放弃分段锁的原因:

  • 加入多个分段锁浪费内存空间。
  • 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待

jdk8 放弃了分段锁而是用了 Node 锁,减低锁的粒度,提高性能

并使用 CAS 操作来确保 Node 的一些操作的原子性,取代了锁。

put 时首先通过 hash 找到对应链表过后,查看是否是第一个 Node,

如果是, 直接用 cas 原则插入,无需加锁然后, 如果不是链表第一个 Node, 则直接用链表第一个 Node 加锁,这里加 的锁是 synchronized。

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

public class HashMapDemo {/*HashMap是线程不安全的,不能并发操作的ConcurrentModificationException  并发修改异常   遍历集合,并删除集合中的数据Hashtable 是线程安全的 public synchronized V put(K key, V value)-->独占锁锁直接加到了put方法上,锁粒度比较大,效率比较低用在低并发情况下可以Map<String,Integer> map = Collections.synchronizedMap(new HashMap<>());直接获得一把线程安全的锁ConcurrentHashMap*/public static void main(String[] args) {ConcurrentHashMap<String,Integer> map = new ConcurrentHashMap<>();//模拟多个线程对其操作for (int i = 0; i < 20; i++) {new Thread(()->{map.put(Thread.currentThread().getName(), new Random().nextInt());System.out.println(map);}).start();}}
}

7.2 CopyOnWriteArrayList和CopyOnWriteSet

ArrayList 是线程不安全的,在高并发情况下可能会出现问题, Vector 是线 程安全的. 但是在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修 改原有的数据,因此如果每次读取都进行加锁操作,其实是一种资源浪费我们 应该允许多个线程同时访问 List 的内部数据,毕竟读操作是线程安全的

JDK 中提供了 CopyOnWriteArrayList 类,将读取的性能发挥到极致,取是完 全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作,只有写入和写入之 间需要进行同步等待,读操作的性能得到大幅度提升。 CopyOnWriteArrayList 类的所有可变操作(add,set 等等)都是通过创建底 层数组的新副本来实现的当 List 需要被修改的时候,并不直接修改原有数组 对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。写完之后,再 将修改完的副本替换成原来的数据,这样就可以保证写操作不会影响读操作了。

CopyOnWriteArrayList在add方法中加了lock锁,又因为它的复制机制所以可以保证写和读不相互影响

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

CopyOnWriteArraySet 的实现基于 CopyOnWriteArrayList,不能存储重复数据.

CopyOnWriteArrayList和CopyOnWriteSet区别在于:

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

7.3 辅助类 CountDownLatch

CountDownLatch 这个类使一个线程等待其他线程各自执行完毕后再执行。是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完 毕后,计数器的值就-1,当计数器的值为 0 时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

package juc;import java.util.concurrent.CountDownLatch;public class CountDownLatchDemo {/*CountDownLatch 辅助类  递减计数器使一个线程 等待其他线程执行结束后再执行相当于一个线程计数器,是一个递减的计数器先指定一个数量,当有一个线程执行结束后就减一 直到为0 关闭计数器这样线程就可以执行了*/public static void main(String[] args) throws InterruptedException {CountDownLatch downLatch = new CountDownLatch(6);//计数for (int i = 0; i <6 ; i++) {new Thread(()->{System.out.println(Thread.currentThread().getName());downLatch.countDown();//计数器减一操作}).start();}downLatch.await();//关闭计数System.out.println("main线程执行");}
}

结果:

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)

7.4 辅助类 CyclicBarrier

CyclicBarrier 是一个同步辅助类,让一组线程到达一个屏障时被阻塞,直到最 后一个线程到达屏障时,屏障才会开门

package juc;import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;public class CyclicBarrierDemo {/*CyclicBarrier 让一组线程到达一个屏障时被阻塞,直到最 后一个线程到达屏障时,屏障才会开门是一个加法计数器,当线程数量到达指定数量时,开门放行*/public static void main(String[] args) {CyclicBarrier c = new CyclicBarrier(5, ()->{System.out.println("大家都到齐了 该我执行了");});for (int i = 0; i < 5; i++) {new Thread(()->{System.out.println(Thread.currentThread().getName());try {c.await();//加一计数器} catch (InterruptedException e) {e.printStackTrace();} catch (BrokenBarrierException e) {e.printStackTrace();}}).start();}}
}

执行结果:

JAVA并发编程(2)——(如何保证原子性,原子类,CAS乐观锁,JUC常用类)