目录
一、什么是JUC?
二、进程和线程?
三、Locks包
①传统的Synchtronized同步锁关键字。
同步代码块:
同步方法:
②Lock锁
四、消费者生产者问题。
五、8锁现象。
六、集合类不安全
①List(ArrayList)不安全
②Set(hashSet)不安全
③Map(hashSet)不安全
七、Callable
八、常用的辅助类
1.CountDownLatch 计数器
2.CycliBarriers
3.Semaphore
九、读写锁
十、阻塞队列
十一、线程池
七大参数
四大拒绝策略
十二、四大函数接口
①function接口:函数型(给x返y的意思)
②.predicate 断言式接口(判断是否正确)
③Consumer消费型函数(只输入,不返还)
④supplier生产型函数接口 (不给,只返回)
十三、流式计算(运用链式编程)
十四、Forkjoin 分之合并
十五、异步回调
十六、JMM+Volatile
①JMM
②volatile
十七、单例模式
十八、原子引用(解决CAS的ABA问题)
十九、CAS
--什么是自旋锁?
二十、AQS(基于cas的锁同步框架)
一、什么是JUC?
JUC是java.util.concurrent包名的简写,是关于并发编程的API。与JUC相关的有三个包:
java.util.concurrent(许多辅助类等等)、java.util.concurrent.atomic、java.util.concurrent.locks。
一点知识:
高并发是一种状态,如果大量请求访问网关接口。这种情况会发生大量执行操作,如数据库操作、资源请求、硬件占用等。这就需要对接口进行优化。
而多线程是处理高并发的一种手段。
多线程 是一种异步处理的一种方式,在同一时刻最大限度的利用计算机资源。
二、进程和线程?
-一般来说一个程序就是一个进程,比如qq.exe,可以在任务管理器看到有许多进程。线程就比如qq中发送消息的这个功能任务。在以往操作系统中,进程是资源分配的基本单位,也是执行任务的基本单位。
-进程有多个线程组成,最少含有一个线程,进程是资源分配的单位,而线程可以共享进程的资源,是进行调度和执行的最小单位。线程因为基本不拥有资源,其创建和销毁的成本较低。
三、Locks包
①传统的Synchtronized同步锁关键字。
synchronized是java关键字,synchronized是利用java提供的原⼦性内置锁(monitor 对象),每个对象中都内置了⼀个 ObjectMonitor 对象。这种内置的并且使⽤者看不到的锁也被称为监视器锁
synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁。
synchronized底层也比较简单。
首先我们要知道一个对象在内存里是怎么组成的,是由对象头,数据信息,和填充部分(虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。)组成。一个空对象占有八个字节,就是自动填充成的。
同步代码块:
对象头关联一个叫monitor的监控器对象,底层由c++源码构成。当获得这个对象就相对于你是这个对象的owner,执行monitorenter,进行加一。进入一次加一一次,退出即monitorexit,使其减一,归于0之后该对象才能被别的线程获得。
同步方法:
即进入同步方法时,会有一个标识,叫ACC_SYNCHTRONIZED
如果识别到了这个标识,也就会隐性的执行enter和exit方法,也相当于对monitor对象owner的争取
所synchronized锁有三种使用方式:
①修饰代码块,即同步代码块。锁的对象可以指定。
②修饰静态方法,锁的对象是类的calss对象。
③修饰普通方法,锁的对象是调用当前方法的实例对象。
②Lock锁
lock接口实现类有三个
Lock 接口: 支持予以不同(重入,公平等)的锁规则: 1. 公平锁 2.非公平锁 3.可重入锁
实现lock接口的锁
①公平锁:线程排队获得,不可插队。
②非公平锁:可以插队,3s 3h的线程任务,可以不用3s等3h,提高了效率。
③可重入锁:就是当获取了一个锁之后,再次获得同一个锁不会出现死锁现象。
④无障碍锁:StampedLock(加强版的读写锁),读锁和写锁是完全互斥的。也就是说在进行数据输入的时候,读取操作需要进行等待。等待写锁释放之后才可以继续进行读锁相应的处理。
⑤悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
⑥乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用版本号机制和CAS算法实现的乐观锁思想。
⑦读写锁:也是一种独占锁,ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。写锁可重入读锁。
所有读写锁的实现必须保证一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
四、消费者生产者问题。
该问题可以使用三种方式:
一,synchronized修饰资源类。
package com.thread.pcpattren;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class SynchronizedDemo {
private final List list = new ArrayList<>(6);
//因为消费者生产者是模拟对list进行操作实现,必须使用同一把锁
//消费者
public void consumer() throws InterruptedException {
//判断
synchronized (list) {
while (list.size() == 0) {
list.wait();
}
//操作
try {
System.out.println(Thread.currentThread().getName() + "开始消费!");
int index = (int) (Math.random()*(list.size()));
// String productName = list.get(index);
String productName = list.remove(index);
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + productName +"---消费成功!" + "商品还剩:" +
list.size());
} catch (Exception e) {
e.printStackTrace();
} finally {
list.notify();
}
}
}
//生产者
public void producer() throws InterruptedException {
//判断
synchronized (list) {
while (list.size() == 6) {
list.wait();
}
//操作
try {
System.out.println(Thread.currentThread().getName() + "开始生产!");
list.add("product:" + (list.size() + 1));
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "product:" + list.size() + 1 +
"---生产成功!" + "空间还剩:" + (6 - list.size()));
} catch (Exception e) {
e.printStackTrace();
} finally {
list.notify();
}
}
}
}
二、lock锁
public class LockDemo {
private final List list = new ArrayList<>(6);
private Lock lockList = new ReentrantLock();
private Condition condition = lockList.newCondition();
//因为消费者生产者是模拟对list进行操作实现,必须使用同一把锁
//消费者
public void consumer() throws InterruptedException {
lockList.lock();
//判断
//操作
try {
//While中的执行语句运行完毕后,还要进行继续判断条件是否符合循环条件,
//根据判断的条件,返回执行语句或继续运行下面的程序。
while (list.size() == 0) {
condition.await();
}
System.out.println(Thread.currentThread().getName() + "开始消费!");
int index = (int) (Math.random() * (list.size()));
// String productName = list.get(index);
String productName = list.remove(index);
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + productName + "---消费成功!" + "商品还剩:" +
list.size());
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lockList.unlock();
}
}
//生产者
public void producer() throws InterruptedException {
lockList.lock();
//判断
try {
while (list.size() == 6) {
condition.await();
}
//操作
System.out.println(Thread.currentThread().getName() + "开始生产!");
list.add("product:" + (list.size() + 1));
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "product:" + list.size() +
"---生产成功!" + "空间还剩:" + (6 - list.size()));
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lockList.unlock();
}
}
}
③阻塞队列,以后说
五、8锁现象。
其实就是为了了解锁是什么,锁作用对象。
1.两个同步方法,一个对象,synchronized修饰,就是同一把this锁,谁先拿到锁,谁就先执行。另一个线程就阻塞。
2.两个同步方法,两个对象,其实就等于两个当前实例对象,所以互不影响。
3.两个静态同步方法,无论几个对象,也就相对于同一把锁,是class对象锁,谁先拿到锁,谁就先执行,另一个线程就阻塞。
4.一个同步方法,一个普通方法,因为没有互斥关系,就正常按照调用顺序执行,如果睡眠了(即执行时间很长),那么就不用睡眠的先执行。
5.两个普通方法也按正常顺序执行
6.一个静态同步方法,一个同步方法,两个锁也是不一样的,即不互斥,正常执行。
六、集合类不安全
①List(ArrayList)不安全
查看其源码可以发现add方法没有锁,那么多线程的情况下,一个线程正在写,另一线程过来可以抢夺,导致数据不一致,即并发修改导致的异常
当并发修改很多的时候,可能报并发修改错。 不报错的情况下多次运行,数组中输出元素个数不确定,即在多线程下运行不稳定。
public class ListDemo {
public static void main(String[] args) {
List list = new ArrayList<>();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
//并发修改
list.add(UUID.randomUUID().toString().substring(0,3));
System.out.println(Thread.currentThread().getName()+"成功");
//遍历输出 底层是iterator迭代,有一个标识modcount,如果在遍历的时候修改了,modcount的值就会变化
?????? 使用foreach应该是读写并发。
System.out.println(Thread.currentThread().getName()+"-----"+list);
}, i+"").start();
}
}
方法:1,使用线程安全的ArrayList --- Vector
注:但是从Java源码可以看到,Vector是从JDK1.0出现的,而ArrayList是从JDK1.2出现的。
如果Vector能够完美替换ArrayList的话,ArrayList就没有存在的价值。
实际上是,Vector的方法加锁,虽然能够保证数据的一致性,但并发性急剧下降!
因此,并不能通过加锁的方法完美解决问题。
2.使用写时复制的ArrayList CopyOnWriteArrayList
即并发写或删除的时候,不会直接加锁,会复制一份数组元素进行修改,修改完成之后再赋值回去,这样每一次修改都不是对原数组进行修改,自然不会产生并发修改异常,读每次都是读原数组。
3.使用Collections工具类的,将集合包装成线程安全的集合 SynchtronizedList(list类型参数);
Tips:ArrayList用for循环遍历比iterator迭代器遍历快,linkedList用iterator迭代器遍历比for循环遍历快, 实现RandomAccess接口的List集合采用一般的for循环遍历,而未实现这接口则采用迭代器。
②Set(hashSet)不安全
基本和list一样,就是没加锁,add就会并发修改异常。
1.使用CopyOnWriteArraySet
2.使用Collections类下包装的线程安全的Set
③Map(hashSet)不安全
1.使用ConcurrentHashMap
2.使用Collections类下包装的线程安全的Map
3.使用hashTable,线程安全的hashMap
七、Callable
calable与Runnable区别?
1、一个是call方法,一个是run方法
2、call方法有返回值,run方法没有
3、call方法可以抛出异常,可以细粒度化的对多线程进行控制和管理??。run方法不行
注意:一般来说我们创建一个线程是使用new Thread(使用lamda表达式实现runnable接口),但是Thread里面只能传runnable实现类类型的参数,而有一个futureTask的类实现了runnable接口。所以可以采用以下方式。
八、常用的辅助类
1.CountDownLatch 计数器
也就是说,该辅助类是一个计数器功能,其中有两个常用方法countdown方法减一,和await阻塞后续进程。例子:main班长关门,必须全部同学都离开了教师,才能关门。
2.CycliBarriers
循环屏障不会阻塞后续进程,我通俗理解,他只是要执行一个runnable任务需要达到一定数量线程要求。一组线程一起完成一个任务。
//使到达屏障数之前,都通过await方法进入屏障阻塞
public class CyclicBarrierDemo {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,
new Runnable() {
@Override
public void run() {
System.out.println("人到齐了,开会!");
}
});
for(int i = 0; i < 7 ;i++){
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName() + "进入会议室!");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},i+"").start();
}
System.out.println(Thread.currentThread().getName() + "在吃东西!");
// cyclicBarrier.await();
}
}
3.Semaphore
信号量一个作用是有多个资源类的互斥访问
//信号量一个作用是有多个资源类的互斥访问,另一个是并发控制线程数
//20条
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3); //多少资源可以同时被并发访问
for(int i = 0;i<10;i++){
new Thread(()->{
try{
semaphore.acquire();//-1
System.out.println(Thread.currentThread().getName() +"获得资源!");
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() +"释放资源!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release(); //+1
}
},i+"").start();
}
}
}
九、读写锁
lock锁下可重入读写锁
读写分离,独占写锁,共享读锁
package com.juc.locks;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class RWDemo {
public static void main(String[] args) {
RW rw = new RW();
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
rw.write(finalI);
}, i+"线程").start();
}
for (int i = 0; i < 10; i++) {
new Thread(() -> {
rw.read();
}, i+"线程").start();
}
}
}
class RW {
private volatile List list = new ArrayList();
private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
//普通锁
// private Lock lock=new ReentrantLock();
public void write(int i) {
System.out.println(Thread.currentThread().getName() + "开始写入数据");
list.add(i);
System.out.println(Thread.currentThread().getName() +"写入成功");
}
public void read() {
if (list.size() != 0) {
System.out.println(Thread.currentThread().getName() + "开始读入数据");
list.get((int) (Math.random() * (list.size()))); //[0,size-1]
System.out.println(Thread.currentThread().getName() + "读入" +
list.get((int) (Math.random() * (list.size()))) + "成功");
}
}
}
十、阻塞队列
阻塞队列常用:ABQ,LBQ,SynchtronizedQueue。线程池里使用了阻塞队列。
见我讲集合那里的详细说明。
给自己总结 集合框架的一些问题_L_eraser的博客-CSDN博客
通常使用阻塞队列完成消费者-生产者问题。
package com.juc.queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingDeque;
public class PCDemo {
public static void main(String[] args) {
PC pc = new PC();
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(() -> {
try {
pc.consumer();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, i + "消费者").start();
}
for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(() -> {
try {
pc.producer(finalI);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, i + "生产者").start();
}
}
}
class PC {
private ArrayBlockingQueue blockingDeque = new ArrayBlockingQueue(10);
//消费
public void consumer() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "开始消费");
Object a = blockingDeque.take(); //里面没有物品时,自动阻塞,即不打印消费成功,等生产出来了,再自动释放
System.out.println(Thread.currentThread().getName() + "消费:" + a + " 成功");
}
//生成
public void producer(Integer i) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "开始生产");
blockingDeque.put(i);//方法里面加了lock锁,当满了,就自动阻塞,不生产成功
System.out.println(Thread.currentThread().getName() + "生产成功");
}
}
十一、线程池
三大方法,七大参数,四大策略
线程池池化技术的好处:1.线程池可以有许多空闲线程,可以省去新建,销毁的时间使用线程去执行任务。
2.线程池可以用来管理多个线程
3.基于1,线程池可以提高计算机执行任务的效率
三大方法:ExecutorService pool = Executors.newFixedThreadExecutor(int i); 固定大小的线程池
ExecutorService pool = Executors.newSingleThreadExecutor();单一线程池
ExecutorService pool = Executors.newCachedThreadExecutor();可变大小的线程池,空闲线程最多存在60s,就会被销毁。
ExecutorService pool = Executors.newScheduledThreadExecutor(int i);周期性线程池,采用延迟工作队列。
七大参数
四大拒绝策略
1.默认拒绝策略,抛出异常。
2.哪来的哪执行。例如,返回主线程执行该任务。
3.抛弃最老任务策略,直接丢弃阻塞队列里面最老的任务
4.直接抛弃策略,直接把他丢了,不执行。
十二、四大函数接口
新型程序员需要掌握:lamda表达式,链式编程,四大函数接口,流式计算。
①function接口:函数型(给x返y的意思)
②.predicate 断言式接口(判断是否正确)
③Consumer消费型函数(只输入,不返还)
④supplier生产型函数接口 (不给,只返回)
重写get方法
十三、流式计算(运用链式编程)
Stream 中文称为 “流”,通过将集合转换为这么一种叫做 “流” 的元素序列,通过声明性方式,能够对集合中的每个元素进行一系列并行或串行的流水线操作。
换句话说,你只需要告诉流你的要求,流便会在背后自行根据要求对元素进行处理,而你只需要 “坐享其成”。
package com.juc.stream;
import java.util.*;
//只有集合才有stream化
public class StreamDemo {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(new User("ali",20));
list.add(new User("fli",45));
list.add(new User("sol",16));
list.add(new User("uoo",8));
list.add(new User("qds",51));
//找到偶数年龄,名字排序,大写升序输出
list.stream().
filter 返回一个包含将 给定函数应用于此流的元素 的结果的流。
filter((user)->{ return user.getAge() % 2 == 0;}).
filter((user -> {return user.getAge() > 10;})).
//Stream map(Function super T, ? extends R> mapper);
//返回一个{@code intstream } ,其中包含将 given 函数应用于此流的元素的结果。
map(((user) -> {return user.getName().toUpperCase();})).
// Stream sorted(Comparator super T> comparator);
// 返回一个 由这个流的元素 组成的流,并 在结果流中使用元素时 对每个元素执行提供的操作。
sorted((user1,user2)->{ return user2.compareTo(user1);}).
limit(1).// 在丢弃流中的其他元素之后,返回一个指定大小的由该流元素组成的流。*
// 如果此流包含少于{@code n }元素,则返回空流。
forEach(System.out::println);
//void forEach(Consumer super T> action);
//PrintStream out = System.out; “函数式接口 变量名 = 类实例::方法名” 的方式对该方法进行引用
//println方法的源码得知println是PrintStream类中的一个非静态方法
}
}
class User{
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public User() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
-------------输出:SOL
十四、Forkjoin 分之合并
将一个大的任务拆分成多个子任务fork方法,进行并行处理,最后将子任务结果合并join方法成最后的计算结果,并进行输出。
package com.ogj.forkjoin; import java.util.concurrent.RecursiveTask; public class ForkJoinDemo extends RecursiveTask{ private long star; private long end; //临界值 private long temp=1000000L; public ForkJoinDemo(long star, long end) { this.star = star; this.end = end; } @Override protected Long compute() { if((end-star) package com.juc.fork; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinTask; import java.util.stream.LongStream; public class Test { public static void main(String[] args) throws ExecutionException, InterruptedException { test1(); test2(); test3(); } public static void test1(){ long star = System.currentTimeMillis(); long sum = 0L; for (long i = 1; i < 20_0000_0000; i++) { sum+=i; } long end = System.currentTimeMillis(); System.out.println("for循环:sum="+"时间:"+(end-star)); System.out.println(sum); } public static void test2() throws ExecutionException, InterruptedException { long star = System.currentTimeMillis(); //池化技术 ForkJoinPool forkJoinPool = new ForkJoinPool(); //新建任务, new ForkDemo(0, 20_0000_0000) ForkJoinTasktask = new ForkDemo(0L, 20_0000_0000L); //将任务提交给池 ForkJoinTask submit = forkJoinPool.submit(task); //获取submit返回值 long aLong = submit.get(); System.out.println(aLong); long end = System.currentTimeMillis(); System.out.println("ForkJoin:sum="+"时间:"+(end-star)); } public static void test3(){ long star = System.currentTimeMillis(); //Stream并行流() long sum = LongStream.range(0L, 20_0000_0000L).// (0,20000000000) // rang 差别就是rangeClosed包含最后的结束节点,range不包含。用数学区间可以表示为[startInclusive,endExclusive] parallel().//并行流执行 提高效率 reduce(0, Long::sum); // public final class Long // public static long sum(long a, long b) 0表示初始化结果=0 System.out.println(sum); long end = System.currentTimeMillis(); System.out.println("流式计算:sum="+"时间:"+(end-star)); } } ---------结果: for循环:sum=时间:805 1999999999000000000 1999999999000000000 ForkJoin:sum=时间:589 1999999999000000000 流式计算:sum=时间:350 排序问题是我们工作中的常见问题。目前也有很多现成算法是为了解决这个问题而被发明的,例如多种插值排序算法、多种交换排序算法。而并归排序算法是目前所有排序算法中,平均时间复杂度较好(O(nlgn)),算法稳定性较好的一种排序算法。它的核心算法思路将大的问题分解成多个小问题,并将结果进行合并。
归并算法对1万条随机数进行排序只需要2-3毫秒,对10万条随机数进行排序只需要20毫秒左右的时间,对100万条随机数进行排序的平均时间大约为160毫秒(这还要看随机生成的待排序数组是否本身的凌乱程度)。可见归并算法本身是具有良好的性能的。使用JMX工具和操作系统自带的CPU监控器监视应用程序的执行情况,可以发现整个算法是单线程运行的,且同一时间CPU只有单个内核在作为主要的处理内核工作。
所以ForkJoin框架算是对归并算法的优化,在同时使用多核运行原来的单核程序,每个核心上的程序之间互不交叉。这不会改变算法原来的时间复杂度。
十五、异步回调
什么是异步?
异步:相对于一群学生想问老师问题,但是老师以及在和别人讲解,然后其他学生就把自己的问题写在纸条上,然后离开做自己的事了。明天等老师回答了,老师就来找他们,告诉他们答案。
同步:相对于一群学生想问老师问题,但是老师以及在和别人讲解,然后其他学生就把自己的问题写在纸条上,然后等在这。等老师回答完别人了,再告诉他们答案。
异步回调的实现依赖于多线程或者多进程
软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。
同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用
回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;
异步调用是一种类似消息或事件的机制,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。
①无返回值的异步回调方法,创建任务
②有返回值的异步回调方法
package com.juc.sync; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class SyncDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { //有返回值的异步回调,重写supplier函数式接口的方法 CompletableFuturecompletableFuture = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName()+"执行异步方法:"); try { TimeUnit.SECONDS.sleep(2); int i = 1 / 1;//报错 } catch (InterruptedException e) { e.printStackTrace(); } return 1024; //supplier函数式接口的返回值 }); // return uniWhenCompleteStage(null, action); // public CompletableFuture whenComplete(BiConsumer super T, ? super Throwable> action) //重写BiConsumer方法 //链式编程!! System.out.println(completableFuture.whenComplete((t, u) -> { //success 回调 System.out.println("t=>" + t); //正常的返回结果 System.out.println("u=>" + u); //抛出异常的 错误信息 }).exceptionally((e) -> { //error回调 System.out.println("错误回调:"+e.getMessage()); return 404; }).get()); //正确了,就可以get到原结果 ;如果发生了异常,get可以获取到exceptionally返回的值 } } -------异步任务计算1/0:结果报错! ForkJoinPool.commonPool-worker-1执行异步方法: t=>null u=>java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero 错误回调:java.lang.ArithmeticException: / by zero 404 -------执行正确计算 ForkJoinPool.commonPool-worker-1执行异步方法: t=>1024 u=>null 1024 -->get取得的结果 十六、JMM+Volatile
①JMM
java内存模型实际上是不存在的,他是一组规范,为了解决一部分意义上的线程不安全读写内存变量,JMM内部定义一套happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。
1.顺序规定。即一个线程内必须保证串行执行。也就是单线程从上到下执行。
2.锁规定。解锁必须发生在对同一个锁的加锁之前。也就是加锁之后必须解锁才能再加锁。(成对出现)
3.volatile规定。volatile写必须在volatile读之前。也就是volatile读保证了都是读主内存的旧值,如果线程一旦对volatile变量进行了写,那么必须立马刷新到主内存中,保证了对所有线程可见。
4.启动规定。线程启动start操作优先于其所有操作,也就是说如果线程A调用线程B的start方法之前,修改了某个变量值,那么Bstart之后,改变量的改变对B可见。
5.等待规定。线程的join操作置后于所有操作,也就是说如果线程A.join(B),就是A线程要等待B线程完成再执行,如果在B线程返回之前B修改了某个变量的值,那么返回之后,B的修改对A可见。
6.中断规定。线程调用interrupt方法的执行,优先于该中断事件被代码所检测到,也就是说可以通过Thread.interrupted()方法检测线程是否中断。???
7.终结规定。对象构造方法的结束优先于finalize方法。也就是初始化了才可能被回收。
8.传递规定。如果A优先B,B优先C,那么A优先C。
当多线程下该规定都不符合的情况下,除靠同步锁来保证原子性、可见性以及有序性外。还能靠volatile关键字。
②volatile
volatile是JVM层面的轻量级锁,被它修饰的变量,它有三个性质:
1.保证变量线程可见的。
阻止死循环。 如果num没有被volatile修饰,则不会及时更新回其工作空间,会一直死循环。
2.不保证原子性。
要保证原子性的进行一个变量的++,可以采用JUC下atomic包里的AtomicInteger类型来创建num变量。private static volatile AtomicInteger number = new AtomicInteger();
然后使用其自加一的方法,number.incrementAndGet(); //底层是CAS保证的原子性
volatile 保证可见性。
3.禁止指令重排。
理解指令重排
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:①编译器优化的重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
也就是说,如果是多线程执行,虽然单线程下语句改变顺序语义没有问题,但是线程直接的变量有语义联系,所以会出现问题。
②指令并行的重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序
③内存系统的重排:由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题。
volatile内存区的读写,都加内存屏障(内存屏障,又称内存栅栏,是一个CPU指令)
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2, 在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2, 在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2, 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2, 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。下面看一个非常典型的禁止重排优化的例子单例模式实现方式之一-----DCL懒汉式
以下为普通的懒汉式单例模式:
//1.分配对象内存空间
//2.初始化对象
//3.设置instance指向刚分配的内存地址,此时instance!=null
2,3,可以调换顺序,可能!=null但是并没有初始化对象
十七、单例模式
①单例模式并不安全,虽然构造器私有,但是可以通过反射破解。
也就是可以生成多个实例。
package com.juc.volatiles; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; //使用反射破坏单例模式 public class ReflectDLC { //懒汉式单例模式 private static boolean key = false; private ReflectDLC() { //看起来很安全???? 为什么要加锁 synchronized (ReflectDLC.class) { if (key == false) { key = true; } else { throw new RuntimeException("不要试图使用反射破坏异常"); } } System.out.println(Thread.currentThread().getName() + " ok"); } private volatile static ReflectDLC lazyMan; //双重检测锁模式 简称DCL懒汉式 public static ReflectDLC getInstance() { //需要加锁 if (lazyMan == null) { synchronized (ReflectDLC.class) { if (lazyMan == null) { lazyMan = new ReflectDLC(); } } } return lazyMan; } //单线程下 是ok的 //但是如果是并发的 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException { //普通方式下,都是一个对象实例 // ReflectDLC instance1 = ReflectDLC.getInstance(); // ReflectDLC instance2 = ReflectDLC.getInstance(); Field key = ReflectDLC.class.getDeclaredField("key"); key.setAccessible(true); ConstructordeclaredConstructor = ReflectDLC.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); //无视了私有的构造器 ReflectDLC instance2 = declaredConstructor.newInstance(); key.set(instance2, false); //本来创建了实例之后key应该为true,但是这里又改了,使得可以再次创建实例 //相对于instance2.key = false; ReflectDLC instance1 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); System.out.println(instance1 == instance2); } } -----------利用反射成功创建两个对象 main ok main ok com.juc.volatiles.ReflectDLC@1fb3ebeb com.juc.volatiles.ReflectDLC@548c4f57 false ②使用静态内部类实现懒汉式单例模式
//静态内部类 public class Holder { private Holder(){ } public static Holder getInstance(){ return InnerClass.holder; } public static class InnerClass{ private static final Holder holder = new Holder(); } } ----实现原因: 1.当外部要创建Hoder实例时,通过getInstance方法会调用静态内部类属性,满足类初始化条件,所以会给内部类静态属性赋值为new Holder()。 2.因为该属性是final类型,所以不会有第二个对象。 类初始化的条件: 1.一个类要创建实例(new对象),需要先加载进内存并初始化 2.main方法所在的类,需要先加载进内存并初始化 3.静态内部类和非静态内部类一样,都是在被调用(调用静态方法或属性)时才会被加载并初始化 4.加载静态内部类的时候,会先加载外部类,再加载静态内部类(但静态内部类的加载不需要依附外部类:Inner.INNER) 初始化是为类的静态变量赋予正确的初始值。 准备阶段(为类的静态变量分配内存,并设置默认初始值)和初始化阶段看似有点矛盾,其实是不矛盾的。 如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说)。 到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。③饿汉式
public class Hungry { private byte[] data1=new byte[1024*1024]; private byte[] data2=new byte[1024*1024]; private byte[] data3=new byte[1024*1024]; private byte[] data4=new byte[1024*1024]; private Hungry(){ } private final static Hungry hungry = new Hungry(); //直接new出来 final修饰则只有一个 public static Hungry getInstance(){ return hungry; //直接返回 } }十八、原子引用(解决CAS的ABA问题)
原子引用使用版本号机制的方法实现了乐观锁思想。
ABA问题:在多线程计算中,ABA问题发生在同步期间,当一个位置被读取两次,两个读取具有相同的值,并且“值相同”用于指示“没有任何改变”。
但是,另一个线程可以在两个读取之间执行并更改值,执行其他工作,然后更改该值,然后改回去,从而欺骗第一个线程“没有任何改变”,即使第二个线程的工作结果没有错,但是也是违反了该假设。
并发1(上):获取出数据的初始值是A,后续计划实施CAS乐观锁,期望数据仍是A的时候,修改才能成功
并发2:将数据修改成B
并发3:将数据修改回A
并发1(下):CAS乐观锁,检测发现初始值还是A,进行数据修改
CAS一般是做判断条件,对资源进行修改操作,如果有人先一步进入了其中,对资源进行了修改,那么及时判断还是能成功,但是并发1(下)里,实际的东西已经不一样了。
AtomicStampedReference
atomicStampedRef = new AtomicStampedReference(100,1) atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
AtomicStampedReference它内部不仅维护了对象值,还维护了一个时间戳(我这里把它称为时间戳,实际上它可以是任何一个整数,它使用整数来表示状态值)。
当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。
当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。
版本号自增操作等等。
AtomicStampedReference源代码里使用的是CopyOnWrite的策略来保证线程安全,更改前保持pair的应用,每次更改都会新生成一个pair。
十九、CAS
CAS 即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
CAS是一种乐观锁思想的实现,它总是认为自己可以成功完成操作。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的
CAS缺点:
- 循环会耗时;
- 一次性只能保证一个共享变量的原子性;
- ABA问题
public class casDemo { //CAS : compareAndSet 比较并交换 public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(2020); //实际值 //boolean compareAndSet(int expect, int update) //期望值、更新值 //如果实际值 和 我的期望值相同,那么就更新 //如果实际值 和 我的期望值不同,那么就不更新 System.out.println(atomicInteger.compareAndSet(2020, 2021)); System.out.println(atomicInteger.get()); //CAS 是CPU的并发原语 atomicInteger.getAndIncrement(); //++操作 ---实际值--》2021 //因为期望值是2020 实际值却变成了2021 所以会修改失败 System.out.println(atomicInteger.compareAndSet(2020, 2021)); System.out.println(atomicInteger.get()); } }AtomicInteger类,会发现里面有个类:Unsafe类
sun.misc.Unsafe是JDK内部用的工具类。它通过暴露一些Java意义上说“不安全”的功能给Java层代码,来让JDK能够更多的使用Java代码来实现一些原本是平台相关的、需要使用native语言(例如C或C++)才可以实现的功能。该类不应该在JDK核心类库之外使用。
--什么是自旋锁?
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。
无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。
对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态/阻塞状态。
但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
public class SpinlockDemo { //int 0 //thread null AtomicReferenceatomicReference=new AtomicReference<>(); //加锁 public void myLock(){ Thread thread = Thread.currentThread(); System.out.println(thread.getName()+"===> mylock"); //t1 //自旋锁 while (!atomicReference.compareAndSet(null,thread)){ System.out.println(Thread.currentThread().getName()+" ==> 自旋中~"); } } //解锁 public void myunlock(){ Thread thread=Thread.currentThread(); System.out.println(thread.getName()+"===> myUnlock"); //t1 atomicReference.compareAndSet(thread,null); } } class TestSpinLock { public static void main(String[] args) throws InterruptedException { //测试 SpinlockDemo spinlockDemo=new SpinlockDemo(); new Thread(()->{ spinlockDemo.myLock(); try { TimeUnit.SECONDS.sleep(3); } catch (Exception e) { e.printStackTrace(); } finally { spinlockDemo.myunlock(); } },"t1").start(); TimeUnit.SECONDS.sleep(1); new Thread(()->{ spinlockDemo.myLock(); try { TimeUnit.SECONDS.sleep(3); } catch (Exception e) { e.printStackTrace(); } finally { spinlockDemo.myunlock(); } },"t2").start(); } } 二十、AQS(基于cas的锁同步框架)
AbstractQueuedSynchronizer,抽象队列式的同步器。AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用ReentrantLock/Semaphore/CountDownLatch。
AQS 核⼼思想是:
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态(尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,⼀旦获取到锁,其他的线程将会被阻塞进⼊阻塞队列⾃旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。)。
如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是⽤ CLH 队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。AQS原理图:



