常见锁策略&锁优化策略&CAS机制

定时器思考问题:

1.使用堵塞队列是否比优先级队列更安全?

2.使用wait()好还是sleep()好?

3.我们重写的CompareTo方法正确吗?

4.是否还有别的方法来实现定时器?关键词:时间轮

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问题,我们如何应对这种问题?下一篇博客再聊!!

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值