【Java学习】JUC并发编程

71 阅读9分钟

什么是JUC

JUC 就是 java.util.concurrent 工具包的简称。这是一个处理线程的工具包,JDK1.5 开始出现的。

进程和线程回顾

进程

进程(Process) 是计算机中程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。

线程

线程(Thread) 是操作系统能够进行运算调度的最小单位,CPU调度和执行的基本单位,是进程内独立执行的一个单元执行流。

Java默认有两个线程:

  • main主线程
  • GC守护线程

并发(单核CPU)

多个线程操作同一个资源,将资源类丢入线程,同一时刻,多线程交替执行。

并发编程:充分利用CPU的资源

并行(多核CPU)

同一时刻,多线程同时执行。

线程状态

  • 新生 New
  • 运行 Runable
  • 阻塞 Blocked
  • 等待 Waiting
  • 超时等待 Timed_Waiting
  • 终止 Terminated

wait和sleep的区别

  • 来自不同的类
    • wait -> Object
    • sleep -> Thread
  • 关于锁的释放
    • wait会释放锁
    • sleep不会释放锁
  • 使用的范围
    • wait必须在同步代码块中
    • sleep在哪里都可以使用

Lock锁(重点)

传统Synchronized

本质:队列、锁。

Lock三部曲

  • new ReetrantLock();
  • lock.lock(); // 加锁
  • lock.unlock(); //解锁

Synchronized和Lock区别

  • Synchronized是内置的Java关键字,Lock是一个Java类。
  • Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁。
  • Synchronized会自动释放锁,Lock必须手动释放锁,如果不释放锁,会造成死锁
  • Synchronized中阻塞的方法会一直等,Lock不一定会一直等待下去。
  • Synchronized可重入锁,不可以中断,非公平,Lock可重入锁,可以判断锁,非公平(可自己设置)。
    • 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁。(对象获取锁后还可以再次获取该锁)
  • Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码。
  • Synchronized锁的是方法的调用者,谁先拿到谁先执行。

生产者和消费者

线程之间的通信问题:生产者和消费者问题。

  • 等待唤醒
    • 虚假唤醒:虚假唤醒是一种现象,它只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,但由于多个线程执行的顺序不同,后面竞争到锁的线程在获得时间片时条件已经不再满足,线程应该继续睡眠但是却继续往下运行的一种现象。
  • 通知唤醒

Lock中的Condition方法:

  • 替代同步监视器。
  • Condition类的精确等待与精确唤醒只是相对于Java提供的wait/notify、wait/notifyAll的机制的,在原先的机制下,所有的的线程只能等在一个地方,无论他们等待的条件是什么,只要被notifyAll了,就都会被唤醒,醒来都会检查自己等待的条件是否被满足。而每一个Condition变量就是一个条件,线程可以准确地睡在自己需要的条件上,这样每次被唤醒的线程都是等待的条件被满足的线程,这样便可以减少唤醒的线程的数量,降低系统负担,提高效率。

集合类不安全

CopyOnWriteArrayList

多线程情况下ArrayList是不安全的 -> java.util.ConcurrentModificationException 并发修改异常

解决方法:

  • List list = new Vector<>();
  • List list = Collections.synchronizedList(new ArrayList<>());
  • List list = new CopyOnWriteArrayList;

CopyOnWriteArraySet

多线程情况下HashSet也是不安全的 -> CopyOnWriteArraySet

HashSet的底层:HashMap

ConcurrentHashMap

多线程情况下Map也是不安全的 -> ConcurrentHashMap

Callable

Callable接口类似于Runnable,它们都是为其实例可能被另一个线程执行的类设计的。但Runnable不返回结果,也不能抛出异常。

Callable:

  • 可以有返回值
  • 可以抛出异常
  • 方法call()

常用的辅助类

CountDownLatch

允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。一般用作计数器。

  • countDownLatch.countDown(); // 数量-1
  • countDownLatch.await(); // 等待计数器归零,然后向下执行

每次有线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await()就会被唤醒,继续执行。

CyclicBarrier

允许一组线程全部等待彼此达到共同屏障点的同步辅助。循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。屏障被称为循环,因为它可以在等待的线程被释放之后重新使用。

Semaphore

一个计数信号量。用于多个共享资源互斥使用,并控制最大的线程数。

  • semaphore.acquire(); 获得,如果已经满了,等待,等待被释放为止。
  • semaphore.release(); 释放,会将当前的信号量释放+1,然后唤醒等待的线程。

ReadWriteLock读写锁

更加细粒度的控制。

  • 写的时候,同时只希望有一个线程在写 -> 写锁(独占锁);
  • 读的时候,所有线程都可以读(不可以写)-> 读锁(共享锁)。

阻塞队列 BlockingQueue

  • 写入:如果队列满了,就必须阻塞等待;
  • 取出: 如果队列是空的,也必须阻塞等待生产。

Collection下:

  • Queue -> BlockingQueue -> ArrayBlockingQueue & LinkedBlockingQueue
  • Set -> ArraySet & HashSet
  • List -> ArrayList & LinkedList

阻塞队列使用场景:

  • 多线程并发处理
  • 线程池

四组API:

  • 抛出异常
    • 队列已满还要add元素
    • 队列已空还要remove元素
    • element() 获取队首元素
  • 不会抛出异常,有返回值
    • offer()
    • poll()
    • peek() 检测队首元素
  • 阻塞 等待
    • put()
    • take()
  • 超时 等待
    • offer(,,)
    • poll(,)

同步队列synchronousQueue

  • 没有容量
  • 进去一个元素,必须等待取出来之后,才能往里面再放一个元素
  • put()
  • take()

线程池(重点)

池化技术

程序运行的本质:占用系统的资源,优化资源的使用。

池化技术:事先准备好一些资源,有人要用,就拿走,用完还回来。

线程池的好处

  • 降低资源的消耗
  • 提高响应的速度
  • 方便管理

线程复用、控制最大并发数、管理线程

最大线程(池的最大大小)如何定义

  • CPU密集型:几核,就是几个线程。
  • IO密集型:判断程序中十分耗IO的线程(个数),最大线程 > 这个数量,或者是它的两倍。

四大函数式接口FunctionalInterface

函数式接口:只有一个方法的接口。

  • 简化编程模型,在新版本的框架底层大量应用。
  • 函数式接口可以用lambda表达式简化
  • Function 函数型接口
  • Predicate 断定型接口
    • 有一个输入参数,返回值只能是bool值
  • Supplier 供给型接口
    • 没有参数,只有返回值
  • Consumer 消费型接口
    • 只有输入,没有返回值

Stream流式计算

Stream流式计算

集合、mysql本质用来存储东西,计算应该交给流来操作。

分支合并 ForkJoin

什么是ForkJoin

ForkJoin在JDK 1.7,并行执行任务,提高效率,大数据量!

大数据:Map Reduce(把大任务拆分为小任务)

ForkJoin特点:工作窃取

怎么使用ForkJoin

  • 通过 forkjoinPool 来执行
  • 计算任务 forkjoinPool.execute(ForkJoinTask task)
  • 计算类要继承 ForkJoinTask

异步回调 CompletableFuture

  • 异步执行
  • 成功回调
  • 失败回调

JMM

什么是JMM

JMM:Java内存模型,不是真实存在的东西,是一种概念和约定。

关于JMM的一些同步的约定

  • 线程解锁前,必须把共享变量 立即 刷回主存。
  • 线程加锁前,必须读取主存中的最新值到工作内存中。
  • 加锁和解锁是同一把锁。

内存交互8大指令: image.png 问题: image.png 因此,JMM对这8大指令的使用制定了一些规则,需要线程A知道主内存中的值发生了变化。

volatile关键字

Volatile是Java虚拟机提供轻量级的同步机制。

  • 保证可见性
  • 不保证原子性
    • 原子性:不可分割
    • 线程A在执行任务的时候,不能被打扰,也不能被分割,要么同时成功,要么同时失败
    • 如果不加lock和synchronized,怎样保证原子性
      • 使用原子类(Integer -> AtomicInteger),保证原子性(底层CAS)
  • 禁止指令重排
    • 指令重排:计算机执行程序并不是按你写的方式去执行的。
      • 源代码 -> 编译器优化的重排 -> 指令并行也可能会重排 -> 内存系统也会重排 -> 执行
      • 处理器在进行指令重排的时候,会考虑:数据之间的依赖性
    • CPU指令中的内存屏障:
      • 保证特定的操作的执行顺序
      • 可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)

深入单例模式

volatile在单例模式中用的最多。

饿汉式单例

单例模式 -> 构造器私有

DCL懒汉式单例

双重检测锁模式的懒汉式单例。

反射可以破坏单例模式 -> 再升级 -> 再破坏。

反射不能破坏枚举。

深入理解CAS

什么是CAS

compareAndSet:比较并交换(比较当前工作内存中的值和主内存中的值)

  • 如果我期待的值达到了,就进行更新;否则,就不更新。

缺点:

  • 循环会耗时(自旋锁)
  • 一次性只能保证一个共享变量的原子性
  • 存在ABA问题

Unsafe类

提供了硬件级别的原子操作。

原子引用解决ABA问题

CAS -> ABA问题(狸猫换太子)

(B更快,A以为内存中的值没有被动过)-> 乐观锁解决(乐观锁是一种思想,数据更新的时候检测是否发生冲突) 截屏2023-04-27 21.22.30.png

原子引用

带版本号的原子操作。

各种锁的理解

公平锁、非公平锁

  • 公平锁:非常公平,不能插队,必须先来后到。
  • 非公平锁:非常不公平,可以插队(默认都是非公平锁)。

可重入锁(ReentrantLock)

可重入锁(递归锁):拿到外面的锁之后,就可以拿到里面的锁,自动获得。

自旋锁(SpinLock)

不断去尝试,直到成功为止。指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

死锁排查

死锁:两个线程互相争抢资源。

排查:

  • 使用 jps -l 定位进程号
  • 是用 jstack 进程号 查看进程信息(堆栈信息)