/images/avatar.png

LockSupport详解

LockSupport简介 LockSupport用来创建锁和其他同步类的基本线程阻塞原语。简而言之,当调用LockSupport.park时,表示当前线程将会等待,直至获得许可,当调用LockSupport.unpark时,必须把等待获得许可的线程作为参数进行传递,好让此线程继续运行。 核心函数分析 在分析LockSupport函数之前,先引入sun.misc.Unsafe类中的park和unpark函数,因为LockSupport的核心函数都是基于Unsafe类中定义的park和unpark函数,下面给出两个函数的定义: 1 2 public native void park(boolean isAbsolute, long time); public native void unpark(Thread thread); 说明: 对两个函数的说明如下: park函数,阻塞线程,并且该线程在下列情况发生之前都会被阻塞: ① 调用unpark函数,释放该线程的许可。② 该线程被中断。③ 设置的时间到了。并且,当time为绝对时间时,isAbsolute为true,否则,isAbsolute为false。当time为0时,表示无限等待,直到unpark发生。 unpark函数,释放线程的许可,即激活调用park后阻塞的线程。这个函数不是安全的,调用这个函数时要确保线程依旧存活。 park函数 park函数有两个重载版本,方法摘要如下 1 2 public static void park(); public static void park(Object blocker); 说明: 两个函数的区别在于park()函数没有没有blocker,即没有设置线程的parkBlocker字段。park(Object)型函数如下。 1 2 3 4 5 6 7 8 9 10 public static void park(Object blocker) { // 获取当前线程 Thread t = Thread.currentThread(); // 设置Blocker setBlocker(t, blocker); // 获取许可 UNSAFE.park(false, 0L); // 重点方法:重新可运行后再此设置Blocker,其他线程执行unpark()后继续 setBlocker(t, null); } 说明: 调用park函数时,首先获取当前线程,然后设置当前线程的parkBlocker字段,即调用setBlocker函数,之后调用Unsafe类的park函数,之后再调用setBlocker函数。

线程池

线程池 线程池(ThreadPool) 是一种基于池化思想管理线程的工具,看过new Thread源码之后我们发现,频繁创建线程销毁线程的开销很大,会降低系统整体性能。线程池维护多个线程,等待监督和管理分配可并发执行的任务。 优点 降低资源消耗:通过线程池复用线程,降低创建线程和释放线程的损耗 提高响应速度:任务到达时,无需等待即刻运行 提高线程的可管理性:使用线程池可以进行统一的分片、调优和监控线程 提供可扩展性:线程池具备可扩展性,研发人员可以向其中增加各种功能,比如延时,定时,监控等 适用场景 连接池:预先申请数据库连接,提升申请连接的速度,降低系统的开销 快速响应用户请求:服务器接受到大量请求时,使用线程池是很适合的,它可以大大减少线程的创 建和销毁的次数,提高服务器的工作效率。 在实际开发中,如果需要创建5个以上的线程,就可以用线程池来管理。 线程池参数 corePoolSize:核心线程数,可以理解为空闲线程数,即便线程空闲(Idle),也不会回收 maxPoolSize :最大线程数,线程池可以容纳线程的上限 keepAliveTime :线程保持存活的时间,超过核心线程数的线程存活空闲时间超过keepAliveTime后就会被回收 workQueue :工作队列,直接交换队列SynchronousQueue,无界队列LinkedBlockingQueue ,有界队列ArrayBlockingQueue threadFactory:线程工厂,用来创建线程的工厂,线程都是出自于此工厂 Handler:线程无法接收任务时的拒绝策略 线程池原理 线程池原理 提交任务,如果线程数小于corePoolSize即使其他线程处于空闲状态,也会创建一个新线程来运行任务 如果线程数大于corePoolSize,但少于maxPoolSize,将任务放入工作队列 如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程来运行任务。 如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝该任务。 自动创建线程 newFixedThreadPool:固定数量线程池,无界任务阻塞队列 newSingleThreadExecutor :一个线程的线程池,无界任务阻塞队列 newCachedThreadPool :可缓存线程的无界线程池,可以自动回收多余线程 newScheduledThreadPool :定时任务线程池 手动创建线程 根据不同的业务场景,自己设置线程池的参数、线程名、任务被拒绝后如何记录日志等 如何设置线程池大小? CPU密集型:线程数量不能太多,可以设置为与相当于CPU核数 IO密集型:IO密集型CPU使用率不高,可以设置的线程数量多一些,可以设置为CPU核心数的2倍 拒绝策略 拒绝时机:①最大线程和工作队列有限且已经饱和,②Executor关闭时 抛异常策略:AbortPolicy,说明任务没有提交成功 不做处理策略:DiscardPolicy,默默丢弃任务,不做处理 丢弃老任务策略:DiscardOldestPolicy,将队列中存在最久的任务给丢弃 自产自销策略:CallerRunsPolicy,那个线程提交任务就由那个线程负责运行 Future与FutureTask FutureTask叫未来任务,可以将一个复杂的任务剔除出去,交给另一个线程来完。它是Future的实现类 Future用法01-用线程池submit方法提交任务,返回值Future任务结果 用线程池提交任务,线程池会立即返回一个空的Future容器 当线程的任务执行完成,线程池会将该任务执行结果填入Future中 此时就可以从Future获取执行结果 Future用法02-用FutureTask来封装任务,获取Future任务的结果 用FutureTask包装任务,FutureTask是Future和Runnable接口的实现类 可以使用new Thread().start()或线程池执行FutureTask 任务执行完成,可以从FutureTask中获取执行结果

线程协作工具与并发容器

线程协作工具类 线程协作工具类,控制线程协作的工具类,帮助程序员让线程之间的协作变得更加简单 CountDownLatch计数门闩: 倒数结束之前,一直处于等待状态,直到数到0,等待线程才继续工作。 场景:购物拼团、分布式锁 方法: new CountDownLatch(int count) await():调用此方法的线程会阻塞,支持多个线程调用,当计数为0,则唤醒线程 countdown():其他线程调用此方法,计数减1 Semaphore信号量: 限制和管理数量有限的资源的使用 场景:Hystrix、Sentinel限流 方法: new Semaphore ((int permits) 可以创建公平的非公平的策略 acquire():获取许可证,获取许可证,要么获取成功,信号量减1,要么阻塞等待唤醒 release():释放许可证,信号量加1,然后唤醒等待的线程 CyclicBarrier循环栅栏: 线程会等待,直到线程到了事先规定的数目,然后触发执行条件进行下一步动作 场景:并行计算 方法: new CyclicBarrier(int parties, Runnable barrierAction)参数1集结线程数,参数2凑齐之后执行的任务 await():阻塞当前线程,待凑齐线程数量之后继续执行 Condition接口: 控制线程的“等待”和“唤醒” 方法: await():阻塞线程 signal():唤醒被阻塞的线程 signalAll()会唤起所有正在等待的线程。 注意: 调用await()方法时必须持有锁,否则会抛出异常 Condition和Object#await/notify方法用法一样,两者await方法都会释放锁 对比 协作工具对比 并发容器 什么是并发容器? 针对多线程并发访问来进行设计的集合,称为并发容器 JDK1.5之前,JDK提供了线程安全的集合都是同步容器,线程安全,只能串行执行,性能很差。 JDK1.5之后,JUC并发包提供了很多并发容器,优化性能,替代同步容器 CopyOnWriteArrayList CopyOnWriteArrayList底层数组实现,使用复制副本进行有锁写操作,适合读多写少,允许短暂的数据不一致的场景。 CopyOnWrite思想:平时查询时,不加锁,更新时从原来的数据copy副本,然后修改副本,最后把原数据替换为副本。修改时,不阻塞读操作,读到的是旧数据。 优缺点 优点:对于读多写少的场景, CopyOnWrite这种无锁操作性能更好,相比于其它同步容器 缺点:①数据一致性问题,②内存占用问题及导致更多的GC次数 ConcurrentHashMap ConcurrentHashMap详解 并发队列 队列是线程协作的利器,通过队列可以很容易的实现数据共享,并且解决上下游处理速度不匹配的问题,典型的生产者消费者模式 什么是阻塞队列 带阻塞能力的队列,阻塞队列一端是给生产者put数据使用,另一端给消费者take数据使用 阻塞队列是线程安全的,生产者和消费者都可以是多线程 take方法:获取并移除头元素,如果队列无数据,则阻塞 put方法:插入元素,如果队列已满,则阻塞 阻塞队列又分为有界和无界队列,无界队列不是无限队列,最大值Integer.MAX_VALU 常用阻塞队列 ArrayBlockingQueue 基于数组实现的有界阻塞队列 LinkedBlockingQueue 基于链表实现的无界阻塞队列 SynchronousQueue不存储元素的阻塞队列 PriorityBlockingQueue 支持按优先级排序的无界阻塞队列 DelayQueue优先级队列实现的双向无界阻塞队列 LinkedTransferQueue基于链表实现的无界阻塞队列 LinkedBlockingDeque基于链表实现的双向无界阻塞队列 ThreadLocal ThreadLocal是线程本地变量类,在多线程并执行过程中,将变量存储在ThreadLocal中,每个线程中都有独立的变量,因此不会出现线程安全问题。

AQS代码分析

AQS 上图中有颜色的为Method,无颜色的为Attribution。 总的来说,AQS框架共分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据。 当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。 原理概览 AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。 1 private volatile int state;//共享变量,使用volatile修饰保证线程可见性 AQS数据结构 AQS数据结构 线程两种资源共享方式 Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock。 Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁: 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 ReentrantReadWriteLock可以看成是组合式,因为ReentrantReadWriteLock是读写锁允许多个线程同时对某一资源进行读。 不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。 属性值的含义 waitStatus 当前节点在队列中的状态 thread 表示处于该节点的线程 prev 前驱指针 predecessor 返回前驱节点,没有的话抛出npe nextWaiter 指向下一个处于CONDITION状态的节点(由于本篇文章不讲述Condition Queue队列,这个指针不多介绍) next 后继指针 waitStatus(节点状态) 0,表示当前节点在sync queue中,等待着获取锁。 SIGNAL 为-1,表示线程已经准备好了,就等资源释放了,表示当前节点的后继节点包含的线程需要运行,需要进行unpark操作。 CANCELLED 为1,表示线程获取锁的请求已经取消了 CONDITION 为-2,表示当前节点在等待condition,也就是在condition queue中,节点线程等待唤醒 PROPAGATE 为-3,表示当前场景下后续的acquireShared能够得以执行。 AQS独占模式加锁 AQS共享模式加锁 AQS重要方法与ReentrantLock的关联 方法 AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法 protected boolean isHeldExclusively() 该线程是否正在独占资源。只有用到Condition才需要去实现它。 protected boolean tryAcquire(int arg) 独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。 protected boolean tryRelease(int arg) 独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。 protected int tryAcquireShared(int arg) 共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 protected boolean tryReleaseShared(int arg) 共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。 默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。

AQS分析

引子 类图外层 重入锁ReentrantLock类关系图,它是实现了Lock接口的类。NonfairSync和FairSync都继承自抽象类Sync,在ReentrantLock中有非公平锁NonfairSync和公平锁FairSync的实现。在重入锁ReentrantLock类关系图中,我们可以看到NonfairSync和FairSync都继承自抽象类Sync,而Sync类继承自抽象类AbstractQueuedSynchronizer(简称AQS)。如果我们看过JUC的源代码,会发现不仅重入锁用到了AQS, JUC 中绝大部分的同步工具也都是基于AQS构建的 AQS简介 AQS(全称AbstractQueuedSynchronizer)即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)。 AQS是JUC并发包中的核心基础组件,其本身是一个抽象类。理论上还是利用管程实现的,在AQS中,有一个volatile修饰的state,获取锁的时候,会读写state的值,解锁的时候,也会读写state的值。所以AQS就拥有了volatile的happensbefore规则。加锁与解锁的效果上与synchronized是相同的。 由类图可以看到,AQS是一个FIFO的双向队列,队列中存储的是thread,其内部通过节点head和tail记录队首 和队尾元素,队列元素的类型为Node。 类图 AQS实现原理 AQS是一个同步队列,内部使用一个FIFO的双向链表,管理线程同步时的所有被阻塞线程。双向链表这种数据结构,它的每个数据节点中都有两个指针,分别指向直接后继节点和直接前驱节点。所以,从双向链表中的任意一个节点开始,都可以很方便地访问它的前驱节点和后继节点。 我们看下面的AQS的数据结构,AQS有两个节点head,tail分别是头节点和尾节点指针,默认为null。AQS中的内部静态类Node为链表节点,AQS会在线程获取锁失败后,线程会被阻塞并被封装成Node加入到AQS队列中;当获取锁的线程释放锁后,会从AQS队列中的唤醒一个线程(节点)。 AQS的数据结构 场景01-线程抢夺锁失败时,AQS队列的变化【加锁】 AQS的head、tail分别代表同步队列头节点和尾节点指针默认为null 当第一个线程抢夺锁失败,同步队列会先初始化,随后线程会被封装成Node节点追加到AQS队列中。 假设:当前独占锁的的线程为ThreadA,抢占锁失败的线程为ThreadB。 2.1 同步队列初始化,首先在队列中添加Node,thread=null 2.2 将ThreadB封装成为Node,追加到AQS队列 当下一个线程抢夺锁失败时,继续重复上面步骤。假设:ThreadC抢占线程失败 AQS队列加锁 场景02-线程被唤醒时,AQS队列的变化【解锁】 ReentrantLock唤醒阻塞线程时,会按照FIFO的原则从AQS中head头部开始唤醒首个节点中线程。 head节点表示当前获取锁成功的线程ThreadA节点。 当ThreadA释放锁时,它会唤醒后继节点线程ThreadB,ThreadB开始尝试获得锁,如果ThreadB获得锁成功,会将自己设置为AQS的头节点。ThreadB获取锁成功后,AQS变化如下: AQS队列解锁 锁源码分析 锁的获取 锁的获取 锁的释放 锁的释放 公平锁和非公平锁源码实现区别 公平锁和非公平锁在获取锁和释放锁时有什么区别呢? 非公平锁与非公平锁释放锁是没有差异,释放锁时调用方法都是AQS的方法。 非公平锁与非公平锁获取锁的差异 我们可以看到上面在公平锁中,线程获得锁的顺序按照请求锁的顺序,按照先来后到的规则获取锁。如果线程竞争公平锁失败后,都会到AQS同步队列队尾排队,将自己阻塞等待锁的使用资格,锁被释放后,会从队首开始查找可以获得锁的线程并唤醒。 而非公平锁,允许新线程请求锁时,可以插队,新线程先尝试获取锁,如果获取锁失败,才会AQS同步队列队尾排队。 读写锁ReentrantReadWriteLock 可重入锁ReentrantLock是互斥锁,互斥锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低,所以就出现了读写锁。 读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的互斥锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞。 读写锁的主要特性: 公平性:支持公平性和非公平性。 重入性:支持重入。读写锁最多支持65535个递归写入锁和65535个递归读取锁。 锁降级:写锁能够降级成为读锁,但读锁不能升级为写锁。遵循获取写锁、获取读锁在释放写锁的次序

Java锁

Java锁简介 Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。 ReentrantLock重入锁 它具有与使用 synchronized 相同的一些基本行为和语义,但是它的API功能更强大,重入锁相当于synchronized 的增强版,具有synchronized很多所没有的功能。它是一种独享锁(互斥锁),可以是公平锁,也可以是非公平的锁。 ReentrantReadWriteLock读写锁 它维护了一对锁,ReadLock读锁和WriteLock写锁。读写锁适合读多写少的场景。基本原则:读锁可以被多个线程同时持有进行访问,而写锁只能被一个线程持有。可以这么理解:读写锁是个混合体,它既是一个共享锁,也是一个独享锁。 StampedLock重入读写锁 JDK1.8引入的锁类型,是对读写锁ReentrantReadWriteLock的增强版。 Java锁分类 按上锁方式划分 隐式锁:synchronized,不需要显示加锁和解锁 显式锁:JUC包中提供的锁,需要显示加锁和解锁 按特性划分 悲观锁/乐观锁 按照线程在使用共享资源时,要不要锁住同步资源,划分为悲观锁和乐观锁 悲观锁:JUC锁,synchronized 乐观锁:CAS,关系型数据库的版本号机制 重入锁/不可重入锁 按照同一个线程是否可以重复获取同一把锁,划分为重入锁和不可重入锁 重入锁:ReentrantLock、synchronized 不可重入锁:不可重入锁,与可重入锁相反,线程获取锁之后不可重复获取锁,重复获取会发生死锁 公平锁/非公平锁 按照多个线程竞争同一锁时需不需要排队,能不能插队,划分为公平锁和非公平锁。 公平锁:new ReentrantLock(true)多个线程按照申请锁的顺序获取锁 非公平锁:new ReentrantLock(false)多个线程获取锁的顺序不是按照申请锁的顺序(可以插队) synchronized 独享锁/共享锁 按照多个线程能不能同时共享同一个锁,锁被划分为独享锁和共享锁。 独享锁:独享锁也叫排他锁,synchronized,ReentrantLock, ReentrantReadWriteLock 的WriteLock写锁 共享锁:ReentrantReadWriteLock的ReadLock读锁 自旋锁: 实现:CAS、轻量级锁 分段锁: 实现:ConcurrentHashMap ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 无锁/偏向锁/轻量级锁/重量级锁 这四个锁是synchronized独有的四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。 它们是JVM为了提高synchronized锁的获取与释放效率而做的优化 四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。 Synchronized和JUC的锁对比 Java已经提供了synchronized,为什么还要使用JUC的锁呢? Synchronize的缺陷: 第一: Synchronized无法控制阻塞时长,阻塞不可中断 使用Synchronized,假如占有锁的线程被长时间阻塞(IO、sleep、join),由于线程阻塞时没法释放锁,会导致大量线程堆积,轻则影响性能,重则服务雪崩 JUC的锁可以解决这两个缺陷 第二::读多写少的场景中,当多个读线程同时操作共享资源时,读操作和读操作不会对共享资源进行修改,所以读线程和读线程是不需要同步的。如果这时采用synchronized关键字,就会导致一个问题,当多个线程都只是进行读操作时,所有线程都只能同步进行,只能有一个读线程可以进行读操作,其他读线程只能等待锁的释放而无法进行读操作。 Synchronized不论是读还是写,均需要同步操作,这种做法并不是最优解 JUC的ReentrantReadWriteLock锁可以解决这个问题 锁优化 减少锁持有时间 减少锁持有时间 减少锁粒度 将大对象拆分成小对象,增加并行度,降低锁竞争。 ConcurrentHashMap允许多个线程同时进入 锁分离 根据功能进行锁分离 ReadWriteLock在读多写少时,可以提高性能。 锁消除 锁消除是发生在编译器级别的一种锁优化方式。 有时候我们写的代码完全不需要加锁,却执行了加锁操作。 锁消除时指虚拟机即时编译器再运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。 锁消除的主要判定依据来源于逃逸分析的数据支持。意思就是:JVM会判断在一段程序中的同步明显不会逃逸出去从而被其他线程访问到,那JVM就把它们当作栈上数据对待,认为这些数据时线程独有的,不需要加同步。此时就会进行锁消除。 ​

JUC-并发编程利器

JUC简介 从JDK1.5起,Java API 中提供了java.util.concurrent(简称JUC)包,在此包中定义了并发编程中很常用的工具,比如:线程池、阻塞队列、同步器、原子类等等。JUC是 JSR 166 标准规范的一个实现,JSR166 以及 JUC 包的作者是同一个人 Doug Lea 。 JUC概览 JUC层次 Atomic包 从JDK 1.5开始提供了java.util.concurrent.atomic包(以下简称Atomic包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。可以解决volatile原子性操作变量的问题。 Atomic包里的类 基本类型:AtomicInteger整形原子类… 引用类型:AtomicReference引用类型原子类… 数组类型: AtomicIntegerArray整形数组原子类… 对象属性修改类型:AtomicIntegerFieldUpdater原子更新整形字段的更新器… JDK1.8新增:DoubleAdder双浮点型原子类、LongAdder长整型原子类… 虽然原子类很多,但原理几乎都差不多,其核心是采用CAS进行原子操作 CAS CAS即compare and swap(比较再替换),同步组件中大量使用CAS技术实现了Java多线程的并发操作。整个AQS、Atomic原子类底层操作,都可以看见CAS。甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。 CAS本质是一条CPU的原子指令,可以保证共享变量修改的原子性。。 CAS缺陷 CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个地方:循环时间太长、只能保证一个共享变量原子操作、ABA问题。 循环时间太长:如果CAS一直不成功呢?如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。 原子类AtomicInteger#getAndIncrement()的方法 只能保证一个共享变量原子操作:看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了。 ABA问题:CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量绑定一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。

volatile详解

volatile简介 volatile可以保证多线程场景下变量的可见性和有序性。如果某变量用volatile修饰,则可以确保所有线程看到变量的值是一致的。 可见性:保证此变量的修改对所有线程的可见性。 有序性:禁止指令重排序优化,编译器和处理器在进行指令优化时,不能把在volatile变量操作(读/写)后面的语句放到其前面执行,也不能将volatile变量操作前面的语句放在其后执行。遵循了JMM的happens-before规则 volatile实现原理剖析 volatile实现内存可见性原理:内存屏障(Memory Barrier) 内存屏障(Memory Barrier)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序 写操作时,通过在写操作指令后加入一条store屏障指令,让本地内存中变量的值能够刷新到主内存中 读操作时,通过在读操作前加入一条load屏障指令,及时读取到变量在主内存的值 volatile缺陷:原子性Bug 原子性的问题:虽然volatile可以保证可见性,但是不能满足原子性 volatile适合使用场景 变量真正独立于其他变量和自己以前的值,在单独使用的时候,适合用volatile 对变量的写入操作不依赖其当前值:例如++和–运算符的场景则不行 该变量没有包含在具有其他变量的不变式中 synchronized和volatile比较 volatile不需要加锁,比synchronized更轻便,不会阻塞线程 synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性 与synchronized相比volatile是一种非常简单的同步机制

synchronized详解

synchronized简介 保证方法或代码块在多线程环境运行时,同一个时刻只有一个线程执行代码块。 JDK1.6之前,synchronized的实现依赖于OS底层互斥锁的MutexLock,存在严重的性能问题 JDK1.6之后,Java对synchronized进行的了一系列优化,实现方式也改为Monitor(管程)了 一句话:有了Synchronized,就线程安全了,保证原子性、可见性、有序性 可以修饰方法(静态和非静态)和代码块 同步代码块的锁:当前对象,字节码对象,其他对象 非静态同步方法:锁当前对象 静态同步方法:锁是当前类的Class对象 synchronized原理剖析 如何解决可见性问题?Happens-before规则 JMM对于Synchronized的规定: 加锁前:必须把自己本地内存中共享变量的最新值刷到主内存 加锁时:清空本地内存中的共享变量,从主内存中读取共享变量最新的值 Synchronized是如何实现同步的呢 同步操作主要是monitorenter和monitorexit两个jvm指令实现。背后原理是Monitor(管程) 什么是Monitor Monitor意译为管程,直译为监视器。所谓管程,就是管理共享变量及对共享变量操作的过程。让这个过程可以并发执行。 Java所有对象都可以做为锁,为什么? 因为每个对象都都有一个Monitor与之关联。然后线程对monitor执行lock和unlock操作,相当于对对象执行上锁和解锁操作。 Synchronized里面不可以直接使用lock和unlock方法,但当我们使用了synchronized之后,JVM会自动加入两个指令monitorenter和monitorexit,对应的就是lock和unlock操作。 当一个monitor对象被线程持有后,它将处于锁定状态。对于一个 monitor 而言,同时只能有一个线程能锁定monitor,其它线程试图获得已被锁定的 monitor时,都将被阻塞。当monitor被释放后,阻塞中的线程会尝试获得该 monitor锁。一个线程可以对一个 monitor 反复执行 lock 操作,对应的释放锁时,需要执行相同次数的 unlock 操作。 Monitor如何解决了线程安全问题? 管程解决互斥问题的思路:就是将共享变量及其对共享变量的操作统一封装起来。 管程 为什么所有对象都可以作为锁? 因为每个对象都都有一个Monitor对象与之关联。然后线程对monitor执行lock和unlock操作,相当于对对象执行上锁和解锁操作。 什么是锁优化 在JDK 1.6之前,synchronized使用传统的锁(重量级锁)实现。它依赖于操作系统(互斥量)的同步机制,涉及到用户态和内核态的切换、线程的上下文切换,性能开销较高,所以给开发者留下了synchronized关键字性能不好的印象。 如果只有一个线程运行时并没有发生资源竞争、或两个线程交替执行,使用传统锁机制无疑效率是会比较低的。 JDK1.6中为了减少这两个场景,获得锁和释放锁带来的性能消耗,同步锁进行优化引入:偏向锁和轻量级锁。 同步锁一共有四种状态,级别从低到高依次是:无锁,偏向锁,轻量级锁,重量级锁。这四种状态会随着竞争激烈情况逐渐升级。 偏向锁则是基这样一个想法:只有一个线程访问锁资源(无竞争)的话,偏向锁就会把整个同步措施都消除,并记录当前持有锁资源的线程和锁的类型。 轻量级锁是基于这样一个想法只有两个线程交替运行时,如果线程竞争锁失败了,先不立即挂起,而是让它飞一会儿(自旋),在等待过程中,可能锁就被释放了,这时该线程就可以重新尝试获取锁,同时记录持有锁资源的线程和锁的类型。 锁升级(无锁→偏向锁→轻量级锁→重量级锁) 在Java中,synchronized共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级,锁可以升级但不能降级。 偏向锁:是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。 轻量级锁:(自旋锁)是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程同样不会阻塞。长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting) 重量级锁:此忙等是有限度的。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改 参考文章 synchronized详解 不可不说的Java“锁”事

Java内存模型(JMM)

并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。 同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。 Java的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的 Java 程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。 CPU和缓存一致性 在多核 CPU 中每个核都有自己的缓存,同一个数据的缓存与内存可能不一致 为什么需要CPU缓存?随着CPU技术发展,CPU执行速度和内存读取速度差距越来越大,导致CPU每次操作内存都要耗费很多等待时间。为了解决这个问题,在CPU和物理内存上新增高速缓存。 程序在运行过程中会将运算所需数据从主内存复制到CPU高速缓存,当CPU计算直接操作高速缓存数据,运算结束将结果刷回主内存。 Java内存模型(Java Memory Model) Java为了保证满足原子性、可见性及有序性,诞生了一个重要的规范JSR133,Java内存模型简称JMM JMM定义了共享内存系统中多线程应用读写操作行为的规范 JMM规范定义的规则,规范了内存的读写操作,从而保证指令执行的正确性 JMM规范解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题 Java实现了JMM规范因此有了Synchronized、Volatile、锁等概念 JMM的实现屏蔽各种硬件和操作系统的差异,在各种平台下对内存的访问都能保证效果一致 JMM内存模型抽象结构 JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 java 程序员提供内存可见性保证。 Java内存模型的抽象示意图如下: Java内存模型的抽象示意图 JMM定义共享变量何时写入,何时对另一个线程可见 线程之间的共享变量存储在主内存 每个线程都有一个私有的本地内存,本地内存存储共享变量的副本 本地内存是抽象的,不真实存在,涵盖:缓存,写缓冲区,寄存器等 JMM线程操作内存基本规则 线程操作共享变量必须在本地内存中,不能直接操作主内存的 线程间无法直接访问对方的共享变量,需经过主内存传递 happens-before规则 在JMM中,使用happens-before规则来约束编译器的优化行为,允许编译期优化,但需要遵守一定的Happens-Before规则。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before的关系! 与程序员密切相关的 happens-before 规则如下: 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。 volatile 变量规则:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。 happens-before的实现 注意:对于程序员来说,理解以上happens-before规则即可,JMM设计happens-before的目标就是屏蔽编译器和处理器重排序规则的复杂性