Java并发编程、JUC(java.util.concurrent包)
参考:JUC详解
概念辨析
进程、线程、管程
进程
进程:进程是一个具有一定独立功能的程序 关于某个数据集合的一次运行活动。
它是操作系统动态执行的基本单元,是操作系统结构的基础。
在传统的操作系统中,进程既是系统资源分配和调度的基本单元 ,也是程序执行的基本单元 。
在当代面向线程设计的计算机结构中,进程是线程的容器 。
程序是指令、数据及其组织形式的描述,进程是正在运行的程序的实例 。
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
进程则是执行程序的一次过程,它是一个动态的概念。是系统资源分配的单位。
线程
线程:是操作系统能够进行运算调度的最小单位 。它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流, 一个进程中可以并发执行多个线程,每条线程并行执行不同的任务。
线程是独立调度和分派的基本单位。
线程是系统分配处理器时间资源的基本单元,或者说进程内独立执行的一个单元执行流。是程序执行的最小单位。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计算器pc,线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间,他们从同一堆中分配对象,可以访问相同的变量和对象,这就是的线程间通信更简便、高效。
关系:
- 每个进程只有一个方法区和堆,每一个线程都有一个虚拟机栈和一个程序计数器,多个线程共用一个方法区和堆。
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 同一进程中的多条线程将共享该进程中的全部系统资源:计算资源、内存资源。
- 一个进程可以有很多线程,每条线程并行执行不同的任务。可并发执行。
线程的状态
新建(NEW)、准备就绪(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、等待超时(TIMED_WAITING)、终止(TERMINATED)。
管程
管程:管程是一种程序结构,又称为监视器。结构内的多个子程序形成的工作线程互斥访问的共享资源。它提供了一种机制,线程可以临时放弃互斥访问,等待条件满足时才重新获取执行权。
JVM中同步是基于进入和退出**管程对象(Monitor)**实现的,每个对象都会有一个管程对象,管程会随着Java对象一同创建和销毁。
线程执行首先要持有管程对象,然后才能执行方法,当方法完成后会释放管程,方法在执行时会持有管程,其他线程无法再获取同一个管程。
串行、并行、并发
串行:即表示所有任务按照先后顺序依次进行。串行每次只能取并且处理一个任务。
并行:同一时刻,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。并行的效率从代码层次上强依赖于多进程/多线程,从硬件角度上则依赖于多核CPU。
并发:同一个时间段内,两个或多个程序交替使用系统计算资源,使得所有程序可以同时运行(宏观上是同时,微观上仍是顺序执行)。这里的"同时运行"表示的不是真的同一时刻有多个线程同时运行的现象,而是看起来多个线程同时运行。单核CPU任意时刻只能运行一个线程。并发是同一时刻多个线程访问同一个资源。
wait和sleep
- sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
- sleep不会释放锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
- 它们都可以被interrupted方法中断。
用户线程和守护线程
用户线程:指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。
守护线程:是指在程序运行的时候在后台提供一种通用服务的线程,用来服务于用户线程;不需要上层逻辑介入,当然我们也可以手动创建一个守护线程。(用白话来说:就是守护着用户线程,当用户线程死亡,守护线程也会随之死亡)。
同步和异步
volatile关键字
volatile 是 Java 虚拟机提供的轻量级的同步机制
三大特性:
- 保证可见性
- 不保证原子性
- 禁止指令重排
什么是指令重排:程序的执行顺序可能不是按照你的书写顺序去执行!
源代码 ---> 编译器优化的重排 --->指令并行也可能会重排 --->内存系统也会重排 -->执行
为什么volatile可以禁止指令重排?
系统中的CPU指令,内存屏障。它的作用:
保证特定操作的执行顺序
保证某些变量的内存可见性( 利用这些特性volatile实现了可见性 )
JMM内存模型
JVM在设计时候考虑到,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。此时,就需要上述的JMM规定来保证多个线程操作的安全性。
四组内存交互操作
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量
实施use、store操作之前,必须经过assign和load操作
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,
必须重新load或assign操作初始化变量的值
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
对一个变量进行unlock操作之前,必须把此变量同步回主内存
synchronized关键字
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是synchonized括号里配置的对象
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
synchronized关键字8锁问题
锁(重点)
公平锁、非公平锁
公平锁:先来后到,一定要排队执行。
非公平锁:可以插队。
悲观锁、乐观锁
可重入锁(递归锁)、自旋锁、读锁、写锁
可重入锁:又称递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象得是同一个对象),不会因为之前已经获取过锁还没有释放而死锁。
自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
JUC中的锁lock
Lock是一个接口,它有三个实现类:常用是ReentrantLock(可重入锁)
ReentrantLock(可重入锁)
ReentrantReadWriteLock.ReadLock(可读写锁.读锁,共享锁)
ReentrantReadWriteLock.WriteLock(可读写锁.写锁,独占锁)。
ReentrantLock(可重入锁)的方法:
加锁:lock()
尝试获取锁:tryLock()
解锁:unlock
synchronized关键字和锁的对比
- Synchronized是java内置的一个关键字,Lock是工具包下java提供的一个接口
- Synchronized无法判断锁的状态,Lock可以判断是否获取到了锁
- Synchronized会自动释放锁,Lock必须要手动释放锁,否则会产生死锁
- Synchronized遇到线程阻塞时会一直等待,Lock锁不一定会等待下去
- Synchronized是已设置好的可重入锁、不可中断、非公平锁;Lock是可以手动设置的可重入锁、中断(是否),公平(是否)
- 正是由于Synchronized的全自动性,对于少量代码时,Synchronized更适用,Lock适用于大量代码内容
JUC介绍
在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类。包括:
java.util.concurrent包
java.util.concurrent.atomic包
java.util.concurrent.locks包
JUC框架结构
JUC中的辅助工具类(tools包)
CoundownLatch(闭锁)
是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
减法计数器 ,当待执行任务数量置零时,执行后续任务,否则处于阻塞状态。
方法:
countDownLatch.countDown() --> 计数器-1
countDownLatch.await() -->阻塞,等待计数器归0
Semaphore(信号量)
是一个计数信号量,它的本质是一个"共享锁"。信号量维护了一个信号量许可集。线程可以通过调用 acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。
semaphore.acquire() --> 从信号量中获取许可证,获取不到,等待
semaphore.release() --> 释放许可证到信号量中
CyclicBarrier(栅栏)
一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点,并且在释放等待线程后可以重用。
加法计数器 ,当任务数量达到指定数量时,执行后续任务,否则处于阻塞状态。
cyclicBarrier.await() --> 表示本线程到达屏障点,并等待任务数量达到指定数量。
FutureTask(异步任务)
JUC中的并发安全容器类
阻塞队列
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
ConcurrentLinkedQueue
Deque
ArrayDeque
LinkedList
CopyOnWriteArrayList
背景:并发修改触发异常。多个线程向List集合中插入数据时,java.util.ConcurrentModificationException 并发修改异常。
解决方法:
- 使用Vector
- 使用Collections.synchronizedList将不安全的容器转换为安全容器
- 使用JUC包下的CopyOnWriteArrayList。
CopyOnWrite 写入时复制,cow计算机程序设计领域的一种优化策略。
相较于Vector的优势,CopyOnWriteArrayList使用lock锁的方式,效率更高
读写分离。
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
CopyOnWriteArraySet
与上类似。
ConcurrentHashMap
与上类似。
ConcurrentSkipListSet
JUC中关于异步操作的类
Future接口
FutureTask实现类
CompletableFuture类
//没有返回值得异步回调
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步回调方法。。。");
});
//先打印外部输出
System.out.println("1111");
//阻塞 待任务中程序执行完毕
future.get();
//供给型接口 有返回值的异步输出
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("有返回值型接口。。");
int a=10/0;
return 1024;
});
Callable接口
Executor接口
是Java里面线程池的顶级接口,但它只是一个执行线程的工具,真正的线程池接口是ExecutorService。
ThreadPoolExecutor
JUC中的原子变量(atomic包)
是JDK提供的一组原子操作类,包含有AtomicBoolean、AtomicInteger、AtomicIntegerArray等原子变量类,他们的实现原理大多是持有它们各自的对应的类型变量value,而且被volatile关键字修饰了。这样来保证每次一个线程要使用它都会拿到最新的值。
AtomicBoolean
AtomicInteger
AtomicReference
JUC中的锁(locks包)
是JDK提供的锁机制,相比synchronized关键字来进行同步锁,功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁。
ReentrantLock(独占锁)
它是独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。
ReentrantReadWriteLock
它包括子类ReadLock和WriteLock。ReadLock是共享锁,而WriteLock是独占锁。
LockSupport
它具备阻塞线程和解除阻塞线程的功能,并且不会引发死锁。
线程、线程池
创建线程
【java】Java并发编程--Java实现多线程的4种方式
线程池
为什么使用线程池技术
程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用的优化手段
池化技术:提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升性能等。
线程池的工作原理::控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用,控制最大并发数,管理线程。
第一:降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
创建线程池
三大方式、七大参数、四种拒绝策略
三大创建方式(实际开发中均不建议使用)
单一线程线程池:ExecutorService threadPool = Executors.newSingleThreadExecutor()
固定数量的线程池:ExecutorService threadPool = Executors.newFixedThreadPool(num)
可伸缩大小的线程池: ExecutorService threadPool = Executors.newCachedThreadPool()
线程池执行任务:threadPool.execute(Runnable runnable)支持使用lambda表达式;
七大参数
以上三种创建线程池的方法均会调用到的构造函数和参数释义如下:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 超时等待的时间
TimeUnit unit, // 超时等待的时间单位
BlockingQueue<Runnable> workQueue, // 待执行任务的队列
ThreadFactory threadFactory, // 线程的创建工厂,默认的Executors.defaultThreadFactory()
RejectedExecutionHandler handler // 拒绝策略
){}
实际开发中,不要使用工具类提供的三种创建方式,而是自定义线程池,阿里巴巴代码规约中有明确说明:
8. 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 各个方法的弊端:
1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。
2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
四种拒绝策略
当待执行任务数大于线程池的最大容量时,线程池的拒绝策略:
ThreadPoolExecutor.AbortPolicy :抛出异常提示
ThreadPoolExecutor.CallerRunsPolicy:此任务从哪个线程进来的,会被退回到哪个线程执行,如main方法进入,此任务交由main()线程执行
ThreadPoolExecutor.DiscardPolicy:任务会被忽略
ThreadPoolExecutor.DiscardOldestPolicy :与线程池中已有的任务进行竞争
如何定义最大线程数量
分为两种方式:
CPU密集型:获取当前电脑的最大线程数量,作为最大线程数量的参数即可。
Runtime.getRuntime().availableProcessors()
IO密集型:当程序中调用IO资源较多时,最大线程数量应大于IO任务数量,最好为2倍。 如IO任务数量为15时,最大线程数量定义为30。
Java并发编程常考问题
Java多线程常见面试题汇总:JUC详解
JUC常考问题:JUC实战必备-全是精华