进程和线程
进程和线程
本质区别:每个进程拥有自己的一整套变量,而线程则共享数据。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程。
守护线程和用户线程
用户线程:自定义的线程(主线程结束了,用户线程还在,JVM存活)
守护线程:垃圾回收线程(主线程结束了,没有用户线程,都是守护线程,JVM结束)(isDaeon)
线程创建方式
- 继承Thread
- 使用Runnable接口
- 使用Callable<Class>接口和Future(与Runnable的区别:可以返回结果和抛出异常)
- 线程池
线程的生命周期
线程的基本方法
- wait
- sleep
- yield:让当前线程释放CPU
- interrupt
- join
- notify
sleep(休眠)和wait(等待)
- sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用
- sleep不会释放锁,不需要占用锁。wait会释放锁,但前提是当前线程占有锁
- 都可以暂停线程的执行,都可以被interrued方法中断
- sleep() 通常被用于暂停执行,wait() 通常被用于线程间交互/通信
- sleep() 方法执行完成后,线程会自动苏醒。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。
start和run
- 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行
- 在调用了线程的 start 方法后,线程会在后台执行,无须等待 run 方法体的代码执行完毕,就可以继续执行下面的代码
终止线程的四种方式:
- 正常运行结束
线程的上下文切换
上下文:指线程切换时 CPU 寄存器和程序计数器所保存的当前线程的信息。
上下文切换
1.
2.
3.
4.
引起线程上下文切换的原因
- 当前正在执行的任务完成,系统的 CPU 正常调度下一个任务。
- 当前正在执行的任务遇到 I/O 等阻塞操作,调度器挂起此任务,继续调度下一个任务。
线程、进程调度
线程调度
- 抢占式调度
- 协同式调度
进程调度
- 先来先服务调度算法
- 短作业优先调度算法
- 非抢占式优先调度算法
- 抢占式优先调度算法
- 高响应比优先调度算法
- 时间片轮转法
- 多级反馈队列调度算法
多线程共享数据
线程安全
什么是线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行问题,也不需要进行额外的同步,而调用这个对象的行为都可以获得正确的结果,那这个对象便是线程安全的。
三种特性
- 原子性 : 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。synchronized 可以保证代码片段的原子性。
- 可见性 :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
- 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。
ArrayList 线程安全方案
- 使用Vector
- 使用Collections.synchronizedList(new ArrayList<>())
- CopyOnWriteArrayList<>() 写时复制技术
HashSet 线程安全方案
- CopyOnWriteArraySet<>()
HashMap 线程安全方案
- ConcurrentHashMap<>()
ThreadLocal
实现每一个线程都有自己的专属本地变量。
每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
ConcurrentHashMap
分段锁的思想
线程池
作用
- 降低资源消耗:重复利用已创建的线程降低线程创建和销毁造成的消耗。(线程复用)(高效)
- 提高线程可管理性:使用线程池进行统一的分配,调优和监控(控制操作系统最大并发数)(安全)
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
线程池创建方式
- ThreadPoolExecutor
- newFixedThreadPool:固定大小的线程池
- newSingleThreadExecutor:单个线程的线程池
- newCachedThreadPool:可缓存的线程池
- newScheduledThreadPool:可做任务调度的线程池
- newWorkStealingPool :足够大小的线程池,JDK 1.8 新增
工作流程
线程池的保护策略(拒绝策略)
- ThreadPoolExecutor.AbortPolicy
- ThreadPoolExecutor.CallerRunsPolicy
- ThreadPoolExecutor.DiscardPolicy
- ThreadPoolExecutor.DiscardOldestPolicy
- 自定义
Executor框架
- 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。
- 把创建完成的实现 Runnable/Callable接口的 对象直接交给ExecutorService执行:
ExecutorService.execute(Runnable command)
。也可以把 Runnable 对象或Callable 对象提交给 ExecutorService执行(ExecutorService.submit(Runnable task)
或ExecutorService.submit(Callable <T> task)
)。 - 如果执行
ExecutorService.submit(…)
,将返回一个实现Future接口的对象(submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给ExecutorService 执行。 - 最后,主线程可以执行
FutureTask.get()
方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)
来取消此任务的执行。
ThreadPoolExecutor 类
参数:
- ⭐corePoolSize:核心线程数定义了最小可以同时运行的线程数量
- ⭐maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
- ⭐workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中
- keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁
- unit : keepAliveTime 参数的时间单位。
- threadFactory :executor 创建新线程的时候会用到。
- handler :饱和策略
Java锁
管程(Monitor监视器)(锁)
一种同步机制,同一时间只能有一个线程对资源访问。线程操作需要先持有管程对象,才能执行方法,执行完释放管程对象。
乐观锁
特性:在每次读取数据时都认为别人不会修改该数据,所以不会上锁
过程:比较当前版本号与上一次的版本号,如果版本号一致,则更新,如果版本号不一致,则重复进行读、比较、写操作
实现:Java乐观锁大部分通过CAS实现
CAS:在对数据操作之前首先会比较当前值跟传入的值是否一样,如果一样则更新,否则不执行更新操作,直接返回失败状态
悲观锁(不支持并发)
特性:在每次读取数据时都认为别人会修改数据,所以每次在读写数据时都会上锁
实现:Java悲观锁大部分基于AQS架构实现
AQS:锁会先尝试以 CAS 乐观锁去获取锁,如果获取不到,则会转为悲观锁
自旋锁
自旋锁:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(也叫作自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗。
注意:线程在自旋时会占用 CPU,在线程长时间自旋获取不到锁时,将会产 CPU 的浪费,甚至有时线程永远无法获取锁而导致 CPU 资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁。
优点:自旋锁可以减少 CPU 上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的 CPU 耗时明显少于线程阻塞、挂起、再唤醒时两次 CPU 上下文切换所用的时间。
缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起 CPU 的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。
Synchronized
synchronized:提供线程安全的操作,是独占锁、悲观锁、可重入锁、重量级锁。
Java锁的本质:Java 中的每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象。对代码块加锁是通过在前后分别加上 monitorenter 和 monitorexit 指令实现的,对方法是否加锁是通过一个标记位来判断的
JDK1.6时:引入了适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等以提高锁的效率。
锁膨胀:锁从偏向锁升级到轻量级锁,再升级到重量级锁的这种过程。
实现原理:
- ContentionList:锁竞争队列,所有请求锁的线程都被放在竞争队列中
- EntryList:竞争候选列表,在 Contention List 中有资格成为候选者来竞争锁资源的线程被移动到了 Entry List 中
- WaitSet:等待集合,调用 wait 方法后被阻塞的线程将被放在 WaitSet 中
- OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为 OnDeck
- Owner:竞争到锁资源的线程被称为 Owner 状态线程
- !Owner:在 Owner 线程释放锁后,会从 Owner 的状态变成!Owner。
ReentrantLock
避免死锁:
- 响应中断
- 可轮询锁(tryLock)
- 定时锁
Synchronized 和 ReentrantLock的比较
相同点
- 都是可重入锁。可重入锁(递归锁):指在同一线程中,在外层函数获取到该锁之后,内层的递归函数仍然可以继续获取该锁。
- 都用于控制多线程对共享对象的访问。
- 都保证了可见性和互斥性。
- 都是重量级锁
不同点
5. ReentrantLock 显式获取和释放锁;synchronized 隐式获取和释放锁。为了避免程序出现异常而无法正常释放锁,在使用 ReentrantLock 时必须在 finally 控制块中进行解锁操作;synchronized 不会发生死锁。
6. ReentrantLock 可响应中断、可轮回,为处理锁提供了更多的灵活性。
7. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的。
8. ReentrantLock 可以定义公平锁。
9. ReentrantLock 通过 Condition 可以绑定多个条件。
10. 二者的底层实现不一样:synchronized 是同步阻塞,采用的是悲观并发策略;Lock 是同步非阻塞,采用的是乐观并发策略。
11. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是由内置的语言实现的。
12. 我们通过 Lock 可以知道有没有成功获取锁,通过 synchronized 却无法做到。
13. Lock 可以通过分别定义读写锁提高多个线程读操作的效率。竞争激烈时,Lock性能远远优于synchronized
14. synchronized不能响应中断
注意:获取锁和释放锁的次数要相同
释放锁的次数 > 获取锁的次数,Java 抛出java.lang.IllegalMonitorStateException 异常
释放锁的次数 < 获取锁的次数,该线程就会一直持有该锁,其他线程将无法获取锁资源
公平锁与非公平锁
公平锁
概念:在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程。
特性:效率相对低
原理:维护一个锁线程等待队列,基于该队列进行锁的分配
非公平锁
概念:在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待
特性:线程饿死,效率高
AtomicInteger
原子操作同步类,使得同步操作(线程安全操作)更加方便、高效。
AtomicInteger 的性能通常是 synchronized 和 ReentrantLock 的好几倍。
读写锁(ReadWriteLock)
原因
为了提高性能
特性
多个读锁不互斥,读锁与写锁互斥。
缺点
- 造成锁饥饿,一直读没有写
- 读时候不能写,写时侯可以读(写锁降级为读锁)
读锁:
共享锁
会发生死锁:1线程修改的时候,等待2线程读完;2线程修改的时候,等待1线程读完
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();
rwLock.readLock().unlock();
写锁:
独占锁
会发生死锁
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.writeLock().lock();
rwLock.writeLock().unlock();
锁降级
将写锁降级为读锁
步骤:获取写锁 -> 获取读锁 -> 释放写锁
共享锁和独占锁
独占锁:也叫互斥锁,每次只允许一个线程持有该锁(悲观锁)(比如:ReentrantLock)
共享锁:允许多个线程同时获取该锁(乐观锁)
重量级锁和轻量级锁
重量级锁:基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态与内核态之间切换,相对开销较大。(比如:synchronized)重量级锁需要在用户态和核心态之间做转换,所以 synchronized 的运行效率不高
轻量级锁:在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(即互斥操作),如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁。
偏向锁
偏向锁:同一个锁被同一个线程多次获取的情况,但是消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)
目的:是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次 CAS(Compare and Swap)原子操作,而偏向锁只需要在切换 ThreadID 时执行一次 CAS 原子操作,因此可以提高锁的运行效率。
升级:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
分段锁
分段锁并非一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化,以提高并发效率。(ConcurrentHashMap)
锁优化
- 减少锁持有的时间
减少锁持有的时间指只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。 - 减小锁粒度
减小锁粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高。减小锁粒度最典型的案例就是 ConcurrentHashMap 中的分段锁。 - 锁分离
锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能。操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBlockingQueue 从头部取出数据,并从尾部加入数据。 - 锁粗化
锁粗化指为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分得太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升。在这种情况下,建议将关联性强的锁操作集中起来处理,以提高系统整体的效率。 - 锁消除
在开发中经常会出现在不需要使用锁的情况下误用了锁操作而引起性能下降,这多数是因为程序编码不规范引起的。这时,我们需要检查并消除这些不必要的锁来提高系统的性能。
死锁
定义
在有多个线程同时被阻塞时,它们之间若相互等待对方释放锁资源,就会出现死锁。
解决方法
为了避免出现死锁,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁。
产生原因
- 系统资源不足
- 推进顺序非法
- 资源分配不当
new Thread(()->{
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "持有锁a,试图获取锁b");
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "持有锁b");
}
}
},"AA").start();
new Thread(()->{
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "持有锁b,试图获取锁a");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "持有锁a");
}
}
},"BB").start()
Volatile
具有稍弱的同步机制,用于确保将变量的更新操作通知到其他线程
特性:
- 保证该变量对所有线程可见,在一个线程修改了变量的值后,新的值对于其他线程是可以立即获取的
- 禁止指令重排,即volatile 变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取 volatile类型的变量时总会返回最新写入的值
保证并发环境下线程安全的条件
- 对变量的写操作不依赖于当前值(比如不能保证i++), 或者说是单纯的变量赋值(boolean flag = true)
- 该变量没有被包含在具有其他变量的不变式中,也就是说在不同的volatile变量之间不能互相依赖,只有在状态真正独立于程序内的其他内容时才能使用volatile。
Java阻塞队列
消费者阻塞:在队列为空时,消费者端的线程都会被自动阻塞(挂起),直到有数据放入队列,消费者线程会被自动唤醒并消费数据
生产者阻塞:在队列已满且没有可用空间时,生产者端的线程都会被自动阻塞(挂起),直到队列中有空的位置腾出,线程会被自动唤醒并生产数据
Java阻塞队列的实现
名称 | 说明 |
---|---|
ArrayBlockingQueue | 基于数组结构实现的有界阻塞队列 |
LinkedBlockingQueue | 基于链表结构实现的有界阻塞队列 |
PriorityBlockingQueue | 支持优先级排序的无界阻塞队列 |
DelayQueue | 基于优先级队列实现的无界阻塞队列 |
SynchronousQueue | 用于控制互斥操作的阻塞队列 |
LinkedTransferQueue | 基于链表结构实现的无界阻塞队列 |
LinkedBlockingDeque | 基于链表结构实现的双向阻塞队列 |
Java并发关键字(JUC辅助类)
-
CountDownLatch(倒计时器)
允许一个或多个线程一直等待其他线程的操作执行完后再执行相关操作。 -
CyclicBarrier(循环屏障)
让一组线程等待至某个状态之后再全部同时执行 。
线程到达cyclicBarrier.await();
后进入Barrier状态,并不执行,等到达到状态后一起执行。 -
Semaphore(信号量)
acquire()获取许可
release()释放许可
举例:打印任务和打印机 -
volatile
用于确保将变量的更新操作通知到其他线程。
CAS
概念:比较并交换。CAS 算法 CAS(V,E,N)包含 3 个参数,V表示要更新的变量,E 表示预期的值,N 表示新值。在且仅在 V 值等于 E 值时,才会将V 值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
特性:乐观锁
CAS自旋等待:
ABA
ABA问题:取出内存中某时刻的数据,然后在下一时刻进行比较、替换,在这个时间差内可能数据已经发生了变化,导致产生 ABA 问题。
⭐AQS
全称:AbstractQueuedSynchronizer 自定义队列同步器
是什么:用来构建锁和同步器的框架
多线程编程步骤
- 创建资源类
- 在资源类创建方法
1.判断
2.干活
3.通知 - 创建多个线程,调用资源类的操作方法
- 防止虚假唤醒
FutureTask
class MyThread2 implements Callable {
@Override
public Object call() throws Exception {
return 200;
}
}
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
new Thread(futureTask, "name").start();