1、基础概念
进程、线程、管程
-
进程
-
系统中一个运行的程序就是一个进程,每个进程都有自己的内存空间和系统资源
-
进程是操作系统分配资源的基本单位
-
-
线程
-
也叫做轻量级进程,一个进程中有多个线程,这些线程共享一个进程中的资源
-
线程是CPU调度的基本单位
-
-
管程
-
Monitor(监视器),锁,是一种同步机制,保证同一时间只有一个线程可以访问同步的代码
-
Monitor对象和Java对象同一时间创建并销毁,底层由C++实现
-
用户线程和守护线程
-
用户线程:系统的工作线程,完成程序需要完成的业务
-
守护线程:为用户线程服务的后台线程,若用户线程全部结束,就只剩下守护线程,守护线程没有服务的对象,守护线程随着JVM结束运行s
-
isDaemon():判断是否为守护线程
-
setDaemon(true/false):true表示设置为守护线程,false表示设置为用户线程;但是需要在start()之前设置
线程的五种状态
-
新建
-
当使用new创建一个线程但没调用start()时,此时的状态是新建状态
-
-
就绪
-
调用start()后,线程就进入就绪状态,但此时不会立即执行run(),需要等待获取CPU资源(线程运行的顺序可能和创建的顺序不一致)
-
-
运行
-
当线程获取到CPU时间片后,就会进入运行状态,开始执行run()
-
yield():释放当前线程CPU执行权,也有可能下一次此下安城又抢到CPU执行权
-
-
阻塞
-
当遇到以下情况,线程就会进入阻塞状态
-
调用sleep(),使线程睡眠
-
调用wait(),使线程等待
-
当线程去获取同步锁,锁正在被其他线程持有
-
调用阻塞式IO方法
-
调用suspend(),挂起线程
-
join():线程A中调用线程B的join(),此时线程A就进入阻塞状态,直到线程B执行完后,线程A才结束阻塞状态
-
-
-
死亡
-
当run()正常执行结束或抛出异常都会使线程进入死亡状态
-
stop():强制结束线程
-
线程优先级
-
线程分为10个优先级,1-10表示,1为最低优先级,5为默认优先级
-
优先级越高并不意味着越先执行,只是线程获取CPU时间片的次数越多
run()和start()
-
创建线程重写run(),启动线程用start()
-
当start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行,但并不意味着线程就会立即运行;当cpu分配给它时间时,才开始执行run()方法(如果有的话)
-
start()调用run(),run()被重写,run()中包含的是线程的主体
volatile和synchronized
-
volatile是线程同步的轻量级实现,volatile性能比synchronized好;volatile只能修饰变量,synchronized能修饰方法,代码块
-
多线程访问volatile不会阻塞,而synchronized会阻塞
-
volatile能保证数据的可见性,但是不能保证原子性;synchronized既可以保证原子性,也可以保证可见性
-
volatile解决的是变量在多个线程之间的可见性,synchronized解决的是多线程访问公共资源的同步性
2、Lock接口
synchronized
-
synchronized是自动获得锁和自动解锁
-
synchronized实现同步,Java中每个对象都可以作为锁
-
对于普通方法,锁是当前实例
-
对于静态方法,锁是当前类的Class对象
-
对于同步代码块,锁是synchronized括号配置的对象
-
-
synchronized本身是非公平锁
-
使用this.wait()进行等待
this.wait();
-
synchronized可重入锁
Object o = new Object(); new Thread(() -> { synchronized (o) { System.out.println(Thread.currentThread().getName() + " 外层"); synchronized (o){ System.out.println(Thread.currentThread().getName() + " 中层"); synchronized (o){ System.out.println(Thread.currentThread().getName() + " 内层"); } } } },"线程A").start();
-
常用方法:
-
this.wait():把当前线程挂起
-
this.notify():唤醒阻塞的线程
-
this.notifyAll:唤醒其他所有的阻塞的线程
-
Lock
-
lock需要手动加锁和手动解锁,一般使用Lock的实现类ReentrantLock(可重入锁)创建锁
-
可重入锁:某个线程已经获得某个锁,可以再次获取相同的锁而不会出现死锁
-
-
ReentrantLock()里面参数可填true或false,默认false
-
true表示公平锁,使得每个线程都会运行,但是效率低
-
false表示非公平锁,随机分配,效率高,但是会出现线程饿死的情况
-
private final Lock lock = new ReentrantLock();
-
使用lock的newCondition()创建Condition对象,再进行condition.await()进行等待
private final Condition condition = lock.newCondition(); condition.await();
-
lock可重入锁
ReentrantLock lock = new ReentrantLock(); new Thread(() -> { try { lock.lock();//加锁 System.out.println(Thread.currentThread().getName() + " 外层"); try { lock.lock();//加锁 System.out.println(Thread.currentThread().getName() + " 内层"); }finally { lock.unlock();//解锁 } }finally { lock.unlock();//解锁 } },"线程1").start();
-
常用方法:
-
condition.await():把当前线程挂起
-
condition.signal():唤醒阻塞的线程
-
虚假唤醒
-
使用this.wait()或者condition.await()进行等待;当被唤醒时,由于等待是在哪里睡,就在哪里醒,因此在等待时的条件不能是if,应是while,此时会一直判断
while (number != 0) { this.wait(); }
while (number != 0) { condition.await(); }
3、集合不安全问题
ArrayList线程不安全问题
-
使用Vector
List<String> list = new Vector<>();
-
使用Collections工具类的synchronizedList()
List<String> list = Collections.synchronizedList(new ArrayList<>());
-
使用CopyOnWriteArrayList(写时复制技术)
-
写时复制技术
-
当有多个线程请求同一资源时,他们会共同获取相同的引用指向相同的资源,若某个线程试图修改内容时,系统会复制一份副本给该线程,该线程修改完成后,将副本和原来的资源进行合并
-
-
List<String> list = new CopyOnWriteArrayList<>();
HashSet线程不安全问题
-
使用CopyOnWriteArraySet(写时复制技术)
Set<String> set = new CopyOnWriteArraySet<>();
HashMap线程不安全问题
-
使用ConcurrentHashMap(底层使用了volatile保证数据安全性)
Map<String,String> map = new ConcurrentHashMap<>();
4、线程池
ExecutorService
-
真正的线程池接口。常见子类ThreadPoolExecutor
-
void execute(Runnable command):执行任务,无返回值,一般用来执行Runnable
-
<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
-
void shutdown():关闭连接池
-
Exectors
-
工具类、线程池的工厂类,用于创建并返回不同类型的线程池
-
newCachedThreadPool():可缓存的线程池
-
newFixedThreadPool(n):固定线程数量的线程池
-
newScheduledThreadPool(n):可定时调度的线程池
-
newSingleThreadExecutor():单个线程的线程池
-
newWorkStealingPool():任务窃取的线程池
-
ThreadPoolExecutor
-
自定义线程池:创建ThreadPoolExecutor()的七个参数
-
int corePoolSize:常驻线程数量
-
int maximumPoolSize:最大线程数量
-
long keepAliveTime:线程存活时间
-
TimeUnit unit:线程存活时间的单位
-
BlockingQueue<Runnable> workQueue:阻塞队列
-
ThreadFactory threadFactory:线程工厂,用于创建线程
-
RejectedExecutionHandler handler:拒绝策略
-
ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, 5, 2L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); try { for (int i = 1; i <= 10; i++) { executor.execute(() -> { System.out.println(Thread.currentThread().getName() + "线程执行了"); }); } }catch (Exception e){ e.printStackTrace(); }finally { executor.shutdown(); }
阻塞队列
-
在队列的头部有一个线程,负责把数据添加到队列中;若队列满,再添加数据时队列就处于阻塞状态
-
在队列的尾部有一个线程,负责把数据从队列中取出来;若队列空,再取出数据时队列就处于阻塞状态
5、常用辅助类
CountDownLatch
-
减少计数:每个线程执行一次就减少一个数量级,减少到0的时候再继续执行之后的代码
CountDownLatch countDownLatch = new CountDownLatch(5);//设置初始值 for (int i = 1; i <= 5; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 号线程执行了"); countDownLatch.countDown();//计数减1 }, String.valueOf(i)).start(); } countDownLatch.await();//值没有变为0的时候就一直等待,值为0的时候才执行下面的代码 System.out.println(Thread.currentThread().getName() + " 所有线程执行完毕");
CyclicBarrier
-
循环栅栏,和减少计数相反,当线程执行的个数达到设置的个数时,再执行之后的代码
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> { System.out.println("所有线程执行完毕"); }); for (int i = 1; i <= 7; i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + "号线程执行了"); cyclicBarrier.await();//等待,当七个线程执行完成后,才会执行CyclicBarrier里的代码 } catch (Exception e) { e.printStackTrace(); } },String.valueOf(i)).start(); }
Semaphore
-
信号量:多个线程进入有限个数的资源(例如:6辆车停到3个停车位),可以用作分布式限流
Semaphore semaphore = new Semaphore(3);//许可数量 for (int i = 1; i <= 6; i++) { new Thread(() -> { try { semaphore.acquire();//抢到车位 System.out.println(Thread.currentThread().getName() + " 抢到车位"); TimeUnit.SECONDS.sleep(new Random().nextInt(5));//设置随机停车时间(5s内) System.out.println(Thread.currentThread().getName() + " ---离开停车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release();//释放停车位 } },String.valueOf(i)).start(); }
-
acquire():若存在可用资源,会在acquire()处返回true,Semaphore资源个数减1;若没有可用资源,线程会阻塞在acquire()内
-
release():释放资源,Semaphore资源个数加1,若acquire()中存在等待的线程,则等待的线程会被唤醒,从acquire()方法返回
6、Callable
-
Callable和Runnable的区别:Callable有返回值,Runnable没有返回值
-
创建线程时,new Thread()里需要传入Runnable,但不支持Callable,而FutureTask实现了Runnable接口,在new Future()就可以传入Callable
FutureTask<Integer> futureTask = new FutureTask<>(() -> { System.out.println(Thread.currentThread().getName()); return 1111; }); new Thread(futureTask,"线程A").start(); System.out.println("线程A返回值:" + futureTask.get());//可获取Callable返回值
-
FutureTask(未来任务),异步并行执行
-
主线程让子线程去完成一个任务,启动一个异步的子线程,子线程执行完成后,主线程可获取子线程执行完成的结果
-
-
线程池 + Future异步多线程任务配合
//优点:线程异步执行,显著提高程序的执行效率 ExecutorService pool = Executors.newFixedThreadPool(3);//创建线程池,并设置最大连接数 FutureTask<String> task1 = new FutureTask<>(() -> { TimeUnit.MILLISECONDS.sleep(200); return "task1 over"; }); pool.submit(task1);//提交Runnable任务以执行并返回代表该任务的Future FutureTask<String> task2 = new FutureTask<>(() -> { TimeUnit.MILLISECONDS.sleep(200); return "task2 over"; }); pool.submit(task2);//提交Runnable任务以执行并返回代表该任务的Future TimeUnit.MILLISECONDS.sleep(200);//主线程 pool.shutdown();
//缺点:get()容易造成阻塞,因此一般放在最后 //get()方法造成阻塞解决:设置最大登录时间,若超过设置的时间,则抛TimeoutException FutureTask<String> task = new FutureTask<>(() -> { TimeUnit.SECONDS.sleep(3); return "over"; }); new Thread(task,"线程A").start(); //System.out.println(task.get());//只有得到结果后才继续执行之后的代码,容易造成阻塞,一般放在最后 System.out.println(task.get(1,TimeUnit.SECONDS));//设置最大等待时间,若超过设置的时间,则抛TimeoutException System.out.println(Thread.currentThread().getName() + "线程");
7、CompletableFuture
Future
-
Future接口中定义了操作异步任务执行的方法,比如获取异步任务的结果、取消任务的执行、判断任务是否取消、判断任务执行是否完毕。例:主线程让一个子线程去执行任务,子线程可能比较耗时,可通过Future把这个任务放入子线程中执行,主线程去执行其他任务,过一会才去获取子任务的执行结果或变更的任务状态
-
Future接口可为主线程开一个分支任务,专门为主线程处理耗时和费力的复杂业务
-
异步多线程任务三个特点:多线程、有返回值、异步任务
-
优点:future+线程池异步多线程任务配合,能显著提高程序执行效率
-
缺点:get()方法会一直等到返回结果才继续往下执行,容易导致阻塞
-
解决方案1:把get()放在程序后面,但是也要等待很长时间
-
解决方案2:在get()中指定最大等待时间,但是会抛出超时异常
-
解决方案3:使用isDone轮询判断是否执行完毕,执行完成后再调用get(),但是会耗费更多CPU资源
-
Calable创建线程
-
Callable接口和Runnable接口创建线程的方式有点类似,也是需要通过Thread类来创建线程。由于Thread类的构造函数中没有Callable接口,使用FutureTask类来作为连接创建线程
-
FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable接口和Future接口,并且FutureTask中有一个构造函数可传入Callable对象,因此Callable创建的是有返回值的异步线程
-
FutureTask中的get()方法来获取Callable接口中的call()方法的返回值
借助FutureTask创建Callable异步线程:
FutureTask<Integer> futureTask = new FutureTask<>(() -> { System.out.println(Thread.currentThread().getName() + "执行"); return 1111; }); new Thread(futureTask,"线程1").start(); System.out.println("线程1返回值:" + futureTask.get());
CompletableFuture
-
CompletableFuture实现了Future接口和CompletionStage接口,CompletionStage代表异步计算过程中的某个阶段,一个阶段完成后可能会触发另一个阶段
-
CompletableFuture提供强大的Future的扩展功能,可简化异步编程的复杂性,提供了函数式编程的能力,可通过回调的方式处理计算结果,还提供了转换和组合CompletableFuture的方法
-
CompletableFuture可代表一个明确完成的Future,也可代表一个完成阶段CompletionStage,支持在计算完成后触发一些函数或执行某些动作
-
get()在Future计算完成后会一直处于阻塞状态,isDone()又耗费CPU资源,对于真正的异步处理希望的是可通过传入回调函数,在Future结束时自动调用该回调函数,这样就不用等待结果
-
CompletableFuture提供了一种类似观察者模式的机制,可以让任务完成后通知监听的一方
-
多个任务前后依赖组合处理
-
多个异步任务的计算结果组合起来,后一个异步任务的计算结果需要前一个异步任务的值
-
多个异步计算合成一个异步计算,这几个异步计算相互独立,同时后面的又依赖前一个处理的结果
-
//异步调用,没有返回值 CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> { System.out.println(Thread.currentThread().getName() + "future1"); }); future1.get(); System.out.println(); //异步调用,有返回值 CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> { System.out.println(Thread.currentThread().getName() + "future2"); return 1111; }); future2.whenComplete((t,u) -> {//当完成任务时执行 System.out.println("t:" + t + ",u:" + u);//t为方法返回值,u为异常 }).get();
-
CompletableFuture的优点:
-
异步任务结束时,自动回调某个对象的方法
-
主线程设置好后回调后,不再关心异步任务的执行,异步任务之间可顺序执行
-
异步任务出错时,会自动调用某个对象的方法
-
CompletableFuture高级编程
函数式接口名称 | 方法名称 | 参数 | 返回值 |
---|---|---|---|
Runnable | run | 无参数 | 无返回值 |
Function | apply | 1个参数 | 有返回值 |
Consume | accept | 1个参数 | 无返回值 |
Supplier | get | 无参数 | 有返回值 |
BiConsumer | accept | 2个参数 | 无返回值 |
-
在用于封装数据的类上加上 @Accessors(chain = true) 就可用链编程式设置值
student.setId(1).setName("Tom").setMajor("English");
-
CompletableFuture中的join()和get()功能相同,区别是get()需要处理异常,join()不需要处理异常
CompletableFuture常用方法
-
get(),等到返回结果才继续往下执行,容易导致阻塞
-
get(long timeout, TimeUnit unit),指定最大等待时间。若在指定时间内得到返回的结果则继续往下执行,若在指定时间内没有得到返回结果就抛出TimeoutException
-
join(),和get()功能类似,也会导致阻塞,但是join()不需要处理异常
-
getNow(T valueIfAbsent),若没有完成计算,返回设定的值;若完成计算,返回计算完的结果。此方法不会导致阻塞
-
complete(T value),若没有完成计算,返回true和设定的值;若完成计算,返回false和计算完的结果。此方法不会导致阻塞
-
-
thenApply(Function f),上一步的计算结果可以传递给下一步,有返回结果,遇到异常就退出
-
handle(Function f,Execption e),上一步的计算结果可以传递给下一步,遇到异常可以携带异常继续往下执行
-
thenAccept(Consumer c),接收上一步的计算结果并处理,无返回结果
-
thenRun(Runnable r),不能接收上一步的结果,只处理自己的逻辑
-
applyToEither(),对比两个异步线程谁先完成
-
thenCombine(),将连个异步任务的计算结果合并
CompletableFuture的线程池选择
-
如果不传入自定义线程池,都使用默认的线程池ForkJoinPool
-
如果传入自定义线程池
-
若执行第一个任务传入的是自定义线程池
-
若调用thenRun、thenApply、thenAccept方法,其他没有传入自定义线程池的也使用的自定义线程池
-
若调用thenRunAsync、thenApplyAsync、thenAcceptAsync方法,其他没有传入自定义线程池的使用的是默认的线程池ForkJoinPool
-
-
8、锁
悲观锁和乐观锁
-
悲观锁
-
当一个线程使用数据时会加锁
-
synchronized和Lock的实现类都是悲观锁
-
悲观锁适用于写操作多的场景,先加锁可确保写操作时数据正确
-
-
乐观锁
-
当一个线程使用数据时不会加锁,而是更新数据的时候判断数据是否被修改过。判断规则是使用版本号机制或者CAS算法(Java原子类中的递增操作就是通过CAS自旋实现)
-
乐观锁适用于读操作多的场景,不加锁能使读操作的性能大幅提升
-
synchronized锁
-
synchronized是一种悲观锁
-
一个资源类若创建一个对象,此时只有一把对象锁,如果有多个synchronized方法,那么某一个时刻内,只要一个线程去调用其中的一个synchronized方法,其他的线程只能等待;synchronized锁的是当前对象this,而不是synchronized声明的方法;被锁定后,其他线程不能进入到当前对象的其他synchronized方法;所有的普通同步方法用的都是同一把锁this
-
一个资源类若创建多个对象,此时有多个对象锁,不会涉及线程同步
-
使用static synchronized修饰的方法,此时只有一个类锁,锁的是当前类的Class对象,不管创建多少对象还是只有一个锁
-
一个线程试图访问同步代码块时首先必须获得锁,正常退出或抛出异常时必须释放锁
-
为什么任何一个对象都可以成为一个锁?
-
synchornized底层使用C++实现,每个对象都带着一个对象监视器,每个被锁住的对象都会和Monitor关联起来
-
-
synchronized锁的原理
-
synchronized同步代码块底层使用monitorenert和monitorexit实现加锁和解锁
-
synchronized普通同步方法有一个标识ACC_SYNCHRONIZED,每次调用时检查方法的标识,若设置了,执行线程会持有monitor锁,再执行方法,执行完后释放monitor
-
synchronized静态同步方法有ACC_STATIC和ACC_SYNCHRONIZED两个标识,以此来区分是否为静态同步方法
-
公平锁和非公平锁
-
使用ReentrantLock()创建锁的时候传入true或fasle,标识公平锁或非公平锁,默认非公平锁
-
公平锁,使得每个线程都会运行,但是效率低
-
非公平锁,随机分配,效率高,但是会出现线程饿死的情况
-
为什么默认是非公平锁?
-
非公平锁能更充分利用CPU时间片,减少CPU空闲状态
-
线程切换需要很大的开销
-
可重入锁
-
可重入锁又叫递归锁,指在同一个线程外层方法获取锁的时候,再进入该线程的内层方法会自动获取和锁(内层锁对象和外层锁对象是同一个),不会因为之前已获取过还没释放而阻塞
-
ReentrantLock和synchornized都是可重入锁,可重入锁可以一定程度上避免死锁
-
synchronized是隐式锁,在synchronized修饰的方法或代码块的内部调用本类其他synchronized修饰方法或代码块时,是可以获取到锁的
-
ReentrantLock是显示锁,需要手动改加锁和释放锁;在单线程下加锁的次数和解锁的次数可以不相同,但是在多线程下加锁几次必须要解锁几次
-
-
可重入锁的原理
-
每个锁对象都有一个锁计数器和一个指向持有该锁的线程的指针
-
执行monitorenert时,若目标锁对象的计数器为0,说明它没有被其他线程所持有,JVM将该锁对象的持有线程设置为当前线程,并将计数器加1
-
在目标锁对象计数器不为0情况下,若锁对象的持有线程是当前线程,JVM将其计数器加1,否则就等待持有的线程释放锁
-
当执行monitorexit时,JVM将锁对象计数器减1,计数器为0时表示锁已被释放
-
死锁
-
两个线程执行的时候各自持有自己的锁,并且试图获取对方的锁,两个线程都处于等待对方释放锁的情况,造成死锁
-
产生死锁的原因(必须同时满足4种条件才会产生死锁)
-
互斥:线程本就是互斥,不可避免;
-
占用且等待:一次申请所有资源可避免
-
不可抢占:使占用的这个线程区申请其他资源的时候,若申请不到就主动释放目前占有的资源
-
循环等待:按顺序申请资源进行预防
-
-
死锁的排查
-
命令:① jps -l ,② jstack 进程id
-
图形化界面:jconsole
-
读写锁
-
写锁(独占锁),读锁(共享锁)
-
一个资源可以被多个读线程访问,或可以被一个写线程访问
-
不能同时存在读写线程,读和读共享、读和写互斥、写和写互斥
锁的区别
-
无锁:多线程前方多资源,数据不一致
-
独占锁synchronized和ReentrantLock
-
写和写、读和写、读和读都是独占的,同一时刻只能有一个线程访问
-
读和读不能共享,效率低
-
-
读写锁ReentrantReadWriteLock
-
写和写、写和读是独占的,读和读可以共享,提高性能
-
缺点:①会造成锁饥饿;②写的时候可以读,但读的时候不能写
-
读写锁降级:把写锁降级为读锁
-
过程:①获取写锁;②获取读锁;③释放写锁;④释放读锁
-
-
9、线程中断和LockSupport
中断机制
-
由来
-
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自行停止,Thread类中的stop、suspend、resume都被废弃
-
没有办法立即停止一个线程,然而停止线程显得很重要,因此提供了一种用于停止线程的协商机制-中断,也称中断标识协商机制
-
-
特点
-
中断只是一种协商机制,没有提供任何语法,中断的过程完全是需要自己实现
-
若要中断一个线程,就需要调用interrupt(),而该方法仅仅是将线程对象的中断标识设置为true;然后需要自己实现不断地监测当前线程的标识位,若为true,表示别的线程请求这个线程中断,此时做什么需要自己实现
-
每个线程对象都有一个中断标识位,用于表示线程是否被中断,true表示中断,false表示未中断;通过调用线程对象的interrupt()将该线程的标识位设置为true,可以在别的线程中调用,也可以在本线程中调用
-
-
中断机制三个方法
-
interrupt():实例方法,仅设置线程中的中断状态为true,发起一个协商而不会立刻停止线程
-
对一个线程调用interrupt()时:
-
若线程处于正常运行状态,只会将该线程的中断标志位设置为true,被设置中断标志位的线程将继续正常运行,不受影响。interrupt()
-
若线程处于被阻塞的状态(sleep、wait、join),在别的线程中调用当前线程对象的interrupt(),那么线程将立即退出被阻塞的状态,此线程的中断状态将被清除,并抛出InterruptedException异常
-
若线程已经运行完成,设置中断标志位不会起作用,因为interrupt()只会对运行中的线程生效
-
-
-
isInterrupted():实例方法,通过检查中断标志位判断当前线程是否被中断,返回值为布尔类型
-
interrupted():静态方法,判断线程是否被中断(返回值为布尔类型),然后清除当前中断状态;若连续两次调用此方法,则第二次调用返回false,因为连续调用两次的结果可能不一样
-
返回当前线程的中断状态,判断当前线程是否被中断
-
将当前线程的中断状态清除,重新设置为false
-
-
-
中断线程的三种实现
-
通过volatile实现
-
通过AtomicBoolean实现
-
通过Thread类自带的中断api实例方法实现
-
LockSupport
-
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,所有方法都是静态方法,可以让线程在任意位置阻塞,LockSupport和每个使用它的线程都有一个许可关联
-
线程阻塞需要消耗许可,这个许可最多一个
-
线程阻塞和唤醒的三种方式:
-
Object类中的wait()和notify()/notifyAll()
-
若先唤醒后阻塞,就会一直阻塞
-
-
Condition中的await()和signal()
-
若先唤醒后阻塞,就会一直阻塞
-
-
LockSupport中的park()和unpark()
-
若先唤醒后阻塞,不会阻塞,原因:先调用unpark()会给线程一个许可,之后调用park()时,由于有这个许可,就可以消费这个许可,正常退出
-
-
-
LockSupport使用一种许可来实现阻塞和唤醒,每个线程最多只能有一个许可
-
park()
-
许可默认没有不能执行,一开始调用park(),当前线程会被阻塞,直到别的线程给当前线程给一个许可,park()才会被唤醒
-
调用park()时若有许可,就会消耗这个许可然后正常退出;若无许可,就必须阻塞等待别人发放许可
-
-
unpark()
-
调用unpark()时,会给一个许可给当前线程,自动唤醒使用park()阻塞的线程,之前阻塞中的park()方法会立即返回
-
调用unpark()时,会增加一个许可,但许可最多之能有一个,累加无效;同时调用多次unpark()只会给线程一个许可,若再调用park()两次或两次以上,由于许可消费完了,就会阻塞,等待别人发放许可
-
-
10、JMM
Java内存模型
-
JVM规范中定义的一种Java内存模型(java memory model,java内存模型),用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各个平台都能达到性能一致的内存访问效果
-
JMM本身是一种抽象的概念,并不真实存在,仅仅描述的是一组规范,通过这组规范定义了各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,都是围绕多线程的原子性、可见性和有序性展开的
JMM三大特性
-
可见性
-
当一个线程修改了某个共享变量的值,其他线程是否能够立即知道变更,JMM规定了所有的变量都存储在主内存中
-
系统主内存共享变量数据修改被写入的时机时不确定的,多线程并发下很可能出现脏读,所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量主内存副本拷贝,线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接修改主内存中的变量。不同线程之间也无法直接访问对方的工作内存中的变量,线程间变量值的传递需要通过主内存来完成
-
-
原子性
-
一个操作是不可被打断的,多线程环境下,操作不能被其他线程干扰
-
-
有序性
-
对于一个线程执行而言,代码都是从上往下有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。只要程序的最终结果于它的顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序
-
指令重排可以保证穿行语义一致,但是不能保证多线程间的语义也一致。即两行以上不相干的代码在执行的时候有可能先执行的不是第一条,代码的执行顺序会被优化
-
单线程下,确保程序最终执行结果和代码顺序执行的结果一致,处理器在进行重排序时必须要考虑指令间的数据依赖性
-
多线程下,由于编译器优化重排的存在,多个线程中使用的变量能否保证一致性是无法确定的
-
多线程对变量的读写过程
-
定义的所有变量都存储在物理主内存中
-
每个线程都有自己独立的工作内存, 里面保存该线程使用到的变量的副本
-
线程对共享变量所有的操作都必须先在本线程中的工作内存中操作,最后写回主内存,不能直接从主内存中进行写操作
-
不同线程间无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存进行
先行发生原则happens-before
-
在JMM中,如果一个操作执行的结果需要对另一个操作可见性或代码重排序,那么这两个操作间必须存在先行发生原则(逻辑上的先后关系)
-
先行发生原则可判断数据是否存在竞争,线程是否安全的有用手段。因为有了先行发生原则,在高并发情况下,并不需要处处都使用volatile或synchronized保证线程安全
-
先行发生原则总原则:
-
若一个操作happens-before另一个操作,则第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
-
两个操作之间存在happens-before关系,并不一定按照happens-before原则指定的顺序来执行,若重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序也是合法的
-
-
先行发生之8条原则:
-
次序规则
-
单线程中,按照代码的执行顺序,写在前面的操作先行发生于写在后面的操作
-
-
锁定规则
-
一个unlock操作先行发生于后面(时间上的先后)对同一个锁的lock操作
-
-
volatile变量规则
-
对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面(时间上的先后)的读是可见的
-
-
传递规则
-
若操作A线性发生于操作B,而操作B又先行发生于操作C,则可以推出操作A线性发生于操作C
-
-
线程启动规则
-
Thread对象的start()方法先行发生于此线程的每一个动作
-
-
线程中断规则
-
对线程interrupt()的调用先行发生于被中断线程的代码监测到中断事件的发生,可通过Thread.interrupt()监测是否中断;即要先调用interrupt()设置中断标志位,之后才能监测到中断发送
-
-
线程终止规则
-
线程中所有的操作都先行发生于对此线程的终止监测,可通过isAlive()监测线程是否已终止运行
-
-
对象终结规则
-
一个对象的初始化先行发生于它执行finalize()之前,即一个对象将要被回收之前这个对象一定被初始化过
-
-
11、volatile
特性
-
volatile只满足JMM中的可见性和有序性,并不满足原子性,volatile在有序性中还可禁止指令重排
-
volatile内存语义
-
当写一个volatile变量时,JMM会把改线程对应的本地内存中的共享变量值立即刷新回主内存中
-
当读一个volatile变量时,JMM会把改线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量
-
因此volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
-
-
volatile可见性
-
写操作的话,线程私有内存将最新值刷新到主内存中
-
读操作的话:线程私有内存中的数据失效,会读取到主内存中最新的值
-
-
volatile有序性(禁重排)
-
编译器和处理器为了优化程序性能对指令重新排序;不存在数据依赖关系,可以重新排序;存在数据依赖关系,禁止重排序
-
禁重排
-
当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。保证volatile读之后的操作不会重排到volatile读之前
-
当第一个操作为volatile写时,不论第一个操作是什么,都不能重排序。保证volatile写之前的操作不会重排到volatile写之后
-
当第一个操作为volatile写时,第二个操作是volatile读时,不能重排序。
-
-
-
volatile没有原子性
-
JVM只保证从主内存加载到线程工作内存的值是最新的,仅是数据加载时是最新的
-
在多线程环境下,先加载该变量,再进行计算,计算完后但并未赋值,此时可能另一个线程读取并提交了该变量;各线程私有内存和主内存公共内存中变量不同步,导致数据不一致(有点类似乐观锁)
-
若要保证原子性,需要加锁(synchronized或lock)
-
volatile变量不适合参与到依赖当前值的运算(i++),通常volatile用来保存boolean值和int值
-
-
总结:
-
volatile写之前的操作,都禁止重排序到volatile之后
-
volatile读之后的操作,都禁止重排序到volatile之前
-
volatile写之后volatile读,禁止重排序
-
内存屏障
-
内存屏障也称内存栅栏,是一种同步屏障指令,是CPU或编译器对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可开始执行此点之后的操作,阻止两边的指令重排序
-
内存屏障是一种JVM指令,JMM的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些指令,volatile实现了JMM中的可见性和有序性(禁重排),但volatile无法保证原子性
-
内存屏障之前的所有写操作都要回写到主内存,所有的读操作都能获得内存屏障之前的所有写操作的最新结果(可见性)
-
volatile禁重排的行为
-
在每个volatile写操作前面插入一个StoreStore屏障;可保证在volatile写之前,前面所有的普通写操作都已经刷新到主内存
-
在每个volatile写操作后面插入一个StoreLoad屏障;可避免volatile写之后可能有的volatile读/写操作重排序
-
在每个volatile读操作后面插入一个LoadLoad屏障;禁止上面的volatile读与下面的普通读重排序
-
在每个volatile读操作后面插入一个LoadStore屏障;禁止上面的volatile读与下面的普通写重排序
-
12、CAS
原子类
-
java.util.concurrent.atomic包下的类
有无CAS对比
-
没有CAS之前:多线程下不使用原子类保证线程安全i++,读变量的时候需要加上volatile,写的时候加synchronized
-
有CAS之后:多线程下使用原子类保证线程安全i++,读和写的时候都不需要使用volatile和synchronized,原理类似乐观锁
CAS介绍
-
compare and swap(compare and set),比较并交换(比较并设置),实现并发算法时的技术
-
CAS包含三个操作数:内存位置、预期原值、更新值
-
执行CAS操作的时候,将内存位置的值与预期原值比较:
-
若匹配,自动将该位置值更新为新值
-
若不匹配,不做处理,多个线程同时执行CAS操作只会有一个成功
-
-
CAS有3个操作数:线程私有内存中的值V,主内存中的旧值A,线程私有内存中修改后的新值B
-
当V和A相同时,将V修改为B,否则什么都不做或重来,重来的这种行为称为自旋(do-while循环)
-
-
CAS如何保证线程安全
-
CAS是非阻塞原子性操作,通过硬件保证比较并更新的原子性
-
CAS是一条CPU的原子指令(cmpxchg指令),不会造成数据不一致问题,UnSafe提供的CAS方法底层实现为CPU指令cmpxchg
-
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,若是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功后执行CAS操作,CAS原子性实际是CPU实现独占的,比synchronized的排他时间短很多,性能更好
-
UnSafe类
-
CAS的核心类,由于Java方法无法直接访问底层OS,需要通过本地native方法访问,此时可基于UnSafe类直接操作特定内存的数据
-
UnSafe类存在sun.misc包中,内部方法操作可以像C的指针直接操作内存
-
Java中CAS操作的执行依赖于UnSafe类的方法,JVM会实现出CAS汇编指令,是完全依赖硬件的,通过它实现原子操作
-
UnSafe类中所有方法都是native修饰的,UnSafe类中的方法都直接调用OS底层资源执行任务
-
变量valueOffset,表示该变量值在内存中的偏移地址,UnSafe是根据内存偏移地址获取数据的
-
变量value用volatile修饰,保证多线程之间的内存可见性
-
CAS功能是判断内存某个位置的值是否为预期值,若是就更改为最新值,此过程是原子的
-
原子类主要利用CAS + volatile + native方法保证原子操作,避免synchronized高开销
-
CAS是一种系统原语,属于OS用语范畴,由若干条指令构成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被打断,也就是CAS是一条CPU的原子指令,不会造成数据不一致问题
CAS缺点
-
若循环时间长,开销就大
-
会导致ABA问题
-
CAS算法实现一个重要的前提需要取出内内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。比如:线程1从内存位置V取出A,线程2也从内存V取出A,且线程2进行操作将A变成B,然后线程2又将内存V的数据变成A,此时线程1进行CAS操作发现内存V中仍然是A,操作成功。尽管线程1CAS操作成功,但不代表这个过程没有问题
-
13、原子操作类
基本类型原子类
-
AtomicInteger
-
AtomicBoolean
-
AtomicLong
数组类型原子类
-
AtomicIntegerArray
-
AtomicLongArray
-
AtomicReferenceArray
引用类型原子类
-
AtomicReference
-
AtomicStampedReference
-
携带版本号的引用类型原子类,可以解决ABA问题;通过版本号可判断修改过几次
-
-
AtomicMarkableReference
-
原子更新带有标记位的引用类型对象;只能判断是否被修改过(true/false)
-
对象的属性修改原子类
-
AtomicIntegerFieldUpdater
-
基于反射的实用程序,可对指定类的volatile int字段进行原子更新
-
-
AtomicLongFieldUpdater
-
基于反射的实用程序,可对指定类的volatile long字段进行原子更新
-
-
AtomicReferenceFieldUpdater
-
基于反射的实用程序,可对指定类的volatile引用字段进行原子更新
-
-
使用目的
-
以一种线程安全的方式操作非线程安全对象内的某些字段
-
-
使用要求
-
更新的对象属性必须使用public volatile修饰
-
因为对象的属性修改类型原子类都是抽象类,所以每次都要用静态方法newUpdater()创建一个更新器,并设置想要更新的类和属性
-
原子操作增强类
-
DoubleAccumulator
-
一个或多个变量共同维护使用提供的函数更新的运行double值
-
-
DoubleAdder
-
一个或多个变量共同维持最初的零和double总和
-
-
LongAccumulator
-
一个或多个变量共同维护使用提供的函数更新的运行long值
-
使用给定的累加器函数和标识元素创建新实例
-
-
LongAdder
-
一个或多个变量共同维持最初为零的总和为long
-
LongAdder是创建一个初始总和为0的新加法器
-
当多个线程更新用于收集统计信息但不用于细粒度同步控制的目的的公共和时,此类优于AtomicLong。在低更新争用下,这两个类具有相似特征。在高争用情况下,此类预期吞吐量更高,但代价是空间消耗更高
-
LongAdder的基本思路是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的值进行CAS,热点就被分散,冲突的概率小了很多。若要获取真正的long值,只需将各个槽中的变量值累加返回。
-
sum()会将所有Cell数组中的value和base累加作为返回值,核心思想是将之前AtomicLong的一个value更新压力分散到多个value中去,从而降级更新热点。value = base + Cell数组求和
-
总结:LongAdder在无竞争情况下和AtomicLong一样,对同一个base进行操作,当出现竞争关系时采用化整为零分散热点的做法,用空间换时间,用一个Cell数组,将value拆分进这个数组。多个线程需要同时对value进行操作的时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组的某个下标,再对该下标对应的值进行自增操作。当所有线程操作完毕,将数组所有值和base加起来作为最终结果
-
add()原理
public void add(long x) { //as是Striped64中的cells数组属性 //b是Striped64中的base属性 //v是当前线程hash到Cell中存储的值 //m是cells的长度减1,hash是作为掩码使用 //a是当前线程hash到Cell Cell[] as; long b, v; int m; Cell a; //首次线程((as = cells) != null)一定是false,此时走caseBase方法,以CAS的方式更新base值, 仅当只有CAS失败时,才会走到if中 //条件1:cells不为空 //条件2:CAS操作base失败,说明其他线程先一步修改了base正在竞争 if ((as = cells) != null || !casBase(b = base, b + x)) { //true表示无竞争,false表示竞争激烈,多个线程hash到同一个Cell,可能要扩容 boolean uncontended = true; //条件1:cells为空 //条件2:应该不会出现 //条件3:当前线程所在的Cell为空,说明当前线程还没更新过Cell,应初始化一个Cell //条件4:更新当前线程所在的Cell失败,说明竞争激烈,多个线程hash到同一个Cell,应扩容 if (as == null || (m = as.length - 1) < 0 || //getProbe()返回的是线程中的threadLocalRandomProbe字段 //它通过随机数生成的一个值,对于一个确定的线程这个值是固定的(除非刻意修改) (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) longAccumulate(x, null, uncontended);//调用Striped64中的方法处理 } }
-
如果Cells表为空,尝试用CAS更新base字段,成功则退出
-
如果Cells表为空,CAS更新base字段失败,出现竞争,uncontended为true,调用longAccumulate
-
如果Cells表非空,但当前线程映射的slot为空,uncontended为true,调用longAccumulate
-
如果Cells表非空,且当前线程映射的slot非空,CAS更新Cell的值,成功则返回;否则uncontended为false,调用longAccumulate
-
-
14、ThreadLocal
概念
-
实现每个线程都有自己专属的本地变量副本,解决让每个线程绑定自己的值,通过getter和setter,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题
常用方法
-
get()
-
返回当前线程的此线程局部变量副本中的值
-
-
initialValue()
-
返回此线程局部变量的当前线程的"初始值"。
-
该方法将被调用的第一次一个线程访问与可变get()方法,除非线程先前调用的set(T)方法,在这种情况下initialValue()将不被调用的线程。通常,每个线程最多调用一次此方法,但如果后续调用remove()后跟get(),则可以再次调用此方法。
-
这个实现类只返回null,若希望线程局部变量具有除null之外的初始值,ThreadLocal必须对ThreadLocal进行子类化,并且重写此方法。通常,将使用匿名内部类
-
-
remove()
-
删除此线程局部变量的当前线程值
-
-
set(T value)
-
将此线程局部变量的当前线程副本设置为指定值
-
-
withInitial(Supplier<? extends S> supplier)
-
创建一个线程局部变量
-
注意点
-
必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally块进行回收
ThreadLocal总结
-
每个Thread内有自己的实例副本且该副本只由当前线程自己使用,其他Thread不可访问,就不存在多线程间共享问题,统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
-
如何保证不争抢
-
使用synchronized或者Lock控制资源的访问顺序
-
使用ThreadLocal,每个线程一份,没有争抢
-
Thread、ThreadLocal和ThreadLocalMap
-
Thread类中
ThreadLocal.ThreadLocalMap threadLocals = null;
-
ThreadLocal类中
set(T value): void get(): T static class ThreadLocalMap { ... }
-
ThreadLocalMap类中
table: Entry[] sizen: int set(ThreadLocal key,Object value) getEntry(ThreadLocal key): Entry
-
ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry对象。当为ThreadLocal变量赋值,就是以当前ThreadLocal实例为key,值为value的Entry往这个ThreadLocalMap中存放
-
JVM内部维护了一个线程版的Map<ThreadLocal,Value>(通过ThreadLocal对象的set方法,把ThreadLocal作为key,放入ThreadLocalMap中),每个线程要使用这个Thread的时候,用当前线程去Map里获取,这样让每个线程都拥有自己独立的变量,没有竞争
ThreadLocal内存泄露
-
ThreadLocalMap是一个保存ThreadLocal对象的map,经过了两层包装的ThreadLocal对象
-
第一层包装是使用WeakReference<ThreadLocal<?>>将ThreadLocal对象变为一个弱引用的对象
-
第二层包装是定义了一个专门的类Entry来扩展WeakReference<ThreadLocal<?>>
-
-
强引用
-
不会被回收
-
-
软引用
-
内存不足即回收
-
-
弱引用
-
下一次GC即回收
-
ThreadLocalMap使用的弱引用的Entry,原因:
-
function01方法执行完后,栈帧销毁强引用ThreadLocal也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
-
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及value指向的对象不能被gc回收,造成内存泄漏
-
若这个key引用是弱引用就大概率减少内存泄露的问题
-
-
使用弱引用可以使ThreadLocal对象在方法执行完后顺利被回收且Entry引用指向为null
-
-
-
虚引用
-
随时都可能被回收
-
虚引用需要PhantomReference类实现,与其他几种引用不同,虚引用并不会决定对象的生命周期。若一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列联合使用
-
虚引用主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的通知机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象
-
设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作
-
-
在ThreadLocal生命周期里,当不再使用ThreadLocal里的数据时,需要调用remove()清理脏数据;针对ThreadLocal内存泄露的问题,都会通过expungeStaleEntry、cleanSomeSlots、replaceStaleEntry这三个方法清理掉key为null的脏Entry
15、对象内存布局和对象头
-
在HotSpot虚拟机里,对象在堆内存中的存储布局划分为三个部分
-
对象头(Header)
-
实例数据(Instance Data)
-
对齐填充(Padding)
-
对象头
-
对象标记
-
占用空间8个字节
-
默认存储对象的HashCode、分代年龄和锁标志位等信息
-
这些信息都是与对象自身定义无关的数据,因此Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据
-
它会根据对象的状态复用自己的存储空间,也就是在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化
-
-
类元信息/类型指针
-
占用空间8个字节
-
存储指向该对象类元数据的首地址
-
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
-
实例数据
-
存放类的属性数据信息,包括父类的属性信息
对齐填充
-
保证8个字节的倍数
-
填充的数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐
16、synchronized和锁升级
synchronized
-
高并发时,同步调用应该考量锁的性能损耗。能用无锁数据结构,就不用锁;能锁区块,就不锁整个方法体;能用对象锁,就不用类锁;尽可能使加锁的代码块工作量小,避免在锁代码块中调用RPC方法
-
用锁能实现数据的安全性,但是会带来性能下降;无锁能基于线程并行提升程序性能,但是带来安全性下降
-
synchronized用的锁存在对象头里的Mark Word中锁升级功能主要依赖Mark Word中锁标志位和释放偏向锁标志位
锁升级
-
锁升级的过程
-
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
-
-
锁指向
-
偏向锁:Mark Word存储的是偏向的线程ID
-
轻量锁:Mark Word存储的是指向线程栈中Lock Record的指针
-
重量锁:Mark Word存储的是指向堆中的monitor对象的指针
-
无锁
-
锁标志位为001
-
初始状态,一个对象被实例化后,若没有被任何线程竞争锁,那么它就为无锁状态
偏向锁
-
锁标志位为101
-
当线程A第一次竞争到锁时,通过修改Mark Word中的偏向线程ID,开启偏向模式;若不存在其他线程竞争,那么持有偏向锁的线程永远不需要同步
-
当一段同步代码一直被同一个线程多次访问,由于只有一个线程(此线程成为锁的偏向线程),那么该线程在后续访问时便会自动获得锁,减少了从用户态到内核态和从内核态到用户态的切换
-
偏向锁的出现就是为了解决只有在一个线程执行同步时提高性能
-
偏向锁会偏向第一个访问锁的线程,若之后该锁没有被其他线程访问,则持有偏向锁的线程将永远不需要触发同步。即偏向锁在资源没有竞争情况下消除了同步语句,连CAS就不需要做了,提高了程序性能
-
在锁第一次被拥有的时候,记录下偏向线程ID。偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的Mark Word里面是不是放的自己的线程ID)。
-
如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
-
如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID
-
竞争成功,表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁
-
竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。
-
-
-
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的
-
偏向锁默认线程执行4s后才开启,可设置-XX:BiasedLockingStartupDelay=0可让线程一启动就立刻开启偏向锁
-
当有另外的线程来逐步竞争锁的时候,就不能使用偏向锁,要升级为轻量级锁
-
偏向锁的撤销
-
偏向锁使用等到竞争出现才释放的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行
-
第一个线程正在执行synchronized方法(处于同步代码块),还没有执行完,其他线程来抢,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
-
第一个线程执行完synchronized(退出同步代码块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向
-
-
-
由于偏向锁开销大,JDK15废弃偏向锁
轻量锁
-
锁标志位为00
-
存在多线程竞争,但是任意时刻最多只有一个线程竞争,不存在锁竞争激烈的情况,也没有线程阻塞,本质就是自旋锁CAS
-
轻量锁是为了在线程近乎交替执行同步块时提高性能
-
在没有多线程竞争的前提下,通过CAS减少重量锁使用OS互斥产生的性能消耗。先自旋,不行了再升级阻塞
-
当关闭偏向锁或多线程竞争偏向锁导致偏向锁升级为轻量锁
-
若线程A获取到锁,此时线程B又来抢该对象的锁,由于该对象的锁被线程A拿到, 当前该锁已经是偏向锁了。而线程B再争抢时发现对象头Mark World中的线程ID不是线程Be而是线程A的ID,线程B就会进行CAS尝试获取锁
-
若锁获取成功,直接替换掉原来的Mark World中的线程ID,重新偏向其他线程,该锁会保持偏向锁状态
-
若锁获取失败,偏向锁升级为轻量锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量锁由原来持有偏向锁的线程持有,继续执行同步代码,而正在竞争的线程B进入自旋等待获得该轻量锁
-
-
-
自适应自旋锁
-
若线程自旋成功,则下次自旋的最大次数也增加;若很少自旋成功,则下次减少自旋的次数甚至不自旋,避免CPU空转
-
-
轻量锁和偏向锁区别
-
偏向锁只有一个线程,轻量锁有多个线程
-
轻量锁每次退出同步块都需要释放锁,偏向锁是在竞争发生时才释放锁
-
重量锁
-
锁标志位为10
-
有大量的线程参与锁的竞争,冲突性很高
-
重量锁基于Monitor实现的,monitor enter和monitor exit
哈希码
-
锁升级为轻量锁或重量锁后,Mark World保存的是线程栈帧里的锁记录指针和重量锁指针,已经没有保存哈希码和GC年龄了,这些信息移动到哪里去了?
-
在无锁状态下,Mark Word中可以存储对象的hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的hash code值并将该值存储到Mark Word中
-
对于偏向锁,在线程获取偏向锁时,会用Thread lD和epoch值覆盖hash code所在的位置
-
若一个对象的hashCode()方法已经被调用过一次之后,这个对象不能被设置偏向锁
-
若一个对象处于偏向锁并调用了hashCode(),则撤销偏向模式,升级为重量锁
-
-
升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含hash code,所以轻量级锁可以和hash code共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头
-
升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头
-
锁的优缺点
-
偏向锁
-
优点:加速和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距
-
缺点:若线程间存在锁竞争,会带来额外的锁撤销的消耗
-
-
使用场景:单线程环境下,在不存在锁竞争的时候进入同步代块使用偏向锁
-
-
轻量锁
-
优点:竞争的线程不会阻塞,提高程序响应速度
-
缺点:若始终得不到锁竞争的线程,使用自旋会消耗CPU
-
使用场景:竞争不激烈的情况,追求响应时间,同步块执行时间较短(类似乐观锁)
-
-
重量锁
-
优点:线程竞争不会自旋,不会消耗CPU
-
缺点:线程阻塞,响应时间慢
-
使用场景:竞争激烈的情况,追求吞吐量,同步块执行时间较长
-
锁消除
-
若局部对象作为锁对象,这个锁对象并没有被共用扩散到其他线程使用;根本没有加这个锁对象的底层机器码,消除了锁的使用
锁粗化
-
若方法中首尾相接,前后相邻的都是同一个锁对象,JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
17、AQS
介绍
-
AbstractQueuedSynchronizer(抽象的队列同步器)
-
用来实现锁或其他同步器组件的公共基础部分的抽象实现,主要用于解决锁分配给“谁”的问题
-
为实现阻塞锁和相关的同步器提供一个框架,它依赖于FIFO的队列(虚拟的双向队列),根据一个int值标识状态,通过占用和释放,改变状态值
-
-
CountDownLatch、CyclicBarrier、Semaphore、ReentrantLock和ReentrantReadWriteLock底层都使用的AQS
作用
-
加锁会导致阻塞,有阻塞就需要排队,实现排队就需要队列
-
抢到资源的线程就可处理业务,抢不到资源的需要使用一种排队机制,抢占资源失败的线程继续等待,但等候线程仍然保留获取锁的可能且获取锁的流程仍继续
-
若共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体(实际是单链表,但是一个虚拟的双向队列)实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果
18、读写锁和邮戳锁
锁演变
-
无锁 -> 独占锁 -> 读写锁 -> 邮戳锁
读写锁
-
一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程,ReentrantReadWriteLock是一种悲观锁
-
只有在读多写少的情况下,读写锁才具有较高性能的体现
锁降级
-
ReentrantReadWriteLock锁降级:将写锁降级为读锁,锁的严苛程度变强叫做升级,反之叫做降级
-
获取写锁 -> 获取读锁 -> 释放写锁,写锁能降级为读锁
-
若一个线程持有了写锁,在没有释放写锁的情况下,还可以继续获取读锁
-
当读锁被使用时,若有线程尝试获取写锁,该写线程会被阻塞。因此,需要先释放所有读锁,才可获取写锁
-
锁降级是为了让当前线程感知到数据变化,目的是保证数据可见性
StampedLock
-
邮戳锁的引出:锁饥饿问题,若当前1000个线程有99个读线程和1和写线程,有可能999个读线程长时间抢到锁,那1个写线程就造成锁饥饿。但也可使用公平锁解决锁饥饿问题,但是是以牺牲系统吞吐量为代价的
-
邮戳锁是一种乐观锁,其他线程尝试获取写锁时不会阻塞。对短的只读代码,使用乐观模式可减少竞争并提高吞吐量
-
邮戳锁特点
-
所有获取锁的方法,都返回一个邮戳Stamp,为0表示获取失败,其余表示成功
-
所有释放锁的方法,都需要一个邮戳Stamp,此Stamp必须和成功获取锁时得到的Stamp一致
-
邮戳锁是不可重入的,若一个线程持有了写锁,再去获取写锁会造成死锁
-
三种访问模式
-
Reading(悲观读模式):和ReentrantReadWriteLock的读锁类似
-
Writing(写模式):和ReentrantReadWriteLock的写锁类似
-
Optimistic reading(乐观读模式):无锁机制,类似乐观锁,支持读写并发,乐观认为读取时没有人修改,若被修改再升级为悲观读模式
-
-
-
邮戳锁缺点
-
不支持可重入
-
邮戳锁的悲观锁和写锁都不支持条件变量(Condition)
-
使用StampedLock不能调用interrupt()中断操作
-