【java基础】多线程总结

前言

1.你怎么理解多线程?

从以下几个方面回答:

是什么

多线程是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

为什么存在

因为单线程处理能力低打个比方,一个人去搬砖与几个人去搬砖,一个人只能同时搬一车,但是几个人可以同时一起搬多个车。

如何实现

在Java里如何实现线程,Thread、Runnable、Callable。

存在的问题

线程可以获得更大的吞吐量,但是开销很大,切换线程需要的时间、还会产生死锁

怎么解决

采用无锁编程、使用线程池,重复利用线程,避免线程创建与销毁的开销、使用定时锁,超过时间自动释放。

2.线程同步

ReentrantLock可重入锁是jdk内置的一个锁对象
synchronized也支持可重入锁。但是它是一个关键字,是一种语法级别的同步方式,称为内置锁

wait/Condition

wait/notifyAll方式跟ReentrantLock/Condition方式的原理是一样的。
Java中每个对象都拥有一个内置锁,在内置锁中调用wait,notify方法相当于调用锁的Condition条件对象的await和signalAll方法

ThreadLocal

是一种把变量放到线程本地的方式来实现线程同步的。

使用ThreadLocal<UserInfo> userInfo = new ThreadLocal<UserInfo>()的方式,让每个线程内部都会维护一个ThreadLocalMap,里边包含若干了 Entry(K-V 键值对),每次存取都会先获取当前线程,然后得到该线程对象中的Map,然后与Map交互。

Semaphore

Semaphore信号量被用于控制特定资源在同一个时间被访问的个数。类似连接池的概念,保证资源可以被合理的使用

同步工具包

一般情况下,我们不会使用wait/notifyAll或者ReentrantLock这种比较底层的类,而是使用并发包下提供的一些工具类。
CountDownLatch 计数器
CyclicBarrier 栅栏

AbstractQueuedSynchronizer

AQS是很多同步工具类的基础,比如ReentrentLock里的公平锁和非公平锁,Semaphore里的公平锁和非公平锁,CountDownLatch里的锁等他们的底层都是使用AbstractQueuedSynchronizer完成的

3.线程通信

线程执行状态流程图

image.png | left | 711x402

等待通知机制

wait/notify

wait与sleep的区别

如果调用wait方法,会释放当前锁持有的monitor,而sleep是没有释放锁的

join()

控制线程执行顺序

volatile

  1. 主内存和工作内存,直接与主内存产生交互,进行读写操作,保证可见性;
  2. 禁止 JVM 进行的指令重排序。

管道通信

4.锁

是什么

锁是在在不同线程竞争资源的情况下对资源进行同步控制的工具

synchronized

通常和wait,notify,notifyAll一块使用。
wait:释放占有的对象锁,释放CPU,进入等待队列只能通过notify/all继续该线程。
sleep:则是释放CPU,但是不释放占有的对象锁,可以在sleep结束后自动继续该线程。
notify:唤醒等待队列中的一个线程,使其获得锁进行访问。
notifyAll:唤醒等待队列中等待该对象锁的全部线程,让其竞争去获得锁。

lock

拥有synchronize相同的语义,但是添加一些其他特性,如中断锁等候和定时锁等候,所以可以使用lock代替synchronize,但必须手动加锁释放锁

两者的区别

  • 性能:资源竞争激烈的情况下,lock性能会比synchronized好;如果竞争资源不激烈,两者的性能是差不多的
  • 用法:synchronized可以用在代码块上,方法上。lock通过代码实现,有更精确的线程语义,但需要手动释放,还提供了多样化的同步,比如公平锁、有时间限制的同步、可以被中断的同步
  • 原理:synchronized在JVM级别实现,会在生成的字节码中加上monitorenter和monitorexit,任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。monitor是JVM的一个同步工具,synchronized还通过内存指令屏障来保证共享变量的可见性。lock使用AQS在代码级别实现,通过Unsafe.park调用操作系统内核进行阻塞
  • 功能:比如ReentrantLock功能更强大
    1. ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁,所谓的公平锁就是先等待的线程先获得锁
    2. ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
    3. ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制

我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。

锁优化

5.有几种锁

悲观锁

假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。synchronized关键字的实现也是悲观锁。

乐观锁

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。原子变量类就是使用了乐观锁的一种实现方式CAS实现的。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

下面是摘自《阿里云-java编码规范》

并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次

可重入锁

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁

可中断锁

可中断锁:顾名思义,就是可以interrupt()中断的锁。 在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。 非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。 ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

6.线程池

起源

new Thread弊端:

  • 每次启动线程都需要new Thread新建对象与线程,性能差。线程池能重用存在的线程,减少对象创建、回收的开销。
  • 线程缺乏统一管理,可以无限制的新建线程,导致OOM。线程池可以控制可以创建、执行的最大并发线程数。
  • 缺少工程实践的一些高级的功能如定期执行、线程中断。线程池提供定期执行、并发数控制功能

线程池时核心参数

  • corePoolSize:核心线程数量,线程池中应该常驻的线程数量
  • maximumPoolSize:线程池允许的最大线程数,非核心线程在超时之后会被清除
  • workQueue:阻塞队列,存储等待执行的任务
  • keepAliveTime:线程没有任务执行时可以保持的时间
  • unit:时间单位
  • threadFactory:线程工厂,来创建线程
  • rejectHandler:当拒绝任务提交时的策略(抛异常、用调用者所在的线程执行任务、丢弃队列中第一个任务执行当前任务、直接丢弃任务)

创建线程的逻辑

以下任务提交逻辑来自ThreadPoolExecutor.execute方法:

  1. 如果运行的线程数 < corePoolSize,直接创建新线程,即使有其他线程是空闲的
  2. 如果运行的线程数 >= corePoolSize
    2.1 如果插入队列成功,则完成本次任务提交,但不创建新线程
    2.2 如果插入队列失败,说明队列满了
    2.2.1 如果当前线程数 < maximumPoolSize,创建新的线程放到线程池中
    2.2.2 如果当前线程数 >= maximumPoolSize,会执行指定的拒绝策略

阻塞队列的策略

  • 直接提交。SynchronousQueue,它将任务直接提交给线程而不保持它们。如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界maximumPoolSizes 以避免拒绝新提交的任务。
  • 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
  • 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

7.并发包工具类

Java多线程与高并发(四):java.util.concurrent包

概览图

image.png | left | 827x449

JUC中有非常多的类,将部分类按功能进行分类,分别是:

  1. 原子atomic
  2. 比synchronized功能更强大的__lock__包
  3. 线程调度管理工具
  4. 线程安全与并发工具集合
  5. 线程池

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架,它是JUC并发包中的核心基础组件。

  • 同步队列:AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
  • 继承实现:AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法acquire/release来管理同步状态。
  • 同步状态维护:AQS使用一个int类型的成员变量state来表示同步状态,当state > 0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。

CountDownLatch

计数器闭锁是一个能阻塞主线程,让其他线程满足特定条件下主线程再继续执行的线程同步工具。

new CountDownLatch(10)首先设置计数器的数到AQS的state中,当调用await方法之后,A线程阻塞,随后每次其他线程调用countDown的时候,将state减1,直到计数器为0的时候,A线程继续执行。

Semaphore

信号量是一个能阻塞线程且能控制统一时间请求的并发量的工具。比如能保证同时执行的线程最多200个,模拟出稳定的并发量。

new Semaphore(3)传入的3就是AQS中state的值,也是许可数的总数,在调用acquire时,检测此时许可数如果小于0,就将被阻塞,然后将线程构建Node进入AQS队列

CyclicBarrier

可以让一组线程相互等待,当每个线程都准备好之后,所有线程才继续执行的工具类

与CountDownLatch类似,都是通过计数器实现的,当某个线程调用await之后,计数器减1,当计数器大于0时将等待的线程包装成AQS的Node放入等待队列中,当计数器为0时将等待队列中的Node拿出来执行。

与CountDownLatch的区别:

  1. CDL是一个线程等其他线程,CB是多个线程相互等待
  2. CB的计数器能重复使用,调用多次

ReentrantLock

名为可重入锁,其实synchronized也可重入,是JDK层级上的一个并发控制工具

Condition

条件对象的意义在于对于一个已经获取锁的线程,如果还需要等待其他条件才能继续执行的情况下,才会使用Condition条件对象。

与ReentrantLock结合使用,类似wait与notify。

Future、FutureTask

不是AQS的子类,但是能拿到线程执行的结果非常有用。

Future接口

Future是Java 5添加的类,用来描述一个异步计算的结果。你可以使用isDone方法检查计算是否完成,或者使用get阻塞住调用线程,直到计算完成返回结果,你也可以使用cancel方法停止任务的执行。

CompletableFuture类

但其实在项目中使用到最多的Future类是1.8提供的这个类,因为虽然Future以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞方式得到任务的结果,阻塞的方式显然和我们的异步编程的初衷相违背。

Callable与Runnable

Runnable:run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果
Callable:泛型接口,call()函数返回的类型就是传递进来的V类型,同时能结合lambda使用

BlockingQueue

为什么会有阻塞队列?
如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者暂停等待一下(阻塞生产者线程)或者继续将产品放入队列中

LockSupport

LockSupport是一个非常方便实用的线程阻塞工具类,他可以在线程内任意位置让线程阻塞,和Thread.suspend()相比,它填补了resume()在线程挂起之前执行,而导致的线程无法继续执行的情况。和Object.wait()相比,他又不需要先获得对象锁,也不会抛出InterruptedException异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值