一、几个基础概念

1.同步和异步

  • 同步:同步和异步通常用来形容一次方法的调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
  • 异步:异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pGxUvip6-1609060970396)(C:\Users\13924\AppData\Roaming\Typora\typora-user-images\image-20201214172502260.png)]

2.并发和并行

  • 并发:两者都可以表示两个或者多个任务一起执行,并发表示单位时间段内,多个任务执行,并发更偏重于多个任务交替执行。
  • 并行:表示单位时间内,多个任务同时执行

3.临界区

用来表示一种公共资源或者说是共享资源可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。

4.阻塞和非阻塞

  • 阻塞:阻塞和非阻塞指的是调用者(程序)在等待返回结果(或输入)时的状态。阻塞时,在调用结果返回前,当前线程会被挂起,并在得到结果之后返回。
  • 非阻塞:非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前线程。因此对应非阻塞的情况,调用者需要定时轮询查看处理状态。

5.死锁、饥饿和活锁

  • 死锁:两个或者两个以上的进程(或线程)在执行过程中, 因争夺资源而造成的一种互相等待的现象, 若无外力作用,他们将无法推进下去
  • 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。一直有线程级别高的暂用资源,线程低的一直处在饥饿状态
  • 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试、失败、尝试、失败。在这期间线程状态会不停的改变

6.并发级别

  1. 阻塞:一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行
  2. 无饥饿:对于非公平锁来说,系统允许高优先级的线程插队,这样又可能导致低优先级线程产生饥饿。对于公平锁来说,不管新来的线程优先级多高,要想获得资源,就需要排队,不会出现饥饿的现象
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KeliX5PB-1609060970398)(C:\Users\13924\AppData\Roaming\Typora\typora-user-images\image-20201214174304141.png)]
  3. 无障碍:无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。遇到数据不一致时,回滚即可
  4. 无锁:无障碍基础上要求 必然有一个线程能够在有限步内完成操作离开临界区
  5. 无等待:要求所有的线程都必须在有限步内完成

7.java内存模型(JMM)

1.JMM

  • JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

java 报名系统并发 java并发怎么学_java 报名系统并发

2.JVM对Java内存模型的实现

  • 在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区
    JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。
  • 所有基本类型的局部变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于基本类型的局部变量,一个线程可以传递一个副本给另一个线程,当它们之间是无法共享的。
  • 堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的局部变量,它都会被存储在堆区。 一个局部变量如果是原始类型,那么它会被完全存储到栈区。 一个局部变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
  • 对于一个对象的成员方法,这些方法中包含局部变量,仍需要存储在栈区,即使它们所属的对象在堆区。 对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h6kKUZiW-1609060970404)(C:\Users\13924\AppData\Roaming\Typora\typora-user-images\image-20201214180905605.png)]

3.原子性

指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰

4.可见性

当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改

5.有序性

在并发场景下,程序的执行可能会发生乱序。即指令重排

二、java并行程序基础

1.线程的状态

  • NEW状态表示刚刚创建的线程,这种线程还没有开始执行
  • RUNNABLE状态,当线程调用start()方法时线程开始执行,准备所需要的资源,然后进入运行状态。
  • BLOCKED:如果线程在执行过程中遇到了synchronized同步块,就会进入阻塞状态,这时线程就会暂停执行,直到获得请求的锁。
  • TERMINATED:终止状态
  • WAITING和TIMED_WAITING都表示等待状态,他们的区别在于WAITING会进入一个无时间限制的等待,WIMED_WAITING会进行一个有时限的等待。一般来说,WAITING的线程正式在等待一些特殊事件,比如,通过wait方法等待的线程在等待notify方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则会进入TERMINATED状态,表示结束

2.线程的基本操作

(1)新建线程

  • 继承Thread类
  • 实现Runnable接口
    本质上时调用Thread类的构造方法
public Thread(Runnable target)

(2)终止线程

  • Thread类提供了一个stop()的方法。如果使用了stop方法,就可以立即将一个线程终止。但强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。
  • 由于stop()方法可能导致的数据不一致问题,建议自己定义一个stop方法自定义何时退出线程

(3)线程中断

  • 严格来讲,线程中断并不会立即使线程立即退出,而是给线程发送一个通知,告知目标线程有人希望你退出。至于目标线程接到通知后如何处理,则完全由目标线程自行决定。
  • 主要实现方法
public void Thread.interrupt()  // 中断线程
public boolean Thread.isInterrupt()  // 判断是否被中断
public static boolean Thread.interrupted()  // 判断是否被中断,并清除当前中断状态
  • Thread.interrupt()方法是一个实例方法。它通知目标线程中断,也就是设置中断标志位。中断标志位表示当前线程已经被中断了。
  • Thread.isInterrupted()方法也是实例方法,他判断当前线程是否又被中断(检查中断标志位)。
  • Thread.interrupted()用来判断当前线程的中断状态,但同时会清除当前线程的中断标志位状态。
  • Thread.sleep()方法由于中断而抛出异常时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标记位。

(4)等待和通知

  • 当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。比如,线程A调用了wait()方法,那么线程A就会停止继续执行,而转为等待状态。线程A会一直等到其他线程调用了notify()方法才会继续执行
  • 注意:
  • notify()方法会从等待队列中随机唤醒一个线程,这个选择是不公平的
  • wait()方法并非随便调用的,他必须包含在对应的synchronzied语句中,无论时wait()方法还是notify()方法都需要先获得目标对象的一个监视器

(5)挂起和继续执行

  • suspend():挂起
  • resume():继续执行
  • 不推荐这两个方法,因为suspend在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被他暂用的锁时,都会被牵连,导致无法正常继续运行。直到对应的线程上进行了resume操作,被挂起的线程才能继续,从而其他所有阻塞在相关锁上的线程也可以继续执行。但是如果resume操作意外的在suspend前就执行了,那么被挂起的线程可能很难有机会被继续执行。
  • 并且更严重的是:它所占用的锁不会被释放,因此可能会导致整个系统工作不正常。而且,对于被挂起的线程,从他的线程状态上看,居然还是Runnable,这也会严重影响我们对系统当前状态的判断。

(6)等待线程结束和谦让

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-phvG056P-1609060970406)(C:\Users\13924\AppData\Roaming\Typora\typora-user-images\image-20201220160949298.png)]

  • join的本质是让调用线程wait在当前线程对象实例上

3.并发下的ArrayList

  • 出现的问题:
  • 下标越界异常
  • 数据不一致
  • 解决方案
  • 使用Vertor集合
  • 使用Collections.synchroizedList()返回一个加锁的容器
  • 使用JUC中的CopyOnWriteArrayList类进行替换

4.并发下的HashMap

  • 出现的问题
  • 在jdk1.7多线程环境下HashMap容易出现死循环;因为HashMap中扩容机制的transfer函数所导致。transfer函数在转移元素的过程中,使用的是头插法,也就是链表的顺序会反转。这时就可能导致死循环
  • 解决办法
  • HashTable
  • ConcurrentHashMap
  • Collections类的synchronizedMap(Map m)方法
public static Map m=Collections.synchronizedMap(new HashMap());
  • Collections.synchronizedMap()会生成一个名为 SynchronizedMap 的 Map。它使用委托,将自
    己所有Map相关的功能交给传入的HashMap实现,而自己则主要负责保证线程安全
  • 通过mutex实现对这个m的互斥操作。比如,对于Map.get()方法,它的实现口下:
public V get(Object key) {

synchronized (mutex) {
    return m.get(key);
}
}
  • 这个包装的Map可以满足线程安全的要求。但是,它在多线程环境中的性能表现并不算太好。无论是对Map的读取或者写入,都需要获得mut ex的锁,这会导致所有对Map的操作全部进入等待状态,直到mutex锁可用。如果并发级别不高,一般也够用。但是,在高并发环境中,我们也有必要寻求新的解决方案

三、JDK并发包

1.同步控制

(1)重入锁

  • 重入锁使用java.util.concurrent.locks.ReentrantLock类
  • 开发人员必须手动指定何时加锁,何时释放锁。因此,重入锁对逻辑控制的灵活性要远远好于synchronized
  • 在退出临界资源区时,必须手动释放锁,否则,其他线程就没有机会再访问临界区了
  • 重入锁可以对资源加不止一把锁,但在释放时,也必须释放同等数量
  • 中断响应:
  • 程序在等待锁的过程中,程序可以根据需要取消对锁的请求
  • lockInterruptibly()方法
  • 锁申请等待时长:
  • 可以使用tryLock()方法进行一次限时的等待
  • 公平锁:
  • 在大多数情况下,锁的申请都是非公平的
  • 公平锁的一大特点是,不会产生饥饿现象
  • 可通过实例化时的构造方法设置公平锁
ReentrantLock lock = new ReentrantLock(true);
  • ReentrantLock的几个重要方法
  • lock():获得锁,如果锁己经被占用,则等待。
  • lockInterruptibly():获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,返回true,失败返回false。该方法不等待,立即返回。
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
  • unlock():释放锁。
  • 可重入锁实现的三个要素:
  • 第一,是原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有。
  • 第二,是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
  • 第三,是阻塞原语park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。
  • ReenTrantLock可重入锁(和synchronized的区别)总结
  • 可重入性:
    从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
  • 锁的实现:
    Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
  • 性能的区别:
    在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
  • 功能区别:
    便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
    锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
  • ReenTrantLock独有的能力:
  1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  3. ReenTrantLock提供了一种能够中断等待锁(中断响应)的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

(2)Condition条件

Condition类中提供了类似synchronized机制中wait和notify类似的功能

  • await() 方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal() 或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和Object.wait()方法很相似。
  • awaitUninterruptibly()方法与await()方法基本相同,但是它并不会在等待过程中响应中断。
  • singal()方法用于唤醒一个在等待中的线程。相对的singalAll()方法会唤醒所有在等待中的线程。这和Obejct.notify()方法很类似。

(3)信号量

  • 信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。
  • 构造方法
public Semaphore(int permits)
public Semaphore (int permits, boolean fair) // 第二个参数可以指定是否为公平锁
  • 在构造信号量时,必须要指定信号量的准入数,即同时能申请多少个许可
  • 主要方法
public void acquire()
public void acquireUninterruptibly() 
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit) 
public void release()
  • acquire()方法尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断。
  • acquireUninterruptibly()方法和acquire()方法类似,但是不响应中断。
  • tryAcquire()尝试获得一个许可,如果成功返回true ,失败则返回false ,它不会进行等待,立即返回。
  • release()用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问

(4)ReadWriteLock读写锁

  • 读写分离锁可以有效地帮助减少锁竞争,以提升系统性能

2.线程池

为了避免系统频繁的创建和销毁线程,我们可以让创建的线程进行复用

(1)JDK对线程池的支持

  • java.util.concurrent包

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHnDxRnE-1609060970408)(C:\Users\13924\AppData\Roaming\Typora\typora-user-images\image-20201223221130490.png)]

  • ThreadPoolExecutor表示一个线程池。Executors类则扮演着线程池工厂的角色,通过Executors可以取得一个拥有特定功能的线程池。

(2)各类线程池:

public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newSingleThreadScheduledExecutor() 
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
  • 线程池种类:
  • newFixedThreadPool()方法:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • newSingleThreadExecutor()方法:该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • newCachedThreadPool()方法:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • newSingleThreadScheduledExecutor()方法:该方法返回一个 ScheduledExecutorService 对象,线程池大小为 1。ScheduledExecutorService 接口在 ExecutorService 接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。
  • newScheduledThreadPool()方法:该方法也返回一个 ScheduledExecutorService 对象,但该线程池可以指定线程数量。

(3)核心参数

  • corePoolSize:指定了线程池中的核心线程数量。
  • maximumPoolSize:指定了线程池中的最大线程数量。
  • keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间。即,超过corePoolSize的空闲线程,在多长时间内,会被销毁。
  • unit: keepAliveTime 的单位。
  • workQueue:任务队列,被提交但尚未被执行的任务。
  • threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  • handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。

(4)任务队列

  • 参数workQueue指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。根据队列功能分类,在ThreadPoolExecutor的构造函数中可使用以下几种 BlockingQueue。
  • 直接提交的队列:该功能由SynchronousQueue对象提供。SynchronousQueue是一个特殊的BlockingQueue。SynchronousQueue没有容量,每一个插入操作都要等待一个相应的删除操作,反之,每一个删除操作都要等待对应的插入操作。如果使用SynchronousQueue提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲的进程,则尝试创建新的进程,如果进程数量己经达到最大值,则执行拒绝策略。因此,使用SynchronousQueue队列,通常要设置很大的maximumPoolSize值,否则很容易执行拒绝策略。
  • 有界的任务队列:有界的任务队列可以使用ArrayBlockingQueue实现。ArrayBlockingQueue
    的构造函数必须带一个容量参数,表示该队列的最大容量,当使用有界的任务队列时,若有新的任务需要执 行,如果线程池的实际线程数小于 corePoolSize,则会优先创建新的线程,若大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创
    建新的进程执行任务。若大于maximumPoolSize ,则执行拒绝策略。可见,有界队列仅当在任务队列装满时,才可能将线程数提升到corePoolSize以上,换言之,除非系统非常繁忙,否则确保核心线程数维持在在corePoolSize。
  • 无界的任务队列:无界任务队列可以通过LinkedBlockingQueue类实现。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,线程池会生成新的线程执行任务,但当系统的线程数达到corePoolSize后,就不会继续增加。若后续仍有新的任务加入,而又没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。
  • 优先任务队列:优先任务队列是带有执行优先级的队列。它通过PriorityBlockingQueue实现 , 可以控制 任务的执行先后顺序。它是一个特殊的无界队列。无论是有界队列ArrayBlockingQueue,还是未指定大小的无界队列LinkedBlockingQueue都是按照先进先出算法处理任务的。而PriorityBlockingQueue则可以根据任务自身的优先级顺序先后执行,在确保系统性能的同时,也能有很好的质量保证(总是确保高优先级的任务先执行)

(5)拒绝策略

  • AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
  • CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
  • DiscardOledestPolicy策略:该策略将丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
  • DiscardPolicy策略:该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,我觉得这可能是最好的一种方案了吧

(6)扩展线程池

ThreadPoolExecutor中的Worker内部类提供了beforeExecute()、afterExecute()、terminated()三个接口对线程运行前,运行后,结束进行控制

3.JDK并发容器

(1)并发集合简介

  • ConcurrentHashMap :这是一个高效的并发HashMap。你可以理解为一个线程安全的HashMap。
  • CopyOnWriteArrayList:这是一个List,从名字看就是和ArrayList是一族的。在读多写少的场合,这个List的性能非常好,远远好于Vector
  • ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看做一个线程安全的LinkedList
  • BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
  • ConcurrentSkipListMap:跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找

(2)高效读写的队列ConcurrentLinkedQueue

ConcurrentLinkedQueue应该算是在高并发环境中性能最好的队列,内部实现采用了大量的CAS无锁算法

(3)高效读取:不变模式下的CopyOnWriteArrayList

  • 读取是完全不用加锁的,并且更好的消息是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。
  • 所谓CopyOnWrite就是在写入操作时,进行一次自我复制。换句话说,当这个List需要修改时,并不修改原有的内容(这对于保证当前在读线程的数据一致性非常重要),而是对原有的数据进行一次复制,将修改的内容写入副本中。写完之后,再将修改完的副本替换原来的数据。这样就可以保证写操作不会影响读了。

(4)数据共享通道:BlockingQueue

BlockingQueue 是一个接口,并非一个具体的实现

  • ArrayBlockingQueue是基于数组实现的,而LinkedBlockingQueue基于链表。因此,ArrayBlockingQueue更适合做有界队列,因为队列中可容纳的最大元素需要在队列创建时指定 (毕竟数组的动态扩展不太方便)。
  • LinkedBlockingQueue适合做无界队列,或者那些边界值非常大的队列,因为其内部元素可以动态增加,它不会因为初值容量很大,而一口气吃掉你一大半的内存
  • BlockingQueue会让服务线程在队列为空时,进行等待,当有新的消息进入队列后,自动将线程唤醒

(5)随机数据结构:跳表(SkipList)

  • 跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是O(log n)。所以在并发数据结构中,JDK使用跳表来实现一个Map。
  • 跳表的另外一个特点是随机算法。跳表的本质是同时维护了多个链表,并且链表是分层的
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w7gwZoJD-1609060970409)(C:\Users\13924\AppData\Roaming\Typora\typora-user-images\image-20201224220148783.png)]
  • 最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集,一个元素插入哪些层是完全随机的。
  • 跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的
  • ConcurrentSkipListMap是实现跳表的一种结构