java个人知识小结

一级标题

内存知识

JMM(Java内存模型,Java Memory Model)本身是一种抽象的概念,并不真实存在。它描述的是一组规则或规范,通过这组规范,定了程序中各个变量的访问方法。JMM关于同步的规定:
1)线程解锁前,必须把共享变量的值刷新回主内存;
2)线程加锁前,必须读取主内存的最新值到自己的工作内存;
3)加锁解锁是同一把锁;

在Java的内存模型中分为主内存和工作内存,Java内存模型规定所有的变量存储在主内存中,每条线程都有自己的工作内存,可以访问这些变量。

由于JVM运行程序的实体是线程,创建每个线程时,JMM会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域。

但线程对变量的操作(读取、赋值等)必须在工作内存中进行。因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写会主内存中。

关键字

AtomicInteger

原子整型类型,使用原子整型的num可以保证原子性,也就是number++的时候不会被抢断。

num.getAndIncrement();

CAS:实际值、内存值、预期值,当预期值和内存值一致时才会将数据更新到实际值中

volatile

volatile是Java虚拟机提供的轻量级的同步机制,它有3个特性:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

countDownLatch

作用:一个或一组线程在开始之下操作之前,必须要等到其它线程执行完才可以
属性:party 初始化的时候设置此值
这个值代表的是一把共享锁,它的值代表这把锁被几个线程锁住;
countdownlatch.countDown()这个方法表示当前线程释放持有的共享锁,将数字进行减1,实际上调用的是sync.releasedShared()这个方法;
countdownlatch.await()这个方法表示当前线程被阻塞

cycleBarrier

作用:允许一组线程全部等待彼此达到共同屏障点的同步辅助类;屏障是循环的 ,因为它可以在等待的线程被释放之后重新使用。

  • public CyclicBarrier(int parties)
  • public CyclicBarrier(int parties, Runnable barrierAction)
    第一个参数表示的是当前屏障处相互等待的线程个数
    第二个参数是一个runnable实现类,每次在所有线程到达屏障点时运行一次

Semaphore

作用:根据属性int值判断有多少许可给线程使用,当线程要使用的许可不足时, 则调用的线程则会被阻塞

  • acquire() 线程占用一个许可.
  • acquire(int) 线程占用int个许可
  • acquireUninterruptibly() 线程占用一个许可, 调用不可以打断
  • acquireUninterruptibliy(int) 线程占用int个许可,调用并且不可打断
  • release() 释放许可
    底层仍然是通过sync实现
// 假设传入的参数为1.
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 将调用线程封装了共享型Node, 加入到双向链表的队尾
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            // 记录node的前任
            final Node p = node.predecessor();
            // 前任是头节点, 则尝试去获锁
            if (p == head) {
                int r = tryAcquireShared(arg);
                // 获锁成功,设置头节点,并且进行传播
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 获锁失败, 判断是否进行睡眠, 若不睡眠就进行下次循环.
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

Semaphore获取锁的过程总结为如下:

  1. 判断是否满足获取锁条件, 关键方法nonfairTryAcquireShared.
  2. 若获取锁成功,则也会修改state.
  3. 若获取锁失败,关键方法doAcquireSharedInterruptibly阻塞的获取锁.
    1、添加到双向链表
    2、若是头节点后继,则尝试获取锁, 否者则判断进入睡眠等待唤醒, 唤醒后继续执行3.2
    3、若不进入睡眠,则直接运行到3.2步

Synchronized

使用方式:
  1. 作用于实例方法
  2. 作用于静态方法
  3. 作用于静态代码块
synchronized的可重入性

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

线程中断与唤醒

三个方法

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位;
另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须在线程代码中手动判断是否处于中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。

等待唤醒机制与synchronized
synchronized (obj) {
       obj.wait();
       obj.notify();
       obj.notifyAll();         
 }

在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,即调用者三个方法的前提条件是必须先获取对象的锁。

Java虚拟机对synchronized的优化:
偏向锁

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。

多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

ReentrantLock

ReentrantLock 支持公平锁和非公平锁,可重入锁 ReentrantLock的底层是通过 AQS[链接] 实现。

AQS原理
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

condition

(1)通过Condition能够更加精细的控制多线程的休眠与唤醒。

(2)对于一个锁,我们可以为多个线程间建立不同的Condition。

在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),我们可以通过condition对某个线程进行阻塞或者唤醒;
如果采用Object类中的wait(), notify(), notifyAll()实现的话,当写入数据之后需要唤醒读线程时,不可能通过notify()或notifyAll()明确的指定唤醒读线程,而只能通过notifyAll唤醒所有线程,但是notifyAll无法区分唤醒的线程是读线程,还是写线程。所以,通过Condition能够更加精细的控制多线程的休眠与唤醒。

线程的生命周期:
new -> start -> run -> block -> dead
线程wait()进入阻塞状态时,必须通过notify()或者notifyall()来唤醒线程,但问题是只能不能根据条件进行唤醒,只能唤醒所有的阻塞线程,而condition很好的解决了这一问题。
在这里插入图片描述
关键点在起内部类Sync,其实现了AbstractQueuedSynchronizer,ReentrantLock的操作大部分都最终调用到了Sync和AbstractQueuedSynchronizer。

Thread和ThreadLocal

Thread

创建线程
  1. 继承thread
  2. 实现runnable
  3. 实现callable

主要区别:
callable有返回值;
callable不能直接作为函数式接口传入;

多线程排队执行
public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new Thread(() -> {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread1 running");
    });

    Thread thread2 = new Thread(() -> System.out.println("thread2 running"));
    thread1.start();
    thread1.join();
    thread2.start();
}

使用join()实现线程排队执行

ThreadLocal

  1. ThreadLocal可以用来保存当前线程的独有属性,对应公共资源,但是此独有属性无法传递给子线程;
  2. InheritableThreadLocal可以将当前线程的独有属性传递给子线程;

当InheritableThreadLocal与线程池结合使用的时候存在问题:
池化技术本质上是对线程的反复使用,当线程是被二次使用的时候并没有再重新初始化init()线程,而是直接使用已经创建过的线程,所以这里的值并不会被再次操作。

  1. TransmittableThreadLocal

这个类是阿里开源的,继承并加强了InheritableThreadLocal,解决了nheritableThreadLocal与线程池结合使用存在的问题。

线程池

ThreadPoolExecutor

这个类是JDK中的线程池类,继承自Executor, Executor 顾名思义是专门用来处理多线程相关的一个接口,所有县城相关的类都实现了这个接口,里面有一个execute()方法,用来执行线程,线程池主要提供一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁的额外开销,提高了响应的速度。

参数

  • corePoolSize:核心线程数,最低线程数
  • maximunPoolSize:最大线程数
  • KeepAliveTime:空闲线程存活时间
  • timeUnit:时间单位
  • workQueue:用于缓存任务的阻塞队列,SynchronousQueue 、LinkedBlockingQueue 和 ArrayBlockingQueue,分为有界队列和无界队列
  • ThreadFactory:指定创建线程的工厂。(可以不指定)
  • rejectedExectionHandler:拒绝策略
策略BB
ThreadPoolExecutor.AbortPolicy()抛出RejectedExecutionException异常。默认策略
ThreadPoolExecutor.CallerRunsPolicy()由向线程池提交任务的线程来执行该任务
ThreadPoolExecutor.DiscardPolicy()抛弃当前的任务
ThreadPoolExecutor.DiscardOldestPolicy()抛弃最旧的任务(最先提交而没有得到执行的任务)

在这里入图片描述

Executors(工具类)

Executors为线程迟工具类,相当于一个工厂类,用来创建合适的线程池,返回ExecutorService类型的线程池。

ThreadPoolTaskExecutor

参数

  • corePoolSize:核心线程数
  • maxPoolSize:最大线程数
  • KeepAliveSeconds:线程池维护线程所允许的空闲时间
  • queueCapacity:队列最大长度
  • rejectedExecutionHandler:拒绝策略

rejectedExectutionHandler参数字段用于配置绝策略,常用拒绝策略如下

AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException

CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。

DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。

DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。

线程池状态

  • running:允许提交并处理任务
  • shutdown:不允许提交新任务,但会处理完已提交的任务
  • stop:不允许提交新任务,也不会处理阻塞队列中未执行的任务,并设置正在执行的线程的中断标志位
  • tidying:所有任务执行完毕,池中工作的线程数为0,等待执行terminated()勾子方法
  • terminated:terminated()勾子方法执行完毕

IO

IO调用和IO模型

mysql

SQL执行顺序

from-> on -> join -> where -> group by -> sum、count、max、avg -> having -> select -> distinct -> order by -> limit

索引区别

聚集索引和普通索引
在这里插入图片描述
两个B+树索引分别如上图:

(1)id为PK,聚集索引,叶子节点存储行记录;

(2)name为KEY,普通索引,叶子节点存储PK值,即id;

索引执行

  • 主键索引:直接通过PK找到数据

  • 普通索引:如粉红色路径,需要扫码两遍索引树:

    (1)先通过普通索引定位到主键值id=5;

    (2)在通过聚集索引定位到行记录;

    这就是所谓的回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。

可以通过索引覆盖进行查询优化,常见的方法是:将被查询的字段,建立到联合索引里去。目的是避免二次回表查询。

Using filesort表示在索引之外,需要额外进行外部的排序动作。导致该问题的原因一般和order by有者直接关系,一般可以通过合适的索引来减少或者避免。

mysql 的内连接、左连接、右连接有什么区别?

  1. 内连接,显示两个表中有联系的所有数据;
  2. 左链接,以左表为参照,显示所有数据,右表中没有则以null显示
  3. 右链接,以右表为参照显示数据,,左表中没有则以null显示

举例:
人员表关联出入记录表,person join record
左连接以人员数据为主,删除人员的出入记录无法查看;
右连接以出入记录为主,删除人员对应的出入记录关联的人员信息为null

索引失效

类似于查询优化

  1. 被索引字段使用了表达式计算
  2. 被索引字段使用了内置函数
  3. like 使用了 %X 模糊匹配
  4. 索引字段不是联合索引字段的最左字段
  5. or 分割的条件,如果 or 左边的条件存在索引,而右边的条件没有索引,不走索引

查询优化方法

  1. 对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引
  2. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
  3. 应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描
  4. 应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描,改为使用union
  5. in 和 not in 也要慎用,否则会导致全表扫描,使用exists代替
 select * from A where id in (select id from B)

当B表的数据集必须小于A表的数据集时,用in优于exists
当A表的数据集系小于B表的数据集时,用exists优于in

  1. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描
  2. 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描
  3. 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致

数据库锁机制及原理

数据库锁分类
  • 悲观锁
    悲观锁一般就是我们通常说的数据库锁机制,以下讨论都是基于悲观锁
    悲观锁主要表锁、行锁、页锁。在MyISAM中只用到表锁,不会有死锁的问题,锁的开销也很小,但是相应的并发能力很差。innodb实现了行级锁和表锁,锁的粒度变小了,并发能力变强,但是相应的锁的开销变大,很有可能出现死锁。同时innodb需要协调这两种锁,算法也变得复杂。InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁
  • 乐观锁
    乐观锁一般是指用户自己实现的一种锁机制,假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。乐观锁的实现方式一般包括使用版本号和时间戳 (也就是在数据库中添加了版本号和时间戳字段,以便检测)
    表锁和行锁都分为共享锁和排他锁,而更新锁是为了解决行锁升级(共享锁升级为独占锁)的死锁问题
    innodb中表锁和行锁一起用,所以为了提高效率才会有意向锁 (意向共享锁和意向排他锁)

select语句也会加锁,加的是元数据读锁,即MDL读锁

行锁

共享锁

允许其它事务读,但是不允许写

  • 当一个事务执行select语句时,数据库系统会为这个事务分配一把共享锁,来锁定被查询的数据。在默认情况下,数据被读取后,数据库系统立即解除共享锁。例如,当一个事务执行查询“SELECT * FROM accounts”语句时,数据库系统首先锁定第一行,读取之后,解除对第一行的锁定,然后锁定第二行。这样,在一个事务读操作过程中,允许其他事务同时更新accounts表中未锁定的行。
  • 如果数据资源上放置了共享锁,还能再放置共享锁和更新锁
  • 具有良好的并发性能,当数据被放置共享锁后,还可以再放置共享锁或更新锁。所以并发性能很好。
排他锁

排他锁不允许其他事务读和写

  • 当一个事务执行insert、update或delete语句时,数据库系统会自动对SQL语句操纵的数据资源使用独占锁(即排他锁)
  • 独占锁不能和其他锁兼容,如果数据资源上已经加了独占锁,就不能再放置其他的锁了。同样,如果数据资源上已经放置了其他锁,那么也就不能再放置独占锁了
  • 最差。只允许一个事务访问锁定的数据,如果其他事务也需要访问该数据,就必须等待
更新锁

更新锁在的初始化阶段用来锁定可能要被修改的资源,这可以避免使用共享锁造成的死锁现象。例如,对于以下的update语句:

UPDATE accounts SET balance=900 WHERE id=1

更新操作需要分两步:读取accounts表中id为1的记录 –> 执行更新操作

如果在第一步使用共享锁,再第二步把锁升级为独占锁,就可能出现死锁现象。例如:两个事务都获取了同一数据资源的共享锁,然后都要把锁升级为独占锁,但需要等待另一个事务解除共享锁才能升级为独占锁,这就造成了死锁

  • 加锁与解锁: 当一个事务执行update语句时,数据库系统会先为事务分配一把更新锁。当读取数据完毕,执行更新操作时,会把更新锁升级为独占锁
  • 兼容性: 更新锁与共享锁是兼容的,也就是说,一个资源可以同时放置更新锁和共享锁,但是最多放置一把更新锁。这样,当多个事务更新相同的数据时,只有一个事务能获得更新锁,然后再把更新锁升级为独占锁,其他事务必须等到前一个事务结束后,才能获取得更新锁,这就避免了死锁
  • 并发性能: 允许多个事务同时读锁定的资源,但不允许其他事务修改它

意向锁(意向共享锁和意向排他锁)

这两中类型的锁共存的问题考虑这个例子:事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁

在意向锁存在的情况下,上面的判断可以改成
  • 判断表是否已被其他事务用表锁锁表
  • 发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞

申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请

数据库隔离级别

  • 读未提交,造成脏读(Read Uncommitted)
    什么锁都没有,一个事务写的过程数据被另一个事务读走,造成脏读
    加上写锁(独占锁),写的过程中不允许读
  • 读已提交(Read Committed)
    两次读取同一条数据的过程中,该数据被修改,两次读取不一致,造成幻读
    加上读锁(共享锁),当前事务读取过程中其它事务可以读,不可以修改
  • 可重复读(Repeatable Read)
  • 串行化(Serializable)
    解决幻读问题,在同一个事务中,同一个查询多次返回的结果不一致。事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读是由于并发事务增加记录导致的,这个不能像不可重复读通过记录加锁解决,因为对于新增的记录根本无法加锁。需要将事务串行化,才能避免幻读。

元数据锁(MDL锁)

  • 当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁
  • 读锁和写锁互相排斥

间隙锁

在这里插入图片描述

图中id值为8的记录加了gap锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新记录,其实就是id列的值(3,8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3,8)中的新记录才可以被插入。

还有什么临键锁、插入意向锁等等

MVCC(多版本并发控制-概念)

是一种用来解决读-写冲突的无锁并发控制的概念,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照
作用:为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
场景:在可重复读和串行化级别下生效

mvcc实现原理

基于undolog、版本链、readview。

  • redolog:数据恢复

  • undolog:数据回滚

  • 每一条数据会有三个隐藏字段:
    DB_TRX_ID – 记录插入或更新该行的最后一个事务的事务 ID
    DB_ROLL_PTR – 指向改行对应的 undolog 的指针
    DB_ROW_ID – 单调递增的行 ID,他就是 AUTO_INCREMENT 的主键 ID

  • 当前读
    像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读
    像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

消息队列

使用场景

  1. 异步
  2. 解耦

消息的幂等性

防止同一条消息重复消费

  • 版本号
  • 唯一 ID + 指纹码 机制,利用数据库主键去重
  • 利用redis的原子性去实现

布隆过滤器

  • 组成:一个很长的二进制向量和一系列随机映射函数
  • 作用:过滤一个元素是否在一个集合中,一定不存在或者可能存在
  • 优点:
    • 时间复杂度低,增加和查询元素的时间复杂度O(N),(N为哈希函数的个数,通常情况比较小)
    • 保密性强,布隆过滤器不存储元素本身
    • 存储空间小,如果运行存在一定的误判,布隆过滤器是非常节省空间的(相比其他数据结构如Set集合)
  • 使用场景:解决Redis缓存穿透问题(面试重点)

增加元素

往布隆过滤器增加元素,添加的key需要根据k个无偏hash函数计算得到多个hash值,然后对数组长度进行取模得到数组下标的位置,然后将对应数组下标的位置的值置为1

  • 通过k个无偏hash函数计算得到k个hash值
  • 依次取模数组长度,得到数组索引
  • 将计算得到的数组索引下标位置数据修改为1
    例如,key = Liziba,无偏hash函数的个数k=3,分别为hash1、hash2、hash3。三个hash函数计算后得到三个数组下标值,并将其值修改为1.

查询元素

布隆过滤器最大的用处就在于判断某样东西一定不存在或者可能存在,而这个就是查询元素的结果。其查询元素的过程如下:

  • 通过k个无偏hash函数计算得到k个hash值
  • 依次取模数组长度,得到数组索引
  • 判断索引处的值是否全部为1,如果全部为1则存在(这种存在可能是误判),如果存在一个0则必定不存在

误判

关于误判,其实非常好理解,hash函数在怎么好,也无法完全避免hash冲突,也就是说可能会存在多个元素计算的hash值是相同的,那么它们取模数组长度后的到的数组索引也是相同的,这就是误判的原因。例如李子捌和李子柒的hash值取模后得到的数组索引都是1,但其实这里只有李子捌,如果此时判断李子柒在不在这里,误判就出现啦!因此布隆过滤器最大的缺点误判只要知道其判断元素是否存在的原理就很容易明白了!

JVM

JVM栈堆概念,何时销毁对象

  1. 类在程序运行的时候就会被加载,方法是在执行的时候才会被加载,如果没有任何引用了,Java自动垃圾回收,也可以用System.gc()开启回收器,但是回收器不一定会马上回收。
  2. 静态变量在类装载的时候进行创建,在整个程序结束时按序销毁;
  3. 实例变量在类实例化对象时创建,在对象销毁的时候销毁;
  4. 局部变量在局部范围内使用时创建,跳出局部范围时销毁;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值