java多线程面试题

简介: java多线程面试题

线程和进程的区别是什么?

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地 址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一 个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的 地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程 序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进 行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

创建线程的方式

比较常见的一个问题了,一般就是两种:

1)继承 Thread 类

2)实现 Runnable 接口 至于哪个好,不用说肯定是后者好,因为实现接口的方式比继承类的方式更灵 活,也能减少程序之间的耦合度

概括的解释下线程的几种可用状态

• 新建 new。

• 就绪 放在可运行线程池中,等待被线程调度选中,获取 cpu。

• 运行 获得了 cpu。

• 阻塞 o

               等待阻塞 执行 wait() 。

               同步阻塞 获取对象的同步琐时,同步锁被别的线程占用。

               其他阻塞 执行了 sleep() 或 join() 方法)。

• 死亡。

现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执 行?

这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟 悉。这个多线程问题比较简单,可以用 join 方法实现。

public class Main {
    public static void main(String[] args){
        new Thread(new Task1()).start();
        new Thread(new Task2()).start();
        new Thread(new Task3()).start();
 
    }
     static class Task1 implements Runnable {
        @Override
        public void run() {
            System.out.println("任务1开始执行");
            // 任务1的代码
            System.out.println("任务1执行完毕");
        }
    }
     static class Task2 implements Runnable {
        @Override
        public void run() {
            System.out.println("任务2开始执行");
            // 任务2的代码
            System.out.println("任务2执行完毕");
        }
    }
     static class Task3 implements Runnable {
        @Override
        public void run() {
            System.out.println("任务3开始执行");
            // 任务3的代码
            System.out.println("任务3执行完毕");
        }
    }
}

线程 yield()方法有什么用?

Yield 方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。 它是一个静态方法而且只保证当前线程放弃 CPU 占用而不能保证使其它线程一定 能占用 CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行

Fork/Join 框架是干什么的?

大任务自动分散小任务,并发执行,合并小任务结果

sleep 方法和 wait 方法有什么区别

sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不 同点在于如果线程持有某个对象的监视器,sleep 方法不会放弃这个对象的监 视器,wait 方法会放弃这个对象的监视器;

sleep 就是正在执行的线程主动让出 cpu,cpu 去执行其他线程,在 sleep 指定的时 间过后,cpu 才会回到这个线程上继续往下执行,如果当前线程进入了同步锁,sleep 方法并不会释放锁,即使当前线程使用 sleep 方法让出了 cpu,但其他被同步锁挡住 了的线程也无法得到执行。wait 是指在一个已经进入了同步锁的线程内,让自己暂时 让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用 了 notify 方法(notify 并不释放锁,只是告诉调用过 wait 方法的线程可以去参与获 得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果 notify 方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在 notfiy 方法后 增加一个等待和一些代码,看看效果),调用 wait 方法的线程就会解除 wait 状态和 程序可以

请说出你所知道的线程同步的方法。

wait():使一个线程处于等待状态,并且释放所持有的对象的 lock。

sleep():使一个 正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉 InterruptedException 异常。

notify():唤醒一个处于等待状态的线程,注意的是在 调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒 哪个线程,而且不是按优先级。

notityAll():唤醒所有处入等待状态的线程,注意并 不是给所有唤醒线程一个对象的锁,而是让它们竞争

再次得到锁后继续向下运行

synchronized 和 java.util.concurrent.locks.Lock 的异同?

主要相同点:Lock 能完成 synchronized 所实现的所有功能。 主要不同点:Lock 有比 synchronized 更精确的线程语义和更好的性能。 synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且必须在 finally 从句中释放。Lock 还有更强大的功能,例如,它的 tryLock 方法可以非阻塞方式去拿 锁。

stop() 和 suspend() 方法为何不推荐使用?

反对使用 stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象 处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出 真正的问题所在。 suspend() 方法容易发生死锁。调用 suspend() 的时候,目标线程会停下来,但却仍 然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被 "挂 起" 的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任 何一个锁定的资源,就会造成死锁。所以不应该使用 suspend(),而应在自己的 Thread 类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait() 命其进入等待状态。若标志指出线程应当恢复,则用一个 notify() 重新启动线程。

4、start()方法和 run()方法的区别

只有调用了 start()方法,才会表现出多线程的特性,不同线程的 run()方法 里面的代码交替执行。如果只是调用 run()方法,那么代码还是同步执行的, 必须等待一个线程的 run()方法里面的代码全部执行完毕之后,另外一个线程 才可以执行其 run()方法里面的代码。

Runnable 接口和 Callable 接口的区别

Runnable 接口中的 run()方法的返回值是 void,它做的事情只是纯粹地去执 行 run()方法中的代码而已;Callable 接口中的 call()方法是有返回值的,是 一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要 原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多 久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们 能 做 的 只 是 等 待 这 条 多 线 程 的 任 务 执 行 完 毕 而 已 。 而 Callable+Future/FutureTask 却可以获取多线程运行的结果,可以在等待时 间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用

CyclicBarrier 和 CountDownLatch 的区别

两个看上去有点像的类,都在 java.util.concurrent 下,都可以用来表示代 码运行到某个点上,二者的区别在于:

1)CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到 所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不 是,某线程运行到某个点上之后,只是给某个数值-1 而已,该线程继续运行。

2)CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务。

3) CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为 0 该 CountDownLatch 就不可再用了。

volatile 关键字的作用

多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修 饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一 定是最新的数据。

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性, 详 细 的 可 以 参 见 java.util.concurrent.atomic 包 下 的 类 , 比 如 AtomicInteger

如何在两个线程之间共享数据

通 过 在 线 程 之 间 共 享 对 象 就 可 以 了 , 然 后 通 过 wait/notify/notifyAll 、 await/signal/signalAll 进行唤起和等待,比方说阻塞队列 BlockingQueue 就是为线程之间共享数据而设计的

ThreadLocal 有什么用

简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维 护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔 离,数据不共享,自然就没有线程安全方面的问题了

ThreadLocal 用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全 局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用 同步的时候,我们可以选择 ThreadLocal 变量。 每个线程都会拥有他们自己的 Thread 变量,它们可以使用 get()\set() 方法去获取他 们的默认值或者在线程内部改变他们的值。ThreadLocal 实例通常是希望它们同线程 状态关联起来是 private static 属性。

1、Java中的ThreadLocal类允许我们创建只能被同⼀个线程读写的变量。因此,如果⼀段代码含有⼀个ThreadLocal变量 的引⽤,即使两个线程同时执⾏这段代码,它们也⽆法访问到对⽅的ThreadLocal变量。 1、概念:线程局部变量。在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同⼀个变量,显 然是不⾏的,并且我们也知道volatile这个关键字也是不能保证线程安全的。那么在有⼀种情况之下,我们需要满⾜这样⼀个条件:变 量是同⼀个,但是每个线程都使⽤同⼀个初始值,也就是使⽤同⼀个变量的⼀个新的副本。这种情况之下ThreadLocal就⾮常适⽤,⽐ 如说DAO的数据库连接,我们知道DAO是单例的,那么他的属性Connection就不是⼀个线程安全的变量。⽽我们每个线程都需要使⽤ 他,并且各⾃使⽤各⾃的。这种情况,ThreadLocal就⽐较好的解决了这个问题。

2、原理:从本质来讲,就是每个线程都维护了⼀个map,⽽这个map的key就是threadLocal,⽽值就是我们set的那个值,每次 线程在get的时候,都从⾃⼰的变量中取值,既然从⾃⼰的变量中取值,那肯定就不存在线程安全问题,总体来讲,ThreadLocal这个 变量的状态根本没有发⽣变化,他仅仅是充当⼀个key的⻆⾊,另外提供给每⼀个线程⼀个初始值。

3、实现机制:每个Thread对象内部都维护了⼀个ThreadLocalMap这样⼀个ThreadLocal的Map,可以存放若⼲个 ThreadLocal

4、应⽤场景:当很多线程需要多次使⽤同⼀个对象,并且需要该对象具有相同初始化值的时候最适合使⽤ThreadLocal。

wait()方法和 notify()/notifyAll()方法在放弃对象监视器时有什么

区别 wait()方法和 notify()/notifyAll()方法在放弃对象监视器的时候的区别在于: wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线 程剩余代码执行完毕才会放弃对象监视器

怎么检测一个线程是否持有对象监视器

有方法可以判断某个线程是否持 有对象监视器:Thread 类提供了一个 holdsLock(Object obj)方法,当且仅 当对象 obj 的监视器被某条线程持有的时候才会返回 true,注意这是一个 static 方法,这意味着"某条线程"指的是当前线程。

synchronized 和 ReentrantLock 的区别

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各 样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上: (1)ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁 (2)ReentrantLock 可以获取各种锁的信息 (3)ReentrantLock 可以灵活地实现多路通知 另外,二者的锁机制其实也是不一样的。 ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中 mark word

ConcurrentHashMap 的并发度是什么

ConcurrentHashMap 的并发度就是 segment 的大小,默认为 16,这意味 着 最 多 同 时 可 以 有 16 条线程 操 作 ConcurrentHashMap ,这也是ConcurrentHashMap 对 Hashtable 的最大优势,任何情况下,Hashtable 能同时有两条线程获取 Hashtable 中的数据吗?

ReadWriteLock 是什么

首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时 候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、 线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降 低了程序的性能。 因为这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写 锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实 现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥, 读和写、写和读、写和写之间才会互斥,提升了读写的性能。

FutureTask 是什么 这个其实前面有提到过

FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结 果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于 FutureTask 也是 Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

Linux 环境下如何查找哪个线程使用 CPU 最长

这是一个比较偏实践的问题,这种问题我觉得挺有意义的。可以这么做: (1)获取项目的 pid,jps 或者 ps -ef | grep java,这个前面有讲过 (2)top -H -p pid,顺序不能改变 这样就可以打印出当前的项目,每条线程占用 CPU 时间的百分比

Java 中用到的线程调度算法是什么

抢占式。一个线程用完 CPU 之后,操作系统会根据线程优先级、线程饥饿情 况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

Thread.sleep(0)的作用是什么

这个问题和上面那个问题是相关的。由于 Java 采用抢占式 的线程调度算法,因此可能会出现某条线程常常获取到 CPU 控制权的情况, 为 了 让 某 些 优 先 级 比 较 低 的 线 程 也 能 获 取 到 CPU 控 制 权 , 可 以 使 用 Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡 CPU 控制权的一种操作。

多线程中的忙循环是什么?

忙循环就是程序员用循环让一个线程等待,不像传统方法 wait(), sleep() 或 yield() 它 们都放弃了 CPU 控制,而忙循环不会放弃 CPU,它就是在运行一个空循环。这么做 的目的是为了保留 CPU 缓存。 在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。 为了避免重建缓存和减少等待重建的时间就可以使用它了

什么是自旋

很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此 时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态 和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让 等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自 旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的 策略。

什么是 CAS

CAS,全称为 Compare and Swap,即比较-替换。假设有三个操作数:内 存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同 时,才会将内存值修改为 B 并返回 true,否则什么都不做并返回 false。当 然 CAS 一定要 volatile 变量配合,这样才能保证每次拿到的变量是主内存中 最新的那个值,否则旧的预期值 A 对某条线程来说,永远是一个不会变的值 A, 只要某次 CAS 操作失败,永远都不可能成功

什么是 AQS

简单说一下 AQS,AQS 全称为 AbstractQueuedSychronizer,翻译过来应 该是抽象队列同步器。 如果说 java.util.concurrent 的基础是 CAS 的话,那么 AQS 就是整个 Java 并发包的核心了,ReentrantLock、CountDownLatch、Semaphore 等等 都用到了它。AQS 实际上以双向队列的形式连接所有的 Entry,比方说 ReentrantLock,所有等待的线程都被放在一个 Entry 中并连成双向队列, 前面一个线程使用 ReentrantLock 好了,则双向队列实际上的第一个 Entry 开始运行。 AQS 定义了对双向队列所有的操作,而只开放了 tryLock 和 tryRelease 方 法给开发者使用,开发者可以根据自己的实现重写 tryLock 和 tryRelease 方 法,以实现自己的并发功能。

Semaphore 有什么作用

Semaphore 就 是 一 个 信 号 量 , 它 的 作 用 是 限 制 某 段 代 码 块 的 并 发 数 。 Semaphore 有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最 多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完 毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数 中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。

怎么控制同一时间只有 3 个线程运行?

用 Semaphore

线程类的构造方法、静态块是被哪个线程调用的

这是一个非常刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自 身所调用的。 如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了 Thread1,main 函数中 new 了 Thread2,那么: 1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run() 方法是 Thread2 自己调用的 2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方 法是 Thread1 自己调用的

同步方法和同步块

哪个是更好的选择 同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升 代码的效率。请知道一条原则:同步的范围越小越好。 借着这一条,我额外提一点,虽说同步的范围越少越好,但是在 Java 虚拟机 中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这 是有用的,比方说 StringBuffer,它是一个线程安全的类,自然最常用的 append()方法是一个同步方法,我们写代码的时候会反复 append 字符串,

高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行 时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程 池?

这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个 人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于 这个问题,个人看法是: 1)高并发、任务执行时间短的业务,线程池线程数可以设置为 CPU 核数+1, 减少线程上下文的切换 2)并发不高、任务执行时间长的业务要区分开看: a)假如是业务时间长集中在 IO 操作上,也就是 IO 密集型的任务,因为 IO 操作并不占用 CPU,所以不要让所有的 CPU 闲下来,可以加大线程池中的线 程数目,让 CPU 处理更多的业务 b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没 办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文 的切换 c)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于 整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服 务器是第二步,至于线程池的设置,设置参考其他有关线程池的文章。最后, 业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务 进行拆分和解耦。

什么是活锁、饥饿、无锁、死锁?

死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现 了这三种情况,即线程不再活跃,不能再正常地执行下去了。

死锁 死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又相互等 对方释放锁,此时若无外力干预,这些线程则一直处理阻塞的假死状态,形成死锁。 举个例子,A 同学抢了 B 同学的钢笔,B 同学抢了 A 同学的书,两个人都相互占 用对方的东西,都在让对方先还给自己自己再还,这样一直争执下去等待对方还而 又得不到解决,老师知道此事后就让他们相互还给对方,这样在外力的干预下他们 才解决,当然这只是个例子没有老师他们也能很好解决,计算机不像人如果发现这 种情况没有外力干预还是会一直阻塞下去的。

活锁 活锁这个概念大家应该很少有人听说或理解它的概念,而在多线程中这确实存在。 活锁恰恰与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿 到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别 的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。

饥饿 我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执 行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级线程无 法得到执行,这就是饥饿。当然还有一种饥饿的情况,一个线程一直占着一个资源 不放而导致其他线程得不到执行,与死锁不同的是饥饿在以后一段时间内还是能够 得到执行的,如那个占用资源的线程结束了并释放了资源。

无锁 无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时 只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行, 线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下 一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功, 而其他修改失败的线程会不断重试直到修改成功。之前的文章我介绍过 JDK 的 CAS 原理及应用即是无锁的实现。 可以看出,无锁是一种非常良好的设计,它不会出现线程出现的跳跃性问题,锁使 用不当肯定会出现系统性能问题,虽然无锁无法全面代替有锁,但无锁在某些场合 下是非常高效的

什么是乐观锁和悲观锁

1)乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上 锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以 使用版本号机制和 CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提 高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐 观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了 乐观锁的一种实现方式 CAS 实现的

2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲 观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持 有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作 资源了。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁 等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。

两种锁的使用场景 从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一 种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的 时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的 情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反 倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

乐观锁常见的两种实现方式 乐观锁一般会使用版本号机制或 CAS 算法实现。

版本号机制

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次 数,当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数 据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当 前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。 举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值 为 1 ;而当前帐户余额字段( balance )为 $100 。

1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50 ( $100-$50 )。

2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。

3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同 帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提 交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。

4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库 提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操 作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满 足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略, 因此,操作员 B 的提交被驳回。 这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。

CAS 算法

即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程, 即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的 情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数 

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则 不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操 作,即不断的重试。

乐观锁的缺点

1、ABA 问题是乐观锁一个常见的问题

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然 是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能 的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就 会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。 JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是 否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为 给定的更新值。

2、循环时间长开销大

自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。 如果 JVM 能支持处理器提供的 pause 指令那么 效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指 令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体 实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时 候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空 (CPU pipeline flush),从而提高 CPU 的执行效率

3、只能保证一个共享变量的原子操作 CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是 从 JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你 可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利 用 AtomicReference 类把多个共享变量合并成一个共享变量来操作

CAS 与 synchronized 的使用情景

简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少), synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)

1. 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁 进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程, 操作自旋几率较少,因此可以获得更高的性能。

2. 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较 大,从而浪费更多的 CPU 资源,效率低于 synchronized。

补充: Java 并发编程这个领域中 synchronized 关键字一直都是元老级的角 色,很久之前很多人都会称它为 “重量级锁” 。但是,在 JavaSE 1.6 之后进行了 主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级 锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized 的 底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继 续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况 下,可以获得和 CAS 类似的性能;而线程冲突严重的情况下,性能远高于 CAS


相关文章
|
20小时前
|
设计模式 算法 Java
Java的前景如何,好不好自学?,万字Java技术类校招面试题汇总
Java的前景如何,好不好自学?,万字Java技术类校招面试题汇总
|
20小时前
|
存储 网络协议 前端开发
es集群安装,邮储银行java面试
es集群安装,邮储银行java面试
|
20小时前
|
消息中间件 JSON Java
十五,java高级程序员面试宝典
十五,java高级程序员面试宝典
|
20小时前
|
NoSQL 算法 Java
【redis源码学习】持久化机制,java程序员面试算法宝典pdf
【redis源码学习】持久化机制,java程序员面试算法宝典pdf
|
21小时前
|
消息中间件 Java Kafka
Java大文件排序(有手就能学会),kafka面试题2024
Java大文件排序(有手就能学会),kafka面试题2024
|
21小时前
|
机器学习/深度学习 PyTorch 算法框架/工具
神经网络基本概念以及Pytorch实现,多线程编程面试题
神经网络基本概念以及Pytorch实现,多线程编程面试题
|
22小时前
SpringJDK动态代理实现,2024Java面试真题精选干货整理
SpringJDK动态代理实现,2024Java面试真题精选干货整理
|
22小时前
|
安全 前端开发 Java
Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day15
Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day15
|
1天前
|
Java
阅读《代码整洁之道》总结(1),java多线程面试
阅读《代码整洁之道》总结(1),java多线程面试
|
2天前
|
存储 Java
面试官:素有Java锁王称号的‘StampedLock’你知道吗?我:这什么鬼?
面试官:素有Java锁王称号的‘StampedLock’你知道吗?我:这什么鬼?
44 23