八股整理之JUC篇
怎么保证多线程安全synchronized关键字:可以使用synchronized关键字来同步代码块或方法确保同一时刻只有一个线程可以访问这些代码。对象锁是通过synchronized关键字锁定对象的监视器(monitor来实现的。volatile关键字volatile关键字用于变量确保所有线程看到的是该变量的最新值而不是可能存储在本地寄存器中的副本。Lock接口和ReentrantLock类java.util.concurrent.locks.Lock接口提供了比synchronized更强大的锁定机制ReentrantLock是一个实现该接口的例子提供了更灵活的锁管理和更高的性能。原子类Java并发库java.util.concurrent.atomic提供了原子类如AtomicInteger、AtomicLong等这些类提供了原子操作可以用于更新基本类型的变量而无需额外的同步。线程局部变量ThreadLocal类可以为每个线程提供独立的变量副本这样每个线程都拥有自己的变量消除了竞争条件。并发集合:使用java.util.concurrent包中的线程安全集合如ConcurrentHashMap、ConcurrentLinkedQueue等这些集合内部已经实现了线程安全的逻辑。JUC工具类: 使用java.util.concurrent包中的一些工具类可以用于控制线程间的同步和协作。例如Semaphore和CyclicBarrier等。Java中有哪些常用的锁在什么场景下使用内置锁synchronized、ReentrantLock、读写锁ReadWriteLock、乐观锁和悲观锁、自旋锁synchronized 工作原理synchronized是Java提供的原子性内置锁这种内置的并且使用者看不到的锁也被称为监视器锁使用synchronized之后会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。执行monitorenter指令时会尝试获取对象锁如果对象没有被锁定或者已经获得了锁锁的计数器1。此时其他竞争锁的线程则会进入等待队列中。执行monitorexit指令时则会把计数器-1当计数器值为0时则锁释放处于等待队列中的线程再继续竞争锁。synchronized是排它锁当一个线程获得锁之后其他线程必须等待该线程释放锁后才能获得锁而且由于Java中的线程和操作系统原生线程是一一对应的线程被阻塞或者唤醒时时会从用户态切换到内核态这种转换非常消耗性能。从内存语义来说加锁的过程会清除工作内存中的共享变量再从主内存读取而释放锁的过程则是将工作内存中的共享变量写回主内存。实际上大部分时候我认为说到monitorenter就行了但是为了更清楚的描述还是再具体一点。除了用synchronized还有什么方法可以实现线程同步使用ReentrantLock类、使用volatile关键字、使用Atomic类synchronized锁静态方法和普通方法区别锁的对象不同普通方法锁的是当前对象实例this。同一对象实例的synchronized普通方法同一时间只能被一个线程访问不同对象实例间互不影响可被不同线程同时访问各自的同步普通方法。静态方法锁的是当前类的class对象。由于类的class对象全局唯一无论多少个对象实例该静态同步方法同一时间只能被一个线程访问。作用范围不同普通方法仅对同一对象实例的同步方法调用互斥不同对象实例的同步普通方法可并行执行。静态方法对整个类的所有实例的该静态方法调用都互斥一个线程进入静态同步方法其他线程无法进入同一类任何实例的该方法。多实例场景影响不同普通方法多线程访问不同对象实例的同步普通方法时可同时执行。静态方法不管有多少对象实例同一时间仅一个线程能执行该静态同步方法。synchronized锁升级的过程讲一下快手一面整个升级路径是无锁→偏向锁→轻量级锁→重量级锁这是一个单向过程只能升级不能降级。这个机制的核心在于对象头中的MarkWord它会根据锁状态存储不同的信息通过其中的锁标志位来标识当前锁处于什么状态。首先是无锁状态。对象刚创建出来时没有任何线程访问此时MarkWord中存储的是对象的HashCode、分代年龄等信息锁标志位是01偏向标志位是0。这是对象的初始状态还没有涉及任何同步操作。需要注意的是一旦对象调用过hashCode方法它就无法进入偏向锁状态了因为偏向锁需要用MarkWord的空间来存储线程ID而hashCode也要占用这部分空间两者冲突。当第一个线程进入同步块时锁会升级到偏向锁。此时会通过CAS操作在对象头的MarkWord中记录该线程ID锁标志位保持01但偏向标志位变为1。之后这个线程再次进入同步块时只需简单判断MarkWord中的线程ID是否是自己如果是就直接进入完全不需要加锁解锁操作这就极大优化了单线程反复进入同步块的场景。但一旦有第二个线程尝试获取这个锁就说明出现了竞争偏向锁就会被撤销。撤销过程需要等到安全点暂停持有偏向锁的线程检查该线程是否还在执行同步代码然后清除偏向标记这个过程开销不小。撤销偏向锁后就进入轻量级锁阶段。这时JVM会在当前线程的栈帧中创建锁记录空间把对象头的MarkWord复制进去然后通过CAS操作尝试把对象头的MarkWord更新为指向锁记录的指针锁标志位变为00。如果CAS成功说明获取轻量级锁成功。如果失败说明有其他线程在竞争这时线程会进行自旋不断重试CAS操作期望在短时间内持有锁的线程能释放锁。自旋的本质是用CPU时间换取避免线程阻塞的开销所以只适合锁持有时间很短的场景。重量级锁就是传统的synchronized实现它依赖操作系统的互斥量Mutex来实现锁标志位变为10Mark Word指向Monitor对象。未获取锁的线程会被阻塞进入Monitor的等待队列这个过程需要从用户态切换到内核态由操作系统来调度线程的阻塞和唤醒开销非常大。但在竞争激烈、持锁时间长的场景下阻塞线程反而比自旋更节省CPU资源这就是为什么要升级为重量级锁。用空间换时间用复杂度换性能在JDK 15下synchronized 的实际升级路径已经简化为无锁 → 轻量级锁 → 重量级锁废弃偏向锁的主要原因是它让 JVM 实现过于复杂而现代应用尤其是 lambda、record、虚拟线程大行其道的场景很少再从偏向锁中获益。CAS 和 AQS 有什么关系CAS 和 AQS 两者的区别CAS是一种乐观锁机制它包含三个操作数内存位置V)、预期值A和新值B)。CAS操作的逻辑是如果内存位置的值等于预期值A则将其更新为新值B否则不做任何操作。整个过程是原子性的通常由硬件指令支持如在现代处理器上cmpxchg指令可以实现CAS 操作。AQS 是一个用于构建锁和同步器的框架许多同步器如ReentrantLock、Semaphore、CountDownLatch等都是基于 AQS 构建的。AQS 使用一个volatile的整数变量state来表示同步状态通过内置的FIFo队列来管理等待线程。它提供了一些基本的操作如acquire获取资源和release(释放资源)这些操作会修改state的值并根据state的值来判断线程是否可以获取或释放资源。AQS 的acquire操作通常会先尝试获取资源如果失败线程将被添加到等待队列中并阻塞等待。release操作会释放资源并唤醒等待队列中的线程。CAS 和 AQS 两者的联系CAS 为 AQS 提供原子操作支持Threadlocal作用原理具体里面存的key value是啥会有什么问题如何解决?ThreadLocal是Java中用于解决线程安全问题的一种机制它允许创建线程局部变量即每个线程都有自己独立的变量副本从而避免了线程间的资源共享和同步问题。ThreadLocal作用线程隔离ThreadLocal为每个线程提供了独立的变量副本这意味着线程之间不会相互影响可以安全地在多线程环境中使用这些变量而不必担心数据竞争或同步问题。降低耦合度在同一个线程内的多个函数或组件之间使用ThreadLocal可以减少参数的传递降低代码之间的耦合度使代码更加清晰和模块化。性能优势由于ThreadLocal避免了线程间的同步开销所以在大量线程并发执行时相比传统的锁机制它可以提供更好的性能。Java中想实现一个乐观锁都有哪些方式1、CASCompare and Swap操作2、版本号控制增加一个版本号字段记录数据更新时候的版本每次更新时递增版本号。在更新数据时同时比较版本号若当前版本号和更新前获取的版本号一致则更新成功否则失败。3、时间戳CAS 有什么缺点1、ABA问题ABA的问题指的是在CAS更新的过程中当读取到的值是A然后准备赋值的时候仍然是A但是实际上有可能A的值被改成了B然后又被改回了A这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。Java中有AtomicStampedReference来解决这个问题他加入了预期标志和更新后标志两个字段更新时不光检查值还要检查当前的标志是否等于预期标志全部相等的话才会更新。就是新加一个版本号验证2、循环时间长开销大自旋CAS的方式如果长时间不成功会给CPU带来很大的开销。3、只能保证一个共享变量的原子操作只对一个共享变量操作可以保证原子性但是多个则不行多个可以通过AtomicReference来处理或者使用锁synchronized实现。为什么不能所有的锁都用CASCAS操作是基于循环重试的机制如果CAS操作一直未能成功线程会一直自旋重试占用CPU资源。在高并发情况下大量线程自旋会导致CPU资源浪费。volatile和sychronized比较Synchronized解决了多线程访问共享资源时可能出现的竞态条件和数据不一致的问题保证了线程安全性。Volatile解决了变量在多线程环境下的可见性和有序性问题确保了变量的修改对其他线程是可见的。Synchronized: Synchronized是一种排他性的同步机制保证了多个线程访问共享资源时的互斥性即同一时刻只允许一个线程访问共享资源。通过对代码块或方法添加Synchronized关键字来实现同步。Volatile:Volatile是一种轻量级的同步机制用来保证变量的可见性和禁止指令重排序。当一个变量被声明为Volatile时线程在读取该变量时会直接从内存中读取而不会使用缓存同时对该变量的写操作会立即刷回主内存而不是缓存在本地内存中。非公平锁吞吐量为什么比公平锁大公平锁在整个执行过程中线程会从运行状态切换到休眠状态再从休眠状态恢复成运行状态但线程每次休眠和恢复都需要从用户态转换成内核态而这个状态的转换是比较慢的所以公平锁的执行速度会比较慢。非公平锁获取锁不用遵循先到先得的规则从而避免了线程休眠和恢复的操作这样就加速了程序的执行效率。线程池拒绝策略除了默认的 AbortPolicy直接抛异常还有 CallerRunsPolicy让提交任务的主线程自己执行缓解压力、DiscardPolicy直接静默丢弃新任务、DiscardOldestPolicy丢弃队列里最旧的任务再提交新任务有线程池参数设置的经验吗核心线程数corePoolSize设置的经验CPU密集型corePoolSize CPU核数 1避免过多线程竞争CPUIO密集型corePoolSize CPU核数 x 2或更高具体看IO等待时间线程池和三个线程同时并发比有什么优势线程池的优势主要体现在资源复用降低开销线程创建销毁需要内核态操作如系统调用开销较大。线程池会复用核心线程避免频繁创建线程尤其在任务量大时能显著减少资源消耗。而三个固定线程若任务超过负载要么排队等待效率低要么需要临时新建线程开销高。控制并发强度避免资源耗尽线程池通过最大线程数和队列限制并发数防止线程无限制创建比如瞬间1000个任务直接创建1000个线程可能导致CPU内存耗尽)。而三个线程若面对突发大量任务要么处理不过来任务堆积)要么手动扩容线程难以控制)。任务管理更灵活线程池支持任务排队、优先级调度、拒绝策略等能应对任务峰值如秒杀场景的突发请求)。而三个线程的并发方式缺乏这些机制任务处理逻辑会和业务代码耦合如手动写队列、处理超时)。线程池中shutdown ()shutdownNow()这两个方法有什么作用shutdown调用后状态置为 SHUTDOWN已提交但未开始执行的队列任务仍会按顺序继续执行直到全部完成只对处于空闲等待状态的 worker线程调用interrupt()。此后不能再往线程池中添加任何任务否则将拋出RejectedExecutionException 异常而 shutdownNow 为STOP并试图停止所有正在执行的线程不再处理还在池队列中等待的任务当然它会返回那些未执行的任务。它试图终止线程的方法是通过调用Thread.interrupt() 方法来实现的但是这种方法的作用有限如果线程中没有sleep、wait、Condition、定时锁等应用interrupt()方法是无法中断当前的线程的。所以ShutdownNow()并不代表线程池就一定立即就能退出它可能必须要等待所有正在执行的任务都执行完成了才能退出。提交给线程池中的任务可以被撤回吗可以当向线程池提交任务时会得到一个Future对象。这个Future对象提供了几种方法来管理任务的执行包括取消任务。取消任务的主要方法是Future接口中的cancelboolean mayInterruptIfRunning方法。这个方法尝试取消执行的任务。参数mayInterruptIfRunning指示是否允许中断正在执行的任务。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2630057.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!