2025-06-05
内卷九股文
0

目录

高效并发
线程的基础概念
进程与线程
多线程
串行、并行、并发
同步异步、阻塞非阻塞
线程的创建
继承Thread类 重写run方法
实现Runnable接口 重写run方法
基于线程池构建线程
线程状态
新建(New):
运行(Runnable):
无限期等待(Waiting):
限期等待(Timed Waiting):
阻塞(Blocked):
结束(Terminated):
源码
线程的使用
线程的常用方法
获取当前线程
线程的名字
线程的优先级
线程的让步
线程的休眠
线程的强占
守护线程
线程的等待和唤醒
线程的结束方式
stop方法(不用)
使用共享变量(很少会用)
interrupt方式
线程的安全
互斥同步
非阻塞同步

在梳理了Java内存模型的脉络以后,就讲到线程的基本状态,了解线程存在哪些状态才能对线程安全保障有更清晰的认知。其中JUC中线程安全,有 互斥同步非阻塞同步 两种方案,本质就是有锁和无锁的区别,在资源竞争比较大的情况下,无锁的性能要远远高于有锁的;说到了锁,那不得不说一下JVM在锁的方面做出的优化,这也是面试常问synchronized锁升级的细节点。

高效并发

衡量一个服务性能的高低好坏,每秒事务处理数(Transactions Per Second, TPS)是重要的指标之一,它代表着一秒内服务端平均能响应的请求总数,而TPS值与程序的并发能力又有非常密切的关系。对于计算量相同的任务,程序线程并发协调得越有条不紊,效率自然就会越高;反之,线程之间频繁争用数据,互相阻塞甚至死锁,将会大大降低程序的并发能力。

线程的基础概念

进程与线程

什么是进程?

进程是指运行中的程序。 比如我们使用钉钉,浏览器,需要启动这个程序,操作系统会给这个程序分配一定的资源(占用内存资源)。

什么线程?

线程是CPU调度的基本单位,每个线程执行的都是某一个进程的代码的某个片段。

举个栗子:房子与人

比如现在有一个100平的房子,这个方式可以看做是一个进程

房子里有人,人就可以看做成一个线程。

人在房子中做一个事情,比如吃饭,学习,睡觉。这个就好像线程在执行某个功能的代码。

所谓进程就是线程的容器,需要线程利用进程中的一些资源,处理一个代码、指令。最终实现进程锁预期的结果。

进程和线程的区别:

  • 根本不同:进程是操作系统分配的资源,而线程是CPU调度的基本单位。
  • 资源方面:同一个进程下的线程共享进程中的一些资源。线程同时拥有自身的独立存储空间。进程之间的资源通常是独立的。
  • 数量不同:进程一般指的就是一个进程。而线程是依附于某个进程的,而且一个进程中至少会有一个或多个线程。
  • 开销不同:毕竟进程和线程不是一个级别的内容,线程的创建和终止的时间是比较短的。而且线程之间的切换比进程之间的切换速度要快很多。而且进程之间的通讯很麻烦,一般要借助内核才可以实现,而线程之间通讯,相当方面。

多线程

什么是多线程?

多线程是指:单个进程中同时运行多个线程。

多线程的不低是为了提高CPU的利用率。

可以通过避免一些网络IO或者磁盘IO等需要等待的操作,让CPU去调度其他线程。

这样可以大幅度的提升程序的效率,提高用户的体验。

比如Tomcat可以做并行处理,提升处理的效率,而不是一个一个排队。

不如要处理一个网络等待的操作,开启一个线程去处理需要网络等待的任务,让当前业务线程可以继续往下执行逻辑,效率是可以得到大幅度提升的。

多线程的局限

  • 如果线程数量特别多,CPU在切换线程上下文时,会额外造成很大的消耗。
  • 任务的拆分需要依赖业务场景,有一些异构化的任务,很难对任务拆分,还有很多业务并不是多线程处理更好。
  • 线程安全问题:虽然多线程带来了一定的性能提升,但是再做一些操作时,多线程如果操作临界资源,可能会发生一些数据不一致的安全问题,甚至涉及到锁操作时,会造成死锁问题。

串行、并行、并发

什么是串行:

串行就是一个一个排队,第一个做完,第二个才能上。

什么是并行:

并行就是同时处理。(一起上!!!)

什么是并发:

这里的并发并不是三高中的高并发问题,这里是多线程中的并发概念(CPU调度线程的概念)。CPU在极短的时间内,反复切换执行不同的线程,看似好像是并行,但是只是CPU高速的切换。

并行囊括并发。

并行就是多核CPU同时调度多个线程,是真正的多个线程同时执行。

单核CPU无法实现并行效果,单核CPU是并发。

同步异步、阻塞非阻塞

同步与异步:执行某个功能后,被调用者是否会主动反馈信息

阻塞和非阻塞:执行某个功能后,调用者是否需要一直等待结果的反馈。

两个概念看似相似,但是侧重点是完全不一样的。

同步阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,需要一直等待水烧开。

同步非阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能,但是需要时不时的查看水开了没。

异步阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,需要一直等待水烧开。

异步非阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能。

异步非阻塞这个效果是最好的,平时开发时,提升效率最好的方式就是采用 异步非阻塞 的方式处理一些多线程的任务。

线程的创建

线程的创建分为三种方式:

继承Thread类 重写run方法

启动线程是调用start方法,这样会创建一个新的线程,并执行线程的任务。

如果直接调用run方法,这样会让当前线程执行run方法中的业务逻辑。

java
public class MiTest { public static void main(String[] args) { MyJob t1 = new MyJob(); t1.start(); for (int i = 0; i < 100; i++) { System.out.println("main:" + i); } } } class MyJob extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("MyJob:" + i); } } }

实现Runnable接口 重写run方法

java
public class MiTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread t1 = new Thread(myRunnable); t1.start(); for (int i = 0; i < 1000; i++) { System.out.println("main:" + i); } } } class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("MyRunnable:" + i); } } }

最常用的方式:

  • 匿名内部类方式:
    java
    Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("匿名内部类:" + i); } } });
  • lambda方式:
    java
    Thread t2 = new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("lambda:" + i); } });

实现Callable 重写call方法,配合FutureTask

Callable一般用于有返回结果的非阻塞的执行方法

同步非阻塞。

java
public class MiTest { public static void main(String[] args) throws ExecutionException, InterruptedException { //1. 创建MyCallable MyCallable myCallable = new MyCallable(); //2. 创建FutureTask,传入Callable FutureTask futureTask = new FutureTask(myCallable); //3. 创建Thread线程 Thread t1 = new Thread(futureTask); //4. 启动线程 t1.start(); //5. 做一些操作 //6. 要结果 Object count = futureTask.get(); System.out.println("总和为:" + count); } } class MyCallable implements Callable{ @Override public Object call() throws Exception { int count = 0; for (int i = 0; i < 100; i++) { count += i; } return count; } }

基于线程池构建线程

追其底层,其实只有一种,实现Runnble

线程状态

Java语言定义了6种线程状态,在任意一个时间点中,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间转换。这6种状态分别是:

image.png

新建(New):

创建后尚未启动的线程处于这种状态。

java
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { }); System.out.println(t1.getState()); }

运行(Runnable):

包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可 能正在执行,也有可能正在等待着操作系统为它分配执行时间。

java
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(true){ } }); t1.start(); Thread.sleep(500); System.out.println(t1.getState()); }

无限期等待(Waiting):

处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线 程显式唤醒。以下方法会让线程陷入无限期的等待状态:

  • 没有设置Timeout参数的Object::wait()方法;
  • 没有设置Timeout参数的Thread::join()方法;
  • LockSupport::park()方法。
java
public static void main(String[] args) throws InterruptedException { Object obj = new Object(); Thread t1 = new Thread(() -> { synchronized (obj){ try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); Thread.sleep(500); System.out.println(t1.getState()); }

限期等待(Timed Waiting):

处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:

  • Thread::sleep()方法;
  • 设置了Timeout参数的Object::wait()方法;
  • 设置了Timeout参数的Thread::join()方法;
  • LockSupport::parkNanos()方法;
  • LockSupport::parkUntil()方法。
java
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); Thread.sleep(500); System.out.println(t1.getState()); }

阻塞(Blocked):

线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时 间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。

java
public static void main(String[] args) throws InterruptedException { Object obj = new Object(); Thread t1 = new Thread(() -> { // t1线程拿不到锁资源,导致变为BLOCKED状态 synchronized (obj){ } }); // main线程拿到obj的锁资源 synchronized (obj) { t1.start(); Thread.sleep(500); System.out.println(t1.getState()); } }

结束(Terminated):

已终止线程的线程状态,线程已经结束执行

java
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); Thread.sleep(1000); System.out.println(t1.getState()); }

源码

其实线程的状态,在我们平时使用的 Thread 线程类中就有状态枚举的定义,之前看过好多对于状态讲解的文章,都有问题,纯粹误人子弟

java
public class Thread implements Runnable { ... public enum State { /** * Thread state for a thread which has not yet started. */ NEW, /** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE, /** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {@link Object#wait() Object.wait}. */ BLOCKED, /** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * <ul> * <li>{@link Object#wait() Object.wait} with no timeout</li> * <li>{@link #join() Thread.join} with no timeout</li> * <li>{@link LockSupport#park() LockSupport.park}</li> * </ul> * * <p>A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called {@code Object.wait()} * on an object is waiting for another thread to call * {@code Object.notify()} or {@code Object.notifyAll()} on * that object. A thread that has called {@code Thread.join()} * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * <ul> * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> * </ul> */ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; } /** * Returns the state of this thread. * This method is designed for use in monitoring of the system state, * not for synchronization control. * * @return this thread's state. * @since 1.5 */ public State getState() { // get current thread state return jdk.internal.misc.VM.toThreadState(threadStatus); } ... }

线程的使用

线程的常用方法

获取当前线程

Thread的静态方法获取当前线程对象

java
public static void main(String[] args) throws ExecutionException, InterruptedException { // 获取当前线程的方法 Thread main = Thread.currentThread(); System.out.println(main); // "Thread[" + getName() + "," + getPriority() + "," + group.getName() + "]"; // Thread[main,5,main] }

线程的名字

在构建Thread对象完毕后,一定要设置一个有意义的名称,方面后期排查错误

java
public static void main(String[] args) throws ExecutionException, InterruptedException { Thread t1 = new Thread(() -> { System.out.println(Thread.currentThread().getName()); }); t1.setName("模块-功能-计数器"); t1.start(); }

线程的优先级

其实就是CPU调度线程的优先级、

java中给线程设置的优先级别有10个级别,从1~10任取一个整数。

如果超出这个范围,会排除参数异常的错误

java
public static void main(String[] args) throws ExecutionException, InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { System.out.println("t1:" + i); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { System.out.println("t2:" + i); } }); t1.setPriority(1); t2.setPriority(10); t2.start(); t1.start(); }

线程的让步

可以通过Thread的静态方法yield,让当前线程从运行状态转变为就绪状态。

java
public static void main(String[] args) throws ExecutionException, InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { if(i == 50){ Thread.yield(); } System.out.println("t1:" + i); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("t2:" + i); } }); t2.start(); t1.start(); }

线程的休眠

Thread的静态方法,让线程从运行状态转变为等待状态

sleep有两个方法重载:

  • 第一个就是native修饰的,让线程转为等待状态的效果
  • 第二个是可以传入毫秒和一个纳秒的方法(如果纳秒值大于等于0.5毫秒,就给休眠的毫秒值+1。如果传入的毫秒值是0,纳秒值不为0,就休眠1毫秒)

sleep会抛出一个InterruptedException

java
public static void main(String[] args) throws InterruptedException { System.out.println(System.currentTimeMillis()); Thread.sleep(1000); System.out.println(System.currentTimeMillis()); }

线程的强占

Thread的非静态方法join方法

需要在某一个线程下去调用这个方法

如果在main线程中调用了t1.join(),那么main线程会进入到等待状态,需要等待t1线程全部执行完毕,在恢复到就绪状态等待CPU调度。

如果在main线程中调用了t1.join(2000),那么main线程会进入到等待状态,需要等待t1执行2s后,在恢复到就绪状态等待CPU调度。如果在等待期间,t1已经结束了,那么main线程自动变为就绪状态等待CPU调度。

java
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println("t1:" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); for (int i = 0; i < 10; i++) { System.out.println("main:" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if (i == 1){ try { t1.join(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } }

守护线程

默认情况下,线程都是非守护线程

JVM会在程序中没有非守护线程时,结束掉当前JVM

主线程默认是非守护线程,如果主线程执行结束,需要查看当前JVM内是否还有非守护线程,如果没有JVM直接停止

java
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) { System.out.println("t1:" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.setDaemon(true); t1.start(); }

线程的等待和唤醒

可以让获取synchronized锁资源的线程通过wait方法进去到锁的等待池,并且会释放锁资源

可以让获取synchronized锁资源的线程,通过notify或者notifyAll方法,将等待池中的线程唤醒,添加到锁池

notify随机的唤醒等待池中的一个线程到锁池

notifyAll将等待池中的全部线程都唤醒,并且添加到锁池

在调用wait方法和notify以及norifyAll方法时,必须在synchronized修饰的代码块或者方法内部才可以,因为要操作基于某个对象的锁的信息维护。

java
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { sync(); },"t1"); Thread t2 = new Thread(() -> { sync(); },"t2"); t1.start(); t2.start(); Thread.sleep(12000); synchronized (MiTest.class) { MiTest.class.notifyAll(); } } public static synchronized void sync() { try { for (int i = 0; i < 10; i++) { if(i == 5) { MiTest.class.wait(); } Thread.sleep(1000); System.out.println(Thread.currentThread().getName()); } } catch (InterruptedException e) { e.printStackTrace(); } }

线程的结束方式

线程结束方式很多,最常用就是让线程的run方法结束,无论是return结束,还是抛出异常结束,都可以

stop方法(不用)

强制让线程结束,无论你在干嘛,不推荐使用当然当然方式,但是,他确实可以把线程干掉

java
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } }); t1.start(); Thread.sleep(500); t1.stop(); System.out.println(t1.getState()); }

使用共享变量(很少会用)

这种方式用的也不多,有的线程可能会通过死循环来保证一直运行。

咱们可以通过修改共享变量在破坏死循环,让线程退出循环,结束run方法

java
static volatile boolean flag = true; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(flag){ // 处理任务 } System.out.println("任务结束"); }); t1.start(); Thread.sleep(500); flag = false; }

interrupt方式

共享变量方式

java
public static void main(String[] args) throws InterruptedException { // 线程默认情况下, interrupt标记位:false System.out.println(Thread.currentThread().isInterrupted()); // 执行interrupt之后,再次查看打断信息 Thread.currentThread().interrupt(); // interrupt标记位:ture System.out.println(Thread.currentThread().isInterrupted()); // 返回当前线程,并归位为false interrupt标记位:ture System.out.println(Thread.interrupted()); // 已经归位了 System.out.println(Thread.interrupted()); // ===================================================== Thread t1 = new Thread(() -> { while(!Thread.currentThread().isInterrupted()){ // 处理业务 } System.out.println("t1结束"); }); t1.start(); Thread.sleep(500); t1.interrupt(); }

通过打断WAITING或者TIMED_WAITING状态的线程,从而抛出异常自行处理

这种停止线程方式是最常用的一种,在框架和JUC中也是最常见的

java
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(true){ // 获取任务 // 拿到任务,执行任务 // 没有任务了,让线程休眠 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); System.out.println("基于打断形式结束当前线程"); return; } } }); t1.start(); Thread.sleep(500); t1.interrupt(); }

wait和sleep的区别?

  • 单词不一样。
  • sleep属于Thread类中的static方法、wait属于Object类的方法
  • sleep属于TIMED_WAITING,自动被唤醒、wait属于WAITING,需要手动唤醒。
  • sleep方法在持有锁时,执行,不会释放锁资源、wait在执行后,会释放锁资源。
  • sleep可以在持有锁或者不持有锁时,执行。 wait方法必须在只有锁时才可以执行。

wait方法会将持有锁的线程从owner扔到WaitSet集合中,这个操作是在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。

线程的安全

在前面讲述了JMM内存模型,了解了并发三大特性后,接下来就需要认识一下线程之间的安全问题,那么如何解决线程之间的问题呢?

互斥同步

在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成 monitorentermonitorexit 这两个字节码指令。

根据《Java虚拟机规范》的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行 monitorexit 指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。(synchronized 的可重入的原理)

自JDK 5起 (实现了JSR 166[1]),Java类库中新提供了java.util.concurrent包(下文称J.U.C包),其中的 java.util.concurrent.locks.Lock 接口便成了Java的另一种全新的互斥同步手段。基于Lock接口,用户能够 以非块结构(Non-Block Structured)来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面 去实现同步,这也为日后扩展出不同调度算法、不同特征、不同性能、不同语义的各种锁提供了广阔的空间。

重入锁(ReentrantLock)是Lock接口最常见的一种实现,顾名思义,它与synchronized一样是可重入的。在基本用法上,ReentrantLock也与synchronized很相似,只是代码写法上稍有区别而已。

Synchronized 与 ReentrantLock 锁的主要区别

等待可中断:
是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。

公平锁、非公平锁:
是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平 锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非 公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量。

锁绑定多个条件:
是指一个ReentrantLock对象可以同时绑定多个Condition对象。 在synchronized 中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁; 而ReentrantLock则无须这样做,多次调用newCondition()方法即可。

非阻塞同步

互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的性能开销,因此这种同步也被称为阻塞同步(Blocking Synchronization)。

从解决问题的方式上看,互斥同步属于一种悲观的并发策略,其总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享的数据是否真的会出现竞争,它都会进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加 锁),这将会导致用户态到核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等开销。

基于冲突检测的乐观并发策略,通俗地说就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronization),使用这种措施的代码也常被称为无锁(Lock-Free) 编程。

比较并交换(Compare-and-Swap,下文称CAS)

CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。

CAS具体实现:原子自增 AtomicInteger #incrementAndGet()方法的JDK源码

java
/** * Atomically increment by one the current value. * @return the updated value */ public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }

incrementAndGet()方法在一个无限循环中,不断尝试将一个比当前值大一的新值赋值给自己。如果失败了,那说明在执行CAS操作的时候,旧值已经发生改变,于是再次循环进行下一次操作,直到设置成功为止。

尽管CAS看起来很美好,既简单又高效,但显然这种操作无法涵盖互斥同步的所有使用场景,并且CAS从语义上来说并不是真正完美的,它存在一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?

“ABA”问题

这是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA问题”。J.U.C包为了解决这个问题,提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。不过目前来说这个类处于相当鸡肋的位置,大部分情况下ABA问题不会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更为高效。

CAS存在的问题

1、ABA问题:添加版本号解决,JUC提供了 AtomicStampedReference 原子更新带有版本号的引用类型,通过控制变量值的版本来保证CAS的正确性,解决CAS的“ABA”问题

2、自旋时间过长导致处理器资源的消耗:可以指定自旋次数,JVN提供参数-XX:PreBlockSpin调整,自旋次数的默认值是10次,

3、只能保证一个共享变量的原子操作,对于多个共享变量,需要封装成一个对象,通过 AtomicReference 原子更新的对象引用处理,可以把多个变量放在一个对象里来进行CAS操作

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!