1、Java 线程实现/创建方式
(1)继承Thread类
Thread类本质上是实现了Runnable接口的实例,代表一个线程的实例,通过start()
启动,自动执行run()
方法。
(2)实现Runnable接口
Runnable是一个没有返回值的线程任务类,Java有两种方式进行实现:
- 1、自定义线程类实现Runnable接口,覆写run方法;在主程序中利用Thread类构造器传入自定义线程,覆盖默认Thread实例
- 2、利用匿名类,在Thread构造器中传入匿名类,覆盖默认Thread实例
(3)ExcutorService、Callable、Future 含返回值的线程
- 若需要执行带有返回值的任务,必须实现Callable接口
- 执行Callable任务后,可以获取一个Future对象,等待结果返回,利用
get
方法获取返回值 - 利用ExcutorService,实现线程池执行任务,实现带有返回值的多线程
(4)线程池
线程池是为了解决线程的创建与销毁带来的系统资源消耗问题而实现的缓存策略,
Java提供了四种线程池:
利用 Excutors 的静态方法进行常见线程池,其实际上的顶级父类是ExcutorService接口
- newCachedThreadPool: 根据需要任务数量创建线程,并设立一个缓存时间
- 每调用一次execute,都会遍历一次线程池是否有可用线程,若没有可用线程,则创建一个新的线程执行任务
- 若一个线程超过60秒未使用,该线程将会被回收。
- newFixedThreadPool: 创建一个固定线程数的线程池,以共享的无界队列方式来运行这些线程
- 若全部线程都被任务占用,则新任务会保持在任务队列中等待调度执行。
- 若一个线程因为任务异常导致线程结束,则会重新创建一个线程调度任务队列的任务进行执行。
- newScheduledThreadPool: 创建一个具有定时功能的线程池,利用
schedule()
添加任务并设置执行周期 - newSingleThreadExcutor: 创建一个只有一个线程的线程池,当一个线程因为任务异常而结束,内部重新创建一个新线程替代旧线程执行新任务。
2、线程的生命周期:
线程的生命周期一共有五种:新建、就绪、运行、阻塞、死亡
- 新建状态(New): 当程序使用 new 关键字创建一个线程后,线程处于新建状态,此时JVM为其分配内存并初始化成员变量的值。
- 就绪状态(Runnable): 当线程对象调用 start() 之后,线程处于就绪状态,JVM为其创建方法调用栈和程序计数器,等待调度执行。
- 运行状态(Running): 处于就绪状态的线程获得了CPU,开始执行 run() 的线程方法体。
- 阻塞状态(Blocked): 线程因为某些原因放弃了CPU使用权,暂停运行,直到线程进入可运行状态。有三种阻塞情况
- 1、等待阻塞: 线程对象执行wait(),JVM 会把线程放入等待队列(waiting queue)中。
- 2、同步阻塞: 运行状态的线程在获取同步锁时,若该同步锁被其它线程占用,则JVM 会把线程放入锁池中。
- 3、其它阻塞: 运行状态的线程执行sleep()或join(),当发起IO请求时,JVM会把该线程置为阻塞状态,当sleep状态超时或join()等待线程终止或者超时,或IO处理完毕时,线程重新转入可运行状态。
- 线程死亡: 线程死亡有三种方式
- 1、run 或 call。方法执行完成,线程正常结束
- 2、线程抛出一个未捕获的 Exception 或 Error
- 3、调用线程对象的stop(),可能会导致死锁的发生
3、终止线程
终止线程的四种方式:
- 1、正常结束: 程序运行结束,线程自动结束。
- 2、使用退出标志退出线程: 当一个run()执行完成,线程就自动结束。当线程需要一个条件进行退出,则可以使用一个boolean类型的标志进行退出。(可以使用valatile,同步退出标志)
- 3、调用Interrupt():
- (1)当线程处于阻塞状态时,列入使用了sleep,同步锁的wait(),socket的receiver.accept(),使得线程处于阻塞状态,当方法体调用了interrupt()方法时,会抛出InterruptException异常,只有线程捕获到了该异常,才能让该线程正常结束。
- (2)当线程处于非阻塞状态时,使用
isInterrupt()
判断线程的中断标志来退出循环,当使用interrupt()
时,会将中断标志设为true。
- 4、调用stop(): 调用thread.stop()后,创建子线程的线程会抛出ThreadDeathError的错误,并且释放子线程持有的所有锁,这种操作可能会产生数据不一致,所以通常不会使用这种方式结束线程。
4、sleep 与 wait
- (1)sleep: 属于Thread类方法,线程对象调用该方法导致程序暂停执行指定的时间,并且让出CPU,但依旧持有对象锁并且保持监控状态,当时间过期后自动恢复运行状态。
- (2)wait: 属于Object类方法,线程对象调用该方法导致线程对象进入等待锁定池中并且释放对象锁,只有针对该对象调用 notify() 时,本线程才会进入对象锁定池准备获取对象锁进入运行状态。
5、start 与 run
- (1)start: 线程调用该方法,真正实现多线程执行,无需等待run方法执行完毕,继续执行代码,此时的线程处于就绪状态,并不运- 行。
- (2)run: 线程调用该方法,使得就绪状态的线程开始运行run函数中的代码,run方法结束,即线程结束。
6、Java后台线程
- (1)概念: 后台线程也叫**“守护线程”**,它是为用户线程提供公共服务的线程,在没有用户线程时自动离开。
- (2)优先级: 守护线程是一种公共线程,所以其优先级较其它线程低。
- (3)设置: 在用户线程对象创建之前,用线程对象的
setDaemon(true)
来设置线程为守护线程。 - (4)子线程创建: Daemon线程创建的子线程也是Daemon线程。
- (5)并发性: Daemon线程只存在于JVM,只有停止JVM的运行才能销毁Daemon线程。
- (6)案例: GC线程,是一个经典的守护线程,只有在有可回收对象时,才会执行服务方法,始终以低级别的状态运行。
- (7)生命周期: 与JVM生命周期挂钩,当所有的JVM线程都为守护线程时,JVM就可以退出了。
7、Java锁
(1)乐观锁: 获取数据不需要锁,而更新时先获取版本号,若数据版本与存储的版本相同,则获取锁进行更新;若数据版本与存储的版本不同,则重复 读 - 比较 - 写
的操作。Java中的乐观锁是通过CAS操作实现。
(2)悲观锁: 读写数据都需要获取锁才能进行。Java中的Synchroized 就是一种悲观锁。而AQS框架下的锁是先尝试CAS乐观锁获取锁,若获取不到则转为悲观锁获取,例如ReetrantLock。
(3)自旋锁: 是一种获取锁的机制,若持有锁的线程能够在短时间内释放锁资源,则等待竞争的线程不需要做内核态和用户态的切换进入阻塞挂起状态,只需要等待一点时间(自旋),等待持有锁的线程释放锁后立即可以获取锁。因为自旋是一种持续在CPU中不断尝试获取锁的方式,所以会持续占用CPU,导致CPU做无用功,所以通常需要设置一个自旋等待的最大时间,时间到达还未获取锁资源,则停止自旋并进入阻塞状态。
自旋锁的优缺点:
- 自旋锁不同场景的性能提升情况:
- 锁竞争不激烈的场景: 大幅度提升程序性能,因为自旋的损耗小于阻塞再唤醒的损耗
- 锁竞争激烈的场景或是持有锁的线程长期占用锁执行同步块: 不适用自旋锁,因为自旋随时间的拉长而损耗变大,所以需要关闭自旋锁。
自旋锁如何选择自旋执行时间? 在JDK 1.5 时,自旋时间是固定的,JDK 1.6 引入了适应性自旋锁,通过前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,一个上下文切换的时间是一个最佳自旋时间。
自旋锁的开启:
- JDK 1.6 使用
-XX:+UseSpinning
开启 -XX:PreBlockSpin=10
设置自旋次数- JDK1.7后,由JVM控制开启自旋
(4)Sychronized 同步锁
sychronized 是一种可以将任意一个非NULL对象作为锁的实现方式,属于独占式的悲观锁,同时属于可重入锁。
- Sychronized 作用范围
- 1、作用于方法时,锁住的是对象的实例(this)
- 2、作用于静态方法时,锁住的是Class实例,由于Class的相关数据存储在永久代(元数据区),所以锁住静态方法,相当于锁住了所有调用该方法的线程
- 3、作用于一个对象实例时,锁住的是所有以该对象为锁的代码块,它有多个队列,当多个线程一起访问某个对象监视器时,对象监视器会将这些线程存储在不同的容器中。
- Sychronized 核心组件
- 1、Wait Set: 调用wait()的线程放置在这里。
- 2、Contention List:竞争队列,所有请求锁的线程首先放置在该队列。
- 3、Entry List:竞争队列中可竞争的线程移动到该队列。
- 4、OnDesk: 任意时刻,最多只有一个线程竞争锁资源,该线程被称为OnDesk
- 5、Owner: 当前获取锁的线程对象
- 6、!Owner: 当前释放锁的线程对象
- Sychronized 实现
- 1、JVM从Contention List的队尾取出一个对象作为OnDesk,在并发情况下,会从Contention List取出一部分移动如Entry List作为OnDesk候选线程队列
- 2、Owner线程在unlock时,会从Contention List 迁移出部分线程到Entry List,并指定一个线程为OnDesk(一般是第一个进入的线程)
- 3、Owner线程释放锁时,成为OnDesk的线程竞争锁的优势最大,牺牲一定的公平性换取提高系统吞吐量,这种行为被称为竞争切换
- 4、OnDesk获取锁后,成为Owner,而未获取锁的线程会留在Entry List 中,当Owner线程被wait阻塞,则自动释放锁,并且转入Wait Set,直到notify或notifyAll,转入Entry List
- 5、处于Wait Set、Contention List、Entry List的线程都处于阻塞状态,这种状态是由OS来完成的
- 6、Sychronized 是非公平锁,在线程进入Contention List 之前,会通过自旋来抢占竞争锁资源列表中线程对象的锁资源(包括OnDesk)
- 7、对象加锁是通过monitor对象进行的,
monitorenter和monitorexit
指令组成了加锁的范围,通过标记位来判断是否方法是否加锁 - 8、Sychronized 是重量级锁,需要调用OS相关接口,性能较低
- 9、Java 1.6 对sychronized 进行了优化:适应性自旋、锁清除、锁粗化、轻量级锁及偏向锁等;Java 1.7 和 1.8 对关键字实现机制进行优化
- 10、锁膨胀,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁
- 11、JDK 1.6中默认开启偏向锁和轻量级锁,通过
-XX:-UseBiasedLocking
来禁用偏向锁
(5)ReetrantLock
ReentantLock 是一种可重入锁,继承并实现接口 Lock,除了能完成synchronized所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
Lock接口的主要方法:
- 1、void Lock(): 调用该方法时,若锁空闲,则直接获取锁;若被占用,则阻塞当前线程直到获取锁
- 2、boolean tryLock():调用该方法尝试获取锁,若锁空闲,则获取锁并返回true,否则返回false
- 3、void unlock():调用该方法使当前线程释放锁,若未持有锁则可能会发生异常。
- 4、Condition newCondition(): 条件对象,获取等待通知组件。该组件只有在获取锁之后才能使用,当前线程调用await(),当前线程释放锁。
- 5、getHoldCount(): 查询当前线程执行lock方法的次数
- 6、getQueueLength(): 返回正在等待锁释放的线程数
- 7、getWaitQueueLength(Condition condition): 返回使用同一个条件对象,并且执行了await()的线程数
- 8、boolean hasWaiters(Condition condition): 查询是否存在使用指定条件对象并执行await()的线程
- 9、boolean hasQueuedThread(Thread thread): 查询指定线程是否在等待锁
- 10、boolean hasQueuedThreads: 是否有等待当前锁
- 11、boolean isFairO: 查询该锁是否为公平锁
- 12、boolean isHeldByCurrentThread:当前线程是否被锁定,主要是当前线程是否执行了lock()
- 13、boolean isLock():此锁是否有线程使用
- 14、lockInterruptibly(): 如果当前线程未被中断获取锁
- 15、boolean tryLock(): 尝试获取锁,若能获取锁则true,若未能获取则false
- 16、boolean tryLock(long timeout, TimeUnit unit): 若锁在规定时间内未被一个线程获取,则获取锁
ReentrantLock 与 Synchronized的优势
ReentrantLock 通过lock()
与unlock()
进行加解锁,并且不交由JVM进行控制,需要手动解锁,具有可中断、公平锁、可多个锁的特性。
ReetrantLock 实现
public class MyService {
//1、获取ReetrantLock,通过布尔值来控制锁是否为公平锁
private Lock lock = new ReetrantLock(true);
//2、创建条件等待对象
private Condition condition = lock.newCondition();
//3、创建服务方法
public void testMethod(){
try{
//4、加锁
lock.lock();
//5、执行await();
condition.await();
//6、唤醒线程
condition.signal();
//处理数据...
}catch(InterruptedException e){
e.printStackTrace;
}finally{
//释放锁
lock.unlock();
}
}
}
Condition 与 Object 锁方法的区别
- 1、await 与 wait 等效
- 2、signal 与 notify 等效
- 3、signalAll 与 notifyAll 等效
- 4、ReetrantLock 可指定符合条件的线程唤醒,而Object唤醒是随机的
tryLock、lock、lockInterruptibly的区别
- 1、tryLock 在可获取锁时,直接获取锁并返回true,若未获取锁则返回false,可以通过增加等待时间进行尝试获取锁,若超时则直接返回false
- 2、lock 能获取锁就返回true,不能就一直等待获取锁
- 3、lock 和 lockInterceptibly,两者都能尝试获取锁,都可以因为中断而停止获取锁,但是lock不会抛出异常,而lockInterruptibly会抛出异常。
(6)Semaphore 信号量
Semaphore 是一种基于计数的信号量,可以设定一个阈值,用来控制申请资源的线程数,当多线程进行竞争资源时,就会申请信号量;若申请的信号量超过阈值,则申请的线程将被阻塞,直到有线程归还信号量。 通常信号量被用来限制共享资源池。
通过Semaphore 实现互斥锁: 互斥锁就是只有一个线程获取锁,即Semaphore信号量为1.
Semaphore 与 ReentrantLock: Semaphore 基本能完成ReentrantLock的所有工作,适用方法也相似。
- 获取锁:acquire() (默认使用可响应中断锁)—— lockInterruptibly()
- 释放锁:release() —— unlock()
- 尝试获取锁: tryAcquire() —— tryLock()
- 同样提供可轮询的锁请求与定时锁的功能
- 同样提供公平锁与非公平锁的机制,并也可以在构造函数中设定
- 锁释放都手动释放,所以都需要在finnaly代码块中完成
(7)AtomicInteger
AtomicInteger是一种提供原子操作的Integer类,相同的还有 AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference
等,可以通过 AtomicReference<V>
将一个对象的所有操作都转为原子操作。
(8)可重入锁
可重入锁也叫递归锁,指的是当外层函数获取锁时,内层递归函数仍可以执行递归操作,不受锁的影响。
(9)公平锁与非公平锁
公平锁(Fair): 加锁前检查是否有排队等待的线程,优先排队等待的线程,按照顺序进行加锁。
非公平锁(Nonfair): 加锁前不需要考虑是否有排队等待的线程,若无法直接获得锁,则自动在队尾等待。(性能优于公平锁)
(10)读锁与写锁
为了提高性能,Java提供读写锁,在读的方法上加读锁,在写的方法上加写锁,读写锁是互斥锁,读锁之间并不互斥。
读锁: 可供多线程进行读数据,但不能与写操作进行并发。
写锁: 只能单线程进行写数据,且互斥读操作
Java中读写锁有个接口:java.util.concurrent.locks.ReadWriteLock
,也有更加具体的实现ReentrantReadWriteLock
(11)共享锁与独占锁
Java提供了两种加锁模式:共享锁和独占锁
独占锁模式: 每次只有一个线程能获取锁,属于悲观锁加锁策略,避免了读/读冲突。
共享锁模式: 共享锁模式允许多个线程同时获取锁,并发访问共享资源,属于乐观锁加锁策略。
- 1、AQS内部的Node定义了两个常量 SHARED 和 EXCLUSIVE,标识等待线程的锁获取方式
- 2、Java并发包的读写锁,允许一个资源被多个读操作访问,或被一个写操作访问,但两者不能同时进行
(12)重量级锁与轻量级锁
重量级锁
一种依赖于操作系统的Mutex Lock来实现的一种锁,所以重量级锁需要频繁切换用户态和内核态,而状态的转换需要很长的时间,所以消耗的资源也较多,在开发过程中应尽量减少对重量级锁的使用。
轻量级锁
锁的状态有四种(按锁升级过程进行排序):无锁、偏向锁、轻量级锁、重量级锁
轻量级锁是相对于利用操作系统的互斥量来实现的传统锁而言的,轻量级锁使用的场景是在线程交替访问同步代码块时,同一时间访问同一把锁时,就会触发锁升级过程。
(13)偏向锁
偏向锁的作用是在某个线程在获得锁之后,消除这个锁重入的开销,偏向锁只需要置换ThreadID时依赖一次CAS原子指令,所以在只有一个线程执行同步代码块时进一步提高线程。
(14)分段锁
分段锁不是实际意义上的锁,而是将数据区域分割成一段一段的,每一次线程访问都只访问一段数据,从而形成多线程访问同一容器内的数据。
(15)锁优化
- 减少锁持有时间: 只在需要线程安全的线程上加锁
- 减小锁粒度: 在大对象的某些需要线程安全的代码块上加锁
- 锁分离: 典型的有 读写锁,根据功能进行分离读锁和写锁。
- 锁粗化: 为保证多线程之间有效并发,要求线程持有锁的时间尽量缩短,即在使用完资源后,立即释放锁,但频繁的获取/释放锁产生的资源浪费会很高。
- 锁消除: 锁消除是编译器级别的策略,若在即时编译器时,若发现不可能共享的对象,则可以消除这些对象的所操作,大多数情况都是因为编码不规范引起的。
8、线程基本方法
线程的基本方法有 wait、notify、notifyAll、sleep、join、yield等
(1)线程等待 wait: 调用该方法使得当前线程进入WATTING状态,直到被唤醒,但需要注意的是调用wait方法会释放锁,因此wait一般用于同步方法或同步代码块中。
(2)线程睡眠 sleep: 调用该方法使得当前线程进入WATTING状态,被notify或notifyAll或过期则唤醒,但需要注意的是调用sleep方法不会释放锁。
(3)线程让步 yield: 调用该方法使得当前线程让出CPU,进入竞争队列中。
(4)线程中断 interrupt: 中断一个线程,给这个线程一个通知信号,会影响这个线程内部的一个中断标识位,这个线程不会改变自身状态。
- interrupt() 无法中断处在RUNNING状态的线程
- interrupt() 可以中断处于TIME_WATTING状态的线程
- 在声明抛出InterruptedException异常之前,会清除标记
- 中断状态可以安全的终止一个线程
(5)等待线程终止 join: 当前线程调用join(),则当前线程会转入BLOCKING状态,直到另一个线程结束,当前线程由BLOCKING转为RUNNABLE状态,等待CPU调度。
join使用场景: 当主线程需要等待子程序返回结果时,主线程需要调用子线程的join()方法,当主线程再次被CPU调度,就可以获取子线程返回的结果。
(6)线程唤醒 notify: 随机唤醒指定对象监视器中的单个线程。
(7)其它方法
- 1、isAlive():判断一个线程是否存活。
- 2、activeCount():程序中活跃的线程数。
- 3、enumerate(): 枚举程序中的线程。
- 4、currentThread():得到当前线程。
- 5、isDaemon(): 一个线程是否为守护线程。
- 6、setDaemon():设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线 程依赖于主线程结束而结束)
- 7、setName():为线程设置一个名称。
- 8、setPriority(): 设置一个线程的优先级。
- 9、getPriority(): 获得一个线程的优先级。
9、线程上下文切换
CPU利用时间片轮转的方式,为每个任务都服务一定时间,然后将当前任务状态保存下来,再加载下一个任务的状态后,继续服务下一任务。 “任务的状态保存及再加载的过程,叫做上下文切换”
10、同步锁与死锁
(1)同步锁: 由于多线程同时访问一个数据时,可能会出现数据不一致的问题,所以需要保证线程同步互斥,即并发执行的多个线程,在同一时间内只允许一个程序访问共享资源,Java中 使用了 Synchronized 关键字来取得一个对象的同步锁。
(2)死锁: 多个线程同时被阻塞,它们中的一个或多个都在等待某个资源被释放。
11、线程池原理
线程池的作用是线程可复用、控制最大并发数、管理线程。
(1)线程复用: 通过继承重写Thread类,在其 start 方法中添加不断循环调用Queue传递过来的Runnable对象,调用其run()方法,即可实现线程复用机制。
(2)线程池的组成:
- 线程池管理器: 用于创建并管理线程池
- 工作线程池: 线程池中的线程
- 任务接口: 每个任务必须实现的接口,用于工作线程调度其运行
- 任务队列: 用于存放待处理的任务,提供一种缓冲机制
(3)线程池参数
- corePoolSize: 指定线程池的核心线程数量
- maximumPoolSize: 指定线程池最大线程数量
- keepAliveTime: 临时线程空闲时间
- unit: 时间单位
- workQueue: 任务队列,存放等待执行的任务
- threadFactory: 线程工厂,用于创建线程,一般用于默认即可
- handler: 拒绝策略,当线程池线程全员忙碌时,采用的拒绝任务策略
(4)线程池有哪些拒绝策略
- AbortPolicy: 直接抛异常,结束程序执行
- CallerRunsPolicy: 交付给调用者线程进行执行,即串行化执行该任务
- DiscardOldestPolicy: 丢弃等待时间最久的任务,并尝试提交当前任务
- DiscardPolicy: 直接丢弃任务,且不抛出异常
12、线程池工作过程
(1)创建新的线程池,池中没有线程,任务队列作为参数传入
(2)调用execute()方法时,线程池做判断:
- 如果正在使用的线程数小于核心线程数,则马上创建线程运行该任务
- 如果正在使用的线程数大于等于核心线程数,则将这个任务放入任务队列等待执行
- 如果任务队列达到满阈值,且正在运行的线程数小于线程池最大线程数,则创建临时线程调度执行任务
- 如果任务队列达到满阈值,且正在运行的线程数大于等于线程池最大线程数,则按照拒绝策略执行
(3)当一个线程完成任务后,调度任务队列中的任务执行
(4)当一个线程空闲时间达到线程池最大空闲时间,线程池中运行线程数大于核心线程数,则停止该线程,直至等于核心线程数
13、阻塞队列原理(BlockQueue)
(1)线程阻塞的两种情况:
- 当队列中没有数据时,消费者端的所有线程将被阻塞(挂起),直到有数据放入队列
- 当队列中充满数据时,生产者段的所有线程将被阻塞(挂起),直到队列中有空位置
(2)阻塞队列的主要方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
删除方法 | remove() | poll() | take() | poll(time, unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
- 抛出异常: 抛出一个异常
- 特殊值: 返回一个特殊值(null 或 false)
- 阻塞: 在成功操作之前,一直阻塞线程
- 超时: 放弃前在最大时间内阻塞
(3)Java中的阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,每一个put操作必须等待take操作,否则无法添加元素到队列,所以SynchronousQueue一般用来做传递队列,可以高效解决队列与队列之间数据流通问题。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列,默认情况下采用自然顺序的升序排列,通过自定义实现compareTo()指定排序规则,或初始化PriorityBlockingQueue时,指定构造器中的Comparator来对元素进行排序。
- DelayQueue:使用优先级队列实现的延时无界阻塞队列,队列中的元素必须实现Delayed接口,在创建元素时可以指定可访问新元素的时间间隔。应用场景有:缓存系统设计、定时任务调度。
- ArrayBlockingQueue:由数组实现的有界阻塞队列,按照FIFO的原则对元素进行排序,默认情况下采用非公平策略进行访问队列,但ArrayBlockingQueue构造器提供开启公平策略的参数。
- LinkedBlockingQueue:由链表实现的有界阻塞队列,按照FIFO的原则对元素进行排序,针对生产者端和消费者端分别采用独立的锁控制数据同步,保证生产者与消费者并行操作队列中的数据,提高了整个队列的并发性能,LinkedBlockingQueue默认拥有Integer.MAX_VALUE大小的容量。
- LinkedTransferQueue:由链表组成的无界阻塞队列,相较于别的阻塞队列,多了两个核心功能点:
- transfer(): 当消费者正在等待接收元素,transfer方法可以马上将生产者传入队列的值传递给消费者,而当没有消费者等待时,队列将传入的值存储到tail节点中,等待消费者请求接收。
- tryTransfer(): 用来尝试询问消费者是否接收队列中的值,若没有消费者等待接收元素,则返回false。
- tryTransfer(long time, TimeUnit unit): 用来做一定时间内等待消费者接收元素,若这段时间内未被消费,则返回false。
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列,对比LinkedBlockingQueue,多了双向操作方法,能够有效减少线程的IO竞争,提高了线程的读写效率
14、CyclicBarrier、CountDownLatch、Semaphore
(1)CountDownLatch (线程计数器)
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。
例如:有 一个任务A,它要等待其他4个任务执行完毕之后才能执行。
final CountDownLatch latch = new CountDownLatch(2);
new Thread(){
()->{
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
};
}.start();
new Thread(){
()->{
System.out.println("子线程"+Thread.currentThread().getNameO+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getNameO+"执行完毕");
latch.countDown();
};
}.start();
System.out.println("等待2个子线程执行完毕...");
latch.await();
System.out.println("2个子线程已经执行完毕");
System.out.println("继续执行主线程");
(2)CyclicBarrier(回环栅栏-等待至barrier状态再全部同时执行)
它可以实现让一组线程等待至某个状态之后再全部同时执行,当所有等待线程都被释放以后,CyclicBarrier可以被重用,暂且把这个状态就叫做 barrier,当调用await()方法之后,线程就处于barrier 了。
CyclicBarrier中最重要的方法就是await方法:
- public int await(): 用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
- public int await(long timeout, TimeUnit unit): 让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。
(3)Semaphore (信号量-控制同时访问的线程个数)
Semaphore可以控制同时访问的线程个数,通过 acquire。获取一个许可,如果没有就等待,而release()释放一个许可。
-
Semaphore类中阻塞型方法:
- public void acquire(): 用来获取一个许可,若未获得许可,则会一直等待,直到获得许可。
- public void acquire(int permits): 获取 permits 个许可
- public void release(): 释放已获得的许可
- public void release(int permits): 释放 permits 个许可
-
Semaphore类中非阻塞型方法:
- public boolean tryAcquire(): 尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
- public boolean tryAcquire(long timeout, TimeUnit unit): 尝试获取一个许可,若在指定的时间内获取成功,则返回true,否则返回false
- public boolean tryAcquire(int permits): 尝试获取permits个许可,若获取成功,则返回true,若获取失败,则返回false
- public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 尝试获取 permits 个许可,若在指定的时间内获取成功,则返回true,否则返回false
- availablePermits(): 得到可用的许可数目
15、volatile关键字的作用(变量可见性、禁止重排序)
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。
volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值,所以 volatile 变量适合作为一个共享变量,多个线程可以直接给这个变量赋值。
(1)volatile变量具备两种特性:变量可见性、禁止重排序
- 变量可见性: 保证该变量对所有线程可见,当一个线程修改了变量的值,其他线程是可以立即获取新的值。
- 禁止重排序: volatile禁止了指令重排
(2)volatile变量实现共享的原理: 对非volatile变量进行赋值,每个线程必须先从内存拷贝变量到CPU Cache,如果计算机有多个CPU,那么变量可能会被拷贝到不同的CPU Cache,所以JVM保证每次读变量都从主内存中读,跳过CPU cache。
(3)volatile变量适用场景:
- 对变量的写操作不依赖于当前值(i++),或者是单纯的变量赋值(boolean flag = true)
- 不同的volatile变量之间,不能互相依赖,只有在状态真正独立于程序内其他内容时才能使用volatile
16、如何实现线程之间共享数据
Java里面进行多线程通信的主要方式是共享内存,共享内存主要的关注点有三个:可见性、有序性、原子性。
JMM恰好解决了可见性和有序性,用锁解决了原子性。
常见实现方法:
- (1)数据操作代码抽象到一个方法中,并加上“synchronized”获取同步锁,保证其同步性
- (2)将Runnable对象作为一个类的内部类,将共享数据作为这个类的成员变量,每个线程对共享数据的操作也封装在外部类中,以便实现对数据的操作同步性和互斥性,作为内部类的Runnable对象,调用操作类。
17、ThreadLocal的作用
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
(1)ThreadLocalMap (线程的一个属性)
- 每个线程中都维护了一个ThreadLocalMap对象,可以将本线程的对象存储在其中,各线程可以对应访问到自己的对象。
- 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各方法中通过这个静态ThreadLocal实例的 get() 方法获取自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
(2)使用场景
最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。
18、sychronized 和 ReentrantLock 的区别
(1)共同点
- 都是用来协调多线程对共享对象、变量的访问
- 都是可重入锁,同一线程可以多次获得同一个锁
- 都保证了可见性和互斥性
(2)不同点
- ReentrantLock显示的获得、释放锁;而synchronized隐式获得释放锁
- ReentrantLock可响应中断、可轮回;而synchronized是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
- ReentrantLock是API级别的;而 synchronized 是 JVM 级别的
- ReentrantLock可以实现公平锁
- ReentrantLock可以通过Condition可以绑定多个条件
- 底层实现不同,synchronized是同步阻塞,使用的是悲观并发策略;lock是同步非阻塞,采用的是乐观并发策略
- Lock是一个接口;而synchronized是Java中的关键字,synchronized是内置的语言实现。
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unlock()释放锁,则很可能造成死锁现象, 因此使用Lock时需要在finally块中释放锁。
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时, 等待的线程会一直等待下去,不能够响应中断
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
- Lock可以提高多个线程进行读操作的效率,即实现读写锁等
19、ConcurrentHashMap 并发
(1)减小锁粒度
减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力,ConcurrentHashMap的锁粒度恰好因为分段锁而缩小,使得达到线程安全的成本减少。
(2)ConcurrentHashMap 分段锁
ConcurrentHashMap,它内部细分了若干个小的HashMap,称之为段(Segment)。
默认情况下ConcurrentHashMap被进一步细分为16个段,即可以被16个线程并行访问16个不同的段。
若需要向ConcurrentHashMap添加一个新的表项,首先根据hashCode判断表项应存储的段,然后向对应段加锁,并完成put操作,只要多线程不在同一个段内进行put操作,那么线程间就可以做到真正的并行。
(3)ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成
Segment的数据结构与HashMap类似,其是一种可重入锁ReentrantLock,在ConcurrentHashMap扮演锁的角色。
HashEntry一种链表结构的元素,用于存储键值对数据。
每个Segment守护一个HashEntry数组里的元素,当HashEntry数组的数据进行修改时,必须先获得它对应的 Segment 锁
20、Java中的线程调度
(1)抢占式调度
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制。
系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
(2)协同式调度
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行。
线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题。如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
(3)JVM的线程调度实现(抢占式调度)
Java中线程按照优先级分配CPU时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,优先级低也不代表不会占用执行时间片。
(4)线程让出cpu的情况
- 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作,例如调用 yield() 方法。
- 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上
- 当前运行线程结束
21、进程调度算法
在操作系统中,有三种进程调度算法:优先调度算法、高优先权优先调度算法、基于时间片的轮转调度算法
(1)优先调度算法
- 先来先服务调度算法(FCFS): 当作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入队列的作业,将他们调入内存,为它们分配资源、创建进程,然后放入就绪队列,在进度调度中采用FCFS算法,每次调度都是从就绪队列中选择一个最先进入队列的进程为其分配处理机,该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。
- 短作业(进程)优先调度算法:
短作业优先算法(SJF)是从后备队列中调入一个预计运行时间最短的作业,将它们调入内存运行。
短进程优先算法(SPF)是从就绪队列中调入一个预计运行时间最短的进程,将处理机分配给它,使它立即执行直到完成,或发生某事件而阻塞放弃处理机时再调度。
(2)高优先权优先调度算法: 为解决紧迫型作业,使之进入系统后便获得优先处理,引入最高优先权优先调度算法(FPF)。
当用于作业调度时,系统从后备队列中选择若干个优先权最高的作业装入内存。
当用于进程调度时,系统将处理机分配给就绪队列中优先权最高的进程。
- 非抢占式优先权调度算法: 由于线程无法抢占CPU,如果CPU分配给优先级最高的进程,那么其它进程必须等待该进程结束才有机会获得CPU执行程序。主要用于批处理系统中,也可以用于某些对实时性不高的实时系统。
- 抢占式优先权调度算法: 由于线程可抢占CPU,如果CPU在执行某个优先级最高的进程,当有一个优先级更高的进程请求CPU,则CPU放弃当前进程,保存当前进程的状态后,调度优先级更高的进程执行程序。主要用于实时要求高的实时系统,以及对性能要求较高的批处理和分时系统中。
- 高响应比优先调度算法(极为优秀的调度算法): 由于线程可能会长时间等待CPU调度而无法得到执行,所以引入了动态优先权的概念,动态优先权指的是作业优先权随着等待时间的增长以一定的增长速率进行提高,长作业在等待一定时间后必然能分配到CPU进行处理。
- 当作业等待时间相同时,则按照服务时间越短,优先级越高,适用于短作业
- 当作业服务时间相同时,则按照作业等待时间越长,优先级越高,实现了FCFS
- 针对长作业,该算法也会因为响应比计算,让它得到CPU的优先执行,避免了长时间的等待导致服务崩溃。
(3)基于时间片的轮转调度算法
- 时间片轮转算法: 早起的时间片轮转算法,是系统将就绪进程按照FCFS进行排列,每一次调度时,都会将CPU分配给队首进程,并令其执行一个时间片,一个时间片的时间从几ms到几百ms,当执行时间用完,则由一个计时器发出时钟中断请求,调度程序停止当前进程执行,并将它送往就绪队列的队尾,再将CPU分配给队首进程,循环往复直至完成所有进程任务。
- 多级反馈队列调度算法
- 通过多级就绪队列,每个队列的优先级不同,根据每个队列的优先级分配不同的时间片,优先级高的分配的时间片长。
- 当一个进程进入就绪队列,首先会分配到第一就绪队列,根据FCFS原则等待时间片执行,若时间片结束,但未完成进程任务,则将该进程分配到第二就绪队列,根据FCFS原则等待时间片执行,若仍然未完成,则继续向后排列,直至完成。
- 若优先级高的队列空置时,CPU才会去执行优先级低的队列,当有新进程进入高优先级队列,则CPU将当前执行的线程放入当前队列的队尾,优先执行优先级高的进程。
22、什么是CAS(Compare And Swap)
(1)概念: CAS意思是比较并交换,通过对比存储的值与旧值是否相同,来确定是否数据被修改。
(2)三个参数: V,E,N
- V: 更新的变量(内存值)
- E: 期待值(旧值)
- N: 新值
(3)更新机制: 当且仅当 V = E,才能将 V 设置为 N;若 V != E,则不进行操作,最终 CAS 返回 V 的值。
(4)CAS的线程安全原理: CAS采用乐观锁原则,当多个线程同时使用CAS操作一个变量时,总是只有一个线程能够完成更新,其它线程全部失败,基于这样的原理,CAS也能够实现变量更新的同步性。
(5)原子包 java.util.concurrent.atomic: 这个包提供了一组原子类,这样的原子操作能在多线程环境下具有排他性,CAS是基于CPU指令集的操作,对比需要依赖CPU切换的算法,性能更好。
(6)ABA问题: ABA问题,是CAS算法因时间差而产生的数据不一致问题,但因为最终结果依旧为原结果,所以这个过程依旧能让CAS操作成功,有一部分的乐观锁通过版本号对比的方法来解决ABA问题。
23、什么是AQS
AQS,AbostractQueueSynchronizer 抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于AQS。例如常用的 ReentrantLock、Semaphore、CountDownLatch
AQS维护了一个 volatile int state(代表共享资源)和一个FIFO线程等待队列(多进程竞争资源被阻塞会进入等待队列)。
(1)共享资源 state 的访问方式有三种:
- getState()
- setState()
- compareAndSetState()
(2)AQS的两种资源共享模式:
- Exclusive(独占模式)—— ReentrantLock
- Share(共享模式)—— Semaphore/CountDownLatch
AQS只是一个框架,具体资源的获取/释放需要交给自定义同步器去实现,AQS定义了一个接口
自定义同步器实现获取具体资源:通过state的get/set/CAS方法进行获取,之所以state没有被定义为 abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而在共享模式下只用实现tryAcquiredShared-tryReleaseShared,如果都定义成 abstract,那么每个模式也要去实现另一个模式下的接口,不同的自定义同步器在实现时只需要实现共享资源state的获取与释放的方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。
自定义同步器实现时主要实现方法:
- isHeldExclusively(): 该线程是否正在独占资源,只有用到 condition 才需要去实现它。
- tryAquire(int): 独占方式,尝试获取资源,成功则true,失败则false
- tryRelease(int): 独占方式,尝试释放资源,成功则true,失败则false
- tryAquireShared(int): 共享方式,尝试获取资源,负数表示失败;0表示成功,但资源已经耗尽;整数表示成功,但仍有可用资源。
- tryReleaseShared(int): 共享方式,尝试释放资源,若释放后允许唤醒后等待结点返回 true,否则返回 false
同步器的实现是ABS核心(state 资源状态计数)
同步器的实现是 ABS核心,以ReentrantLock为例,初始化state=0,当线程A lock() 执行时,state + 1,当其它线程到=尝试获取锁时,判断state是否为0,就可以知道是否可以获取锁;当线程A unlock() 时,state - 1,回归零态,所以每一次获取与释放都是对等的,彻底释放锁也就是state回归零态,若线程A 持续获取锁,则state不断 +1 ,这就是可重入锁的实现。