Java面试之并发知识点总结

线程创建方式

  • 继承Thread
  • 实现Runnable
  • 实现Callable(有返回值)

线程状态

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法

  • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

  • 阻塞(BLOCKED):线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态

  • 等待(WAITING):处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。)

    • wait(),join()
  • 超时等待(TIMED_WAITING):处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在指定的时间它们会自动唤醒。

    • wait(long),join(long),sleep(long)
  • 终止(TERMINATED):当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。

sleep与wait区别

  • sleep方法定义在Thread类上,wait方法定义在Object类上

  • sleep不会释放锁,wait会释放锁

  • sleep可以使用在任何代码块,wait必须在同步方法或同步代码块执行

    • 为什么wait必须在同步方法或同步代码块执行?

      • 会产生lost wake up问题

      • 举例

        • 生产者消费者问题

          • 一个消费者调用take,发现buffer.isEmpty
            在消费者调用wait之前,由于cpu的调度,消费者线程被挂起,生产者调用give,然后notify
            然后消费者调用wait (注意,由于错误的条件判断,导致wait调用在notify之后,这是关键)
            如果很不幸的话,生产者产生了一条消息后就不再生产消息了,那么消费者就会一直挂起,无法消费,造成死锁。
        • 解决方案:总是让give/notify和take/wait为原子操作。强制wait/notify在synchronized中

  • 当线程调用wait(),线程会进入到waiting状态,当线程调用sleep(time),或者wait(time)时,进入timed waiting状态

  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

  • wait() 通常被用于多线程之间的通信,sleep() 通常被用于线程休眠,暂停执行。

notify 和notifyAll的区别

  • notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会

  • notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会

    • 锁池

      • 假设线程A已经拥有对象锁,线程B、C想要获取锁就会被阻塞,进入一个地方去等待锁的等待,这个地方就是该对象的锁池;
    • 等待池

      • 假设线程A调用某个对象的wait方法,线程A就会释放该对象锁,同时线程A进入该对象的等待池中,进入等待池中的线程不会去竞争该对象的锁。

yield 和join

  • yield: 线程释放cpu执行权,直接进入就绪状态
  • join: 子线程在主线程中执行join方法,主线程等待子线程结束之后才能继续运行

ThreadLocal

  • 定义:ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离

  • 实现原理:ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap, Map的key为ThreadLocal对象,Map的value为需要缓存的值

  • 应用场景:Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。

  • 内存泄露以及解决方案

    • 内存泄漏:Thread的ThreadLocalMap内部的Entity的key是弱引用,由于key指向的ThreadLocal对象被回收,但value指向的对象是强引用,没被回收,而value却不再使用,这造成了内存泄漏。

      • 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
    • 解决方案:ThreadLocal的get()、set()、remove()的时候都会清除线程ThreadLocalMap里所有key为null的value;使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry(key,value)对象

线程,进程,协程

  • 进程:进程是程序的一次执行过程,是系统进行资源调度和分配的的最小单位。

  • 线程:线程是一个比进程更小的执行单位,是CPU调度和分派的最小单位。

  • 协程:协程非常类似于线程。但是协程是协作式多任务的,而线程典型是抢占式多任务的。这意味着协程提供并发性而非并行性。协程超过线程的好处是它们可以用于硬性实时的语境(在协程之间的切换不需要涉及任何系统调用或任何阻塞调用)

  • 区别与联系

    • 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间
    • 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
    • 线程上下文切换比进程上下文切换要快得多

线程死锁

  • 定义:两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

  • 产生必要条件(死锁原因)

    • 互斥条件:该资源任意一个时刻只由一个线程占用。
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
  • 如何避免死锁?

    • 破坏死锁必要条件

synchronized

  • 解释:synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

  • 使用方式

    • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
    • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁。
    • 修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得当前 class 的锁
  • synchronied和ReentrantLock区别

    • 都是可重入锁
    • synchronized 依赖于 JVM, 而 ReentrantLock 依赖于 API
    • 默认都是非公平锁,而ReentrantLock可以修改为公平锁
    • sychronized是一个关键字,ReentrantLock 是一个类
    • sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock锁的是线程,通过代码中int类型的state标识来标识锁的状态
    • synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。
    • 等待可中断。使用synchronized。如果Thread1不释放,Thread2将一直等待,不能被中断。synchronized也可以说是Java提供的原子性内置锁机制。内部锁扮演了互斥锁(mutual exclusion lock ,mutex)的角色,一个线程引用锁的时候,别的线程阻塞等待。
      使用ReentrantLock。如果Thread1不释放,Thread2等待了很长时间以后,可以中断等待,转而去做别的事情
  • synchronied和volatile区别

    • synchronied是重量级锁;volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比synchronized关键字要好 。
    • volatile 关键字只能用于变量,而 synchronized 关键字可以修饰方法以及代码块 。
    • volatile 关键字能保证数据的可见性,避免指令重排,但不能保证数据的原子性,synchronized 关键字三者都能保证。
    • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

volatile

  • Java虚拟机提供的轻量级同步机制

  • 特性

    • 保证可见性:如果一个变量被volatile所修饰的话,当写一个volatile变量时,JMM会把该线程工作内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程工作内存中的volatile变量缓存无效。其他线程读取该变量时,发现自己的缓存行无效,然后去对应的主存读取最新的值。

    • 保证有序性:在对volatile修饰的成员变量进行读写时,会插入内存屏障,而内存屏障可以达到禁止重排序的效果,从而可以保证有序性

    • 为什么不能保证原子性?eg:主存中有i=1,两个线程A,B 执行i++; A从主存中读取i到A的工作内存,未执行i++,被阻塞; B从主存中读取i到B的工作内存,执行i++,i=2,写回主存,并使其他线程中变量缓存无效;然后,A继续执行i++,写回主存,i=2,与理想i=3不符。

      • 如何保证原子性?

        • synchronized 关键字
        • JUC下的Atomic类(采用CAS),eg: AtomicInteger
        • ReentrantLock
  • 应用场景:双重检查实现单例模式

    • public class Singleton{
      private volatile static Singleton instance = null;

      private Singleton() {

      }

      public static Singleton getInstance() {
      if(instancenull) {
      synchronized (Singleton.class) {
      if(instance
      null)
      instance = new Singleton();
      }
      }
      return instance;
      }
      }

    • 优点:这种实现方式缩小锁的范围,减少锁的开销,既可使实现线程安全的创建实例,又不会对性能造成太大的影响,它只是在第一次创建实例的时候同步,以后就不需要同步了,从而加快运行速度。

    • 为什么要使用volatile?

      • 创建一个对象实例,可以分为三步:
        分配对象内存;
        调用构造器方法,执行初始化;
        将对象引用赋值给变量。

      • 不使用的问题

        • 如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常
      • volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性

ConcurrentHashMap

  • 线程安全问题

    • JDK 1.7: 采用分段锁的方式;一个Segment中包含一个HashEntry数组,ReentrantLock锁定操作的Segment,其他的Segment不受影响;而get()方无需加锁,因为HashEntry 涉及到的共享变量都使用 volatile 修饰保证可见性。
    • JDK 1.8: 中采用了CAS+ synchronized, 上锁的是数组的Node首节点。
  • null值:记录的key、value值均不可为null

  • 扩容

    • JDK 1.7:每个Segment内部单独判断,超过阈值则进行扩容
    • JDK 1.8:支持多个线程同时扩容,在扩容之前先生成一个数组,在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工

JMM(Java内存模型)

  • JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,他描述的时一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
  • JMM屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果
  • Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数,主内存是共享区域,所有线程都可以访问。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行,不能直接读写主内存中的变量。

并发与并行

  • 并发:同一个CPU执行多个任务,按细分的CPU时间片交替执行

    • 并发编程的三大特性

      • 原子性 : 一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。

        • 如何保证原子性

          • 通过 synchronized 关键字定义同步代码块或者同步方法保障原子性。
          • 通过 Lock 接口保障原子性。
          • 通过 Atomic 类型保障原子性。
      • 可见性 :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值的。

        • 如何保证可见性

          • 通过 volatile 关键字标记内存屏障保证可见性。
          • 通过 synchronized 关键字定义同步代码块或者同步方法保障可见性
          • 通过 Lock 接口保障可见性。
          • 通过 Atomic 类型保障可见性。
          • 通过 final 关键字保障可见性
      • 有序性 :即程序执行的顺序按照代码的先后顺序执行。Java 在编译器以及运行期间进行代码优化,代码的执行顺序未必就是编写代码时候的顺序。

        • 如何保证有序性

          • 通过 synchronized关键字 定义同步代码块或者同步方法保障可见性。
          • 通过 Lock接口 保障可见性。
          • 通过 volatile 关键字标记内存屏障避免指令重排。
  • 并行:在多个CPU上同时处理多个任务

Runnable和Callable区别

  • Runnable 接口 不会返回结果或抛出检查异常,但是 Callable 接口 可以

线程池

  • 优点

    • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 创建方式

    • ThreadPoolExecutor(推荐使用)

    • Executor 框架的工具类 Executors(避免使用)

      • FixedThreadPool : 该方法返回一个固定线程数量的线程池。

        • 缺点:允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM
      • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。

        • 缺点:允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM
      • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。

        • 缺点:允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM
  • ThreadPoolExecutor

    • 7大参数

      • corePoolSize : 核心线程数,创建后不会消除,是一种常驻线程;线程数定义了最小可以同时运行的线程数量。

      • maximumPoolSize : 最大允许被创建的线程数;当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

      • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,非核心线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。

      • unit : keepAliveTime 参数的时间单位

      • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中,并开始创建新的非核心线程。

      • threadFactory :线程工厂,用来创建线程

      • handler :饱和策略。如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略处理任务

        • AbortPolicy (默认) :丢弃任务 并抛出RejectedExecutionException异常
        • DiscardPolicy: 丢弃任务,但是不抛出异常
        • DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行任务
        • CallerRunsPolicy: 由调用线程处理该任务
  • execute()和submit()

    • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
    • submit()方法用于提交需要返回值的任务
  • 线程池大小确定方式

    • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
    • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
  • 线程复用原理

    • 将线程和任务解耦,每一个线程不停检查阻塞队列中是否有任务需要被执行,如果有则直接执行任务的run方法。

CAS(Compare And Swap)

  • 思想:三个参数,一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false。

  • 优点:解决了原子操作问题.,保证了一致性,提高了并发性。

  • CAS 自旋过程

    • Step1: 获取主内存中的值放入工作内存:this.getIntVolatile(var1, var2);
    • Step2: 如果主内存中的值和工作内存中的相等,说明从第一步获取值到现在还没有其他线程更新过这个值,此时将本线程要更新的值写入到主内存:this.compareAndSwapInt(var1, var2, var6, var6 + var4)
    • Step3: 如果主内存中的值和工作内存中的不相等,则继续从Step 1开始
  • 缺点

    • 线程不会被挂起,而是在不断的消耗CPU试图获取锁,消耗CPU资源;

    • 只能保证一个共享变量的原子操作。

    • ABA问题

      • 问题描述:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

        • 案例:资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律
      • 解决思路:使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一

        • java中提供的ABA解决方式:AtomicStampedReference
  • java中提供的CAS实现方式:AtomicReference

AQS

  • 定义:AbstractQueuedSynchronizer (抽象队列同步器)

  • 原理:

    • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
  • 资源共享方式

    • Exclusive(独占)
    • Share(共享)

JUC并发容器

  • ConcurrentHashMap

    • 在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。
  • CopyOnWriteArrayList

    • 读:

      • 读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。
    • 写:

      • 当 List 需要被修改的时候,并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。
  • ConcurrentLinkedQueue

    • 主要使用 CAS 非阻塞算法来实现线程安全
  • BlockingQueue

    • ArrayBlockingQueue

      • 一旦创建,容量不能改变。其并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。默认情况下不能保证线程访问队列的公平性
    • LinkedBlockingQueue

      • 基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量
    • PriorityBlockingQueue

  • ConcurrentSkipListMap

守护线程

  • 用户线程

    • 用户线程是独立存在的,不会因为其他用户线程退出而退出。
  • 守护线程

    • eg:GC垃圾回收线程
    • 守护线程是依赖于用户线程,用户线程退出了,守护线程也就会退出,

线程通信

  • 锁与同步

    • 场景:存在线程A,B。A先执行完之后,再由B去执行,怎么办呢?最简单的方式就是当前线程对操作的对象加锁。
  • 轮询

  • wait/notify机制

  • 管道通信

线程共享数据

  • 每个线程执行的代码相同,可以使用同一个Runnable对象

    • eg:卖票系统
  • 将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信

    • eg:存钱取钱系统
  • https://www.cnblogs.com/kaituorensheng/p/9503425.html

锁分类

  • 公平锁和非公平锁

    • 公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证了队列中的第一个先得到锁。

      • 优点:所有的线程都能得到资源,不会饿死在队列中。
      • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
      • ReentrantLock(true)可以创建公平锁
    • 非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时直接去尝试获取锁(插队),获取不到(插队失败),再进入队列等待(失败则乖乖排队),如果能获取到(插队成功),就直接获取到锁。

      • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
      • 缺点:可能导致队列中排队的线程一直获取不到锁或者长时间获取不到锁,活活饿死。
      • synchronized是非公平锁,ReentrantLock默认都非公平锁
  • 可重入锁

    • 当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
    • 同一个线程在外层同步方法获取锁的时候,在进入内层同步方法会自动获取锁
    • 可重入锁在内部维护了一个计数器,用于记录重入次数。自己获得一次,那么计数器+1,释放一次计数器-1,如果计数器为0,说明该线程释放了该锁,否则,锁仍旧被该线程持有
    • 优点:防止死锁
    • synchronized和ReentantLock都是重复锁
  • 自旋锁

    • 当前线程获取锁时,如果发现锁已经被其他线程占有,并不会马上阻塞自己,在不放弃CPU的情况下,多次尝试获取锁

    • 自旋锁与互斥锁的区别

      • 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
  • 独占锁和共享锁

    • 独占锁:如果一个锁仅仅只能被一个线程程拥有,那么他就是独占的;独占锁会保障任何时候都只是有一个线程进行访问,ReentrantLock就是独占锁,synchronized的原理也是独占锁
    • 共享锁:如果一个锁可以同时被多个线程拥有,那么他就是共享的;读写锁ReadWriteLock就是共享锁,可以同时允许多个读线程进行操作

接口幂等性

  • 定义:以相同的请求调用这个接口一次和调用这个接口多次,对系统产生的影响是相同的。如果一个接口满足这个特性,那么我们就说这个
    接口是一个幂等接口。

  • 实现方式

    • (1)唯一id。每次操作,都根据操作和内容生成唯一的id,在执行之前先判断id是否存在,如果不存在则执行后续操作,并且保存到数据库或者redis等;
      (2)服务端提供发送token的接口,业务调用接口前先获取token,然后调用业务接口请求时,把token携带过去,务器判断token是否存在redis中,存在表示第一次请求,可以继续执行业务,执行业务完成后,最后需要把redis中的token删除;
      (3)建去重表。将业务中有唯一标识的字段保存到去重表,如果表中存在,则表示已经处理过了;
      (4)版本控制。增加版本号,当版本号符合时,才能更新数据;
      (5)状态控制。例如订单有状态已支付未支付支付中支付失败,当处于未支付的时候才允许修改为支付中等。

锁升级

  • 锁状态的级别

    • 无锁

      • 无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
    • 偏向锁

      • 偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
    • 轻量级锁

      • 轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。
    • 重量级锁

      • 计数器记录自旋次数(默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁,当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起,等待将来被唤醒。
    • 参考博客:https://segmentfault.com/a/1190000022904663

Guess you like

Origin blog.csdn.net/qq_36940806/article/details/121209015