【JAVA】【面试】【基础篇】- 线程、锁

再快不能快基础,再烂不能烂语言! java

【基础篇】- 线程

线程:一个程序同时执行多个任务,一般,每个任务称为一个线程。
串行: 对于单条线程执行多个任务,例以下载多个任务,须要下载完一个再下载另外一个。
并行:下载多个文件,开启多条线程,多个文件同时下载。面试

  • 建立线程的方式及实现

    1. 继承Thread类建立线程数据库

    (1) 定义Thread类的子类,并重写run方法,该run方法的方法体就表明了线程要完成的任务。所以把run()方法称为执行体。
      (2) 建立Thread子类的实例,即建立了线程对象。
      (3) 调用线程对象那个的start()方法来启动该线程。
    
      知识点:
      Thread.currentThread()方法返回当前正在执行的线程对象。
      GetName()方法返回调用该方法的线程的名字。
    复制代码

    2. 实现Runnable接口建立线程编程

    (1) 定义runnable接口的实现类,并重写接口的run()方法,该run()方法的方法体一样是该线程的线程执行体。
      (2) 建立Runnable实现类的实例,并以此实例做为Thread的target来建立Thread对象,该Thread对象才是真正的线程对象。
      (3) 调用线程对象的start()方法来启动该线程。
    复制代码

    3. 经过Callable和Future建立线程数组

    (1) 建立Callable接口的实现类,并实现call()方法,该call()方法做为线程执行体,而且有返回值。
      (2) 建立Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它经过接收Callable来建立,它同时实现了Future和Runable接口。)
      (3) 使用FutureTask对象做为Thread对象的Target建立并启动新线程。
      (4) 调用FutureTask对象的get()方法来得到子线程执行结束后的返回值。
    复制代码
  • 三种建立线程方法的对比

    (1)采用Runnable、Callable接口的方法建立多线程
      优点:线程类只是实现了Runnable接口或Callable接口,还能够继承其余类。在这种方式下,
      多个线程能够共享同一个target对象,因此很是适合多个相同线程来处理同一份资源的状况,从而能够将CPU,代码,数据分开,造成清晰的模型,
      较好地体现了面对对象的思想。
      劣势:编程稍微复杂,若是要访问当前线程,则必须使用Thread.currentThread()方法。
      
      (2)使用继承Thread类的方式建立多线程
      优点:编写简单,若是须要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this便可得到当前线程。
      劣势: 线程类已经继承了Thread类,因此不能再继承其余父类。
      
      (3) Runable和Callable的区别
      Callable规定(从新)的方法是call(),Runnale规定(重写)的方法是run()。
      Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
      call方法能够抛出异常,run方法不能够。
      运行Callable任务能够拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
      经过Future对象能够了解任务执行状况,可取消任务的执行,还可获取执行结果。
    复制代码
  • sleep() 、join()、yield()有什么区别

    • sleep():缓存

      sleep()方法须要指定等待时间,它可让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可让其余同优先级或高优先级的线程获得执行的机会,也可让低优先级的线程获得执行的机会。可是sleep()方法不会释放"锁标志",也就是说若是有synchronized同步块,其余线程仍然不能访问共享数据。安全

    • wait():bash

      wait()方法须要跟notify()以及notifyAll()两个方法一块儿介绍,这三个方法用于协调多个线程对共享数据的存取,因此必须在synchronized语句块内使用,也就是说,notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁。注意,它们都是Object类的方法,而不是Thread类的方法。数据结构

      wait()方法与sleep()方法的不一样之处在于,wait()方法会释放对象的"锁属性"。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程能够获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的全部线程都移动到该对象的锁等待池。多线程

      除了使用notify()和notifyAll()方法,还可使用带毫秒参数的wait(longtimeout)方法,效果是在延迟timeout毫秒后,被暂停的线程将恢复到锁标志等待池。

      此外,wait(),notify()及notifyAll()只能在synchronized语句中使用,可是若是使用的是ReenTrantLock实现同步,解决方法是使用ReenTrantLock.newCondition()获取一个Condition类对象,而后Condition的await(),signal()以及signalAll()分别对应上面的三个方法。

    • yield():

      yield()方法和sleep()方法相似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程从新回到可执行状态,因此执行yield()的线程有可能在进入到可执行状态后立刻又被执行,另外yield()方法只能使用同优先级或者高优先级的线程获得执行机会,这也和sleep()方法不一样。

    • join():

      join()方法会使当前线程等待join()方法的线程结束后才能继续执行。

  • 说说 CountDownLatch 原理

    CountDownLatch简介:

    闭锁是一种同步工具类,能够延迟线程的进度直到其到达终止状态。闭锁的做用至关于一扇门,在闭锁到达结束状态以前,这扇门一直是关闭的,而且没有任何线程能经过,当到达技术状态时,这扇门会打开并容许全部的线程经过。当闭锁到达结束状态后,将不会再改变状态,所以这扇门将永远保持打开状态。闭锁能够用来确保某些活动直到其余活动都完成后才继续执行。

    CountDownLatch是一种灵活的闭锁实现,它是一个同步辅助类,在完成一组正在其余线程中执行的操做以前,它容许一个或多个线程一直等待。

    CountDownLatch实现原理:

    原文连接:blog.csdn.net/qq_39241239…

CountDownLatch是经过“共享锁”实现的。在建立CountDownLatch中时,会传递一个int类型参数count,该参数是“锁计数器”的初始状态,
表示该“共享锁”最多能被count个线程同时获取。当某线程调用该CountDownLatch对象的await()方法时,该线程会等待“共享锁”可用时,
才能获取“共享锁”进而继续运行,而“共享锁”可用的条件,就是“锁计数器”的值为0!而“锁计数器”的初始值为count,每当一个线程调用
该CountDownLatch对象的CountDown()方法时,才将“锁计数器”-1;经过这种方式,必须有count个线程调用countDown()以后,
“锁计数器”才为0,而前面提到的等待线程才能继续运行!
复制代码
  • 说说 CyclicBarrier 原理

    CyclicBarrier简介:

    栅栏相似于闭锁,它能阻塞一线程直到某个事件发送。栅栏与闭锁的关键区别在于,全部线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待时间,而栅栏用于等待其余线程。

    全部线程相互等待,直到全部的线程到达某一点时才打开栅栏,而后线程能够继续执行

    CyclicBarrier实现原理:

    原文连接:blog.csdn.net/qq_39241239…

CyclicBarrier的源码实现和CountDownLatch大相庭径,CyclicBarrier基于Condition来实现的。CyclicBarrier类的内部有一个计数器,
每一个线程在到达屏障点的时候都会调用await方法将本身阻塞,此事计数器会减1,当计数器为0的时候全部因调用await方法而被阻塞的线程将被唤醒。
复制代码
  • 说说 CountDownLatch 与 CyclicBarrier 区别

    1. 这两个类均可以实现一组线程在到达某个条件以前进行等待,它们内部都有一个计数器,当计数器的值不断的减为0的时候全部阻塞的线程将会被唤醒
    2. CountDownLatch的计数器是有使用者来控制的,调用await方法只是将本身阻塞而不会减小计数器的值。
      CyclicBarrier的计数器是由本身控制,调用await方法不只会将本身阻塞还会将减小计数器的值。
    3. CountDownLatch只能拦截一轮 CyclicBarrier能够实现循环拦截(CyclicBarrier能够实现CountDownLatch的功能,反之则不能)
    4. CountDownLatch的做用是容许1或N个线程等待其余线程完成执行;
      CyclicBarrier则是容许N个线程相互等待。
    5. CountDownLactch的计数器没法被重置;
      CyclicBarrier的计数器能够被重置后使用,所以它被称为是循环的。
  • 说说 Semaphore 原理

    在一个停车场中,车位是公共资源,每辆车就比如一个线程,看门人起的就是信号量的做用。

    信号量是一个非负整数,表示了当前公共资源的可用数目,当一个线程要使用公共资源时,首先要查看信号量,若是信号量的值大于1,则将其减1,而后去占有公共资源。若是信号量的值为0,则线程会将本身阻塞,直到有其余线程释放公共资源。

    在信号量上咱们定义两种操做:acquire(获取)和release(释放)。当一个线程调用acquire操做时,它要么经过成功获取信号量(信号量-1),要么一直等下去,直到有线程释放信号量,或超时。release(释放)实际上会将信号量的值加1,而后唤醒等待线程。

    信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另外一个用于并发线程数的控制。

  • 说说 Exchanger 原理

Exchanger ————交换器,是JDK1.5时引入的一个同步器,从字面上就能够看出,这个类的主要做用是交换数据。

Exchanger有点相似CyclicBarrier,CyclicBarrier是一个栅栏,到达栅栏的线程须要等待其余必定数量的线程到达后,才能经过栅栏。
Exchanger能够当作是一个双向栅栏,如上图:Thread1线程到达栅栏后,会首先观察有没有其它线程已到达栅栏,若是没有就等待,
若是已经有其余线程(Thread2)已经到达了,就会以成对的方式交换各自携带的信息,所以Exchange很是适合于两个线程之间的数据交换。

Exchanger<String> exchanger=new Exchanger<String>();
exchanger.exchange(tool) tool为交换的数据
复制代码
  • ThreadLocal 原理分析

    ThreadLocal简介:

    ThreadLocal,这个类提供线程局部变量,这些变量与其余正常的变量的不一样之处在于,每个访问该变量的线程在其内部都有一个独立的初始化的变量副本;ThreadLocal实例变量一般用private static在类中修饰。

    只要ThreadLocal的变量能被访问,而且线程存活,那每一个线程都会持有ThreadLocal变量的副本。当一个线程结束时,它所持有的全部ThreadLocal相对的实力副本均可被回收。

    ThreadLocal适用于每一个线程须要本身独立的实例且该实例须要在多个方法中被使用(相同线程数据共享),也就是变量在线程间隔离(不一样的线程数据隔离)而在方法或类间共享的场景。

    ThreadLocal原理分析: blog.csdn.net/Mrs_chens/a…

    对象实例与ThreadLocal变量的映射关系是由线程Thread来维护的。

    对象实例与ThreadLocal变量的映射关系是存放在一个Map里面(这个Map是个抽象的Map并非java.util中的Map),
      这个Map是Thread类的一个字段!而真正存放映射关系的Map就是ThreadLocalMap。
      
      在set方法中首先要获取当前线程,而后经过getMap获取当前线程的ThreadLocalMap类型的变量threadLocals,若是存在则直接赋值,若是不存
      在则给该线程ThreadLocalMap变量赋值。赋值的时候这里的this就是调用变量的对象实例自己。
      
      get方法,一样也是先获取当前线程的ThreadLocalMap变量,若是存在则返回值,不存在则建立并返回初始值。setInitialValue()
    复制代码
  • 讲讲线程池的实现原理

    • 线程池简介:

      正常状况下使用线程的时候就会去建立一个线程,可是在并发状况下线程的数量不少,每一个线程执行一个很短的任务就结束了,这样频繁建立线程就会大大下降系统的效率,由于频繁建立线程和销货线程须要时间。

    线程池使得线程能够复用,执行完一个任务,并不被销毁,而是能够继续执行其余的任务。

    线程池的好处,就是能够方便的管理线程,也能够减小内存的消耗。

    • 线程池状态:

      runState: 表示当前线程池的状态,在ThreadPoolExecutor中为一个volatile变量,用来保证线程之间的可见性。

      RUNNING: 当建立完线程后的初始值。

      SHUTDOWN: 调用shutdow()方法后,此时线程池不能接受新的任务,它会等待全部任务执行完毕。

      STOP: 调用shutdownnow()方法后,此时线程池不能接受新的任务,而且会去尝试终止正在执行的任务。

      TERMINATED: 当线程池已处于SHUTDOWN或STOP状态,而且全部工做线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

    • 线程池执行流程:

    任务进来时,首先要执行判断,判断核心线程是否处于空闲状态,
      若是不是,核心线程就会先执行任务,若是核心线程已满,则判断任务队列是否有地方存听任务,
      若是有,就将任务保存在队列中,等待执行,若是满了,在判断最大可容纳的线程数,
      若是没有超出这个数量就开创非核心线程执行任务,若是超出了,就调用handler实现拒绝策略。
      
      handler的拒绝策略:
      第一种(AbortPolicy):不执行新的任务,直接抛出异常,提示线程池已满
      第二种(DisCardPolicy):不执行新的任务,也不抛出异常
      第三种(DisCardOldSetPolicy):将消息队列中的第一个任务替换为当前新进来的任务执行
      第四种(CallerRunsPolicy):直接调用execute来执行当前任务
    复制代码
  • 线程池的几种方式

    • CachedThreadPool: 可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Intger.Max_value,就是无限大,当有须要时建立线程来执行任务,没有须要时回收线程,适用于耗时少,任务量大的状况。
    • SecudleThreadPool: 周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
    • SingleThreadPool: 只有一条线程来执行任务,适用于有顺序的任务的应用场景。
    • FixedThreadPool: 定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。
  • 线程的生命周期

第一步是用new Thread()的方法新建一个线程,在线程建立完成以后,线程就进入了就绪状态(Runnable),
此时建立出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权以后,线程就进入了运行状态(Running),
当线程的任务执行完成以后或者是很是态的调用stop()方法以后,线程就进入了死亡状态。

如下几种状况容易形成线程阻塞:
1. 当线程主动调用了sleep()方法时,线程会进入阻塞状态;
2. 当线程主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回以前,线程也会进入阻塞状态;
3. 当线程进入正在等待某个通知时,会进入阻塞状态;

如何跳出阻塞过程:
1. 当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态
2. 第二种则是在返回一个参数以后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程。
复制代码

【基础篇】- 锁

  • 什么是线程安全

blog.csdn.net/csdnnews/ar…

当多个线程访问某个方法时,无论你经过怎么的调用方式,或者说这些线程如何交替的执行,咱们在主程序中不须要去作任何的同步,这个类的结果行为都是咱们设想的正确行为,咱们就说这个类是线程安全的。

无状态的对象是线程安全的(代码中不包含任何的做用域,也没有引用其余类中的域)。
复制代码
  • 如何确保线程安全?

    • synchronized:用来控制线程同步,保证在多线程环境下,不被多个线程同时执行,确保数据的完整性,通常是加在方法上。当synchronized锁住一个对象后,别的线程要想获取锁对象,那么就必须等这个线程执行完释放锁对象以后才可使用。
    public class ThreadDemo {
           int count = 0; // 记录方法的命中次数
           public synchronized void threadMethod(int j) {
               count++ ;
               int i = 1;
               j = j + i;
           }
        }
    复制代码
    • Lock:Lock是在java1.6被引入进来的,Lock的引入让锁有了可操做性,在须要的时候去手动的获取锁和释放锁

      • Lock()在获取锁的时候,若是拿不到锁,就一直处于等待状态,直到拿到锁
      • tryLock()是有一个Boolean的返回值的,若是没有拿到锁,直接返回false,中止等待,它不会像Lock()那样去一直等待获取锁,tryLock()是能够设置等待的相应时间的。
    private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类
    
    private void method(Thread thread){
       lock.lock(); // 获取锁对象
       try {
           System.out.println("线程名:"+thread.getName() + "得到了锁");
           // Thread.sleep(2000);
       }catch(Exception e){
           e.printStackTrace();
       } finally {
           System.out.println("线程名:"+thread.getName() + "释放了锁");
           lock.unlock(); // 释放锁对象
       }
    }
    复制代码
  • synchronized 与 lock 的区别

    类别 synchronized lock
    存在层次 java内置关键字,在jvm层面 Lock是个java类
    锁状态 没法判断是否获取锁的状态 能够判断是否获取到锁
    锁的释放 会自动释放锁
    (a线程执行完同步代码会释放锁)
    (b线程执行过程当中发生异常会释放锁)
    须要在finally中手动释放锁
    (unlock()方法释放锁)
    不然会形成线程死锁
    锁的获取 使用关键字的两个线程1和线程2
    若是当前线程1得到锁,线程2等待
    若是线程1阻塞,线程2则会一直等待下去
    若是尝试获取不到锁
    线程能够不用一直等待就结束了
    锁类型 可重入,不可中断,非公平 可重入,可判断,可公平
    性能 适合代码少许的同步问题 适合大量同步代码的同步问题
    ------ ------------ ------------
  • 锁的类型

    • 可重入锁: 在执行对象中全部同步方法不用再次得到锁
    • 可中断锁: 在等待获取锁过程当中可中断
    • 公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具备优先获取锁权利
    • 读写锁: 对资源读取和写入的时候拆分为2部分处理,读的时间能够多线程一块儿读,写的时候必须同步的写
  • volatile 实现原理

    volatile一般被比喻成“轻量级的synchronize”,也是并发编程中比较重要的一个关键字。和synchronized不一样,volatile是一个变量修饰符,只能用来修饰变量,没法修饰方法及代码块等。

    使用volatile只须要在声明一个可能被多线程同时访问的变量时,使用volatile修饰符就能够了。

    实现原理:

    为了提交处理器的执行速度,在处理器和内存之间增长了多级缓存来提高。可是因为引入了多级缓存,就存在缓存数据不一致问题。
      
      可是对于volatile变量,当对volatile变量进行读写操做的时候,jvm会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。
      
      可是就算写回到内存,若是其余处理器缓存的值仍是旧的,再执行计算机操做就会有问题,因此在多处理器下,为了保证每一个处理器的缓存是一致的,
      就会实现缓存一致性协议。
      
      缓存一致性协议:每一个处理器经过嗅探在总线上传播的数据来检查本身缓存是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,
      就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操做的时候,会强制从新从系统内存里把数据读处处理器缓存里。
      
      因此,若是一个变量被volatile所修饰的话,在每次数据变化以后,其值都会被强制刷入主存。而其余处理器的缓存因为遵照了缓存一致性协议,
      也会把这个变量的值从主存加载到本身的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
      
      可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的值。
    复制代码
  • synchronized 实现原理

    参考连接: blog.csdn.net/javazejian/…

    synchronized是基于Java对象头的同步锁

    synchronized实现同步的基础:java中的每个对象均可以做为锁

    • 对于普通方法,锁是当前实例对象
    • 对于静态同步方法,锁是当前类的Class对象
    • 对于同步方法块,锁是synchonized括号里配置的对象

    【概念】monitor: 每个对象都有一个监视器锁(monitor),当线程执行时对对象进行加锁,实际上就是将对象的monitor的状态设置为锁定状态,monitorenter指令执行的就是这个动做;线程对对象释放锁就是执行monitorexit指令,将对象的monitor的状态置为无锁状态(假设咱们先不考虑锁的优化)。

    实现原理:

    1. Java虚拟机中的同步(Synchronization)基于进入和退出管理(Monitor)对象实现的。在Java语言中,同步用的最多的地方多是被synchronized修饰的同步方法。同步方法不是由monitorenter和monitorexit指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的。

    • 实例变量:存放类的属性数据信息,包括父类的属性信息,若是是数组的实例部分还包括数组的长度,这部份内存按4字节对齐。

    • 填充数据:因为虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,了解便可。

    • 对象头,它是实现synchronized锁对象的基础。synchronized使用的锁对象是存储在java对象头里的,jvm中采用2个字节来存储对象头(若是对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构由如下组成:

      虚拟机位数 头对象结构 说明
      32/64bit Mark Word 存储对象的hashCode,锁信息或
      或分代年龄或GC标识等信息
      32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM
      经过这个指针肯定该对象是哪一个类的实例

      其中Mark Word在默认状况下存储着对象的HashCode、分代年龄,锁标记等如下是32位JVM的Mard Word默认存储结构

      锁状态 25bit 4bit 1bit
      是不是偏向锁
      2bit
      锁标志位
      无锁状态 对象HashCode 对象分代年龄 0 01

      因为对象头的信息是与对象自身定义的数据没有关系的额外存储成本,所以考虑到JVM的空间效率,Mark Word 被设计成一个非固定的数据结构,以便存储更多有效的数据,它会根据对象自己的状态复用本身的存储空间。

    1. 轻量级锁和偏向锁是Java6对synchronized锁进行优化后新增长的,重量级锁也就是一般说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管理或监视器锁)的起始地址。每一个对象都存在着一个monitor与之关联,对象与其monitor之间的关系存在多种实现方式。如monitor能够与对象一块儿建立销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。在java虚拟机中,monitor是由ObjectMonitor实现的(C++实现)。

    2. ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每一个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList集合,当线程获取到对象的monitor后进入_Owner区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其余线程进入获取monitor(锁)。

    3. 由此看来,monitor对象存在于每一个java对象的对象头中(存储的指针的指向),synchronized锁即是经过这种方式获取锁的,也是为何java中任意对象能够做为锁的缘由,同时也是notify/motifyAll/Wait等方法存在于顶级对象Object中的缘由。

  • volatile和synchronized区别

    • volatile:本质是在告诉jvm当前变量在寄存器(工做内存)中的值是不肯定的,须要从主存中读取;

      synchronized:则是锁定当前变量,只有当线程能够访问该变量,其余线程被阻塞住。

    • volatile:仅能使用在变量级别;

      synchronized:则可使用在变量,方法和类级别的。

    • volatile:仅能实现变量的修改可见性,不能保证原子性;

      synchronized:则能够保证变量的修改可见性和原子性。

    • volatile:不会形成线程的阻塞;

      synchronized:可能会形成线程的阻塞。

    • volatile:标记的变量不会被编译器优化;

      synchronized:标记的变量能够被编译器优化。

  • CAS 乐观锁

    blog.csdn.net/qq_35571554…

    • 悲观锁:

      独占锁是一种悲观锁,而synchronized就是一种独占锁,synchronized会致使其它全部未持有的锁的线程阻塞,而等待持有锁的线程释放锁。 synchronized属于悲观锁,悲观地认为程序中的并发状况严重,因此严防死守。

    • 乐观锁:

      每次不加锁而是假设没有冲突而去完成某项操做,若是由于冲突失败就重试,直到成功为止,而乐观锁用到的机制就是CAS。

    • CAS(Compare And Swap)(比较并替换):

      CAS机制当中使用了3个基本操做数:内存地址V,旧的预期值A,要修改的新值B

      更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改成B。

      CAS底层利用了unsafe提供了原子性操做方法。

      例如:

      1. 在内存地址V当中,存储着值为10的变量。
      2. 此时线程1想要把变量的值增长1。对线程1来讲,旧的预期值A=10,要修改的新值B=11。
      3. 在线程1要提交更新以前,另外一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
      4. 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
      5. 线程1从新获取内存地址V的当前值,并从新计算想要修改的新值。此时对线程1来讲,A=11,B=12。这个从新尝试的过程被称为自旋。
      6. 这一次比较幸运,没有其余线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
      7. 线程1进行SWAP,把地址V的值替换为B,也就是12。
    • CAS的缺点:

      1. CPU开销较大: 在并发量比较高的状况下,若是许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

      2. 不能保证代码块的原子性: CAS机制所保证的只是一个变量的原子性操做,而不能保证整个代码块的原子性。好比须要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

      由于它自己就只是一个锁住总线的原子交换操做啊。两个CAS操做之间并不能保证没有重入现象。

  • ABA 问题

    • 能够发现,CAS实现的过程是先取出内存中某时刻的数据,在下一时刻比较并替换,那么在这个时间差会致使数据的变化,此时就会致使出现“ABA”问题。

    • 什么是”ABA”问题? 好比说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,而且two进行了一些操做变成了B,而后two又将V位置的数据变成A,这时候线程one进行CAS操做发现内存中仍然是A,而后one操做成功。尽管线程one的CAS操做成功,可是不表明这个过程就是没有问题的。

      例如:

      1. 从取款机取50块钱,余额为100
      2. 当线程1执行成功后,当前余额为50,因为内存地址的值改变,致使线程2阻塞
      3. 这时正好有转帐50元信息,线程3执行成功
      4. 当线程2在自旋的过程当中检测到内存地址的值与旧的预期值是一致的,因此就会再次进行取款操做,正常状况下线程2应该是执行失败的,结果因为ABA的问题提交成功了。
    • 解决ABA问题

      当一个值从A更新到B,又更新为A,普通的CAS机制会误判经过检测。

      利用版本号比较就能够有效解决ABA问题。

  • 乐观锁的业务场景及实现方式

    • 乐观锁的应用场景: 在多节点部署或者多线程执行时,同一个时间可能有多个线程更新相同数据,产生冲突,这就是并发问题。这样的状况下会出现如下问题:

      • 更新丢失:一个事务更新数据后,被另外一个更新数据的事务覆盖。
      • 脏读:一个事务读取另外一个事务为提交的数据,即为脏读。
      • 其次还有幻读。

      针对并发引入控制机制,即加锁。

      加锁的目的是在同一时间只有一个事务在更新数据,经过锁独占数据的修改权。

    • 乐观锁的实现方式:

      • version方式:通常是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,不然重试更新操做,知道操做成功。
      update table set x=x+1, version=version+1 where id=#{id} and version=#{version}; 
      复制代码
      • CAS操做方式:即compare and swap或者compare and set,涉及到三个操做数,数据所在的内存值,预期值,新值。当须要更新时,判断当前内存值与以前取到的值是否相等,若相等,则用新值更新,若失败则重试,通常状况下是一个自旋操做吗,即不断的重试。

更详细的面试总结连接请戳:👇👇
juejin.im/post/5db8d9…

【推荐篇】- 书籍内容整理笔记 连接地址
【推荐】【Java编程思想】【笔记】 juejin.im/post/5dbb7a…
【推荐】【Java核心技术 卷Ⅰ】【笔记】 juejin.im/post/5dbb7b…

如有错误或者理解不当的地方,欢迎留言指正,但愿咱们能够一块儿进步,一块儿加油!😜😜

相关文章
相关标签/搜索