[JavaEE] 常见锁策略、自实现定时器

目录

一、锁策略

二、synchronized原理

三、CAS

四、定时器的自实现


一、锁策略

        (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();
        }
    }
}

=========================================================================

最后如果感觉对你有帮助的话,不如给博主来个三连,博主会继续加油的ヾ(◍°∇°◍)ノ゙

  • 21
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值