Java 并发编程封神!从入门到精通,面试再也不怕被问爆
目录synchronized 支持重入吗如何实现的?syncronized锁升级的过程讲一下JVM对Synchornized的优化介绍一下AQSCAS 和 AQS 有什么关系如何用 AQS 实现一个可重入的公平锁Threadlocal作用原理具体里面存的key value是啥会有什么问题如何解决?悲观锁和乐观锁的区别Java中想实现一个乐观锁都有哪些方式CAS 有什么缺点为什么不能所有的锁都用CASCAS 有什么问题Java是怎么解决的voliatle关键字有什么作用指令重排序的原理是什么volatile可以保证线程安全吗volatile和sychronized比较什么是公平锁和非公平锁非公平锁吞吐量为什么比公平锁大Synchronized是公平锁吗ReentrantLock是怎么实现公平锁的什么情况会产生死锁问题如何解决线程池介绍一下线程池的工作原理线程池的参数有哪些线程池工作队列满了有哪些拒接策略有线程池参数设置的经验吗核心线程数设置为0可不可以线程池种类有哪些线程池一般是怎么用的线程池中shutdown ()shutdownNow()这两个方法有什么作用提交给线程池中的任务可以被撤回吗场景多线程打印奇偶数怎么控制打印的顺序单例模型既然已经用了synchronized为什么还要在加volatile3个线程并发执行1个线程等待这三个线程全部执行完在执行怎么实现假设两个线程并发读写同一个整型变量初始值为零每个线程加 50 次结果可能是什么synchronized 支持重入吗如何实现的?synchronized是基于原子性的内部锁机制是可重入的因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法也就是说一个线程得到一个对象锁后再次请求该对象锁是允许的这就是synchronized的可重入性。synchronized底层是利用计算机系统mutex Lock实现的。每一个可重入锁都会关联一个线程ID和一个锁状态status。当一个线程请求方法时会去检查锁状态。如果锁状态是0代表该锁没有被占用使用CAS操作获取锁将线程ID替换成自己的线程ID。如果锁状态不是0代表有线程在访问该方法。此时如果线程ID是自己的线程ID如果是可重入锁会将status自增1然后获取到该锁进而执行相应的方法如果是非重入锁就会进入阻塞队列等待。在释放锁时如果是可重入锁的每一次退出方法就会将status减1直至status的值为0最后释放该锁。如果非可重入锁的线程退出方法直接就会释放该锁syncronized锁升级的过程讲一下无锁-偏向锁-轻量级锁-重量级锁。对象刚创建时对象头的Mark Word为无锁状态无线程关联。当我们第一个线程获取该锁的时候就会升级成偏向锁第一次通过cas方式将线程id放到对象头mark word中修改成功就获取该锁后续该线程再次获取锁时不用cas方式只需检查Mark Word的线程ID是否相等——是则直接获取锁第二个线程尝试获取该锁Mark Word的线程ID不是当前线程id偏向锁失效,升级为轻量级锁,线程通过CAS操作将Mark Word替换为自己的锁记录指针,成功就获取轻量级锁失败了自旋若自旋达到上限仍未获取锁轻量级锁升级为重量级锁未获取锁的线程进入操作系统阻塞队列后续通过操作系统互斥量竞争锁。JVM对Synchornized的优化锁膨胀锁升级synchronized 从无锁升级到偏向锁再到轻量级锁最后到重量级锁的过程它叫做锁膨胀也叫做锁升级。JDK 1.6 之前synchronized 是重量级锁也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态而转换的效率是比较低的。但有了锁膨胀机制之后synchronized 的状态就多了无锁、偏向锁以及轻量级锁了这时候在进行并发操作时大部分的场景都不需要用户态到内核态的转换了这样就大幅的提升了 synchronized 的性能。锁消除JVM 虚拟机如果检测到这段代码的锁只有当前线程能用没有任何其他线程会碰就会将这段代码所属的同步锁消除掉从而到底提高程序性能的目的。锁粗化将多个连续的加锁、解锁操作连接在一起扩展成一个范围更大的锁就是多次小锁”合并成“一次大锁”减少“锁操作的次数”•自适应自旋锁指通过自身循环尝试获取锁的一种方式优点在于它避免一些线程的挂起和恢复操作因为挂起线程和恢复线程都需要从用户态转入内核态这个过程是比较慢的所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。介绍一下AQSAQS的话其实就一个jdk提供的类AbstractQueuedSynchronizer抽象队列同步器是阻塞式锁和相关的同步器工具的框架。内部有一个属性 state 属性来表示资源的状态默认state等于0表示没有获取锁state等于1的时候才标明获取到了锁。通过cas 机制设置 state 状态在它的内部还提供了基于 FIFO 的等待队列是一个双向列表其中tail 指向队列最后一个元素head 指向队列中最久的一个元素其中我们刚刚聊的ReentrantLock底层的实现就是一个AQS。CAS 和 AQS 有什么关系CAS 和 AQS 两者的区别• CAS 是一种乐观锁机制它包含三个操作数内存位置V、预期值A和新值B。CAS 操作的逻辑是如果内存位置 V 的值等于预期值 A则将其更新为新值 B否则不做任何操作。整个过程是原子性的。• AQS 是一个用于构建锁和同步器的框架许多同步器如 ReentrantLock、Semaphore、CountDownLatch 等都是基于 AQS 构建的。AQS 使用一个 volatile 的整数变量 state 来表示同步状态通过内置的 FIFO 队列来管理等待线程。它提供了一些基本的操作如 acquire获取资源和 release释放资源这些操作会修改 state 的值并根据 state 的值来判断线程是否可以获取或释放资源。AQS 的 acquire 操作通常会先尝试获取资源如果失败线程将被添加到等待队列中并阻塞等待。release 操作会释放资源并唤醒等待队列中的线程。CAS 和 AQS 两者的联系• CAS 为 AQS 提供原子操作支持AQS 内部使用 CAS 操作来更新 state 变量以实现线程安全的状态修改。在 acquire 操作中当线程尝试获取资源时会使用 CAS 操作尝试将 state 从一个值更新为另一个值如果更新失败说明资源已被占用线程会进入等待队列。在 release 操作中当线程释放资源时也会使用 CAS 操作将 state 恢复到相应的值以保证状态更新的原子性。如何用 AQS 实现一个可重入的公平锁继承 AbstractQueuedSynchronizer实现可重入逻辑检查当前线程是否已经持有锁如果是则增加锁的持有次数通过 state 变量如果不是尝试使用 CAS操作来获取锁。实现公平性按照队列顺序来获取锁创建锁的外部类创建一个外部类内部持有 AbstractQueuedSynchronizer 的子类对象并提供 lock 和 unlock 方法进行获取锁和释放锁。Threadlocal作用原理具体里面存的key value是啥会有什么问题如何解决?面试官谈谈你对ThreadLocal的理解候选人嗯是这样的~~ThreadLocal 主要功能有两个第一个是可以实现资源对象的线程隔离让每个线程各用各的资源对象避免争用引发的线程安全问题第二个是实现了线程内的资源共享面试官好的那你知道ThreadLocal的底层原理实现吗候选人嗯知道一些~在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量用来存储资源对象当我们调用 set 方法就是以 ThreadLocal 自己作为 key资源对象作为 value放入当前线程的 ThreadLocalMap 集合中当调用 get 方法就是以 ThreadLocal 自己作为 key到当前线程中查找关联的资源值当调用 remove 方法就是以 ThreadLocal 自己作为 key移除当前线程关联的资源值面试官好的那关于ThreadLocal会导致内存溢出这个事情了解吗候选人嗯我之前看过源码我想一下~~是因为ThreadLocalMap 中的 key 被设计为弱引用它是被动的被GC调用释放key不过关键的是只有key可以得到内存释放而value不会因为value是一个强引用。在使用ThreadLocal 时都把它作为静态变量即强引用因此无法被动依靠 GC 回收建议主动的remove 释放 key这样就能避免内存溢出。悲观锁和乐观锁的区别乐观锁的思想最乐观的估计不一定的线程来修改共享变量它在操作共享数据时不先加锁而是直接读取/修改数据操作完成后通过某种机制如版本号、CAS验证数据是否被其他线程修改过。若未被修改则提交若被修改则重试悲观锁的思想最悲观的估计一定会有其它线程来修改共享变量操作共享数据前先主动加锁确保在操作期间其他线程无法修改数据从而避免数据竞争。Java中想实现一个乐观锁都有哪些方式CASCompare and Swap操作版本号控制增加一个版本号字段记录数据更新时候的版本每次更新时递增版本号。在更新数据时同时比较版本号若当前版本号和更新前获取的版本号一致则更新成功否则失败。时间戳在更新数据时在比较时间戳。如果当前时间戳大于数据的时间戳则说明数据已经被其他线程更新更新失败CAS 有什么缺点ABA问题ABA的问题指的是在CAS更新的过程中逻辑是“当前值是否等于预期旧值”但如果数据被修改为其他值后又改回原值如A→B→ACAS会误以为“数据未被修改”导致操作错误。Java中有AtomicStampedReference来解决这个问题加入了预期标志和更新后标志两个字段更新时不光检查值还要检查当前的标志是否等于预期标志全部相等的话才会更新。循环时间长开销大自旋CAS的方式如果长时间不成功会给CPU带来很大的开销。只能保证一个共享变量的原子操作只对一个共享变量操作可以保证原子性但是多个则不行多个可以通过AtomicReference来处理或者使用锁synchronized实现为什么不能所有的锁都用CASCAS操作是基于循环重试的机制如果CAS操作一直未能成功线程会一直自旋重试占用CPU资源。在高并发情况下大量线程自旋会导致CPU资源浪费。CAS 有什么问题Java是怎么解决的CAS 操作中增加版本号Stamp或标记每次修改都更新版本号使验证时不仅检查值还要检查版本号是否匹配。voliatle关键字有什么作用保证变量对所有线程的可见性。禁止指令重排序优化volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序指令重排序的原理是什么在执行程序时为了提高性能处理器和编译器常常会对指令进行重排序但是重排序要满足下面 2 个条件才能进行• 在单线程环境下不能改变程序运行的结果• 存在数据依赖关系的不允许重排序。volatile可以保证线程安全吗volatile关键字可以保证可见性但不能保证原子性因此不能完全保证线程安全。volatile关键字用于修饰变量当一个线程修改了volatile修饰的变量的值其他线程能够立即看到最新的值从而避免了线程之间的数据不一致。但是volatile并不能解决多线程并发下的复合操作问题比如i这种操作不是原子操作如果多个线程同时对i进行自增操作volatile不能保证线程安全。对于复合操作需要使用synchronized关键字或者Lock来保证原子性和线程安全。volatile和sychronized比较Synchronized解决了多线程访问共享资源时可能出现的竞态条件和数据不一致的问题保证了线程安全性。Volatile解决了变量在多线程环境下的可见性和有序性问题确保了变量的修改对其他线程是可见的。Synchronized: Synchronized是一种排他性的同步机制保证了多个线程访问共享资源时的互斥性即同一时刻只允许一个线程访问共享资源Volatile: Volatile是一种轻量级的同步机制用来保证变量的可见性和禁止指令重排序。什么是公平锁和非公平锁公平锁 指多个线程按照申请锁的顺序来获取锁线程直接进入队列中排队队列中的第一个线程才能获得锁。优点在于各个线程公平平等每个线程等待一段时间后都有执行的机会缺点就在于整体执行速度更慢吞吐量更小。非公平锁多个线程加锁时直接尝试获取锁能抢到锁到直接占有锁抢不到才会到等待队列的队尾等待。优点就在于整体执行速度更快吞吐量更大缺点同时也可能产生线程饥饿问题也就是说如果一直有线程插队那么在等待队列中的线程可能长时间得不到运行。非公平锁吞吐量为什么比公平锁大公平锁执行流程获取锁时先将线程自己添加到等待队列的队尾并休眠当某线程用完锁之后会去唤醒等待队列中队首的线程尝试去获取锁锁的使用顺序也就是队列中的先后顺序在整个过程中线程会从运行状态切换到休眠状态再从休眠状态恢复成运行状态但线程每次休眠和恢复都需要从用户态转换成内核态而这个状态的转换是比较慢的所以公平锁的执行速度会比较慢。非公平锁执行流程当线程获取锁时会先通过 CAS 尝试获取锁如果获取成功就直接拥有锁如果获取锁失败才会进入等待队列等待下次尝试获取锁。这样做的好处是获取锁不用遵循先到先得的规则从而避免了线程休眠和恢复的操作这样就加速了程序的执行效率。Synchronized是公平锁吗Synchronized不属于公平锁ReentrantLock是公平锁。ReentrantLock是怎么实现公平锁的ReentrantLock公平锁与非公平锁的 lock() 方法唯一的区别就在于公平锁在获取锁时多了一个限制条件hasQueuedPredecessors() 为 false。公平锁和非公平锁的核心区别如果是公平锁那么一旦已经有线程在排队了当前线程就不再尝试获取锁对于非公平锁而言无论是否已经有线程在排队都会尝试获取一下锁获取不到的话再去排队。tryLock() 方法它不遵守设定的公平原则。线程执行 tryLock() 方法的时候一旦有线程释放了锁那么这个正在 tryLock 的线程就能获取到锁即使设置的是公平锁模式就是tryLock 可以插队。底层调用的就是nonfairTryAcquire()表明了是不公平的。什么情况会产生死锁问题如何解决死锁只有同时满足以下四个条件才会发生互斥条件互斥条件是指多个线程不能同时使用同一个资源。持有并等待条件持有并等待条件是指当线程 A 已经持有了资源 1又想申请资源 2而资源 2 已经被线程 C 持有了所以线程 A 就会处于等待状态但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。不可剥夺条件不可剥夺条件是指当线程已经持有了资源在自己使用完之前不能被其他线程获取线程 B 如果也想使用此资源则只能在线程 A 使用完并释放后才能获取。环路等待条件环路等待条件指的是在死锁发生的时候两个线程获取资源的顺序构成了环形链避免死锁问题就只需要破环其中一个条件就可以线程池介绍一下线程池的工作原理线程池是为了减少频繁的创建线程和销毁线程带来的性能损耗线程池分为核心线程池线程池的最大容量还有等待任务的队列提交一个任务如果核心线程没有满就创建核心线程执行如果满了就是会加入等待队列如果等待队列满了判断是否达到最大线程数量若没有就会创建空闲线程执行如果都达到最大线程数量就会按照丢弃策略进行处理。线程池的参数有哪些corePoolSize线程池核心线程数量。maximumPoolSize限制了线程池能创建的最大线程总数包括核心keepAliveTime空闲时间当线程池中线程的数量大于corePoolSize并且某个线程的空闲时间超过了 keepAliveTime那么这个线程就会被销毁。unit空闲时间的单位。workQueue工作队列。threadFactory线程工厂。用来给线程取名字。handler拒绝策略。线程池工作队列满了有哪些拒接策略常用的四种拒绝策略包括CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy1.AbortPolicy直接抛出异常默认策略2.CallerRunsPolicy用调用者所在的线程来执行任务3.DiscardOldestPolicy丢弃阻塞队列中靠最前的任务并执行当前任务4.DiscardPolicy直接丢弃任务有线程池参数设置的经验吗参考回答① 高并发、任务执行时间短 -- CPU核数1 减少线程上下文的切换② 并发不高、任务执行时间长IO密集型的任务 -- (CPU核数 * 2 1)计算密集型任务 -- CPU核数1 ③ 并发高、业务执行时间长解决这种类型任务的关键不在于线程池而在于整体架构的设计看看这些业务里面某些数据是否能做缓存是第一步增加服务器是第二步至于线程池的设置设置参考2核心线程数设置为0可不可以可以当核心线程数为0的时候会创建一个非核心线程进行执行。当核心线程数为 0 时来了一个任务之后会先将任务添加到任务队列同时也会判断当前工作的线程数是否为 0如果为 0则会创建空闲线程来执行任务。线程池种类有哪些参考回答在jdk中默认提供了4种方式创建线程池第一个是newCachedThreadPool创建一个可缓存线程池如果线程池长度超过处理需要可灵活回 收空闲线程若无可回收则新建线程。第二个是newFixedThreadPool 创建一个定长线程池可控制线程最大并发数超出的线程会在队列 中等待。第三个是newScheduledThreadPool可以执行延迟任务的线程池支持定时及周期性任务执行。第四个是newSingleThreadExecutor 创建一个单线程化的线程池单例的线程池它只会用唯一的工作线程来执行任 务保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。线程池一般是怎么用的资源风险应该手动 new ThreadPoolExecutor 来创建线程池Executors的快捷方法如newFixedThreadPool的无界队列、newCachedThreadPool的无限扩线程易导致内存溢出OOM可控性手动new ThreadPoolExecutor可自定义核心参数核心/最大线程数、有界队列、拒绝策略适配业务场景可观测性需为线程池命名方便排查问题建议监控队列积压、线程数膨胀等状态提前预警风险。线程池中shutdown ()shutdownNow()这两个方法有什么作用shutdown()置SHUTDOWN状态执行中任务继续不再接受新任务加任务会抛RejectedExecutionException。shutdownNow()置STOP状态中断所有正在执行的线程用Thread.interrupt()丢弃队列待执行任务并返回未执行任务列表但因线程可能不响应中断如无sleep/wait未必立即退出。提交给线程池中的任务可以被撤回吗提交给线程池的任务可以撤回核心通过Future.cancel(boolean mayInterruptIfRunning)实现未开始的任务直接取消不会执行正在执行的任务若mayInterruptIfRunningtrue调用Thread.interrupt()尝试中断需任务响应中断如含sleep()/wait()否则无法停止若mayInterruptIfRunningfalse不中断任务继续执行完已完成的任务取消无效。场景多线程打印奇偶数怎么控制打印的顺序利用wait()和notify()来控制线程的执行顺序。public class PrintOddEven { private static final Object lock new Object(); private static int count 1; private static final int MAX_COUNT 10; public static void main(String[] args) { Runnable printOdd () - { synchronized (lock) { while (count MAX_COUNT) { if (count % 2 ! 0) { System.out.println(Thread.currentThread().getName() : count); lock.notify(); } else { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }; Runnable printEven () - { synchronized (lock) { while (count MAX_COUNT) { if (count % 2 0) { System.out.println(Thread.currentThread().getName() : count); lock.notify(); } else { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }; Thread oddThread new Thread(printOdd, OddThread); Thread evenThread new Thread(printEven, EvenThread); oddThread.start(); evenThread.start(); } }单例模型既然已经用了synchronized为什么还要在加volatilepublic class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }volatile 确保了对象引用的可见性和创建过程的有序性避免了由于指令重排序而导致的错误。instance new Singleton(); 这行代码并不是一个原子操作它实际上可以分解为以下几个步骤• 分配内存空间。• 实例化对象。• 将对象引用赋值给 instance。没有 volatile 的情况下可能会出现重排序例如先将对象引用赋值给 instance但对象的实例化操作尚未完成。这样其他线程在检查 instance null 时会认为单例已经创建从而得到一个未完全初始化的对象导致错误。3个线程并发执行1个线程等待这三个线程全部执行完在执行怎么实现可以使用 CountDownLatch 来实现 3 个线程并发执行另一个线程等待这三个线程全部执行完再执行的需求。以下是具体的实现步骤• 创建一个 CountDownLatch 对象并将计数器初始化为 3因为有 3 个线程需要等待。• 创建 3 个并发执行的线程在每个线程的任务结束时调用 countDown 方法将计数器减 1。• 创建第 4 个线程使用 await 方法等待计数器为 0即等待其他 3 个线程完成任务。import java.util.concurrent.CountDownLatch; public class CountDownLatchExample { public static void main(String[] args) { // 创建一个 CountDownLatch初始计数为 3 CountDownLatch latch new CountDownLatch(3); // 创建并启动 3 个并发线程 for (int i 0; i 3; i) { final int threadNumber i 1; new Thread(() - { try { System.out.println(Thread threadNumber is working.); // 模拟线程执行任务 Thread.sleep((long) (Math.random() * 1000)); System.out.println(Thread threadNumber has finished.); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 任务完成后计数器减 1 latch.countDown(); } }).start(); } // 创建并启动第 4 个线程等待其他 3 个线程完成 new Thread(() - { try { System.out.println(Waiting for other threads to finish.); // 等待计数器为 0 latch.await(); System.out.println(All threads have finished, this thread starts to work.); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } }假设两个线程并发读写同一个整型变量初始值为零每个线程加 50 次结果可能是什么在没有任何同步机制的情况下两个线程并发对同一个整型变量进行 50 次加 1 操作最终结果可能是 100也可能小于 100最坏的结果是 50也就是最终的结果可能是在 [50, 100] 。小于 100 情况的分析由于对整型变量的 num 操作不是原子操作它实际上包含了三个步骤读取变量的值、将值加 1、将新值写回变量。在多线程环境下可能会出现线程安全问题。例如线程 1 和线程 2 同时读取了变量的当前值然后各自将其加 1最后都将相同的新值写回变量这就导致了一次加 1 操作的丢失。这种情况会多次发生最终结果就会小于 100。import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerAddition { private static AtomicInteger num new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread thread1 new Thread(() - { for (int i 0; i 50; i) { num.incrementAndGet(); } }); Thread thread2 new Thread(() - { for (int i 0; i 50; i) { num.incrementAndGet(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(最终结果: num.get()); } }解决方式通过 synchronized 关键字或 ReentrantLock 确保操作的互斥性。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2483088.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!