JUC
- 1、线程和进程
- 2、LOCK锁(重点)
- 3、线程通信---生产者和消费者问题
- 4、8锁现象(关于锁的8个问题)
- 5、集合类不安全
- 6、Callable
- 7、常用的辅助类
- CountDownLatch
- CyclicBarrier
- Semaphore
- 8、读写锁
- 9、阻塞队列
- 10、线程池(重点)
- 线程池:3大方法,7大参数,4种拒绝策略
- 11、四大函数式接口(重点必须掌握)
- 12、Stream流式计算
- 13、ForkJoin
- 14、异步回调
- 15、JMM
- 16、Volatile
- 17、彻底玩转单例模式
- 18、深入理解CAS
- 19、原子引用
- 20、各种锁的理解
- 公平锁、非公平锁
- 可重入锁
- 自旋锁
- 死锁
1、线程和进程
- 线程、进程
- 线程:一个程序
一个进程往往包含很多线程,至少包含一个
java默认有两个线程:main、GC
- 线程:线程是进程中的实际运作单位,是操作系统能够进行运算调度的最小单位
- Java真的能开启线程吗?
不能,java不能直接操作硬件
Thread中的start方法是一个同步方法,调用时会将此线程加入线程组,然后调用start0方法,start0方法是一个本地方法
//本地方法,调用底层的C++,Java运行在虚拟机,不能直接操作硬件
priavte native void start0();
- 并发、并行
- 并发(单核):多线程操作同一个资源,快速交替
- 并行(多核):多个线程同时执行
cpu多核,多个线程可以同时执行
- 并发编程的本质:充分利用CPU资源
- 线程有几个状态? 6种
- NEW 新生
- RUNNABLE 运行
- BLOCKED 阻塞
- WAITING 等待
- TIMED_WAITING 超时等待
- TERMINATED 终止
- wait/sleep区别
- 来自不同的类
- wait -->Object
- sleep -->Thread
- 关于锁的释放
- wait会释放锁
- sleep不释放,抱着锁睡觉
- 使用的范围不同
- wait必须在同步代码块中
- sleep可以在任何地方睡
- 是否需要捕获异常
- wait不需要捕获异常
- sleep需要捕获异常
- 唤醒方式
- wait需要其他线程调用同一对象的nofity()/nofityAll()才能重新恢复
- sleep在时间到了之后会自动重新恢复
2、LOCK锁(重点)
- 传统Synchronized
本质:队列、锁 - Lock
- 实现类
- ReentrantLock 可重入锁(常用)
- ReentrantReadWriteLock.ReadLock 读锁
- ReentrantReadWriteLock.WriteLock 写锁
- 公平锁与非公平锁
- 公平锁:十分公平,先来后到
- 非公平锁:是十分不公平,可以插队(默认)
- 使用
Lock lock = new 实现类;
//加锁
lock.lock();
//尝试获取锁
//lock.tryLock();
try{
//业务代码
}catch(Exception e){
e.printStackTrace();
}finally{
//解锁
lock.unlock
}
- Synchronized和Lock区别
- Synchronized 是内置的Java关键字;Lock是一个Java接口
- Synchronized 无法判断获取所得状态;Lock可以判断是否获取到了锁
- Synchronized 会自动释放锁;Lock必须手动释放锁,如果不释放锁,死锁
- Synchronized 只要没有获得锁就一直等待;Lock锁不一定会一直等待下去
- Synchronized 可重入锁,不可以中断,非公平;Lock 可重入锁,可以判断锁,默认非公平(可以设置)
- Synchronized 适合锁少量的代码同步问题;Lock适合锁大量的同步代码
- 锁是什么,如何判断锁的是谁
3、线程通信—生产者和消费者问题
- Synchronized版 Synchronized wait() notify()
A、B 两个线程,线程安全 if判断
A、B、C、D四个线程可能会出现虚假唤醒的情况 if判断一定要改成while判断 - JUC版 Lock await() signal()
- Confition 精准通知和唤醒线程
任何一个新的技术,绝对不仅仅覆盖了原来的技术,是优势和补充
4、8锁现象(关于锁的8个问题)
- 如何判断锁的是谁,锁的是谁
**深刻理解锁 **
- 一个对象两个同步方法,因为有锁,Synchronized 锁的对象是方法的调用者,谁先拿到谁先执行
- 增加在第一个线程调用方法里等待4秒,还是谁先拿到谁执行
- 增加了一个普通方法后,先普通方法,再锁方法
- 两个对象两个同步方法,不等待的先执行完,按时间
- 增加两个静态同步方法一个对象,谁先拿到谁先执行,因为static静态方法,类刚加载就有了,锁的是Class
- 两个静态同步方法两个对象,谁先拿到谁先执行,static方法,锁的是Class ,两个对象的Class模板只有一个
- 普通同步方法和静态同步方法一个对象,普通同步方法先执行,静态的同步方法锁的是类,普通同步方法锁的是方法
- 普通同步方法和静态同步方法两个对象,普通同步方法先执行,静态的同步方法锁的是类,普通同步方法锁的是方法
- 小结
- new
- this 具体的某个方法
- static
- 唯一的Class模板
5、集合类不安全
- List不安全
- 并发下ArrayList不安全 (会报出并发修改异常) java.util.ConcurrentModificationException
- (Vector默认就是安全的) 但ArrayList是1.2出的 Vector是1.0出的
List<String> list = new Vector<>();
- 可以用Collections.sysnchroizedList(new ArrayList<>()); 变得安全
List<String> list = Collections.sysnchroizedList(new ArrayList<>);
- 可以用JUC下的CopyOnWriteArrayList<>();变得安全
List<String> list = new CopyOnWriteArrayList<>();
- CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略
- 多个线程调用list时,读取的时候是固定的,写入的时候会有覆盖问题,CopyOnWrite可以在写入的时候避免覆盖,造成数据问题
- CopyOnwrite与Vector相比优势?
- Vector中使用了Sysnchronized锁,影响性能
- CopyOnWrite中使用Lock锁,写入时候先复制一份,写入之后再set回去
- Set不安全
- 同List理可证,HashSet也不安全,会报出(并发修改异常) java.util.ConcurrentModificationException
- 可以用Collections.sysnchroized(new HashSet<>())变得安全
Set<String> set = Collections.sysnchroizedSet(new HashSet<>());
- 可以用JUC下的CopyOnWriteHashSet<>()方法变得安全
Set<String> set = new CopyOnWriteArraySet<>();
- HashSet底层是什么?
- 源码:
//HashSet源码public HashSet() { map = new HashMap<>(); }//add方法源码public boolean add(E e) { return map.put(e, PRESENT)==null; }
HashSet的不可重复就是利用了HashMap中Key不可重复
- Map不安全
- 同理也会抛出并发修改异常
- 可以用Collections.sysnchrized(new HashMap<>())变得安全
Map<String,String> map = Collections.sysnochrizedMap(new HashMap<>());
- 可以用JUC下的ConcurrentHashMap<>()变得安全
Map<String,String> map = new ConcurrentHashMap<>();
6、Callable
- 与Runnable类似,创建线程的第三种方式
- 可以有返回值
- 可以抛出异常
- 方法不同,run()/call()
- 使用方式
new Thread(new FutureTask<Integer>(new CallableTest())).start();
- 因为Thread中只能接收Runnable对象,FutureTask与Runnable和Callable都有关,互相转换,所以需要FutureTask中传入Callable对象,再把FutureTask传入到Thread中进行启动
- 细节
- FutureTask可以get得到Callable的返回值,所以可能会产生阻塞,要把它放在最后一行或者使用异步通信来处理
futureTask.get()
- FutureTask的结果会被缓存,效率高
7、常用的辅助类
CountDownLatch
- 减法计数器
CountDownLatch countDownLatch = new CountDownLatch(6);
-
counDownLatch.countDown();
数量-1 不会阻塞 -
countDownLatch.await();
等待计数器归零 假设计数器变为0,countDownLatch.await就会被唤醒,继续执行
CyclicBarrier
- 加法计数器
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { //达到预设数值之后的业务 });
-
cyclicBarrier.await
等待计数器到数值 假设计数器变为预设的数值,cyclicbarrier.await就会执行
Semaphore
- 信号量
Semaphore semaphore = new Semaphore(3);
- 多个共享资源互斥的时候使用
- 并发限流,控制最大的线程数
-
semaphore.acquire();
获得 假设如果线程满了,等待,等待被释放为止 -
semaphore.release();
释放 将当前信号量+1,然后唤醒等待的线程 放到finally中
8、读写锁
- 读-读 可以共存 读-写不可共存 写-写不可共存
- 创建ReadWriteLock对象
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
- 读锁readWriteLock.readLock() 共享锁
readWriteLock.readLock().lock();
读锁上锁readWriteLock.readLock().unlock();
读锁释放 - 写锁readWriteLock.writeLock() 独占锁
readWriteLock.writeLock().lock();
写锁上锁readWriteLock.writeLock().unlock();
写锁释放
9、阻塞队列
- BlockingQueue不是新的东西 BlockingQueue和List和Set同级
- 使用情况
- 多线程并发执行、线程池
- LinkedBlockingQueue
- ArrayBlockingQueue
- 队列定义
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(队列大小)
- 添加元素
- add
arrayBlockingQueue.add("a");
如果队列已满会抛出队列已满异常 Queue full - offer
arrayBlockingQueue.offer("a");
如果队列已满不会抛出异常,会返回false - put
arrayBlockingQueue.put("a");
如果队列已满不会抛出异常,会一直等待,知道有空闲位置可以添加元素 - offer(元素,等待时间,时间单位)
arrayBlockingQueue.offer("d",2,TimeUnit.SECONDS);
如果队列已满会等待2秒,2秒过后如果还没有空闲位置可以添加元素,就退出返回false
- 取元素 FIFO先进先出
- remove
arrayBlockingQueue.remove();
如果队列为空会抛出没有异常 NoSuchElementException - poll
arrayBlockingQueue.poll();
如果队列为空不会抛出异常,会返回null值 - take
arrayBlockingQueue.take();
如果队列为空,会一直等待,直到队列中有元素可以被取出 - poll(等待时间,时间单位)
arrayBlockingQueue.poll(2, TimeUnit.SECONDS);
如果队列为空,会等待2秒,2秒过后如果队列中还没有元素可以取出,就退出返回null值
- 查看队首元素
- element
arrayBlockingQueue.element();
如果队列为空会抛出异常 NoSuchElementException - peek
arrayBlockingQueue.peek();
如果队列为空,会返回null值
- 四组API
方式 | 抛出异常 | 有返回值 | 阻塞 等待 | 超时等待 |
添加 | add() | offer() | put() | offer(等待时间,时间单位) |
移除 | remove() | poll() | take() | poll(等待时间,时间单位) |
检测队首元素 | element() | peek() | - | - |
- SysnchronousQueue 同步队列
- 没有容量,添加一个元素必须取出一个元素,之后才能再次添加元素,只能放一个元素 不然会一直等待!
- put
blockingQueue.put("1");
- take
blockingQueue.take();
10、线程池(重点)
线程池:3大方法,7大参数,4种拒绝策略
- 池化技术
程序运行本质:占用系统的资源!优化资源的使用 ->池化技术
线程池、连接池、对象池… 创建 销毁 十分浪费资源
事先准备好资源,有人要用,就来我这里拿,用完之后还给我
- 线程池的好处
- 降低资源的消耗
- 提高相应的速度
- 方便管理
线程服用、可以控制最大并发数、管理线程
- 3大方法
ExecutorService threadPool1 = Executors.newSingleThreadExecutor(); //单个线程的线程池
ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //固定大小的线程池
ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的线程池,最大数量为Integer.MAX_VALUE会造成OOM内存泄漏
最好不要使用Excutors创建线程,可能会出现OOM内存泄漏,有资源耗尽的风险,而是通过ThreadPoolExecutor的方式
ExecutorService threadPool4 = new ThreadPoolExecutor( 2, 5, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());
- 7大参数
- 核心线程池大小
- 最大线程池大小
- 定义方式
- CPU密集型
- 几核CPU就定义为几,可以保证CPU的效率最高
Runtime.getRuntime().availableProcessors();//获取CPU核数
- IO密集型
- 大于程序中十分耗IO的线程数,不会造成阻塞,一般2倍
- 超时未使用就释放
- 超时单位
- 阻塞队列
- 线程工厂 Executors.defaultThreadFactory()
- 拒绝策略
- 4种拒绝策略
- AbortPolicy() 抛出异常 默认
- DiscardPolicy() 直接丢弃
- DiscardOldestPolicy() 抛弃最老的线程,提交新的线程
- CallerRunsPolicy() 交给申请的线程执行
11、四大函数式接口(重点必须掌握)
- 传统程序员:泛型、反射、枚举 (JDK1.5)
- 新时代程序员:lambda表达式、链式编程、函数式接口、Stream流式计算 (JDK1.8)
- @FunctionalInterface函数式接口:只有一个方法的接口,简化编程,再新版本的框架底层大量应用,
- 例如:Runnbale接口
@FunctionalInterfacepublic interface Runnable { public abstract void run();}
- 例如foreach的参数,典型的消费型函数式接口
@FunctionalInterfacepublic interface BiConsumer<T, U> { void accept(T t, U u);}
- 四大函数式接口
只要是函数时接口,就能拿lambda表达式简化
- Function函数型接口 输入参数T返回R类型的数据
@FunctionalInterfacepublic interface Function<T, R> { R apply(T t);}
- Predicate断定型接口 输入一个参数,返回一个布尔类型数据
@FunctionalInterfacepublic interface Predicate<T> { boolean test(T t);}
- Consumer消费型接口 输入一个参数,没有返回值
@FunctionalInterfacepublic interface Consumer<T> { void accept(T t);}
- Supplier供给型接口 不传入参数,返回一个数据T
@FunctionalInterfacepublic interface Supplier<T> { T get();}
12、Stream流式计算
- 什么是Stream流式计算
/** * 题目要求:一行代码实现 * 现在有6个用户,筛选 * 1.ID必须是偶数 * 2.年龄必须大于23岁 * 3.用户名转为大写字母 * 4.用户名倒着输出 * 5.只输出一位用户 */User u1 = new User(1,"a",21);User u2 = new User(2,"b",22);User u3 = new User(3,"c",23);User u4 = new User(4,"d",24);User u5 = new User(6,"e",26);User u6 = new User(8,"f",28);//存储List<User> list = Arrays.asList(u1, u2, u3, u4, u5,u6);//计算list.stream() .filter(user -> {return user.getId()%2==0;}) .filter(user -> {return user.getAge()>23;}) .map(user -> {user.setName(user.getName().toUpperCase()); return user;}) .sorted((user1,user2)->{return user2.getName().compareTo(user1.getName());}) .limit(1) .forEach(System.out::println);
13、ForkJoin
- ForkJoin分支合并JDK1.7就存在了 在大数据量时,并行执行任务,提高效率
- 在必要的情况下,将一个大任务,进行拆分(fork) 成若干个子任务(拆到不能再拆,这里就是指我们制定的拆分的临界值),再将一个个小任务的结果进行join汇总。
- 特点
工作窃取 (维护的都是双端队列) - 使用
- 创建ForkJoinPool,通过ForkJoinPool执行
ForkJoinPool forkJoinPool = new ForkJoinPool();
- 计算任务
forkjoinpool.execute(ForkJoinTask task)//执行,同步 无返回值
forkJoinPool.submit(ForkJoinTask task);//提交,异步 有返回值
- 计算类要继承ForkJoinTask的两个子类:RecursiveAction或者RecursiveTask(V),其中RecursiveAction任务没有返回值,RecursiveTask任务有返回值.这两个子类都有一个抽象的方法,叫做compute(),用来定义任务的逻辑.
14、异步回调
- Future设计的初衷:对将来的某个时间的结果进行建模
- 无返回值的异步调用CompletableFuture.runAsync()
//没有返回值的runAsync异步回调
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("runAsync");
});
System.out.println("================");
Void aVoid = completableFuture.get();//获取阻塞执行结果
//先输出================,等待2秒后输出runAsync
- 有返回值的异步调用CompletableFuture.supplyASync()
//有返回值的supplyAsync异步回调CompletableFuture<Integer> uCompletableFuture = CompletableFuture.supplyAsync(() -> { //int i = 10/0; return 1024;});System.out.println("---------------");System.out.println(uCompletableFuture.whenComplete((t, u) -> { System.out.println(t);//正常的返回结果 System.out.println(u);//错误信息}).exceptionally((e) -> { e.getMessage(); return 233;//可以获取到错误的返回结果}).get());
15、JMM
- 请你谈谈你对Volatile的理解
Volatile时Java虚拟机提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
- 什么是JMM?
- JMM是Java内存模型,不存在东西,是概念、约定
- 关于JMM的一些同步的约定
- 线程解锁前,必须把共享变量刷回主存
- 线程加锁前,必须读取主存中的最新值到工作内存中
- 加锁和解锁是同一把锁
- 8种操作
- lock(锁定)
- unlock(解锁)
- read(读取)
- load(载入)
- use(使用)
- assign(赋值)
- write(写入)
- store(存储)
- 使用规则
- 不允许read和load、write和store操作之一单独出现。即使用了read必须load,使用了write必须stroe
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主存中诞生,不允许工作内存直接使用一个未被初始化的变量。即对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock之后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存种此变量的值,在执行引擎使用这个变量前,必须重新进行load或assign操作初始化变量的值
- 如果一个变量两没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把变量同步回主内存
16、Volatile
- 保证可见性
//不加volatile线程就感知不到num值发生了变化private volatile static int num = 0;
- 不保证原子性
- 原子性:不可分割
- 不使用synchronized和了lock怎么保证num++的原子性?
- 使用JUC包下的原子类 比用锁更高效
- AtomicInteger
private volatile static AtomicInteger num = new AtomicInteger();num.incrementAndGet(); //++i 使用的是CASnum.getAndIncrement(); //i++ 使用的是CAS
- AtomicLong
- AtomicBoolean
- 这些类的底层都与系统挂钩,在内存中修改值,Unsafe是很特殊的存在
- 禁止指令重排
- 指令重排: 计算机并不按照程序员写的去执行
- 源代码–>编译器优化的重排–>指令并行也可能重排–>内存系统也会重排–>执行
处理器在进行指令重排的时候会考虑数据之间的依赖性
int x = 1;//1int y = 2;//2x = x + 1;//3y = x * x;//4//我们所期望的 1234 但可能执行的时候 2134 1324//不可能是 4123
默认 a b x y 这4个值都为零
线程A | 线程B |
x = a; | y = b; |
b = 1; | a = 2; |
正常情况下: a=2 b=1 x=0 y=0
指令重排可能会: a=2 b=1 x=2 y=1
- Volatile可以避免指令重排:
- 内存屏障 CPU指令
- 作用
- 保证特定的操作的执行顺序
- 可以保证某些变量的可见性(利用这些特性,volatile实现了可见性)
- Volatile是可以保持可见性.不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!
17、彻底玩转单例模式
- 饿汉式单例
- 可能会浪费空间
//饿汉式单例public class Hungry { private Hungry(){} private final static Hungry HUNGRY = new Hungry(); public static Hungry getInstance(){ return HUNGRY; }}
- 懒汉式单例
//懒汉式单例模式public class LazyMan { private LazyMan(){} private static LazyMan lazyMan; public static LazyMan getInstance(){ if (lazyMan==null){ lazyMan = new LazyMan(); } return lazyMan; }}
- 单线程下确实单例ok,多线程并发情况下无法单例,需要为class加锁,并且加锁 ===>双重检测锁模式的懒汉式单例 DCL懒汉式
//懒汉式双重检测锁DLC单例模式public class LazyMan { private LazyMan(){} private volatile static LazyMan lazyMan; public static LazyMan getInstance() { if (lazyMan == null) { synchronized (LazyMan.class) { if (lazyMan == null) { lazyMan = new LazyMan();//不是原子性操作 /** * 1、分配内存空间 * 2、执行构造方法,初始化对象 * 3、把对象指向空间 * 期望:123 可能132 此时lazyMan还没有完成构造 * 避免指令重排需要加 volatile */ } } } return lazyMan; }}
//道高一尺,魔高一丈public class LazyMan { private static boolean qwe = false; private LazyMan(){ synchronized (LazyMan.class) { if (qwe==false){ qwe = true; }else { throw new RuntimeException("不要试图拿反射破坏单例"); } } } private volatile static LazyMan lazyMan; public static LazyMan getInstance() { if (lazyMan == null) { synchronized (LazyMan.class) { if (lazyMan == null) { lazyMan = new LazyMan(); } } } return lazyMan; } //反射破坏! public static void main(String[] args) throws Exception { //LazyMan instance1 = LazyMan.getInstance(); Field qwe = LazyMan.class.getDeclaredField("qwe"); qwe.setAccessible(true); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); LazyMan instance1 = declaredConstructor.newInstance(); qwe.set(instance1, false); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance1); System.out.println(instance2); }}
- 静态内部类
//静态内部类public class Holder { private Holder(){} public static Holder getInstance() { return InnerClass.HOLDER; } public static class InnerClass{ private static final Holder HOLDER = new Holder(); }}
单例不安全
- 枚举
public enum EnumSingle { INSTANCE; public EnumSingle getInstance(){ return INSTANCE; }}
- 反射不能破坏枚举
- 枚举也是一个类,只是继承了枚举接口 枚举默认就是单例
- 没有无参构造,只有一个有参构造 (String,int)
18、深入理解CAS
- 什么是CAS
- compareAndSet 比较并交换 CAS是CPU的并发原语
- CAS:比较并交换当前工作内存种的值,如果这个值是期望的,那么则执行操作,如果不是就一直循环(自旋锁)
- CAS缺点:
- 循环会耗时
- 一次只能保证一个共享变量的原子性
- Unsafe类
- Java无法操作内存,Java可以调用C++使用native方法,C++可以调用内存,Unsafe就是Java的后门,可以通过这个类操作内存
- 内存操作,效率很高
- 自旋锁
- ABA问题(狸猫换太子) 原子引用
19、原子引用
解决ABA问题 对应的思想—>乐观锁
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(初始值,版本号 );
20、各种锁的理解
公平锁、非公平锁
- 公平锁:非常公平,先来后到
- 非公平锁:非常不公平,可以插队,(默认都是非公平锁)
Lock lock1 = new ReentrantLock();//默认非公平锁Lock lock2 = new ReentrantLock(true);//设置公平锁
可重入锁
- 所有的锁都是可重入锁(递归锁)
- 拿到外面的锁,就可以拿到里面的锁,自动获得
自旋锁
- spinlock
- 不断尝试,直到解锁为止
死锁
- 死锁?
- 两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象
- 四要素
- 互斥条件
- 请求和保持条件
- 不可剥夺条件
- 环路等待条件
- 解决死锁?
- 破坏四要素
- 死锁排查?
- 查询堆栈信息
- 使用jdk bin目录下,jps定位进程号
jps -l
- 使用
jdstack 进程号
找到死锁问题
- 查看日志