定时器思考问题:
1.使用堵塞队列是否比优先级队列更安全?
1.使用优先级队列更好:使用阻塞队列需要考虑锁的问题,在阻塞队列中有锁,我们在写方法和初始化时也必须用到锁,所以我们自己封装一个优先级队列并且自己配置锁;使用阻塞队列需要考虑的因素更加复杂
2.使用wait()好:
2.1:从业务的角度看,使用sleep,interrupt方法是处理异常的方法,使用sleep是正常一般的方法
2.2:从锁的角度分析:sleep是拿着锁阻塞,wait会释放锁,如果抱着锁sleep就会导致线程安全
3.重写的2compareTo方法是否正确需要看程序运行时是否正确,我们使用下列代码以及运行结果来看是否正确!
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
}, 3000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
}, 2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1000");
}
}, 1000);
}
//如果先出现的是hello 1000,此时说明是正确的顺序
//如果先出现hello 3000,此时说明是错误的顺序
经过我们的实验,发现下列代码是正确的
@Override
public int compareTo(MyTimerTask o) {
// 按照时间来进行比较. 期望最终构造出的是小堆.
return (int) (this.time - o.time);
//return (int) (o.time - this.time);
}
4.使用时间轮的方法来实现定时器:定时器使用单线程扫描,如果该任务执行的时间过长,会导致定时的时间不准确
此时我们可以使用线程池的方式来优化,在JDK中使用ScheduleThreadPoolExecutor类来优化ThreadPoolExecutor
定时器用时间轮来实现,基本思路:类似我们生活中的钟表,设计一个表盘,在每个刻度上设置一段时间,把任务挂在刻度上,比如设置一个一分钟的表盘,有60个刻度,此时把任务挂在上面,设置一个线程指针不停转动,另外建立一个线程池来执行任务,
//简单的时间轮定义Code class TimerOfClock { private volatile int current = 0;//时间轮的指针 private Thread tick;//负责将current指针向前推动 private List<Task>[] slots;//放置任务的槽位 private ThreadPoolExecutor executor;//线程池来执行任务 } class Task { private int circle;//在时间轮的第几圈 private int interval;//任务调度周期 private Runnable runnable;//任务业务逻辑 private volatile boolean running;//执行任务标志位 }
常见的锁策略:
锁策略不仅适用于Java的多线程情况,而且也适用于其他语言
1.乐观锁&悲观锁:
1.乐观锁是预测到代码中的冲突概率比较小,从而消耗的资源(内存资源,时间资源)较少;
2.悲观锁相反,预测到代码中的冲突概率较大,从而消耗的资源(内存资源,时间资源)较多;
3.synchronized既是乐观锁也是悲观锁,synchronized有自适应机制:在代码运行时synchronized能识别出发生冲突的次数,从而预测出冲突大小的概率,如果预测出冲突概率较低,那么synchronized会变成乐观锁,反之变为悲观锁
2.重量级锁&轻量级锁:
1.于乐观锁悲观锁的概念相似,不过我认为他们之间的区别是时间的差异;
2.重量级锁是锁所在的代码消耗的资源较多,轻量级锁是锁所在的代码消耗的资源较多;
3.synchronized既是轻量级锁也是重量级锁:synchronized能识别出发生冲突的次数,然后知道消耗资源的大小,如果消耗资源较多就会变成重量级锁,反之变为轻量级锁
3.自旋锁&挂起等待锁
1.自旋锁的概念是当锁被竞争时,锁对象要不断扫描锁是否归还,当锁被归还时,锁对象能一瞬间拿到锁,消耗的cpu资源多,更多的时候cpu在空转忙等
2.挂起等待锁与之相反:当锁竞争时,锁对象会等待一段时间,不着急拿到锁,等过一段时间后知道锁是否可以被拿到,消耗的cpu资源较少,当被阻塞时,不确定什么时候可以拿到锁,这个过程是不可控的
3.synchronized的轻量级锁方面由自旋锁实现,基于CAS机制实现
4.重量级锁部分基于挂起等待锁实现,通过调用内核的api来实现
4.可重入锁&不可重入锁
1.可重入锁是当一段代码被两个带有相同对象的锁,锁住的时候,这段代码不会被死锁
2.不可重入锁与之相反,当两个带有对象的锁锁代码时,此时这段代码可能会被死锁
3.synchronized就是一个可重入锁
5.公平锁&非公平锁
这种锁策略代表着再竞争锁时怎么竞争?
1.公平锁意味着线程在竞争锁时要遵循先来后到的原则来竞争锁,我们猜测这种锁的实现中有队列这种数据结构
2.非公平锁代表竞争锁时每个人机会相同,谁能抢到算谁的,这未尝不是另一种公平呢?
3.系统本身的调度是抢占式的,它们之间的顺序就是随机的
4.synchronized是非公平锁
6.互斥锁&读写锁
1.synchronized本身就是一个普通的互斥锁,这种锁在代码执行完成之前是不会放弃锁的
2.读写锁代表着在读,写操作之间是有互斥的:
2.1读与读操作之间是不互斥的
2.2读与写操作之间是互斥的
2.3写与写操作之间是互斥的
3.在日常开发中使用读操作大于写操作,每个人之间读不应该是互斥的,应该是独立的,所以会出现读写锁
小结:synchronized是乐观悲观锁,轻量级重量级锁,轻量级锁部分基于自旋锁实现,重量级锁基于挂起等待锁实现,非公平锁,互斥锁
synchronized的优化策略:
1.锁升级
锁最开始的时候并不是重量级锁,而是一步一步进化成重量级锁
synchronized的自适应:
1.未加锁的状态:
2.偏向锁:此时只是一个标记,这个标记占用的资源非常非常少,如果冲突进一步提升,直接变成轻量级锁,这里相当于缓冲区
3.轻量级锁:轻量级锁代表着冲突进一步提升
4.重量级锁:轻量级锁的冲突进一步提升
锁升级的过程是不可逆的
2.锁消除
编译器优化策略
有时候synchronized加了和没加是没区别的,编译器直接就认出来了,在某些经典场景下:单线程情况下不用加锁,编译器直接在字节码文件进行优化,这就是锁消除策略
3.锁粗化
要认识锁粗化,先认识一个概念:
锁的粒度:如果一个锁中的代码量越多,则说明这个锁的粒度越大,反之越小
锁粗化就是增加锁的粒度:将多个锁中的代码转化为一个锁,此时节省了锁资源,这也是synchronized优化的策略
//伪代码
synchronized(locker) {
code1;
}
synchronized(locker) {
code2;
}
synchronized(locker) {
code3;
}
//合并成一段代码
synchronized(locker) {
code1;
code2;
code3;
}
CAS(Compare And Swap)机制
这是一条CPU指令,操作系统提供了api,jvm进行封装,意义是比较和交换
我们可以把CAS想象成一套方法,这一套方法可以比较交换,并且没有线程安全问题,因为这是一条cpu指令,相当于是原子操作,这样的变成我们也称为无锁编程
我们大致写一下CAS的大致流程
boolean CAS(address,reg1,reg2) { if(reg1==*address) { 把address的内存地址的值和reg2寄存器的值进行交换 return true; } return false; }
这里的交换更多的是赋值,因为我们不在乎reg2交换后的值,我们可以近似认为把reg2的值给复制到内存中
在Java中有原子类-对CAS进行的封装-Atomic类
public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(0); atomicInteger.getAndIncrement();//i++ atomicInteger.getAndDecrement();//i-- atomicInteger.decrementAndGet();//--i atomicInteger.incrementAndGet();//++i }
锁竞争机制与之有关,如果判定锁是由某线程持有,使用自旋锁策略,不断循环扫描,直到扫描到该锁不再该线程手里,伪代码如下
class CAS { public Thread thread = null; public void lock() { //通过CAS机制看是否thread线程是否持有锁 //如果持有锁,则不断循环,等待别的线程来自旋锁策略 //若是未持有锁,别的线程持有锁,则直接退出(别的线程设为持有锁的线程) while(!CAS(this.thread,null, Thread.currentThread())) { } } public void unlock() { thread = null; } }
思考:CAS的ABA问题:
即在CAS过程中,可能线程会发生改变,但是还是回到了原来持有锁的线程,即A->B->A过程,那么这种情况锁的持有对象已经发生变化了
CAS期待的是在过程中没有发生变化,如果发生变化了就叫ABS问题,我们如何应对这种问题?下一篇博客再聊!!