目录
一、锁策略
(1)乐观锁&悲观锁
乐观锁:多个线程访问同⼀个共享变量冲突的概率不⼤,并不会真的加锁,⽽是直接尝试访问数据。在访问的同时识别当前的数据是否出现访问冲突,如果发现并发冲突了,则让返回⽤⼾错误的信息,让⽤⼾决定如何去做。
悲观锁:多个线程访问同⼀个共享变量冲突的概率较⼤,会在每次访问共享变量之前都去真正加锁。
(2)重量级锁&轻量级锁
重量级锁:加锁机制重度依赖了 OS 提供了 mutex,上锁的时间开销大,⼤量的内核态⽤⼾态切换 ,很容易引发线程的调度。
轻量级锁:加锁机制尽可能不使⽤ mutex,⽽是尽量在⽤⼾态代码完成,实在搞不定了,再使⽤ mutex。上锁时间开销小,少量的内核态⽤⼾态切换,不太容易引发线程调度。
(3)自旋锁&挂起等待锁
自旋锁:如果获取锁失败,⽴即再尝试获取锁,⽆限循环,直到获取到锁为⽌.优点是,不会放弃CPU,不涉及到线程的堵塞和调度,一旦锁被释放,第一时间能获取到,更高效。缺点是,如果被其他线程持有的时间较久,会一直消耗占用CPU资源。
挂起等待锁:获取锁失败之后,不立即尝试获取锁,等到下次一次操作提醒之后再尝试锁。
(4)公平锁&非公平锁
公平锁:遵守"先来后到,.B⽐C先来的,当A释放锁的之后,B就能先于C获取到锁。
⾮公平锁:不遵守"先来后到",B和C都有可能获取到锁。
(5)可重入锁&不可重入锁
可重入锁:字⾯意思是“可以重新进⼊的锁”,即允许同⼀个线程多次获取同⼀把锁。且JDK提供的所有现成的Lock实现类,包括 synchronized关键字锁都是可重⼊的。
不可重入锁:Java⾥只要以Reentrant开头命名的锁都是可重⼊锁。
(6)读写锁
多线程之间,数据的读取⽅之间不会产⽣线程安全问题,但数据的写⼊⽅互相之间以及和读者之间都 需要进⾏互斥。如果两种场景下都⽤同⼀个锁,就会产⽣极⼤的性能损耗。所以读写锁因此⽽产⽣。读写锁(readers-writerlock),看英⽂可以顾名思义,在执⾏加锁操作时需要额外表明读写意图,复 数读者之间并不互斥,⽽写者则要求与任何⼈互斥。
ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁。这个对象提供了lock/unlock⽅法进⾏加锁解锁,ReentrantReadWriteLock.WriteLock 类表⽰⼀个写锁,这个对象也提供了lock/unlock ⽅法进⾏加锁解锁。
<1>两个线程都只是读⼀个数据,此时并没有线程安全问题.直接并发的读取即可。
<2> 两个线程都要写⼀个数据,线程安全问题。
<3>⼀个线程读另外⼀个线程写,有线程安全问题。
读写锁就是把读操作和写操作分别进⾏加锁.,读锁和读锁之间不互斥。 写锁和写锁之间互斥, 写锁和读锁之间互斥.,读写锁最主要⽤在"频繁读,不频繁写"的场景中。
二、synchronized原理
(1)基本特点:
<1>开始的时候是乐观锁,如果锁冲突频繁就会转化为悲观锁。
<2>开始的时候是轻量级锁,如果锁被持有的时间较长,就转化成重量级锁。
<3>实现轻量级锁的时候大概率用到自旋锁策略。
<4>是一种不公平锁、可重入锁、不是读写锁。
(2)加锁工作过程:
JVM将synchronized锁分为,无锁、偏向锁、轻量级锁、重量级锁状态。会根据情况进行锁升级,且升级为不可逆过程。
偏向锁:不是真的加锁,⽽只是在锁的对象头中记录⼀个标记(记录该锁所属的线程).如果没有其他线程 参与竞争锁,那么就不会真正执⾏加锁操作,从⽽降低程序开销.⼀旦真的涉及到其他的线程竞争,再取 消偏向锁状态,进⼊轻量级锁状态
轻量级锁:随着其他线程进⼊竞争,偏向锁状态被消除,进⼊轻量级锁状态(⾃适应的⾃旋锁)。此处的轻量级锁就是通过CAS来实现,通过CAS检查并更新⼀块内存(⽐如null=>该线程引⽤), 如果更新成功,则认为加锁成功,如果更新失败,则认为锁被占⽤,继续⾃旋式的等待。⾃此处的⾃旋不会⼀直持续进⾏,⽽是达到⼀定的时间/重试次数,就不再⾃旋了。也就是所谓的"⾃适应" 。
重量级锁:如果竞争进⼀步激烈,⾃旋不能快速获取到锁状态,就会膨胀为重量级锁。此处的重量级锁就是指⽤到内核提供的mutex,执⾏加锁操作,先进⼊内核态。 在内核态判定当前锁是否已经被占⽤, 如果该锁没有占⽤,则加锁成功,并切换回⽤⼾态。 如果该锁被占⽤,则加锁失败。此时线程进⼊锁的等待队列,等待被操作系统唤醒。
(3)其他优化操作
<1>锁消除:有些应⽤程序的代码中,⽤到了 synchronized ,但其实没有在多线程环境下。(例如StringBuffer)
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个append的调⽤都会涉及加锁和解锁,但如果只是在单线程中执⾏这个代码,那么这些加锁解,锁操作是没有必要的,⽩⽩浪费了⼀些资源开销。
<2>锁粗化:一段逻辑中如果出现多次加锁解锁,编译器+JVM会⾃动进⾏锁的粗化。实际开发过程中,使⽤细粒度锁,是期望释放锁的时候其他线程能使⽤锁.。但是实际上可能并没有其他线程来抢占这个锁。这种情况JVM就会⾃动把锁粗化,避免频繁申请释放锁。
三、CAS
(1)CAS的概念:全称Compareandswap,字⾯意思:”⽐较并交换“。我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
<1>⽐较A与V是否相等。
<2>如果⽐较相等,将B写⼊V。(⽐较)
<3> 返回操作是否成功(交换)
(2)CAS的作用:
有些原子类利用底层硬件的 CAS(Compare-And-Swap)操作,标准库中提供了 java.util.concurrent.atomic 包,⾥⾯的类都是基于这种⽅式来实现的。 典型的就是AtomicInteger类,其中的getAndIncrement相当于i++操作。
<1>假设两个线程同时调⽤ getAndIncrement,两个线程都读取 value 的值到 oldValue中。(oldValue是⼀个局部变量,在栈上,每个线程有⾃⼰的栈)
<2>线程1先执⾏CAS操作。由于oldValue和value的值相同,直接对value赋值。CAS是直接读写内存的,⽽不是操作寄存器,CAS的读内存,⽐较,写内存操作是⼀条硬件指令,是原⼦的。
<3>线程2再执⾏CAS操作,第⼀次CAS的时候发现oldValue和value不相等,不能进⾏赋值。因此需要进⼊循环,在循环⾥重新读取value的值赋给oldValue。
<4>线程2接下来第⼆次执⾏CAS,此时oldValue和value相同,于是直接执⾏赋值操作。
(3)CAS的ABA问题
ABA的问题:假设存在两个线程 t1 和 t2。有⼀个共享变量 num,初始值为 A。接下来,线程 t1 想使⽤ CAS 把 num 值改成 Z,那么就需要先读取 num 的值,记录到 oldNum 变量中。使⽤ CAS 判定当前 num 的值是否为 A,如果为 A,就修改成 Z。 但是,在 t1 执⾏这两个操作之间,t2 线程可能把 num 的值从 A 改成了 B,⼜从 B 改成了 A。
解决方案:给要修改的值,引⼊版本号。在 CAS ⽐较数据当前值和旧值的同时,也要⽐较版本号是否符合预期。CAS 操作在读取旧值的同时,也要读取版本号。真正修改的时候,,如果当前版本号和读到的版本号相同,则修改数据,并把版本号 +1。如果当前版本号⾼于读到的版本号,就操作失败(认为数据已经被修改过了)。
四、定时器的自实现
定时器的构成:
• ⼀个带优先级队列(不使⽤PriorityBlockingQueue,容易死锁!)
• 队列中的每个元素是⼀个 Task 对象。
• Task中带有⼀个时间属性,队⾸元素就是即将要执⾏的任务。
• 同时有⼀个 worker 线程⼀直扫描队⾸元素,看队⾸元素是否需要执⾏。
1. Timer 类提供的核⼼接为 schedule,⽤于注册⼀个任务,并指定这个任务多⻓时间后执⾏。.
public class MyTimer {
public void schedule(Runnable command, long after) {
// TODO
}
}
2. Task 类⽤于描述⼀个任务(作为 Timer 的内部类)。⾥⾯包含⼀个 Runnable 对象和⼀个 time(毫秒时 间戳)。
class MyTask implements Comparable<MyTask> {
public Runnable runnable;
// 为了⽅便后续判定, 使⽤绝对的时间戳.
public long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
// 取当前时刻的时间戳 + delay, 作为该任务实际执⾏的时间戳
this.time = System.currentTimeMillis() + delay;
}
public void run(){
//执行任务
runnable.run();
}
public long getTime(){
//获取时间戳
return time;
}
@Override
public int compareTo(MyTask o) {
return (int)(this.time - o.time);
}
}
3. Timer 实例中,通过 PriorityQueue 来组织若⼲个 Task 对象。通过 schedule 来往队列中插⼊⼀个个 Task 对象。
class MyTimer {
// 核⼼结构
private PriorityQueue<MyTask> queue = new PriorityQueue<>();
// 创建⼀个锁对象
private static Object locker = new Object();
public void schedule(Runnable runnable,int time){
synchronized (loker){
TimerTask timerTask = new TimerTask(time,runnable);
queue.offer(timerTask);
loker.notify();
}
}
}
4. Timer类中存在⼀个 worker 线程,⼀直不停的扫描队⾸元素,看看是否能执⾏这个任务。
class MyTimer{//存任务,创建线程执行任务。
public static Object loker = new Object();
private PriorityQueue<MyTask> queue = new PriorityQueue<>();
public Timer(){
Thread thread = new Thread(()-> {
while (true) {
synchronized (loker) {
while (queue.isEmpty()) {//创建timer时没有submit任务
try {
loker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
TimerTask timerTask = queue.peek();
if (System.currentTimeMillis() >= timerTask.getTime()) {//gai jin
timerTask.run();//如果执行时间到了 就执行并且排出堆
queue.poll();
} else {
try {
loker.wait(timerTask.getTime() - System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
thread.start();
}
public void schedule(Runnable runnable,int time){
synchronized (loker){
TimerTask timerTask = new TimerTask(time,runnable);
queue.offer(timerTask);
loker.notify();
}
}
}
=========================================================================
最后如果感觉对你有帮助的话,不如给博主来个三连,博主会继续加油的ヾ(◍°∇°◍)ノ゙