JUC
Juc概述
在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,用于定义类似于线程的自定义子系统,包括线程池,异步IO和轻量级任务框架;还提供了用于多线程上下文中的 Collection实现等
volatile
volatile:异变的、不稳定的
在并发编程中的三个特性:
- 互斥性(原子性)
- 内存可见性
- 指令重排序
指令重排序:
创建对象的过程:
1.堆区开辟空间
2.调用构造方法创建对象
3.把地址赋给变量
但是虚拟机可能会让指令优化为1,3,2,在单例模式中,可能返回半个对象
- volatile关键字:当多个线程进行共享操作数据时,可以保证内存中的数据是可见的,相较于synchronized是一种较为轻量级的同步策略
- volatile 不具备"互斥性";
- volatile 不能保证变量的"原子性";
synchronized和volatile的区别:
1.synchronized可以实现 互斥性 和 内存可见性,不能禁止指令重排序
2.volatile可以实现内存可见性和禁止指令重排序,不能保证原子性(互斥性)
例:懒汉单例:
/**
* 单例三个步骤
* 懒汉式
* (1)私有化构造方法
* (2)类内部创建对象
* (3)添加公开的方法,返回这个对象
*/
public class SingleTon {
private SingleTon() {
}
private volatile static SingleTon instance;
public static SingleTon getInstance() {
if(instance==null) { //双重检查 double check
synchronized (SingleTon.class) {
if(instance==null) {
instance=new SingleTon();
//实例化过程
//(1) 堆中开辟空间
//(2) 调用构造方法初始化对象
//(3) 把对象的地址赋值给变量
}
}
}
return instance;
}
}
i++的原子性问题
先明白一点:i++不具有原子性
1.i++的实际操作实际上可以分为三个步骤:读 - 改 - 写
i++可拆分为:
int temp1 = i;
int temp2 = temp1 + 1;
i = temp2;
使用javap -c Demo.class 可以查看源码
2.原子性:就是i++的 读 - 改 - 写是不可分割的三个步骤
3.原子变量:jdk1.5以后,java.util.concurrent.atomic包下,提供了常用的原子变量
1.原子变量中的值,使用volatile修饰,保证了内存的可见性
2.CAS(Compare - And - Swap)算法保证数据的原子性
例:
public class AtomicDemo implements Runnable{
private int num=0;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(++num);
}
}
测试:结果可能为有重复的数字,印证了i++不是原子性的
public static void main(String[] args) {
AtomicDemo atomicDemo=new AtomicDemo();
for(int i=0;i<10;i++) {
new Thread(atomicDemo).start();
}
}
CAS(Compare - And - Swap)算法
CAS(Compare-And-Swap) 算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问;
CAS 是一种无锁的非阻塞算法(属于乐观锁)的实现;
所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS 包含了三个操作数:
进行比较的旧预估值:A
需要读写的内存值:V
将写入的更新值 :B
当且仅当A == V时,V == B,否则,将不做任何操作,并且这个比较交换过程属于原子操作
模拟CAS算法:
public class CompareAndSwapDemo {
public static void main(String[] args) {
CompareAndSwap compareAndSwap = new CompareAndSwap();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int expect = compareAndSwap.get();
boolean b = compareAndSwap.compareAndSwap(expect, new Random().nextInt(101));
System.out.println(b);
}
}).start();
}
}
}
class CompareAndSwap {
private int value;
/**
* 获取值
*
* @return
*/
public synchronized int get() {
return value;
}
public synchronized boolean compareAndSwap(int expect, int newValue) {
if (this.value == expect) {
this.value = newValue;
return true;
}
return false;
}
}
ABA问题:
在一个线程A拿到值,与操作这个值之间,可能由于A一直抢不到操作锁,在这个时间段内,已经有其他线程B把数据修改过了,又有线程C把数据修改为了跟A线程拿到的数据一样的值,这个时候A再去操作,会误认为这就是原来的数据,从而操作。却不知道,这个数据已经被修改过了
public class AbaDemo {
private static AtomicStampedReference<Integer> integer=new AtomicStampedReference<Integer>(0, 0);
public static void main(String[] args) throws Exception{
for(int i=0;i<100;i++) {
//Thread.sleep(10);
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
int stamp = integer.getStamp();
Integer reference = integer.getReference();
if(integer.compareAndSet(reference, reference+1, stamp, stamp+1)) {
System.out.println(reference+1);
break;
}
}
}
}).start();
}
}
}
Lock接口
synchronized的缺陷:
- 获取锁的线程如果由于某种原因,不能及时释放锁(除非发生异常),其他线程只能等待
- 使用同一个锁会进入同一个等待队列,所以需要唤醒所有线程
- 无法实现读写锁操作
案例:使用Lock锁实现三个线程交替输出20遍A,B,C
public class Alternative {
private Lock lock=new ReentrantLock();
//三个对象
Condition conditionA=lock.newCondition();
Condition conditionB=lock.newCondition();
Condition conditionC=lock.newCondition();
private int num=1;// 1 a 2 b 3 c
private int count=1;
public void outputA() {
lock.lock();//上锁
try {
if(num!=1) {
try {
conditionA.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("A");
num=2;
conditionB.signal();
} finally {
lock.unlock();
}
}
public void outputB() {
lock.lock();//上锁
try {
if(num!=2) {
try {
conditionB.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
num=3;
conditionC.signal();
} finally {
lock.unlock();
}
}
public void outputC() {
lock.lock();//上锁
try {
if(num!=3) {
try {
conditionC.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C");
System.out.println("------"+count+"------");
count++;
num=1;
conditionA.signal();
} finally {
lock.unlock();
}
}
}
并发集合
前面学习了List(ArrayList|LinkedList)、Set(HashSet|TreeSet)、Map(HashMap|TreeMap)集合,这些集合只适合在单线程情况下使用。在Collecionts工具类中有synchronized开头方法可以把单线程集合转成支持并发的集合,但是效率不高,很少使用。
问题演示:
public class Demo {
public static void main(String[] args) {
List<String> all = new ArrayList<String>() ;
for (int x = 0; x < 20; x++) {
int temp = x;
new Thread(() -> {
for (int y = 0; y < 30; y++) {
all.add(Thread.currentThread().getName()
+ " - " + temp + " - " + y);
System.out.println(all);
}
}).start();
}
}
}
原因是当你保存的容量个数和你的实际操作数可能不匹配的时候就会出现此异常。
为了更好的实现集合的高并发访问处理,创建了一组新的集合工具类。
➣ List和Set集合:
➣ CopyOnWriteArrayList相当于线程安全的ArrayList,实现了List接口。
CopyOnWriteArrayList是支持高并发的;【效率低,每次添加元素,都存在数组的copy】
➣ CopyOnWriteArraySet相当于线程安全的HashSet,它继承了AbstractSet类,
CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象,
它是通过CopyOnWriteArrayList实现的。
➣ Map集合:
➣ ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap);
它继承于AbstractMap类,并且实现ConcurrentMap接口。
ConcurrentHashMap是通过“锁分段”来实现的,它支持并发;
➣ ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap);
它继承于AbstactMap类,并且实现ConcurrentNavigableMap接口。
ConcurrentSkipListMap是通过“跳表”来实现的,它支持并发;
➣ ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet);
它继承于AbstractSet,并实现了NavigableSet接口。
ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发;
➣ Queue队列:
➣ ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列;
➣ LinkedBlockingQueue是单向链表实现的(指定大小)阻塞队列,该队列按FIFO(先进先出)排序元素;
➣ LinkedBlockingDeque是双向链表实现的(指定大小)双向并发阻塞队列,
该阻塞队列同时支持FIFO和FILO两种操作方式;
➣ ConcurrentLinkedQueue是单向链表实现的无界队列,该队列按FIFO(先进先出)排序元素。
➣ ConcurrentLinkedDeque是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。
案例一:使用CopyOnWriteArrayList实现多线程异步访问
List<String> all = new CopyOnWriteArrayList<String>() ;
for (int x = 0; x < 20; x++) {
int temp = x ;
new Thread(()->{
for (int y = 0; y < 30; y++) {
all.add(Thread.currentThread().getName()
+ " - " + temp + " - " + y) ;
System.out.println(all);
}
}).start();
}
案例二:使用CopyOnWriteArraySet实现多线程异步访问
Set<String> all = new CopyOnWriteArraySet<String>() ;
for (int x = 0; x < 20; x++) {
int temp = x ;
new Thread(()->{
for (int y = 0; y < 30; y++) {
all.add(Thread.currentThread().getName()
+ " - " + temp + " - " + y) ;
System.out.println(all);
}
}).start();
}
ConcurrentHashMap的使用
ConcurrentHashMap是HashMap的多线程版本,在并发情况下使用
Map<String, String> all = new ConcurrentHashMap<String,String>() ;
for (int x = 0; x < 20; x++) {
int temp = x ;
new Thread(()->{
for (int y = 0; y < 10; y++) {
all.put(Thread.currentThread().getName(),
"x = " + temp + "、y = " + y);
System.out.println(all);
}
}).start(); ;
}
Map集合的主要特征是做数据的查询处理操作,所以在ConcurrentHashMap设计的时候考虑到了数据更新的安全性与数据查询的并发性。
JDK1.7之前
ConcurrentHashMap采用锁分段机制,默认并发级别为16。
特点是写的时候同步写入,使用独占锁,读的时候为了保证性能使用了共享锁。(写入时互斥,读取时,可以多线程一起读取)【锁着数组中的元素,第一个元素】
JDK1.8以后
ConcurrentHashMap写的时候采用CAS无锁算法进一步提高写入效率。【当多个线程写的时候,锁着第一个元素,读取元素的视乎没有锁】
ArrayBlockingQueue
ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列,可以作为线程通信同步工具类使用。
案例:使用ArrayBlockingQueue实现生产者消费者
生产者
public class Producer extends Thread {
private ArrayBlockingQueue<String> bq;
public Producer(ArrayBlockingQueue<String> bq){
this.bq=bq;
}
@Override
public void run() {
for(int i=0;i<30;i++){
try {
bq.put(Thread.currentThread().getName()+"生产"+(i+1)+"号产品"); //阻塞方法,不要用add,add方法不阻塞
System.out.println(Thread.currentThread().getName()+"生产"+(i+1)+"号产品");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者
public class Consumer extends Thread {
private ArrayBlockingQueue<String> bq;
public Consumer(ArrayBlockingQueue<String> bq){
this.bq=bq;
}
@Override
public void run() {
for(int i=0;i<30;i++){
try {
String take = bq.take();
System.out.println(Thread.currentThread().getName()+"消费了"+take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类
public static void main(String[] args) {
ArrayBlockingQueue<String> bq=new ArrayBlockingQueue<>(1);
new Producer(bq).start();
new Consumer(bq).start();
}
同步工具类
CountDownLatch(闭锁)
CountDownLatch(闭锁)是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
闭锁可以延迟线程的进度直到其到达终止状态,闭锁可以用来确保某些活动直到其他活动都完成才能继续执行:
1.确保某个计算在其需要的所有资源都被初始化后才能继续执行
2.确保某个服务在其依赖的所有其他服务都已经启动之后才启动
3.等待直到某个操作所有参与者都执行完毕其他线程才能继续执行
理解:阻塞一个线程,等到自己设置的线程数量执行完毕后,再执行
使用案例:
public class CountDownLatchDemo {
public static void main(String[] args) {
//创建了一个闭锁,10个线程使用1个锁(公共资源)
CountDownLatch countDownLatch = new CountDownLatch(10);
//计时
long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
new MyThread(countDownLatch).start();
}
//等待10个线程执行完!!!
try {
System.out.println("开始计时");
countDownLatch.await();//等countDownLatch中的参数变为0
} catch (InterruptedException e) {
e.printStackTrace();
}
//不能直接这么写!!!
// 主线程把for循环执行完之后,不等线程执行完毕,就直接进行运算,打印了
long end = System.currentTimeMillis();
System.out.println("总用时:" + (end - start));
}
static class MyThread extends Thread{
//声明一个闭锁,需要使用构造方法传进来
private CountDownLatch countDownLatch;
public MyThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始执行了...");
int sum = 0;
for (int i = 0; i < 99999999; i++) {
for (int j = 0; j < 99999999; j++) {
sum = i + j;
}
}
System.out.println(Thread.currentThread().getName() + "结束了");
countDownLatch.countDown(); //相当于countDownLatch - 1
}
}
}
开会案例:
public class MeetingDemo {
public static void main(String[] args) {
//公共资源闭锁
CountDownLatch countDownLatch = new CountDownLatch(5);
new BossThread(countDownLatch).start();
for (int i = 0; i < 5; i++) { //创建5个员工
try {
TimeUnit.SECONDS.sleep(2);
new EmpleoyeeThread(countDownLatch).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class BossThread extends Thread {
private CountDownLatch countDownLatch;
public BossThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
System.out.println("老板等待中......共有" + countDownLatch.getCount() + "人开会");
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("好了,最后一个来的,你可以走了......");
}
}
static class EmpleoyeeThread extends Thread{
private CountDownLatch countDownLatch;
public EmpleoyeeThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "到会议室了");
countDownLatch.countDown();
}
}
}
CyclicBarrier(屏障)
CyclicBarrier和CountDownLatch类似(屏障)表面意思理解为可循环使用的屏障,作用是让一组线程在到达一个屏障时被阻塞,等到最后一个线程到达屏障点,才会运行被拦截的线程继续运行。
1.构造函数 CyclicBarrier(int parties) 屏障拦截的线程数量
2.await() 调用该方法时表示线程已经到达屏障,随即阻塞
理解:拦截线程,直到数量达到自己设定的数目,在一起执行【模拟高并发】
注意:设置的屏障数量必须是能够整除需要执行的线程,否则会出现死锁。例如:有10个线程需要执行,屏障设为3(同时只有3个线程可以执行),则先执行3波,最后剩一个线程,则会被堵在屏障那,无法执行。
把屏障看成是一个门,只有设定的数量个的线程合力才能推开
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
for (int i = 0; i < 10; i++) {
try {
TimeUnit.SECONDS.sleep(1);
new MyThread(cyclicBarrier).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class MyThread extends Thread{
private CyclicBarrier cyclicBarrier;
public MyThread(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "准备好了...");
try {
cyclicBarrier.await(); //等待,等到满足个数后,才会执行
} catch (Exception e) {
e.printStackTrace();
}
//同时执行的代码
System.out.println(Thread.currentThread().getName() + "同时执行了。。。");
}
}
}
运行结果
把初始化的屏障改为3:
其余不变
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
10个运动员一起赛跑,有序进场,先到的等待:
public class RunningDemo {
public static void main(String[] args) {
//匿名内部类的作用,等到屏障前面已经达到了10(所设置的线程个数)个线程,就会执行
//这句话一执行(相当于裁判说开始比赛),其他线程再一起执行
CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Runnable() {
@Override
public void run() {
System.out.println("开始比赛");
}
});
for (int i = 0; i < 10; i++) {
try {
TimeUnit.SECONDS.sleep(1);
new Athletes(cyclicBarrier).start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class Athletes extends Thread{
private CyclicBarrier cyclicBarrier;
public Athletes(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "准备好了");
try {
cyclicBarrier.await(); //屏障开始拦人
} catch (Exception e) {
e.printStackTrace();
}
//共同执行的代码
System.out.println(Thread.currentThread().getName() + "开始执行了");
}
}
}
区别
CountDownLatch和CyclicBarrier的区别
- CountDownLatch只能用一次,CyclicBarrier可以reset(),且适合处理更复杂的业务
- CyclicBarrier还有getNumberWaiting 获取当前阻塞的线程数量,isBroken()判断阻塞线程是否被中断
Semaphore(信号量)
Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。就这一点而言,单纯的synchronized 关键字是实现不了的。
用来控制同时访问特定资源的线程数量,通过协调保证合理的使用公共资源
比作控制车流的红绿灯,如马路要控制流量,只限制100辆车通行,其他必须在路口处等待,不能行驶在马路上,当其中有5辆离开马路,那么允许后面5辆进入马路
案例:使用Semaphore信号量控制并发的个数:
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
//10个线程
for (int i = 0; i < 10; i++) {
new MyThread(semaphore).start();
}
}
static class MyThread extends Thread {
private Semaphore semaphore;
public MyThread(Semaphore semaphore) {
this.semaphore = semaphore;
}
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
semaphore.acquire(); //获取锁
System.out.println(Thread.currentThread().getName() + "开始执行" + sdf.format(new Date()));
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "执行结束" + sdf.format(new Date()));
semaphore.release();//释放锁
}
}
}
}