关于锁:消失的死锁从-JSF-线程池满到-JVM-初始化原理剖析-京东云技术团队

一、问题形容在一次上线时,依照失常流程上线后,察看了线上报文、接口可用率十分钟以上,未出现异常状况,后果在上线一小时后忽然收到jsf线程池耗尽的报警,并且该利用一共有30台机器,只有一台机器呈现该问题,迅速下线该机器的jsf接口,复原线上。而后开始排查问题。 报错日志信息:[WARN]2023-04-10 18:03:34.847 [ - ][] |[JSF-23002]Task:java.util.concurrent.FutureTask@502cdfa0 has been reject for ThreadPool exhausted! pool:200, active:200, queue:0, taskcnt: 2159[BusinessPool#:][JSF-SEV-WORKER-225-T-8] 二、问题剖析1、呈现问题起因:a)因为只有一台机器呈现线程池耗尽,其余机器均失常运行。所以第一工夫判断是否为有大量流量负载不平衡导致; b)业务代码存在并发锁; c)业务代码解决工夫较长; d)拜访数据源(如DB、redis)变慢; 排查接口流量UMP监控,依照机器纬度看,发现每个机器流量是平衡的,排除a)项; 排查业务量大的接口UMP KEY监控,依照机器纬度看,失常机器和异样机器耗时基本一致,并于平常统一,无较大差别,排除c)项; 排查数据库监控,无慢sql,读写均无耗时较长的状况,排除d)项; 综上,只剩下b)项,确认问题起因是代码存在并发锁,故开始排查日志及业务代码。 2、依据已确认的起因排查思路:1)down下dump文件,发现极多JSF线程处于RUNNABLE状态,并且堆栈处于SerializersHelper类 "JSF-BZ-22000-223-T-200" #1251 daemon prio=5 os_prio=0 tid=0x00007fd15005c000 nid=0xef6 in Object.wait() [0x00007fce287ac000] java.lang.Thread.State: RUNNABLE at com.jd.purchase.utils.serializer.helper.SerializersHelper.ofString(SerializersHelper.java:79) at com.jd.ldop.pipe.proxy.OrderMiddlewareCBDExportServiceProxy.getAddress(OrderMiddlewareCBDExportServiceProxy.java:97) at com.jd.ldop.pipe.proxy.OrderMiddlewareCBDExportServiceProxy.findOrder(OrderMiddlewareCBDExportServiceProxy.java:211)依据堆栈信息排查代码,发现代码会初始化一个自定义的序列化工厂类:SerializerFactory 并且此时初始化时会打印日志: log.info("register: {} , clazz : {}", serializer.getCode(), serializer.getClass().getName());故依据此日志关键字排查初始化加载日志,发现失常机器都加载了两个序列化对象,只有出问题的那个机器只加载了这一个序列化对象。 于是问题初步定位到出问题的机器初始化ProtoStuffSerializer这个类时失败。 初始化此类时static代码块为: static { STRATEGY = new DefaultIdStrategy(IdStrategy.DEFAULT_FLAGS);}2)开始排查为什么初始化这个类会失败 因为不同机器存在初始化胜利和失败的独立性,首先思考jar包是否抵触 a)于是发现这里存在jar抵触,然而将抵触jar排除后,屡次重启机器后发现仍然存在此ProtoStuffSerializer初始化失败状况。 b)存在死锁,然而失常逻辑下,存在死锁的话,应该所有机器都会存在此类情况,然而此时大略只有5%的几率呈现死锁,并且排查jstack发现200个线程都卡在获取ProtoStuffSerializer。 ...

June 14, 2023 · 3 min · jiezi

关于锁:锁的优化策略

锁的优化策略有以下几种:缩小锁的粒度:将本来粗粒度的锁细化为更细的锁,这样就能缩小竞争和抵触。然而这种策略须要思考细粒度锁的实现和保护老本,以及可能会带来的更多的上下文切换。 防止锁的应用:尝试应用无锁数据结构、乐观锁或无锁算法代替锁,这样能够防止锁带来的性能损失,但也须要思考其适用性和正确性。 锁拆散:在应用锁的状况下,尝试将不同的锁拆散,防止不同的锁之间的竞争和抵触。这种策略须要思考锁的数量和保护老本。 残缺内容请点击下方链接查看: https://developer.aliyun.com/ask/499542?utm_content=g_1000371148 版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

April 28, 2023 · 1 min · jiezi

关于锁:Java常见锁

乐观锁 乐观锁是一种乐观思维,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为他人不会批改,所以不会上锁,然而在更新时会判断此期间数据是否被更新采取在写时先读出以后版本号,而后加锁操作(比拟跟上一次的版本号,如果一样则更新),如果失败则要反复读-比拟-写的操作 java中的乐观锁根本通过 CAS 操作实现的,CAS 是一种更新的原子操作,比拟以后值跟传入值是否一样,一样则更新,否则失败乐观锁 乐观锁是就是乐观思维,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为他人会批改,所以每次在读写数据的时候都会上锁,这样他人想读写这个数据就会block 直到拿到锁 Java 中的乐观锁就是SynchronizedAQS 框架下的锁则是先尝试 cas 乐观锁去获取锁,获取不到,才会转换为乐观锁,如 RetreenLock自旋锁 原理自旋锁原理非常简单,如果持有锁的线程能在很短时间内开释锁资源,那么那些期待竞争锁的线程就不须要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需自旋,等持有锁的线程开释锁后即可立刻获取锁,这样就防止用户线程和内核的切换的耗费。线程自旋需耗费 cup的,如果始终获取不到锁,则线程长时间占用CPU自旋,须要设定一个自旋期待最大事件在最大等待时间内仍未取得锁就会进行自旋进入阻塞状态。自旋锁优缺点 长处自旋锁尽可能的缩小线程的阻塞,这对于锁的竞争不强烈,且占用锁工夫十分短的代码块来说性能能大幅度的晋升,因为自旋的耗费会小于线程阻塞挂起再唤醒的操作的耗费(这些操作会导致线程产生两次上下文切换)毛病 锁竞争强烈或者持有锁的线程须要长时间占用锁执行同步块,不适宜应用自旋锁了,因为自旋锁在获取锁前始终都是占用 cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的工夫很长,线程自旋的耗费大于线程阻塞挂起操作的耗费,其它须要 cup 的线程又不能获取到cpu,造成 cpu 的节约自旋锁工夫阈值(1.6 引入了适应性自旋锁)自旋锁的目标是为了占着 CPU 的资源不开释,等到获取到锁立刻进行解决 自旋执行工夫太长,会有大量的线程处于自旋状态占用 CPU资源,进而会影响整体零碎的性能 JVM 对于自旋周期的抉择,jdk1.5 这个限度是肯定的写死的 在 1.6引入了适应性自旋锁,自旋的工夫不固定,而是由前一次在同一个锁上的自旋工夫以及锁的拥有者的状态来决定,根本认为一个线程上下文切换的工夫是最佳的一个工夫自旋锁的开启 JDK1.6 中-XX:+UseSpinning 开;XX:PreBlockSpin=10 为自旋次数 JDK1.7 后,去掉此参数,由jvm 管制Synchronized 同步锁 关键字,用于解决多个线程间拜访资源同步性问题,保障其润饰的办法或代码块任意时刻只能有一个线程拜访synchronized 它能够把任非NULL 的对象当作锁。他属于独占式乐观锁,同时属于可重入锁。Synchronized作用范畴 作用实例办法时。锁住的是对象的实例(this) 作用静态方法时,锁住的是该类,该 Class所有实例,又因为 Class的相干数据存储在永恒带 PermGen(jdk1.8 则是 元空间),永恒带是全局共享的,因而静态方法锁相当于类的一个全局锁,会锁所有调用该办法的线程.线程A调用一个实例对象非动态Synchronized办法,容许线程B调用该实例对象所属类的动态s办法而不会产生互斥,前者锁的是以后实例对象,后者锁的是以后类 作用于同步代码块 锁住的以后对象,进入同步代码块前须要取得对象的锁Synchronized实现 Synchronized 是一个重量级操作,须要调用操作系统相干接口,性能是低效的,有可能给线程加锁耗费的工夫比有用操作耗费的工夫更多。Java1.6,synchronized 进行了很多的优化,有适应自旋、锁打消、锁粗化、轻量级锁及偏差锁等,效率有了实质上的进步。在之后推出的 Java1.7 与 1.8中,均对该关键字的实现机理了优化。引入了偏差锁和轻量级锁,都是在对象头中有标记位,不须要通过操作系统加锁JDK1.6后的优化 synchronized是依据JVM实现的,该关键字的优化也是在JVM层面实现 而未间接裸露JDK1.6后对锁做了大量优化如偏差锁,轻量锁,自旋锁,自适应锁等等锁次要有四种状态:无锁状态,偏差锁状态,轻量级锁状态,重量级锁状态,他们会随着锁竞争的强烈而逐步降级且这种降级不可降,利用该策略进步取得锁和开释锁的效率ReentrantLock ReentantLock 继承接口Lock并实现了接口中定义的办法,他是一种可重入锁,除了能实现 synchronized所能实现的所有工作外,还提供了诸如可响应中断锁、可轮询锁申请、定时锁等防止多线程死锁的办法。Lock接口次要办法 void lock(): 执行此办法时, 如果锁处于闲暇状态, 以后线程将获取到锁 lock()办法则是肯定要获取到锁,如果锁不可用,就始终期待, 在未取得锁之前,以后线程并不持续向下执行. boolean tryLock():如果锁可用, 则获取锁, 并立刻返回 true, 否则返回 false. tryLock()只是"试图"获取锁, 如果锁不可用,不会导致以后线程阻塞挂起,以后线程依然持续往下执行代码. void unlock() 解锁 isLock():此锁是否有任意线程占用tryLock 和 lock 和 lockInterruptiblytryLock 能取得锁就返回 true,不能就立刻返回false,tryLock(long timeout,TimeUnitunit),能够减少工夫限度,如果超过该时间段还没取得锁,返回falselock 能取得锁就返回 true,不能的话始终期待取得锁 lock 和 lockInterruptibly,如果两个线程别离执行这两个办法,但此时中断这两线程,但此时中断这两个线程, lock 不会抛出异样,而lockInterruptibly 会抛出异样ReentrantLock 与 synchronized ...

February 28, 2023 · 2 min · jiezi

关于锁:互斥锁和信号量有什么不同译

原文地址 什么是信号量?信号量是一个非负变量, 并且在线程之间共享。信号量是一种信号机制,一个线程正在期待信号量能够由另一个线程收回。它应用两个原子操作,1)wait和 2)signal实现线程同步。 信号量实现容许或不容许拜访资源,这取决于它的设置形式。 什么是互斥锁?互斥的残缺模式是互斥对象。它是一种非凡类型的二进制信号,用于管制对共享资源的拜访。它蕴含了一个优先级继承机制来防止扩大优先级反转问题。它容许以后优先级较高的工作在尽可能短的工夫内放弃在阻塞状态。然而,优先级继承并不能解决优先级反转问题,只能最小化其影响。 次要区别:互斥锁是一种锁机制,信号量是一种信号机制互斥锁是一个对象,信号量是一个整数互斥锁没有子类型,信号量有两种子类型,计数信号量和二进制信号量信号量反对wait和signal操作批改,而互斥锁仅能由可能申请或开释资源的过程批改信号量的值应用wait()和signal()这两个办法批改,而互斥锁应用lock和unlock来操作。应用信号量在单个缓冲区的状况下,咱们能够将4KB的缓冲区分成四个1KB的缓冲区。信号量能够与这四个缓冲区相关联。这容许用户和生产者同时在不同的缓冲区上工作。 应用互斥锁互斥锁提供了互斥的性能,不论是生产者还是消费者,都能够持有锁,持有锁的一方能够持续工作,另一方就要期待,在同一时间,只有一个线程能够解决整个缓冲区。 对于互斥和信号量的常见事实只有一个工作能够获取到互斥锁,互斥锁有所有权,只有持有锁的工作能力开释互斥锁。应用互斥锁和信号量的场景是不同的,然而因为实现形式有相似之处,互斥锁也被称为二进制信号量一个家喻户晓的谬误:互斥量和信号量简直雷同,惟一的区别是互斥量可能计数到1,而信号量可能从0计数到N二进制信号量和互斥量之间总是存在不确定性。你可能据说互斥锁是一个二进制信号量,这是不正确的信号量的长处容许多个线程拜访临界区信号量是独立于机器的(因为它们是在内核服务中实现的)不容许多个过程进入临界区。信号量有忙等状态,因为不会浪费时间和资源互斥锁的长处互斥锁只是简略的锁,在进入临界区是持有它,来到时开释因为在任何给定工夫只有一个线程处于临界区内,因而不存在数据竞争,能够始终保持数据一致性信号量的毛病信号量的最大限度之一是优先级反转问题操作系统必须跟踪所有信号量的调用为了防止信号量中的死锁,wait和signal操作须要以正确的程序执行信号量编程是一种简单的办法,因而有可能无奈实现互斥的成果它也不是能够大规模应用的实用办法,因为它们的应用会毁坏模块化程序员应用信号量更容易出错,容易呈现死锁互斥锁的毛病如果一个持有锁的线程休眠或者被强占了CPU,其余线程就没方法继续执行了一次只能容许一个线程拜访临界区失常实现可能会导致忙期待状态,节约CPU工夫

May 9, 2022 · 1 min · jiezi

关于锁:JDK内置锁深入探究

一、序言本文讲述仅针对 JVM 档次的内置锁,不波及分布式锁。 锁有多种分类模式,比方偏心锁与非偏心锁、可重入锁与非重入锁、独享锁与共享锁、乐观锁与乐观锁、互斥锁与读写锁、自旋锁、分段锁和偏差锁/轻量级锁/重量级锁。 上面将配合示例解说各种锁的概念,冀望可能达到如下指标:一是在生产环境中不谬误的应用锁;二是在生产环境中抉择失当的锁。 对锁理解不多的状况下,应该首先保障业务的正确性,而后思考性能,比方万金油synchronized锁或者自带多重属性的ReentrantReadWriteLock锁。不因并发导致业务谬误,不呈现死锁。 随着对锁的理解增多,须要更加精准的抉择各类锁以保障更高性能要求。 二、锁的分类Java 中有两种加锁的形式:一是 synchronized 关键字,二是用 Lock 接口的实现类。 须要通过加(互斥)锁来解决线程平安问题的锁称之为乐观锁;不通过加锁来解决线程平安问题的锁称之为乐观锁。 锁的性能比拟:互斥锁 < 读写锁、自旋锁 < 乐观锁。 读写锁和自旋锁别离从两个不同角度晋升锁的效率,前者通过共享读锁来提高效率;后者通过回避阻塞-唤醒上下文切换来提高效率。 (一)偏心锁/非偏心锁偏心锁和非偏心锁具体实现类有Semaphore、ReentrantLock和ReentrantReadWriteLock。 偏心与否是指参加竞争的线程是否都有机会取得锁,偏心锁:多个线程依照申请锁的程序来获取锁;非偏心锁并不是依照申请锁的程序来获取锁,极其状况下可能会有线程始终无奈获取到锁。 偏心锁保护一个虚构的先进先出队列,依照秩序排队申请获取锁。 1、概念解读为何按锁的申请程序依照先进先出的程序获取锁可能保障偏心?当采纳先进先出的排队机制时,所有处于期待队列中的线程实践上都有机会取得锁,并且随着工夫的推移,取得锁的机会越来越大。 不是依照申请锁的程序来获取锁如何解读?synchronized 锁是典型的非偏心锁,表现形式是所有参加获取锁的线程是否可能取得锁是不可预测的。 偏心锁的深层次外延是只有线程有取得锁的需要,在相对的工夫里,肯定可能取得锁。比方服务器连贯资源,不存在客户端连贯不上的状况,这是偏心锁的典型的利用。 2、锁代码档次示意Semaphore // 非偏心锁Semaphore unfairLock = new Semaphore(5);// 偏心锁Semaphore fairLock = new Semaphore(5,true);ReentrantLock // 非偏心锁ReentrantLock unfairLock = new ReentrantLock();// 偏心锁ReentrantLock fairLock = new ReentrantLock(true);ReentrantReadWriteLock // 非偏心锁ReentrantReadWriteLock unfairLock = new ReentrantReadWriteLock();// 偏心可锁ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);下面提到的 3 个锁的实现类能配置偏心锁或者非偏心锁,真正实现锁的偏心与否是由AbstractQueuedSynchronizer抽象类的子类定义的。 3、优劣比照锁获取锁事件锁的效率备注偏心锁能够乐观预计绝对较低 非偏心锁饥饿状态绝对较高如果对锁没有特地的要求,优先选用非偏心锁偏心锁的效率比非偏心锁低的起因如下: 所有想获取锁的线程必须先到先进先出队列注册,排队能力获取锁,从获取锁的流程上减少额定的操作;有队列必然波及线程的阻塞与唤醒操作,减少了操作系统档次上下文切换调度开销。(二)可重入锁/非可重入锁可重入锁是指某个线程取得特定锁后,同一个线程内能够屡次取得该锁。synchronized关键字、ReentrantLock和ReentrantReadWriteLock属于可重入锁,Jdk 内置除此之外其它的锁都是不可重入锁。 可重入锁有两个重要的个性:同一个线程、反复获取锁。 1、可重入锁必要性剖析可重入锁可能防止同一线程屡次获取锁时的死锁景象。 ...

April 20, 2022 · 2 min · jiezi

关于锁:多线程学习锁升级

前言本篇文章次要学习synchronized关键字在JDK1.6引入的偏差锁和轻量级锁,并围绕synchronized关键字的锁的降级进行展开讨论。本篇文章探讨的锁是通过synchronized加的锁,是不同于java.util.concurrent.locks.Lock的另外一种加锁机制,后续文中提及锁,均指synchronized关键字的锁。 参考资料:《Java并发编程的艺术》 注释一. 锁的应用synchronized能够用于润饰一般办法,静态方法和代码块,拜访被synchronized关键字润饰的内容须要先获取锁,获取的这个锁具体是什么,这里暂不探讨,上面先举例子来看一下synchronized关键字如何应用。 public class SynchronizedLearn { //润饰一般办法 public synchronized void normalSyncMethod() { ...... } //润饰静态方法 public static synchronized void staticSyncMethod() { ...... } //润饰代码块 public void syncCodeBlock() { synchronized (SynchronizedLearn.class) { ...... } }}上述例子中,应用synchronized关键字润饰代码块时,传入了SynchronizedLearn类的类对象,实际上,synchronized关键字无论是润饰办法还是润饰代码块,均须要传入一个对象,咱们这里能够将传入的这个对象了解为锁,只不过在润饰办法时,会隐式地传入对象作为锁,规定如下。 润饰一般办法时,隐式传入的对象为持有一般办法的实例对象自身;润饰静态方法时,隐式传入的对象为持有静态方法的类的类对象。咱们称由synchronized关键字润饰的办法为同步办法,联合上述规定,对由synchronized关键字润饰的同步办法的拜访有如下留神点。 1. 实例对象的所有一般同步办法同一时刻只能由一个线程拜访给出一个例子如下所示。 public class SynchronizedLearn { public synchronized void normalSyncMethod1() { ...... } public synchronized void normalSyncMethod2() { ...... }}某时刻线程A和线程B都持有SynchronizedLearn的同一个实例synchronizedLearn,并且线程A胜利调用实例synchronizedLearn的normalSyncMethod1()办法,此时在线程A执行完normalSyncMethod1()办法以前,线程B都无法访问normalSyncMethod1()和normalSyncMethod2()办法。 2. 类的所有动态同步办法同一时刻只能由一个线程拜访给出一个例子如下所示。 public class SynchronizedLearn { public static synchronized void staticSyncMethod1() { ...... } public static synchronized void staticSyncMethod2() { ...... }}某时刻线程A胜利调用SynchronizedLearn类的staticSyncMethod1()办法,此时在线程A执行完staticSyncMethod1()办法以前,线程B都无法访问staticSyncMethod1()和staticSyncMethod2()办法。 ...

September 2, 2021 · 2 min · jiezi

关于锁:多线程学习队列同步器

前言AbstractQueuedSynchronizer,即队列同步器,通过继承AbstractQueuedSynchronizer并重写其办法能够实现锁或其它同步组件,本篇文章将对AbstractQueuedSynchronizer的应用和原理进行学习。 参考资料:《Java并发编程的艺术》 注释一. AbstractQueuedSynchronizer的应用AbstractQueuedSynchronizer的应用通常如下。 创立AbstractQueuedSynchronizer的子类作为同步组件(例如ReentrantLock和CountDownLatch等)的动态外部类并重写AbstractQueuedSynchronizer规定的可重写的办法;同步组件通过调用AbstractQueuedSynchronizer提供的模板办法来实现同步组件的同步性能。先对AbstractQueuedSynchronizer的可重写的办法进行阐明。AbstractQueuedSynchronizer是基于模板设计模式来实现锁或同步组件的,AbstractQueuedSynchronizer外部保护着一个字段叫做state,该字段示意同步状态,是一个整型变量,AbstractQueuedSynchronizer规定了若干办法来操作state字段,但AbstractQueuedSynchronizer自身并没有对这些办法进行实现,而是要求AbstractQueuedSynchronizer的子类来实现这些办法,上面看一下这些办法的签名和正文。 办法签名正文protected boolean tryAcquire(int arg)独占式地获取同步状态。该办法在独占式获取同步状态以前应该判断是否容许独占式获取,如果容许则尝试基于CAS形式来设置同步状态,设置胜利则示意获取同步状态胜利。protected boolean tryRelease(int arg)独占式地开释同步状态。能够了解为将同步状态还原为获取前的状态。protected int tryAcquireShared(int arg)共享式地获取同步状态。protected boolean tryReleaseShared(int arg)共享式地开释同步状态。protected boolean isHeldExclusively()判断以后线程是否独占以后队列同步器。实际上,AbstractQueuedSynchronizer规定的这些可重写的办法,均会被AbstractQueuedSynchronizer提供的模板办法所调用,在基于AbstractQueuedSynchronizer实现同步组件时,可依据同步组件的理论性能来重写这些可重写办法,而后再通过调用模板办法来实现同步组件的性能。上面看一下AbstractQueuedSynchronizer提供的模板办法。 办法签名正文public final void acquire(int arg)独占式获取同步状态,即独占式获取锁。获取胜利则该办法返回,获取失败则以后线程进入同步队列期待。public final void acquireInterruptibly(int arg) throws InterruptedException独占式获取同步状态,并响应中断。即如果获取同步状态失败,则会进入同步队列期待,此时如果线程被中断,则会退出期待状态并抛出中断异样。public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException同acquireInterruptibly(int arg),并在其根底上指定了等待时间,若超时还未获取同步状态则返回false。public final void acquireShared(int arg)共享式获取同步状态,即共享式获取锁。获取胜利则该办法返回,获取失败则以后线程进入同步队列期待,反对同一时刻多个线程获取到同步状态。public final void acquireSharedInterruptibly(int arg) throws InterruptedException共享式获取同步状态,并响应中断。public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException同acquireSharedInterruptibly(int arg),并在其根底上指定了等待时间,若超时还未获取同步状态则返回false。public final boolean release(int arg)独占式地开释同步状态,即独占式地开释锁。胜利开释锁之后会将同步队列中的第一个节点的线程唤醒。public final boolean releaseShared(int arg)共享式地开释同步状态。public final Collection<Thread> getQueuedThreads()获取在同步队列上期待地线程。上面联合《Java并发编程的艺术》中的例子的简化版,来直观的展现应用AbstractQueuedSynchronizer来实现同步组件的不便,如下所示。 ...

August 14, 2021 · 4 min · jiezi

关于锁:第43问锁用得太多-为什么要调整-Buffer-Pool

问当咱们应用一个事务操作很多数据时, MySQL 有时会报错: The total number of locks exceeds the lock table size 依据官网文档, 咱们须要调大 buffer pool 的大小: 本期试验, 咱们来摸索一下锁用得多与 buffer pool 大小的关系 试验咱们用老办法建一个数据库, 并将 buffer pool 大小调整到了最小值5M, 不便咱们复现问题 当初来模仿一个用锁特地多的事务: 咱们还是用老办法让表翻倍, 来不停地占用锁. 看一下成果: 咱们能够通过 information_schema.INNODB_TRX 来查看事务应用了多少锁, 解释一下上图中标记的这几个状态: trx_tables_locked: 该事务锁了几张表trx_rows_locked: 该事务锁了多少数据行trx_lock_structs: 该事务一共用到了多少个锁构造.一个锁构造用于锁住多个表或多个行trx_lock_memory_bytes: 该事务的锁构造一共用了多少内存再来看看 buffer pool 的状态: 解释一下 Buffer pool 的这两个状态: total 是 Buffer pool 的总页数misc 是 Buffer pool 中非数据页的页数咱们持续造数据, 让该事务应用的锁越来越多, 再来看看状态: 与最后的状态相比, 该事务应用的锁的内存增长了 (1269968 - 24784 = ) 1245184 字节 = 1216 k, 而 buffer pool 非数据页多应用了 (84-8 = ) 76页, 每页16k, 总共 1216 k ...

August 6, 2021 · 1 min · jiezi

关于锁:技术分享-MySQL中查询会锁表

作者:刘晨 网名 bisal ,具备十年以上的利用运维工作教训,目前次要从事数据库利用研发能力晋升方面的工作,Oracle ACE ,领有 Oracle OCM & OCP、EXIN DevOps Master 、SCJP 等国内认证,国内首批 Oracle YEP 成员,OCMU 成员,《DevOps 最佳实际》中文译者之一,CSDN & ITPub 专家博主,公众号"bisal的集体杂货铺",长期保持分享技术文章,屡次在线上和线下分享技术主题。 本文起源:原创投稿 *爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。 咱们晓得,Oracle 中除了应用 select ... for update ,其余查问语句不会呈现锁,即没有读锁,读一致性通过多版本解决的,能够保障在不加锁的状况下,读到同一时间的数据。 前两天共事在微信群推了一篇文章,大略意思就是通过应用 insert into select 做了数据的备份,导致了 select 的表锁住,进而影响了失常的应用。 问题来了,Oracle 中执行的 insert into select 很失常,不会呈现锁表,难道雷同的语句用在了 MySQL ,就会锁住整张表? 咱们能进行验证,MySQL 5.7 中执行如下语句,会呈现什么景象? insert into test_1 select * from test_2;test_1 和 test_2 定义如下,test_1 存在五条记录, mysql> show create table test_1\G;*************************** 1. row *************************** Table: test_1Create Table: CREATE TABLE `test_1` ( `id` int(11) NOT NULL, `name` varchar(10) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb41 row in set (0.04 sec)mysql> show create table test_2\G;*************************** 1. row *************************** Table: test_2Create Table: CREATE TABLE `test_2` ( `id` int(11) NOT NULL, `name` varchar(10) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb41 row in set (0.01 sec)mysql> select * from test_1;+----+--------+| id | name |+----+--------+| 1 | test_1 || 2 | test_2 || 3 | test_3 || 4 | test_4 || 5 | test_5 |+----+--------+5 rows in set (0.01 sec)默认状况下,show engine innodb status 显示的锁信息很无限,能够开启锁监控,如果仅须要在 show engine innodb status 显示具体的锁,能够仅关上 innodb_status_output_locks, ...

June 8, 2021 · 7 min · jiezi

关于锁:第38问分区表到底要上多少锁

问为什么我应用分区表, 有时候是几个锁, 有时候是几百把锁, 阴晴不定 试验咱们先宽油起一个数据库: 建一个分区表: 咱们心愿依据 timestamp 的日期进行分区, id 作为主键. 因为分区键必须是主键, 所以咱们将 timestamp 退出主键中. 上面咱们来钻研一下应用分区表时, 分区表到底会用多少个锁. 先插入两条数据: 场景1:咱们用 RC 隔离级别, 锁定 id = 1 的记录 此时, 查看锁信息: 能够看到: 因为咱们在 where 条件里没有用到分区键 timestamp, 那么 MySQL 要拜访每张表, 就须要给每张表上IX锁. 场景2:这次咱们换成 RR 隔离级别: 查看锁信息: 这次锁数量变成了 64 个, 每个分区表上锁住了 supremum 的 gap 区间. 这很好了解: 咱们让 MySQL 锁住了所有 id=1 可能呈现的中央, 这就包含了所有分区中相干间隙. 场景3:这次咱们在 where 条件里用到分区键: 查看锁信息: ...

June 4, 2021 · 1 min · jiezi

关于锁:做开发这几种锁机制你不得不了解一下

摘要:并发访问共享资源,如果不加锁,可能会导致数据不统一问题,通常为了解决并发拜访问题,咱们都会在访问共享资源之前加锁,保障同一时刻只有一个线程拜访。上面咱们用问答的形式阐明下各种并发锁的概念、优缺点及其利用场景。本文分享自华为云社区《一文带你全面了解各种锁机制》,原文作者:dayu_dls。 并发访问共享资源,如果不加锁,可能会导致数据不统一问题,通常为了解决并发拜访问题,咱们都会在访问共享资源之前加锁,保障同一时刻只有一个线程拜访。上面咱们用问答的形式阐明下各种并发锁的概念、优缺点及其利用场景。 1、什么是互斥锁和自旋锁,各有什么优缺点?互斥锁和自旋锁是最底层的两种锁,其余的很多锁都是基于他们的实现。当线程A获取到锁后,线程B再去获取锁,有两种解决形式,第一种是线程B循环的去尝试获取锁,直到获取胜利为止即自旋锁,另一种是线程B放弃获取锁,在锁闲暇时,期待被唤醒,即互斥锁。 互斥锁会开释以后线程的cpu,导致加锁的代码阻塞,直到线程再次被唤醒。互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮咱们切换线程,存在肯定的性能开销。 (1)当线程加锁失败,内核会把线程的状态由“运行”设置为“睡眠”,让出cpu;(2)当锁闲暇时,内核唤醒线程,状态设置为“就绪”,获取cpu执行;而自旋锁会自用户态由应用程序实现,不波及用户态到内核态的转化,没有线程上下文切换,性能绝对较好。自旋锁加锁过程: (1)查看锁的状态;(2)锁闲暇,获取锁,否则执行(1);自旋锁会利用cpu始终工作直到获取到锁,两头不会开释cpu,但如果被锁住的代码执行工夫较长,导致cpu空转,浪费资源。 2、什么是读写锁?读写锁由读锁和写锁组成。读锁又称为共享锁,S锁,写锁又称为排它锁、X锁,在mysql的事务中大量应用。写锁是独占锁,一旦线程获取写锁,其余线程不能获取写锁和读锁。 读锁是共享锁,当线程获取读锁,其余线程能够获取读锁不能获取写锁。因为并发数据读取并不会扭转共享数据导致数据不统一。读写锁把对共享资源的读操作和写操作别离加锁管制,可能进步读线程的并发性,实用于读多写少的场景。 3、什么是读优先锁、写优先锁、偏心读写锁?读优先锁心愿的是读锁可能被更多的线程获取,能够进步读线程的并发性。线程A获取了读锁,线程B想获取写锁,此时会被阻塞,线程c能够持续获取读锁,直到A和c开释锁,线程B才能够获取写锁。如果有很多线程获取读锁,且加锁的代码执行工夫很长,就到导致线程B永远获取不到写锁。 写优先锁心愿的是写锁可能被优先获取。线程A获取了读锁,线程B想获取写锁,此时会被阻塞,前面获取读锁都会失败,线程A开释锁,线程B能够获取写锁,其余获取读锁的线程阻塞。如果有很多写线程获取写锁,且加锁的代码执行工夫很长,就到导致读线程永远获取不到读锁。 下面两种锁都会造成【饥饿】景象,为解决这种问题,能够减少一个队列,把获取锁的线程(不论是写线程还是读线程)依照先进先出的形式排队,每次从队列中取出一个线程获取锁,这种获取锁的形式是偏心的。 4、什么是乐观锁和乐观锁?乐观锁是先批改共享资源,再用历史数据和以后数据比对验证这段时间共享数据有没有被批改,如果没有被批改,那么更新数据,如果有其余线程更新了共享资源,须要从新获取数据,再更新,验证,周而复始的重试,直到更新胜利。所以当数据更新操作比拟频繁,数据抵触的概率就会比拟大,重试的次数就会多,节约CPU资源。 乐观锁其实全程没有加锁,也叫无锁编程,所以针对读多写少的场景,并发性能较高,典型的实现MVCC,mysql中会应用MVCC构建一致性读来保障可反复读。乐观锁是在访问共享资源之前通通加锁。当并发抵触概率较高时,乐观锁不在实用,乐观锁就排上用场。互斥锁、自旋锁都是乐观锁的实现。 点击关注,第一工夫理解华为云陈腐技术~

May 25, 2021 · 1 min · jiezi

关于列表解析:技术实践丨列存表并发更新时的锁等待问题原理

摘要:当开启transaction,执行updata的语句执行胜利,不执行commit或rollback;再开启另一个窗口,执行upadate语句,会呈现失败(报错:锁期待超时)的状况,然而如果对于上一个窗口执行rollback,此窗口update能够执行胜利,该种状况应思考该表是否为列存表。本文分享自华为云社区《列存表并发更新时时的锁期待问题原理》,原文作者:PFloyd 。 当开启transaction,执行updata的语句执行胜利,不执行commit或rollback;再开启另一个窗口,执行upadate语句,会呈现失败(报错:锁期待超时)的状况,然而如果对于上一个窗口执行rollback,此窗口update能够执行胜利,该种状况应思考该表是否为列存表。 【问题根因】如果应用的是列存表,在事务中执行update操作时,是以CU为单位进行加锁的,所以在事务未提交时并发更新同一CU的其余数据时会呈现锁期待的状况,期待超时的时候会呈现报错 【机制原理】1.CU为压缩单元(Compress Unit),列存储的最小单位,导入数据时生成,生成后数据固定不可更改,单个CU最多存储1列60000行数据。同一列的CU间断存储在一个文件中,当大于1G时,切换到新文件中。其中的Ctid字段标识列存表的一行,由cu_id和CU外行号(cu_id, offset)组成;一次性写入的多条的数据位于同一CU。 2. 为了避免页面同一个元组被两个事务同时更新,在进行update时,都会加上行级锁,对于行存来说是对一行数据加锁,对于列存来说就是对一个CU加锁,当一个事务update未提交时,其余事务是无奈同时去更新同一CU的数据的。 3.进行update操作后,旧元组被标记为deleted,新元组会写到一个新的CU中 【案例剖析】1.依据现场的报错信息,能够确定是并发更新报错;进行update时,会申请行级锁,在申请行级锁之前会申请transactionid锁,期待超时后报错信息为:waiting for ShareLock on transaction xxx after ..ms 2.客户反馈更新的并不是同一条数据,id不同,询问客户后得悉呈现问题的是列存表,查问更新的数据是否处于同一CU。 查问后发现处于同一CU,合乎预期。 3.本地场景复现: 起事务执行update操作: 事务未提交并发更新数据呈现期待: 查问后发现两条数据位于同一cu: 【相干问题】为什么update胜利一次之后,下一次update就不会相互等锁了? 这是因为update胜利之后,旧数据被标记为deleted,新数据写入新的CU,这两条数据不再是同一个CU了,也就不存在这种锁抵触 【解决计划】列存表不适宜频繁的update场景,列存频繁的update容易触发并发更新等锁超时,并且会导致小CU过多,而每个CU都会扩大至8192字节进行对齐,从而导致磁盘空间迅速收缩;频繁的点查或频繁update场景倡议应用行存表。 点击关注,第一工夫理解华为云陈腐技术~

April 17, 2021 · 1 min · jiezi

关于物联网:一文带你剖析LiteOS互斥锁Mutex源代码

摘要:多任务环境下会存在多个工作拜访同一公共资源的场景,而有些公共资源是非共享的临界资源,只能被独占应用。LiteOS应用互斥锁来防止这种抵触,互斥锁是一种非凡的二值性信号量,用于实现对临界资源的独占式解决。本文分享自华为云社区《LiteOS内核源码剖析系列七 互斥锁Mutex》,原文作者:zhushy。 多任务环境下会存在多个工作拜访同一公共资源的场景,而有些公共资源是非共享的临界资源,只能被独占应用。LiteOS应用互斥锁来防止这种抵触,互斥锁是一种非凡的二值性信号量,用于实现对临界资源的独占式解决。另外,互斥锁能够解决信号量存在的优先级翻转问题。用互斥锁解决临界资源的同步拜访时,如果有工作拜访该资源,则互斥锁为加锁状态。此时其余工作如果想拜访这个临界资源则会被阻塞,直到互斥锁被持有该锁的工作开释后,其余工作能力从新拜访该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个工作正在拜访这个临界资源,保障了临界资源操作的完整性。 本文咱们来一起学习下LiteOS互斥锁模块的源代码,文中所波及的源代码,均能够在LiteOS开源站点https://gitee.com/LiteOS/LiteOS 获取。互斥锁源代码、开发文档,示例程序代码如下: LiteOS内核互斥锁源代码包含互斥锁的公有头文件kernelbaseincludelos_mux_pri.h、头文件kernelincludelos_mux.h、C源代码文件kernelbaselos_mux.c。 开发指南文档–互斥锁在线文档https://gitee.com/LiteOS/Lite... 接下来,咱们看下互斥锁的构造体,互斥锁初始化,互斥锁罕用操作的源代码。 1、互斥锁构造体定义和罕用宏定义1.1 互斥锁构造体定义在文件kernelbaseincludelos_mux_pri.h定义的互斥锁管制块构造体有2个,MuxBaseCB和LosMuxCB,前者和后者的前三个成员一样,能够和pthread_mutex_t共享内核互斥锁机制。构造体源代码如下,构造体成员的解释见正文局部。 typedef struct { LOS_DL_LIST muxList; /**< 互斥锁双向链表 */ LosTaskCB *owner; /**< 以后持有锁的工作 */ UINT16 muxCount; /**< 锁被持有的次数*/} MuxBaseCB;typedef struct { LOS_DL_LIST muxList; /**< 互斥锁双向链表 */ LosTaskCB *owner; /**< 以后持有锁的工作 */ UINT16 muxCount; /**< 锁被持有的次数*/ UINT8 muxStat; /**< 互斥锁状态: OS_MUX_UNUSED, OS_MUX_USED */ UINT32 muxId; /**< 互斥锁Id */} LosMuxCB;1.2 互斥锁罕用宏定义零碎反对创立多少互斥锁是依据开发板状况应用宏LOSCFG_BASE_IPC_MUX_LIMIT定义的,互斥锁Id是UINT32类型的,由2局部组成:count和muxId,别离处于高16位和低16位。创立互斥锁,应用后删除时,互斥锁回收到互斥锁池时,互斥锁Id的高16位即count值会加1,这样能够用来示意该互斥锁被创立删除的次数。muxId取值为[0,LOSCFG_BASE_IPC_MUX_LIMIT),示意互斥锁池中各个的互斥锁的编号。 ⑴处的宏用来宰割count和muxId的位数,⑵处互斥锁被删除时更新互斥锁Id,能够看出高16位为count和低16位为muxId。⑶处获取互斥锁Id的低16位。⑷依据互斥锁Id获取对应的互斥锁被创立删除的次数count。⑸处从互斥锁池中获取指定互斥锁Id对应的互斥锁管制块。 ⑴ #define MUX_SPLIT_BIT 16⑵ #define SET_MUX_ID(count, muxId) (((count) << MUX_SPLIT_BIT) | (muxId))⑶ #define GET_MUX_INDEX(muxId) ((muxId) & ((1U << MUX_SPLIT_BIT) - 1))⑷ #define GET_MUX_COUNT(muxId) ((muxId) >> MUX_SPLIT_BIT)⑸ #define GET_MUX(muxId) (((LosMuxCB *)g_allMux) + GET_MUX_INDEX(muxId))2、互斥锁初始化互斥锁在内核中默认开启,用户能够通过宏LOSCFG_BASE_IPC_MUX进行敞开。开启互斥锁的状况下,在系统启动时,在kernelinitlos_init.c中调用OsMuxInit()进行互斥锁模块初始化。 ...

April 12, 2021 · 5 min · jiezi

关于锁:技术分享-MySQL-行锁超时排查方法优化

作者:xuty本文起源:原创投稿 * 爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。 一、纲要#### 20191219 10:10:10,234 | com.alibaba.druid.filter.logging.Log4jFilter.statementLogError(Log4jFilter.java:152) | ERROR | {conn-10593, pstmt-38675} execute error. update xxx set xxx = ? , xxx = ? where RowGuid = ?com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction之前在如何无效排查解决MySQL行锁期待超时问题 文章中介绍了如何监控解决行锁超时报错,过后介绍的监控计划次要是以 shell 脚本 + general_log 来捕捉行锁期待信息,起初感觉比拟麻烦,因而优化后改成用 Event + Procedure 的办法定时在 MySQl 内执行,将行锁期待信息记录到日志表中,并且退出了 pfs 表中的事务上下文信息,这样能够省去登陆服务器执行脚本与剖析 general_log 的过程,更加便捷。 因为用到了 Event 和 performance_schema 下的零碎表,所以须要关上两者的配置,pfs 应用默认监控项就能够,这里次要应用到的是 events_statements_history 表,默认会保留会话 10 条 SQL。 performance_schema = onevent_scheduler = 1 二、步骤目前该办法仅在 MySQL 5.7 版本应用过,MySQL 8.0 未测试。2.1 创立库create database `innodb_monitor`;2.2 创立存储过程use innodb_monitor;delimiter ;;CREATE PROCEDURE pro_innodb_lock_wait_check()BEGIN declare wait_rows int; set group_concat_max_len = 1024000;CREATE TABLE IF NOT EXISTS `innodb_lock_wait_log` ( `report_time` datetime DEFAULT NULL, `waiting_id` int(11) DEFAULT NULL, `blocking_id` int(11) DEFAULT NULL, `duration` varchar(50) DEFAULT NULL, `state` varchar(50) DEFAULT NULL, `waiting_query` longtext DEFAULT NULL, `blocking_current_query` longtext DEFAULT NULL, `blocking_thd_last_query` longtext, `thread_id` int(11) DEFAULT NULL); select count(*) into wait_rows from information_schema.innodb_lock_waits ; if wait_rows > 0 THEN insert into `innodb_lock_wait_log` SELECT now(),r.trx_mysql_thread_id waiting_id,b.trx_mysql_thread_id blocking_id,concat(timestampdiff(SECOND,r.trx_wait_started,CURRENT_TIMESTAMP()),'s') AS duration, t.processlist_command state,r.trx_query waiting_query,b.trx_query blocking_current_query,group_concat(left(h.sql_text,10000) order by h.TIMER_START DESC SEPARATOR ';\n') As blocking_thd_query_history,thread_id FROM information_schema.innodb_lock_waits w JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id LEFT JOIN performance_schema.threads t on t.processlist_id = b.trx_mysql_thread_id LEFT JOIN performance_schema.events_statements_history h USING(thread_id) group by thread_id,r.trx_id order by r.trx_wait_started; end if;END;;2.3 创立事件事件 每隔 5 秒 (通常等于 innodb_lock_wait_timeout 的值)执行一次,继续监控 7 天,完结后会主动删除事件,也能够自定义保留时长。 ...

February 21, 2021 · 2 min · jiezi

关于锁:理解-Go-的互斥锁

背景在应用 Go 构建 Web 应用程序时,所有传入的 HTTP 申请都会被路由到对应解决逻辑的 Goroutine 中。如果应用程序在解决申请的时候,有读写同一块内存数据, 就存在竞态条件的危险。( Spanner 反对 读写锁定 的事务模式,单个逻辑工夫点以原子形式执行一组读写,不存在竞态条件问题)<!--more--> 数据竞争一个很常见的竞态条件场景就是银行账户余额的读写。思考一种状况,有两个 Goroutine 尝试同时将钱存到同一个银行余额中,例如: 指令Goroutine1Goroutine2银行存款余额1读取余额 <- 500元 500元2 读取余额 <- 500元500元3存入100元,写入银行账号 -> 600元 600元4 存入50元,写入银行账号 -> 550元550元只管进行了两次独自的贷款,但因为第二个 Goroutine 互相对账号余额做更改,因而仅第二笔贷款反映在最终余额中。 这种特定类型的竞态条件称为数据竞争。当两个或多个 Goroutine 尝试同时应用一条共享数据(在此示例中为银行余额)时,它们可能会触发,然而操作后果取决于调度程序执行其指令的程序。 Go 官网博客 也列举了数据竞争导致的一些问题: Race conditions are among the most insidious and elusive programming errors. They typically cause erratic and mysterious failures, often long after the code has been deployed to production. While Go's concurrency mechanisms make it easy to write clean concurrent code, they don't prevent race conditions. Care, diligence, and testing are required.Go 提供了许多工具来帮忙咱们防止数据竞争问题。其中包含用于在 Goroutine 之间进行数据通信的 channel ; 用于在运行时监督对内存的非同步拜访的 Race Detector,以及 Atomic 和 Sync 软件包中的各种“Lock”性能。这些性能之一是互斥锁,咱们将在本文的其余部分中介绍。 ...

October 1, 2020 · 2 min · jiezi

关于锁:java锁机制

锁的作用:在Java中synchronized关键字被罕用于保护数据一致性。 synchronized机制是给共享资源上锁,只有拿到锁的线程才能够访问共享资源,这样就能够强制使得对共享资源的拜访都是程序的。 Java开发人员都意识synchronized,应用它来实现多线程的同步操作是非常简单的,只有在须要同步的对方的办法、类或代码块中退出该关键字,它可能保障在同一个时刻最多只有一个线程执行同一个对象的同步代码,可保障润饰的代码在执行过程中不会被其余线程烦扰。应用synchronized润饰的代码具备原子性和可见性,在须要进程同步的程序中应用的频率十分高,能够满足个别的进程同步要求。 锁的状态: 无锁状态无锁没有对资源进行锁定,所有的线程都能拜访并批改同一个资源,但同时只有一个线程能批改胜利。偏差锁偏是偏心、偏差的意思。当没有线程竞争的时候,偏差于第一个取得这个资源的线程。无竞争的状况下把整个同步都打消掉。轻量级锁无竞争的状况下应用CAS打消同步应用的互斥量。 在代码行将进入同步块的时候,JVM 会在栈空间中开拓一块空间(Lock Record),而后将对象头的Mark Word复制到Lock Record 中。JVM会应用CAS操作尝试将对象的MarkWord更新为指向Lock Record,并且将对象的Mark Word 的锁标记位转变为00,示意以后对象处于轻量级锁定状态。如果这个操作失败了,阐明有其余线程于以后线程竞争,虚构机会首先检测对象的Mark Word是否只想以后线程的栈帧,如果是以后线程曾经领有了这个对象的锁,那么间接进入同步块继续执行,否则阐明这个锁曾经被其余线程占用了,那么会收缩为重量级锁,锁标记位变为10,前面期待锁的线程也必须进入阻塞状态。重量级锁用户态转化为内核态,进入阻塞状态。锁的优化:自旋锁打消锁粗化 锁的降级过程:锁的对象:

September 17, 2020 · 1 min · jiezi

关于锁:iOS之多线程漫谈

前言提到线程,那就不得不提CPU,古代的CPU有一个很重要的个性,就是工夫片,每一个取得CPU的工作只能运行一个工夫片规定的工夫。其实线程对操作系统来说就是一段代码以及运行时数据。操作系统会为每个线程保留相干的数据,当接管到来自CPU的工夫片中断事件时,就会按肯定规定从这些线程中抉择一个,复原它的运行时数据,这样CPU就能够继续执行这个线程了。也就是其实就单核CUP而言,并没有方法实现真正意义上的并发执行,只是CPU疾速地在多条线程之间调度,CPU调度线程的工夫足够快,就造成了多线程并发执行的假象。并且就单核CPU而言多线程能够解决线程阻塞的问题,然而其自身运行效率并没有进步,多CPU的并行运算才真正解决了运行效率问题。零碎中正在运行的每一个应用程序都是一个过程,每个过程零碎都会调配给它独立的内存运行。也就是说,在iOS零碎中中,每一个利用都是一个过程。一个过程的所有工作都在线程中进行,因而每个过程至多要有一个线程,也就是主线程。那多线程其实就是一个过程开启多条线程,让所有工作并发执行。多线程在肯定意义上实现了过程内的资源共享,以及效率的晋升。同时,在肯定水平上绝对独立,它是程序执行流的最小单元,是过程中的一个实体,是执行程序最根本的单元,有本人栈和寄存器。明天要讲的内容1、 iOS中的多线程2、 iOS中的各种线程锁3、 你不得不知的runloop 1.1 PthreadsPOSIX线程(POSIX threads),简称Pthreads,是线程的POSIX规范。该规范定义了创立和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都应用Pthreads作为操作系统的线程。 咱们来用Pthreads创立一个线程去执行一个工作: 记得引入头文件`#import "pthread.h"`-(void)pthreadsDoTask{ /* pthread_t:线程指针 pthread_attr_t:线程属性 pthread_mutex_t:互斥对象 pthread_mutexattr_t:互斥属性对象 pthread_cond_t:条件变量 pthread_condattr_t:条件属性对象 pthread_key_t:线程数据键 pthread_rwlock_t:读写锁 // pthread_create():创立一个线程 pthread_exit():终止以后线程 pthread_cancel():中断另外一个线程的运行 pthread_join():阻塞以后的线程,直到另外一个线程运行完结 pthread_attr_init():初始化线程的属性 pthread_attr_setdetachstate():设置脱离状态的属性(决定这个线程在终止时是否能够被联合) pthread_attr_getdetachstate():获取脱离状态的属性 pthread_attr_destroy():删除线程的属性 pthread_kill():向线程发送一个信号 pthread_equal(): 对两个线程的线程标识号进行比拟 pthread_detach(): 拆散线程 pthread_self(): 查问线程本身线程标识号 // *创立线程 int pthread_create(pthread_t _Nullable * _Nonnull __restrict, //指向新建线程标识符的指针 const pthread_attr_t * _Nullable __restrict, //设置线程属性。默认值NULL。 void * _Nullable (* _Nonnull)(void * _Nullable), //该线程运行函数的地址 void * _Nullable __restrict); //运行函数所需的参数 *返回值: *若线程创立胜利,则返回0 *若线程创立失败,则返回出错编号 */ // pthread_t thread = NULL; NSString *params = @"Hello World"; int result = pthread_create(&thread, NULL, threadTask, (__bridge void *)(params)); result == 0 ? NSLog(@"creat thread success") : NSLog(@"creat thread failure"); //设置子线程的状态设置为detached,则该线程运行完结后会主动开释所有资源 pthread_detach(thread);}void *threadTask(void *params) { NSLog(@"%@ - %@", [NSThread currentThread], (__bridge NSString *)(params)); return NULL;}输入后果: ...

August 13, 2020 · 17 min · jiezi

关于锁:java-锁总结

1.基于数据库的乐观锁和乐观锁 有个版本字段,更新的时候先读进去,更新的时候作为where条件update。如果管制版本是状态不是单向的话还是有ABA的问题。单向的没问题。 乐观锁在查问的时候就把数据给锁住。2.基于jdk的乐观锁和乐观锁 synchronized是乐观锁,这种线程一旦失去锁,其余须要锁的线程就挂起的状况就是乐观锁。 CAS操作的就是乐观锁,比拟并替换。每次不加锁而是假如没有抵触而去实现某项操作,如果因为抵触失败就重试,直到胜利为止。这种乐观锁的问题:ABA问题,如果始终再循环对cpu的开销比拟大。不能保障代码块的原子性 CAS机制所保障的只是一个变量的原子性操作,而不能保障整个代码块的原子性。

July 17, 2020 · 1 min · jiezi

技术分享-如何方便的查看-Metadata-Lock

作者:洪斌爱可生南区负责人兼技术服务总监,MySQL  ACE,擅长数据库架构规划、故障诊断、性能优化分析,实践经验丰富,帮助各行业客户解决 MySQL 技术问题,为金融、运营商、互联网等行业客户提供 MySQL 整体解决方案。本文来源:转载自公众号-玩转MySQL*爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。MySQL 的 Metadata Lock 机制是为了保护并发访问数据对象的一致性。DDL、表级锁、全局锁都会产生 metadata lock,写操作在锁释放前会被阻塞,而任何会话持有了 metadata lock 在 processlist 中是看不到的。 当发现其他会话被阻塞,就很难快速找到“罪魁祸首”。之前也曾介绍过《快速定位令人头疼的全局锁》。 最近折腾 MySQL shell 插件时发现了一个方法,也能很方便的查询元数据锁。 在社区 MySQL shell 插件库 https://github.com/lefred/mys... 的 ext.check.get_locks() 函数查看实例上锁情况。要在 MySQL 8.0 以上版本,它用到了 CTE 查询。 如果是 MySQL 5.7,需先开启 metadata 的 instrument。 call sys.ps_setup_enable_instrument('wait/lock/metadata/sql/mdl%')执行此 SQL 查看元数据锁情况,上锁会话、SQL、锁类型能关联显示。 SELECT ps.*, lock_summary.lock_summaryFROM sys.processlist ps INNER JOIN ( SELECT owner_thread_id, GROUP_CONCAT( DISTINCT CONCAT( mdl.LOCK_STATUS, ' ', mdl.lock_type, ' on ', IF( mdl.object_type = 'USER LEVEL LOCK', CONCAT(mdl.object_name, ' (user lock)'), CONCAT(mdl.OBJECT_SCHEMA, '.', mdl.OBJECT_NAME) ) ) ORDER BY mdl.object_type ASC, mdl.LOCK_STATUS ASC, mdl.lock_type ASC SEPARATOR '\n' ) as lock_summary FROM performance_schema.metadata_locks mdl GROUP BY owner_thread_id ) lock_summary ON (ps.thd_id = lock_summary.owner_thread_id) \ G ...

June 18, 2020 · 1 min · jiezi

事务与锁完整版

事务初学的时候,感觉事务的四大特性就那么回事,不就是一堆事要么完成,要么全部失败吗。还有经常说的脏读,幻读,不可重复读根本无法理解,就是那个存款取款的例子,我修改了数据,对方看到我修改的数据,这不很正常吗。现在看来,当时根本就不知道并发是什么鬼,更何谈并发事物了。 然后给你来一堆名词,共享锁,排它锁,悲观锁,乐观锁...... 想想就觉得那时候能记下来已经是奇迹了。 Spring 还给事务弄了一个传播机制的家伙,Spring 事务传播机制可以看这篇文章 。 本文应该来说是对初学者的福音,有一定经验的人看的话应该也会有收获。 事务的四大特性ACID这个是刚入门面试的时候必问一个面试题,刚入行的时候我是硬生生背下来的。 原子性(Atomicity) 一件事情的所有步骤要么全部成功,要么全部失败,不存在中间状态。一致性(Consistency) 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。隔离性(Isolation) 两个事务之间是隔离程度,具体的隔离程度由隔离级别决定,隔离级别有 读未提交的 (read-uncommitted)读提交的 (read-committed)可重复读 (repeatable-read)串行 (serializable)持久性 (Durability) 一个事务提交后,数据库状态就永远的发生改变,不会因为数据库宕机而让提交不生效。一个事务和并发事务事务指的是从开始事务->执行操作->提交/回滚 整个过程,在程序中使用一个连接对应一个事务 -- sql 中的事务START TRANSACTION;select * from question;commit ;// 最原始的 jdbc 事务Connection connection = 获取数据库连接;try{ connection.setAutoCommit(false); // todo something connection.commit();}catch(Exception e){log(e); connection.rollback();}finally{ try{connection.close()}catch(Exception e){log(e);};}并发事务是指两个事务一同开始执行,如果两个事务操作的数据之间有交集,则很有可能产生冲突。这时怎么办呢,其实这也是 临界资源 的一种,在应用程序中,我们解决这类问题的关键是加锁,在数据库的实现也是一样,但在数据库中需要考虑更多。常见的需要考虑的问题有(下面说的我和人都是指一个会话) 对整张表数据加锁还是对当前操作的数据行加锁,这时有表锁和行锁,MyISAM 引擎只支持表锁,而 innodb 支持行锁和表锁如果数据量庞大,比如选到了百万数据,千万数据,不可能一次性全部加锁, 会很影响性能,innodb 是逐条加锁的数据库的操作其实有很大一部分是查询操作,如果锁住数据,任何人都不让进的话,性能也会很低下,所以会有读锁和写锁,也叫共享锁和排它锁根据检测冲突的时间不同,可以在一开始就把数据锁住,直到我使用完,还有就是在真正操作数据的时候才去锁住,就是悲观锁和乐观锁就算是让别人可以读数据,在两个事务也可能互相影响,比如脏读。事务的隔离级别及会带来的问题看过网上的大部分文章,基本都是一个表格来演示两个事务的并发,有的根本就是直接抄的,不知道那作者真的懂了没,其实我们是可以用客户端来模拟两个事务并发的情况的,打开两个 session ,让两个事务互相穿插。 下面的演示都是基于 mysql5.7 版本,查询事务隔离级别和修改隔离级别语句 -- 查看事务隔离级别select @@tx_isolation;-- 修改当前 session 事务隔离级别set session transaction isolation level read uncommitted;set session transaction isolation level read committed ;set session transaction isolation level repeatable read ;set session transaction isolation level serializable;-- 开启事务提交和回滚START TRANSACTION;select * from question;commit ;rollback;准备数据表,暂时先使用 InnoDB 引擎 ...

November 2, 2019 · 3 min · jiezi

了解Mysql的表级锁-深究mysql锁

一、定义每次锁定的是一张表的锁机制就是表级别锁定(table-level)。它是MySQL各存储引擎中粒度最大的锁定机制。二、优缺点优点 实现逻辑简单,开销小。获取锁和释放锁的速度快。由于表级锁一次会将整个表锁定,所以能很好的避免死锁问题。缺点 由于锁粒度最大,因此出现争用被锁定资源的概率也会最高,致使并发度十分低下。三、支持存储引擎使用表级锁定的主要有MyISAM,MEMORY,CSV等一些非事务性存储引擎。四、表级锁类型MySQL的表级锁有两种类型:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。 锁模式的兼容性: 对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写操作;对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的。当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。 五、如何加表锁在执行查询语句(select)前,会自动给涉及的所有表加读锁在执行更新操作(update、delete、insert等)前,会自动给涉及的表加写锁。这个过程并不需要用户干预,因此不需要直接用lock table命令给MyISAM表显式加锁。显示加写锁: // 当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。// 其他线程的读、写操作都会等待,直到锁被释放为止。// test表将会被锁住,另一个线程执行select * from test where id = 3;将会一直等待,直到test表解锁LOCK TABLE test WRITE; 显示加读锁 // test表将会被锁住,另一个线程执行select * from test where id = 3;不会等待// 执行UPDATE test set name='peter' WHERE id = 4;将会一直等侍,直到test表解锁LOCK table test READ;显示释放锁: UNLOCK TABLES;需要注意的是,在同一个SQL session里,如果已经获取了一个表的锁定,则对没有锁的表不能进行任何操作,否则会报错。 // 锁定test表LOCK table test WRITE;// 操作锁定表没问题SELECT * from test where id = 4;// 操作没有锁的表会报错SELECT * from bas_farm where id =1356报错:[Err] 1100 - Table 'bas_farm' was not locked with LOCK TABLES。这是因为MyISAM希望一次获得sql语句所需要的全部锁。这也正是myisam表不会出现死锁的原因。 ...

October 15, 2019 · 1 min · jiezi

Java并发21并发设计模式-Balking模式线程安全的单例模式

上一篇文章中,我们提到可以用“多线程版本的 if”来理解 Guarded Suspension 模式,不同于单线程中的 if,这个“多线程版本的 if”是需要等待的,而且还很执着,必须要等到条件为真。但很显然这个世界,不是所有场景都需要这么执着,有时候我们还需要快速放弃。 需要快速放弃的一个最常见的例子是各种编辑器提供的自动保存功能。自动保存功能的实现逻辑一般都是隔一定时间自动执行存盘操作,存盘操作的前提是文件做过修改,如果文件没有执行过修改操作,就需要快速放弃存盘操作。下面的示例代码将自动保存功能代码化了,很显然 AutoSaveEditor 这个类不是线程安全的,因为对共享变量 changed 的读写没有使用同步,那如何保证 AutoSaveEditor 的线程安全性呢? class AutoSaveEditor{ // 文件是否被修改过 boolean changed=false; // 定时任务线程池 ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor(); // 定时执行自动保存 void startAutoSave(){ ses.scheduleWithFixedDelay(()->{ autoSave(); }, 5, 5, TimeUnit.SECONDS); } // 自动存盘操作 void autoSave(){ if (!changed) { return; } changed = false; // 执行存盘操作 // 省略且实现 this.execSave(); } // 编辑操作 void edit(){ // 省略编辑逻辑 ...... changed = true; }}读写共享变量 changed 的方法 autoSave() 和 edit() 都加互斥锁就可以了。这样做虽然简单,但是性能很差,原因是锁的范围太大了。那我们可以将锁的范围缩小,只在读写共享变量 changed 的地方加锁,实现代码如下所示。 ...

July 12, 2019 · 2 min · jiezi

重入锁最重要的几个方法

这几个方法都是 Lock 接口中定义的:1)lock()获取锁,有以下三种情况:锁空闲:直接获取锁并返回,同时设置锁持有者数量为:1;当前线程持有锁:直接获取锁并返回,同时锁持有者数量递增1;其他线程持有锁:当前线程会休眠等待,直至获取锁为止;2)lockInterruptibly()获取锁,逻辑和 lock() 方法一样,但这个方法在获取锁过程中能响应中断。3)tryLock()从关键字字面理解,这是在尝试获取锁,获取成功返回:true,获取失败返回:false, 这个方法不会等待,有以下三种情况:锁空闲:直接获取锁并返回:true,同时设置锁持有者数量为:1;当前线程持有锁:直接获取锁并返回:true,同时锁持有者数量递增1;其他线程持有锁:获取锁失败,返回:false;4)tryLock(long timeout, TimeUnit unit)逻辑和 tryLock() 差不多,只是这个方法是带时间的。5)unlock()释放锁,每次锁持有者数量递减 1,直到 0 为止。所以,现在知道为什么 lock 多少次,就要对应 unlock 多少次了吧。6)newCondition返回一个这个锁的 Condition 实例,可以实现 synchronized 关键字类似 wait/ notify 实现多线程通信的功能,不过这个比 wait/ notify 要更灵活,更强大!

July 2, 2019 · 1 min · jiezi

ReentrantLock-学习

留白,将写一篇ReentrantLock的实现使用

May 21, 2019 · 1 min · jiezi

一个mysql死锁场景分析

最近遇到一个mysql在RR级别下的死锁问题,感觉有点意思,研究了一下,做个记录。涉及知识点:共享锁、排他锁、意向锁、间隙锁、插入意向锁、锁等待队列 场景隔离级别:Repeatable-Read表结构如下 create table t ( id int not null primary key AUTO_INCREMENT, a int not null default 0, b varchar(10) not null default '', c varchar(10) not null default '', unique key uniq_a_b(a,b), unique key uniq_c(c));初始化数据 insert into t(a,b,c) values(1,'1','1');有A/B两个session,按如下顺序执行两个事务 结果是 B执行完4之后还是一切正常A执行5的时候,被blockB接着执行6,B报死锁,B回滚,A插入数据show engine innodb status中可以看到死锁信息,这里先不贴,先解释几种锁的概念,再来理解死锁过程 共享(S)锁/互斥(X)锁共享锁允许事务读取记录互斥锁允许事务读写记录这两种其实是锁的模式可以和行锁、间隙锁混搭,多个事务可以同时持有S锁,但是只有一个事务能持有X锁 意向锁一种表锁(也是一种锁模式),表明有事务即将给对应表的记录加S或者X锁。SELECT ... LOCK IN SHARE MODE会在给记录加S锁之前先给表加IS锁,SELECT ... FOR UPDATE会在给记录加X锁之前给表加IX锁。这是一种mysql的锁优化策略,并不是很清楚意向锁的优化点在哪里,求大佬指教 两种锁的兼容情况如下 行锁很简单,给对应行加锁。比如update、select for update、delete等都会给涉及到的行加上行锁,防止其他事务的操作 间隙锁在RR隔离级别下,为了防止幻读现象,除了给记录本身,还需要为记录两边的间隙加上间隙锁。比如列a上有一个普通索引,已经有了1、5、10三条记录,select * from t where a=5 for update除了会给5这条记录加行锁,还会给间隙(1,5)和(5,10)加上间隙锁,防止其他事务插入值为5的数据造成幻读。当a上的普通索引变成唯一索引时,不需要间隙锁,因为值唯一,select * from t where a=5 for update不可能读出两条记录来。 ...

May 18, 2019 · 4 min · jiezi

Java锁相关知识总结

锁的种类: synchronize自动锁(最常用) 可以给类、方法、代码块加锁lock手动锁,只能锁代码块儿,且需要手动加锁解锁,忘记解锁会造成死锁volatile轻量级锁,不会造成线程阻塞,只能修饰变量,且只能保证变量的修改可见性,无法保证原子性解决死锁的方法: 1)尽量使用tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。 2)尽量使用java.util.concurrent(jdk 1.5以上)包的并发类代替手写控制并发,比较常用的是ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等等,实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高 3)尽量降低锁的使用粒度,尽量不要几个功能用同一把锁 4)尽量减少同步的代码块悲观锁与乐观锁 悲观锁用于线程冲突率高的场景,用提前加锁保证线程安全乐观锁用于线程冲突率底的场景,用修改前后版本号是否一致保证线程安全未完待续

May 7, 2019 · 1 min · jiezi

JUC读写锁ReentrantReadWriteLock

一、写在前面在上篇我们聊到了可重入锁(排它锁)ReentrantLcok ,具体参见《J.U.C|可重入锁ReentrantLock》 ReentrantLcok 属于排它锁,本章我们再来一起聊聊另一个我们工作中出镜率很高的读-写锁。 二、简介重入锁ReentrantLock是排他锁(互斥锁),排他锁在同一时刻仅有一个线程可访问,但是在大多数场景下,大部分时间都是提供读服务的,而写服务占用极少的时间,然而读服务不存在数据竞争的问题,如果一个线程在读时禁止其他线程读势必会降低性能。所以就有了读写锁。 读写锁内部维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般排他锁有着显著的提升。 读写锁在同一时间可以允许多个读线程同时访问,但是写线程访问时,所有的读线程和写线程都会阻塞。 主要有以下特征: 公平性选择:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。重进入:该锁支持重进入,以读写线程为列,读线程在获取到读锁之后,能再次获取读锁。而写线程在获取写锁后能够再次获取写锁,同时也可以获取读锁。锁降级:遵循获取写锁、读锁再释放写锁的次序,写锁能够降级成为读锁。读写锁最多支持65535个递归写入锁和65535个递归读取锁。 锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁 读写锁ReentrantReadWriteLock实现接口ReadWriteLock,该接口维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。 三、主要方法介绍读写锁ReentrantReadWriteLock 实现了ReadWriteLock 接口,该接口维护一对相关的锁即读锁和写锁。 public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock();}ReadWriteLock定义了两个方法。readLock()返回用于读操作的锁,writeLock()返回用于写操作的锁。ReentrantReadWriteLock定义如下: /** 内部类 读锁*/private final ReentrantReadWriteLock.ReadLock readerLock; /** 内部类 写锁*/private final ReentrantReadWriteLock.WriteLock writerLock;/** 执行所有同步机制 */final Sync sync;// 默认实现非公平锁public ReentrantReadWriteLock() { this(false);}// 利用给定的公平策略初始化ReentrantReadWriteLockpublic ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } // 返回写锁 public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } //返回读锁 public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } // 实现同步器,也是实现锁的核心 abstract static class Sync extends AbstractQueuedSynchronizer { // 省略实现代码 } // 公平锁的实现 static final class FairSync extends Sync { // 省略实现代码 } // 非公平锁的实现 static final class NonfairSync extends Sync { // 省略实现代码 } // 读锁实现 public static class ReadLock implements Lock, java.io.Serializable { // 省略实现代码 } // 写锁实现 public static class WriteLock implements Lock, java.io.Serializable { // 省略实现代码 }ReentrantReadWriteLock 和 ReentrantLock 其实都一样,锁核心都是Sync, 读锁和写锁都是基于Sync来实现的。从这分析其实ReentrantReadWriteLock就是一个锁,只不过内部根据不同的场景设计了两个不同的实现方式。其读写锁为两个内部类: ReadLock、WriteLock 都实现了Lock 接口。 ...

April 30, 2019 · 6 min · jiezi

Java-中关于锁的一些理解

jdk 6 对锁进行了优化,让他看起来不再那么笨重,synchronized有三种形式:偏向锁,轻量级锁,重量级锁. 介绍三种锁之前,引入几个接下来会出现的概念 mark work: 对象头,对象头中存储了一些对象的信息,这个是锁的根本,任何锁都需要依赖mark word 来维持锁的运作,对象头中存储了当前持有锁的线程,hashCode,GC的一些信息都存储在对象头中. 在JVM中,对象在内存中除了本身的数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark word和类型指针。另外对于数组而言还会有一份记录数组长度的数据. 类型指针是指向该对象所属类对象的指针,mark word用于存储对象的HashCode、GC分代年龄、锁状态等信息。在32位系统上mark word长度为32bit,64位系统上长度为64bit。为了能在有限的空间里存储下更多的数据,其存储格式是不固定的,在32位系统上各状态的格式如下: 可以看到锁信息也是存在于对象的mark word中的。当对象状态为偏向锁时,mark word存储的是偏向的线程ID;当状态为轻量级锁时,mark word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁时,为指向堆中的monitor对象的指针. Lock Record:前面对象头中提到了Lock Record,接下来说下Lock Record,Lock Record存在于线程栈中,翻译过来就是锁记录,它会拷贝一份对象头中的mark word信息到自己的线程栈中去,这个拷贝的mark word 称为Displaced Mark Word ,另外还有一个指针指向对象 monitor:monitor存在于堆中,什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。 与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。 Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下: Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULLEntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程RcThis:表示blocked或waiting在该monitor record上的所有线程的个数Nest:用来实现重入锁的计数HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁 (摘自:Java中synchronized的实现原理与应用)说完几个关键概念之后来说一下锁的问题: 偏向锁 偏向锁是锁的级别中最低的锁,举个例子: 在此demo中,获得操作list的一直都是main线程,没有第二个线程参与操作,此时的锁就是偏向锁,偏向锁很轻,jdk 1.6默认开启,当第一个线程进入的时候,对象头中的threadid为0,表示未偏向任何线程,也叫做匿名偏向量 public class SyncDemo1 { public static void main(String[] args) { SyncDemo1 syncDemo1 = new SyncDemo1(); for (int i = 0; i < 100; i++) { syncDemo1.addString("test:" + i); } } private List<String> list = new ArrayList<>(); public synchronized void addString(String s) { list.add(s); }}当第一个线程进入的时候发现是匿名偏向状态,则会用cas指令把mark words中的threadid替换为当前线程的id如果替换成功,则证明成功拿到锁,失败则锁膨胀;当线程第二次进入同步块时,如果发现线程id和对象头中的偏向线程id一致,则经过一些比较之后,在当前线程栈的lock record中添加一个空的Displaced Mark Word,由于操作的是私有线程栈,所以不需要cas操作,synchronized带来的开销基本可以忽略;当其他线程进入同步块中时,发现偏向线程不是当前线程,则进入到撤销偏向锁的逻辑,当达到全局安全点时,锁开始膨胀为轻量级锁,原来的线程仍然持有锁,如果发现偏向线程挂了,那么就把对象的头改为无锁状态,锁膨胀 ...

April 28, 2019 · 1 min · jiezi

数据库MySQL锁机制热备分表

欢迎关注公众号:【爱编码】如果有需要后台回复2019赠送1T的学习资料哦!! 注:本文大都来自互联网,文字较多,基本是概念,若想深入了解,还需各位自己找文章研究。 表锁和行锁机制表锁(MyISAM和InnoDB)表锁的优势:开销小;加锁快;无死锁表锁的劣势:锁粒度大,发生锁冲突的概率高,并发处理能力低加锁的方式:自动加锁。查询操作(SELECT),会自动给涉及的所有表加读锁,更新操作(UPDATE、DELETE、INSERT),会自动给涉及的表加写锁。也可以显示加锁: 共享读锁:lock table tableName read;独占写锁:lock table tableName write;批量解锁:unlock tables;什么场景下用表锁InnoDB默认采用行锁,在未使用索引字段查询时升级为表锁。即便你在条件中使用了索引字段,MySQL会根据自身的执行计划,考虑是否使用索引(所以explain命令中会有possible_key 和 key)。如果MySQL认为全表扫描效率更高,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。 第一种情况:全表更新。事务需要更新大部分或全部数据,且表又比较大。若使用行锁,会导致事务执行效率低,从而可能造成其他事务长时间锁等待和更多的锁冲突。 第二种情况:多表查询。事务涉及多个表,比较复杂的关联查询,很可能引起死锁,造成大量事务回滚。这种情况若能一次性锁定事务涉及的表,从而可以避免死锁、减少数据库因事务回滚带来的开销。 行锁(InnoDB的行锁)行锁的劣势:开销大;加锁慢;会出现死锁行锁的优势:锁的粒度小,发生锁冲突的概率低;处理并发的能力强加锁的方式:自动加锁。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁;当然我们也可以显示的加锁: 共享锁:select * from tableName where … + lock in share more排他锁:select * from tableName where … + for update行锁优化1 尽可能让所有数据检索都通过索引来完成,避免无索引行或索引失效导致行锁升级为表锁。2 尽可能避免间隙锁带来的性能下降,减少或使用合理的检索范围。3 尽可能减少事务的粒度,比如控制事务大小,而从减少锁定资源量和时间长度,从而减少锁的竞争等,提供性能。4 尽可能低级别事务隔离,隔离级别越高,并发的处理能力越低。 InnoDB和MyISAM的最大不同点有两个:一,InnoDB支持事务(transaction);二,默认采用行级锁。加锁可以保证事务的一致性,可谓是有人(锁)的地方,就有江湖(事务);我们先简单了解一下事务知识。 MySQL 事务事务是由一组SQL语句组成的逻辑处理单元,事务具有ACID属性。原子性(Atomicity):事务是一个原子操作单元。在当时原子是不可分割的最小元素,其对数据的修改,要么全部成功,要么全部都不成功。一致性(Consistent):事务开始到结束的时间段内,数据都必须保持一致状态。隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的”独立”环境执行。持久性(Durable):事务完成后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。 事务隔离级别脏读,不可重复读,幻读,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。 更多精彩文章https://www.cnblogs.com/zyy16...https://www.cnblogs.com/itdra... 双机热备概念双机热备双机热备特指基于高可用系统中的两台服务器的热备(或高可用),因两机高可用在国内使用较多,故得名双机热备。从广义上讲,就是对于重要的服务,使用两台服务器,互相备份,共同执行同一服务。当一台服务器出现故障时,可以由另一台服务器承担服务任务,从而在不需要人工干预的情况下,自动保证系统能持续提供服务。 双机热备和备份的区别热备份指的是:High Available(HA)即高可用,而备份指的是Backup,即数据备份的一种,这是两种不同的概念,应对的产品也是两种功能上完全不同的产品。热备份主要保障业务的连续性,实现的方法是故障点的转移。而备份,主要目的是为了防止数据丢失,而做的一份拷贝,所以备份强调的是数据恢复而不是应用的故障转移。 双机热备分类按工作中的切换方式分为:主-备方式(Active-Standby方式)和双主机方式(Active-Active方式)。 主-备方式即指的是一台服务器处于某种业务的激活状态(即Active状态),另一台服务器处于该业务的备用状态(即Standby状态)。双主机方式即指两种不同业务分别在两台服务器上互为主备状态(即Active-Standby和Standby-Active状态)。mysql 双机热备工作原理简单的说就是把 一个服务器上执行过的sql语句在别的服务器上也重复执行一遍, 这样只要两个数据库的初态是一样的,那么它们就能一直同步。当然这种复制和重复都是mysql自动实现的,我们只需要配置即可。我们进一步详细介绍原理的细节, 这有一张图: 上图中有两个服务器, 演示了从一个主服务器(master) 把数据同步到从服务器(slave)的过程。这是一个主-从复制的例子。 主-主互相复制只是把上面的例子反过来再做一遍。 所以我们以这个例子介绍原理。对于一个mysql服务器, 一般有两个线程来负责复制和被复制。当开启复制之后。 1. 作为主服务器Master,  会把自己的每一次改动都记录到 二进制日志 Binarylog 中。 (从服务器会负责来读取这个log, 然后在自己那里再执行一遍。)2. 作为从服务器Slave, 会用master上的账号登陆到 master上, 读取master的Binarylog,  写入到自己的中继日志 Relaylog, 然后自己的sql线程会负责读取这个中继日志,并执行一遍。  到这里主服务器上的更改就同步到从服务器上了。在mysql上可以查看当前服务器的主,从状态。 其实就是当前服务器的 Binary(作为主服务器角色)状态和位置。 以及其RelayLog(作为从服务器)的复制进度。 ...

April 26, 2019 · 1 min · jiezi

JUCAQS共享式源码分析

一、写在前面上篇给大家聊了独占式的源码,具体参见《J.U.C|AQS独占式源码分析》 这一章我们继续在AQS的源码世界中遨游,解读共享式同步状态的获取和释放。 二、什么是共享式共享式与独占式唯一的区别是在于同一时刻可以有多个线程获取到同步状态。 我们以读写锁为例来看两者,一个线程在对一个资源文件进行读操作时,那么这一时刻对于文件的写操作均被阻塞,而其它线程的读操作可以同时进行。当写操作要求对资源独占操作,而读操作可以是共享的,两种不同的操作对同一资源进行操作会是什么样的?看下图 共享式访问资源,其他共享时均被允许,而独占式被阻塞。 独占式访问资源时,其它访问均被阻塞。 通过读写锁给大家一起温故下独占式和共享式概念,上一节我们已经聊过独占式,本章我们主要聊共享式。 主要讲解方法 protected int tryAcquireShared(int arg);共享式获取同步状态,返回值 >= 0 表示获取成功,反之则失败。protected boolean tryReleaseShared(int arg): 共享式释放同步状态。三、核心方法分析*3.1 同步状态的获取public final void acquireShared(int arg) 共享式获取同步状态的顶级入口,如果当前线程未获取到同步状态,将会加入到同步队列中等待,与独占式唯一的区别是在于同一时刻可以有多个线程获取到同步状态。方法源码 public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }方法函数解析 tryAcquireShared(arg):获取同步状态,返回值大于等于0表示获取成功,否则失败。doAcquireShared(arg):共享式获取共享状态,包含构建节点,加入队列等待,唤醒节点等操作。源码分析 同步器的 acquireShared 和 doAcquireShared 方法 //请求共享锁的入口public final void acquireShared(int arg) { // 当state != 0 并且tryAcquireShared(arg) < 0 时才去才获取资源 if (tryAcquireShared(arg) < 0) // 获取锁 doAcquireShared(arg); }// 以共享不可中断模式获取锁private void doAcquireShared(int arg) { // 将当前线程一共享方式构建成 node 节点并将其加入到同步队列的尾部。这里addWaiter(Node.SHARED)操作和独占式基本一样, final Node node = addWaiter(Node.SHARED); // 是否成功标记 boolean failed = true; try { // 等待过程是否被中断标记 boolean interrupted = false; 自旋 for (;;) { // 获取当前节点的前驱节点 final Node p = node.predecessor(); // 判断前驱节点是否是head节点,也就是看自己是不是老二节点 if (p == head) { // 如果自己是老二节点,尝试获取资源锁,返回三种状态 // state < 0 : 表示获取资源失败 // state = 0: 表示当前正好线程获取到资源, 此时不需要进行向后继节点传播。 // state > 0: 表示当前线程获取资源锁后,还有多余的资源,需要向后继节点继续传播,获取资源。 int r = tryAcquireShared(arg); // 获取资源成功 if (r >= 0) { // 当前节点线程获取资源成功后,对后继节点进行逻辑操作 setHeadAndPropagate(node, r); // setHeadAndPropagate(node, r) 已经对node.prev = null,在这有对p.next = null; 等待GC进行垃圾收集。 p.next = null; // help GC // 如果等待过程被中断了, 将中断给补上。 if (interrupted) selfInterrupt(); failed = false; return; } } // 判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt() if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }在acquireShared(int arg)方法中,同步器调用tryAcquireShared(arg)方法获取同步状态,返回同步状态有两种。 ...

April 24, 2019 · 4 min · jiezi

锁开销优化以及-CAS-简单说明

锁开销优化以及 CAS 简单说明[TOC] 锁互斥锁是用来保护一个临界区,即保护一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待。 在谈及锁的性能开销,一般都会说锁的开销很大,那锁的开销有多大,主要耗在哪,怎么提高锁的性能。 锁的开销现在锁的机制一般使用 futex(fast Userspace mutexes),内核态和用户态的混合机制。还没有futex的时候,内核是如何维护同步与互斥的呢?系统内核维护一个对象,这个对象对所有进程可见,这个对象是用来管理互斥锁并且通知阻塞的进程。如果进程A要进入临界区,先去内核查看这个对象,有没有别的进程在占用这个临界区,出临界区的时候,也去内核查看这个对象,有没有别的进程在等待进入临界区,然后根据一定的策略唤醒等待的进程。这些不必要的系统调用(或者说内核陷入)造成了大量的性能开销。为了解决这个问题,Futex就应运而生。 Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex就是通过在用户态的检查,(motivation)如果了解到没有竞争就不用陷入内核了,大大提高了low-contention时候的效率。 mutex 是在 futex 的基础上用的内存共享变量来实现的,如果共享变量建立在进程内,它就是一个线程锁,如果它建立在进程间共享内存上,那么它是一个进程锁。pthread_mutex_t 中的 _lock 字段用于标记占用情况,先使用CAS判断_lock是否占用,若未占用,直接返回。否则,通过__lll_lock_wait_private 调用SYS_futex 系统调用迫使线程进入沉睡。 CAS是用户态的 CPU 指令,若无竞争,简单修改锁状态即返回,非常高效,只有发现竞争,才通过系统调用陷入内核态。所以,FUTEX是一种用户态和内核态混合的同步机制,它保证了低竞争情况下的锁获取效率。 所以如果锁不存在冲突,每次获得锁和释放锁的处理器开销仅仅是CAS指令的开销。 确定一件事情最好的方法是实际测试和观测它,让我们写一段代码来测试无冲突时锁的开销: #include <pthread.h>#include <stdlib.h>#include <stdio.h>#include <time.h>static inline long long unsigned time_ns(struct timespec* const ts) { if (clock_gettime(CLOCK_REALTIME, ts)) { exit(1); } return ((long long unsigned) ts->tv_sec) * 1000000000LLU + (long long unsigned) ts->tv_nsec;}int main(){ int res = -1; pthread_mutex_t mutex; //初始化互斥量,使用默认的互斥量属性 res = pthread_mutex_init(&mutex, NULL); if(res != 0) { perror("pthread_mutex_init failed\n"); exit(EXIT_FAILURE); } long MAX = 1000000000; long c = 0; struct timespec ts; const long long unsigned start_ns = time_ns(&ts); while(c < MAX) { pthread_mutex_lock(&mutex); c = c + 1; pthread_mutex_unlock(&mutex); } const long long unsigned delta = time_ns(&ts) - start_ns; printf("%f\n", delta/(double)MAX); return 0;}说明:以下性能测试在腾讯云 Intel(R) Xeon(R) CPU E5-26xx v4 1核 2399.996MHz 下进行。 ...

April 24, 2019 · 6 min · jiezi

且听我一个故事讲透一个锁原理之synchronized

微信公众号:IT一刻钟大型现实非严肃主义现场一刻钟与你分享优质技术架构与见闻,做一个有剧情的程序员关注可第一时间了解更多精彩内容,定期有福利相送哟。故事从这里展开蜀国有一个皇帝叫蜀道难,他比较难伺候,别的皇帝早朝都是在大殿上同时接见所有大臣,共商国是。他不一样,他说早朝你们不要有事没事都跑过来叽叽喳喳,有事则来,无事则该干啥干啥去,然后安排太监每天早上在大门口守着,每次只允许一个大臣进来汇报情况。“你敢多放进来一个就砍脑袋的干活。“太监赶紧下跪,说“谪!“。第一天,太监传话钦天监求见,皇帝允了,钦天监上殿报曰:”臣禀报,昨日我司夜观星象,西方忽现王星忽明忽暗,恐戎狄那边有乱。““朕知道了,退下吧”。一日无事。第二天,太监传话钦天监求见,皇帝允了。一日无事。第三天,太监传话钦天监求见……一日无事。第四天,钦天监……一日无事。第五天,皇帝不耐烦了,和贾太监说,钦天监这老家伙整天是不是闲着没事,以后他来了不用给我禀报,直接放他上殿讲,讲完让他走吧。国泰民安的日子依旧过着,每天只有钦天监一个人来报告,贾太监每次看到是钦天监来了,也懒得搭理了,直接放他进去了。(这就是偏向锁,稍后我细细道来)又一日,钦天监如往常进殿报道,贾太监站在门口打着盹,忽然耳边传来一个声音:“贾太监,帮我禀告圣上,工部李尚书求见。”“emmm…进去吧…嗯?等等,尚书大人你先等等,钦天监在里面,你等会再来求见吧。”太监一阵后怕,寻思着钦天监还在里面呢,这要是放进去了,我这脑袋可就没了,果然嗜睡误事。过了一会儿,李尚书回来询问求见,被告知钦天监还没走,只好又离去。又过了一会儿,李尚书又回来询问求见,正巧钦天监走了,太监进殿传话说工部李尚书求见,皇帝宣觐见,李尚书进殿上报了一番东南连连大雨,已派人去监察水利,修缮河堤。(这就是轻量级锁)忽一日,西戎狄和北匈奴同时对帝国西方和北方发难,前线战事消息如片片雪花纷纷涌入京城,瞬间殿外来了一群大臣有要事禀告。一会儿这个来问贾公公我可以进去了吗?一会儿那个来问贾公公我可以进去了吗?把贾太监累的哟,一天下来光说“稍后再来”都把嘴皮子磨破了,没几日,贾太监就跪在皇帝面前哭泣道:“圣上啊,快想想办法呀,奴才这身子骨就要交代在门口了。”皇帝一听,说你傻啊,叫他们一个个在门外排队啊,谁叫你要他们稍后来求见的。贾太监细思大喜,觉得有理,次日在门口竖起一个牌子“禀报要事者,这边排队”,贾太监再也不用一个人对着一群人反复回话,只需要每次出来一个,然后传话放进去一个,就可以了。(这就是重量级锁)上面这个故事,分别讲述了synchronized内部四种级别的状态,分别是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。重量级锁状态我们首先从重量级锁开始讲,重量级锁是通过互斥量(Mutex)来实现的,即一个线程进入了synchronized同步块,在未完成任务时,会阻塞后面的所有线程。就像上面的故事所讲的,要禀告要事的大臣只能在大殿门口外一个接一个的阻塞排队。之所以称它为重量级锁,是因为Java线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要依靠操作系统从当前用户态转换到核心态中,这种状态转换需要耗费处理器很多时间,对于简单同步块,可能状态转换时间比用户代码执行时间还要长,导致实际业务处理所占比偏小,性能损失较大。当然这个在虚拟机层面进行了一些比如自旋等待,锁粗化等等的优化,避免陷入频繁的切换状态。在这里我就不细讲了,有兴趣的可以关注我,我后续再和各位看官讲上一讲。轻量级锁状态轻量级锁是JDK6引入的,它的轻量是相较于通过系统互斥量实现的传统锁,轻量锁并不是用来取代重量级锁的,而是在没有大量线程竞争的情况下,减少系统互斥量的使用,降低性能的损耗。轻量级锁是通过CAS(Compare And Swap)机制实现的,即如果锁被其他线程所占用,当前线程会通过自旋来获取锁,从而避免用户态与核心态的转换。就像上面故事所说的,大殿中钦天监在汇报工作,工部尚书要求见,并不需要贾太监每次都进去问一下皇帝,惹得皇帝龙颜大怒,而是大臣自己隔一段时间便来询问贾太监能不能进去,不能就稍后再来问,直到可以进去为止。偏向锁状态偏向锁也是JDK6引入的,它存在的依据是“大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得”。它是通过记录第一次进入同步块的线程id来实现的,如果下一个要进入同步块的线程与记录的线程id相同,则说明这个锁由此线程占有,可以直接进入到同步块,不用执行CAS。就像故事中的,如果每天只有钦天监一个人来的话,就不用贾太监禀告了,贾太监每次一看到钦天监,寻思着,哟,钦天监呢,您自个儿直接进去吧,说完自个儿出来吧。如果说轻量锁是为了消除系统互斥量带来的性能损耗,那么偏向锁就是为了消除CAS带来的性能损耗,使之在无竞争的情况下消除整个同步,性能无限接近非同步。如何通过这四种状态实现性能大幅度提升的Java对象头要说这个问题,我们需要先讲一下Java对象头,每个对象都会有一个对象头,它分为三个部分:内容说明Mark Word存储对象的hashcode或锁信息Class Metadata Address存储到对象类型数据的指针Array length数组的长度(如果当前对象是数组)从表格可见,synchronized锁的信息是存在对象头里一个叫Mark Word的区域里的,考虑到虚拟机的空间效率,Mark Word被设计成非固定的数据结构,会根据对象的状态复用存储空间来存储不同的内容:锁的升级当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word是未锁定,未偏向但可偏向状态,此时Mark Word中的Thread id为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。偏向锁状态—>无锁不可偏向状态/轻量级锁状态当第一个线程尝试进入同步块时,发现Mark Word中线程ID为0,则会使用CAS将自己的线程ID设置到Mark Word中,并且,在当前线程栈中由高到低顺序找到可用的Lock Record,将线程ID记录下。完成这些,此线程就获取了锁对象的偏向锁。当该偏向线程再次进入同步块时,发现锁对象偏向的就是当前线程,会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来统计重入的次数,然后继续执行同步块代码,因为线程栈是私有的,不需要CAS指令进行操作,所以在偏向锁模式下,同一个线程,只会执行一个CAS,之后获取释放锁只需要对Lock Record做操作,性能损耗基本可以忽略。当另外一个线程试图进入同步块时,发现Mark Word中线程ID与自己不相符,这个时候就会引发偏向锁的撤销,变成无锁不可偏向状态或轻量级锁状态,当然,这只是宏观上的描述,严格意义上讲是不准确的,因为里面还存在重偏向机制,这里就不过于深入,在后续的文章中,我会专门出一篇文章,给各位看官详细介绍偏向锁到底是怎么回事。无锁不可偏向状态—>轻量级锁状态当锁对象变成无锁不可偏向状态时,多个线程运行到同步块以后,会检查锁对象状态值标志是否加锁,如果没有锁,就把锁对象的Mark Word信息拷贝存储到当前线程栈桢中Lock Record里,然后通过CAS尝试把对象的Mark Word的值改变成一个指向自己线程的指针。如果成功,则当前线程获得锁对象的轻量级锁,其他线程的CAS就会失败,因为锁对象的Mark Word已经变成一个新的指针了,必须等待线程释放锁,此时其他线程则通过自旋来竞争锁。当获取锁的线程执行完毕释放锁的时候,会将Lock Record里面之前拷贝的值还原到锁对象的Mark Word中。轻量级锁状态—>重量级锁状态当自旋次数超过JVM预期上限,会影响性能,所以竞争的线程就会把锁对象的Mark Word指向重锁,所谓的重锁,实际上就是一个堆上的monitor对象,即,重量级锁的状态下,对象的Mark Word为指向一个堆中monitor对象的指针。然后所有的竞争线程放弃自旋,逐个插入到monitor对象里的一个队列尾部,进入阻塞状态。当成功获取轻量级锁的线程执行完毕,尝试通过CAS释放锁时,因为Mark Word已经指向重锁,导致轻量级锁释放失败,这时线程就会知道锁已经升级为重量级锁, 它不仅要释放当前锁,还要唤醒其他阻塞的线程来重新竞争锁。大概流程如下图所示:这里有一点需注意的是:锁只能升级,不能降级。锁的对比锁优点缺点适用场景偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景轻量级锁竞争的线程不会堵塞,提高了程序的响音速度始终得不到锁的线程,使用自旋会消耗CPU追求响应时间,同步块执行速度非常快重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较慢synchronized的底层实现synchronized无非以下两种:1.对象锁:修饰非静态方法,修饰代码块2.类锁:修饰静态方法,修饰代码块其中按照修饰类型来分,又可以分为代码块同步和方法同步代码块同步代码块同步锁的是对象,使用monitorenter和monitorexit指令实现的。虽然我知道多一行代码少一位看官的定理,但是这里还是必须贴一张代码图,来证明我没有瞎说,是有理有据的“理据服”。想要降服妖怪,就得先将其打回原形,所以我们先对一段简单的代码进行反编译,得到它的字节码。 final Object lock = new Object(); public int subtr(int i){ synchronized (lock){ return i-1; } }字节码:可以看出,monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit插入到同步代码块结束的地方,正常情况下monitorenter和monitorexit是一对一的匹配,而后面又出现了一个monitorexit,是因为那里是异常处,用来保证方法执行异常的时候,可以自动解锁,而不会造成死锁。方法同步方法同步的实现官方没有透露,我们尝试对一个方法同步的代码进行反编译。 public synchronized int add(int i){ return i+1; }字节码:从字节码里也看不到monitorenter和monitorexit,智能发现flags那里,多了一个ACC_SYNCHRONIZED的标示,没什么头绪。不过我猜想,底层应该是锁方法所属的对象或类。这就是synchronized的大致原理,打回原形之后来看,是不是就觉得也不过如此?有什么疑问或更好的解读,可以在下方留言,我们进行愉快友好的磋商交流。如果觉得有用,记得分享~

April 13, 2019 · 1 min · jiezi

[Java并发-3]Java互斥锁,解决原子性问题

在前面的分享中我们提到。一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为“原子性”思考:在32位的机器上对long型变量进行加减操作存在并发问题,什么原因!?原子性问题如何解决我们已经知道原子性问题是线程切换,如果能够禁用线程切换不就解决了这个问题了嘛?而操作系统做线程切换是依赖 CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换。在单核 CPU 时代,这个方案的确是可行的。这里我们以 32 位 CPU 上执行 long 型变量的写操作为例来说明这个问题,long 型变量是 64 位,在 32 位 CPU 上执行写操作会被拆分成两次写操作(写高 32 位和写低 32 位,如下图所示)。图在单核 CPU 场景下,同一时刻只有一个线程执行,禁止 CPU 中断,获得 CPU 使用权的线程就可以不间断地执行,所以两次写操作一定是:要么都被执行,要么都没有被执行,具有原子性。但是在多核场景下,同一时刻,有可能有两个线程同时在执行,一个线程执行在 CPU-1 上,一个线程执行在 CPU-2 上,此时禁止 CPU 中断,只能保证 CPU 上的线程连续执行,并不能保证同一时刻只有一个线程执行,如果这两个线程同时写 long 型变量高 32 位的话,还是会出现问题。同一时刻只有一个线程执行这个条件非常重要,我们称之为互斥。如果我们能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。简易锁模型互斥的解决方案,锁。大家脑中的模型可能是这样的。图线程在进入临界区之前,首先尝试加锁 lock(),如果成功,则进入临界区,此时我们称这个线程持有锁;否则就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁 unlock()。这样理解本身没有问题,但却很容易让我们忽视两个非常非常重要的点:我们锁的是什么?我们保护的又是什么?改进后的锁模型我们知道在现实世界里,锁和锁要保护的资源是有对应关系的,比如我用我家的锁保护我家的东西。在并发编程世界里,锁和资源也应该有这个关系,但这个关系在我们上面的模型中是没有体现的,所以我们需要完善一下我们的模型。图首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源 R;其次,我们要保护资源 R 就得为它创建一把锁 LR;最后,针对这把锁 LR,我们还需在进出临界区时添上加锁操作和解锁操作。另外,在锁 LR 和受保护资源之间,增加了一条连线,这个关联关系非常重要,这里很容易发生BUG,容易出现了类似锁自家门来保护他家资产的事情。Java语言提供的锁锁是一种通用的技术方案,Java 语言提供的synchronized 关键字,就是锁的一种实现。synchronized关键字可以用来修饰方法,也可以用来修饰代码块,基本使用:class X { // 修饰非静态方法 synchronized void foo() { // 临界区 } // 修饰静态方法 synchronized static void bar() { // 临界区 } // 修饰代码块 Object obj = new Object(); void baz() { synchronized(obj) { // 临界区 } }} 参考我们上面提到的模型,加锁 lock() 和解锁 unlock() 这两个操作在Java 编译会自动加上。这样做的好处就是加锁 lock() 和解锁 unlock() 一定是成对出现的。上面的代码我们看到只有修饰代码块的时候,锁定了一个 obj 对象,那修饰方法的时候锁定的是什么呢?这个也是 Java 的一条隐式规则:当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;当修饰非静态方法的时候,锁定的是当前实例对象 this。class X { // 修饰静态方法 synchronized(X.class) static void bar() { // 临界区 }}class X { // 修饰非静态方法 synchronized(this) void foo() { // 临界区 }}锁解决 count+1 问题我们来尝试下用synchronized解决之前遇到的 count+=1 存在的并发问题,代码如下所示。SafeCalc 这个类有两个方法:一个是 get() 方法,用来获得 value 的值;另一个是 addOne() 方法,用来给 value 加 1,并且 addOne() 方法我们用 synchronized 修饰。那么我们使用的这两个方法有没有并发问题呢?class SafeCalc { long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; }}我们先来看看 addOne() 方法,首先可以肯定,被 synchronized 修饰后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行 addOne() 方法,所以一定能保证原子操作,那是否有可见性问题呢?让我们回顾下之前讲一条 Happens-Before的规则。管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。管程,就是我们这里的 synchronized.我们知道 synchronized 修饰的临界区是互斥的,也就是说同一时刻只有一个线程执行临界区的代码;而这里指的就是前一个线程的解锁操作对后一个线程的加锁操作可见.我们就能得出前一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的。按照这个规则,如果多个线程同时执行 addOne() 方法,可见性是可以保证的,也就说如果有 1000 个线程执行 addOne() 方法,最终结果一定是 value 的值增加了 1000。我们在来看下,执行 addOne() 方法后,value 的值对 get() 方法是可见的吗?这个可见性是没法保证的。管程中锁的规则,是只保证后续对这个锁的加锁的可见性,而 get() 方法并没有加锁操作,所以可见性没法保证。那如何解决呢?很简单,就是 get() 方法也 synchronized 一下,完整的代码如下所示。class SafeCalc { long value = 0L; synchronized long get() { return value; } synchronized void addOne() { value += 1; }}上面的代码转换为我们提到的锁模型,就是下面图示这个样子。get() 方法和 addOne() 方法都需要访问 value 这个受保护的资源,这个资源用 this 这把锁来保护。线程要进入临界区 get() 和 addOne(),必须先获得 this 这把锁,这样 get() 和 addOne() 也是互斥的。图锁和受保护资源的关系我们前面提到,受保护资源和锁之间的关联关系非常重要,他们的关系是怎样的呢?一个合理的关系是:受保护资源和锁之间的关联关系是 N:1 的关系上面那个例子我稍作改动,把 value 改成静态变量,把 addOne() 方法改成静态方法,此时 get() 方法和 addOne() 方法是否存在并发问题呢?class SafeCalc { static long value = 0L; synchronized long get() { return value; } synchronized static void addOne() { value += 1; }}如果你仔细观察,就会发现改动后的代码是用两个锁保护一个资源。这个受保护的资源就是静态变量 value,两个锁分别是 this 和 SafeCalc.class。我们可以用下面这幅图来形象描述这个关系。由于临界区 get() 和 addOne() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。图:小结:互斥锁,在并发领域的知名度极高,只要有了并发问题,大家首先容易想到的就是加锁,加锁能够保证执行临界区代码的互斥性。synchronized 是 Java 在语言层面提供的互斥原语,其实 Java 里面还有很多其他类型的锁,但作为互斥锁,原理都是相通的:锁,一定有一个要锁定的对象,至于这个锁定的对象要保护的资源以及在哪里加锁 / 解锁,就属于设计层面的事情。如何一把锁保护多个资源?保护没有关联关系的多个资源当我们要保护多个资源时,首先要区分这些资源是否存在关联关系。同样这对应到编程领域,也很容易解决。例如,银行业务中有针对账户余额(余额是一种资源)的取款操作,也有针对账户密码(密码也是一种资源)的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题,这个还是很简单的。相关的示例代码如下,账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,我们创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,我们创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的,很简单。class Account { // 锁:保护账户余额 private final Object balLock = new Object(); // 账户余额 private Integer balance; // 锁:保护账户密码 private final Object pwLock = new Object(); // 账户密码 private String password; // 取款 void withdraw(Integer amt) { synchronized(balLock) { if (this.balance > amt){ this.balance -= amt; } } } // 查看余额 Integer getBalance() { synchronized(balLock) { return balance; } } // 更改密码 void updatePassword(String pw){ synchronized(pwLock) { this.password = pw; } } // 查看密码 String getPassword() { synchronized(pwLock) { return password; } }}当然,我们也可以用一把互斥锁来保护多个资源,例如我们可以用 this 这一把锁来管理账户类里所有的资源:但是用一把锁就是性能太差,会导致取款、查看余额、修改密码、查看密码这四个操作都是串行的。而我们用两把锁,取款和修改密码是可以并行的。用不同的锁对受保护资源进行精细化管理,能够提升性能 。这种锁还有个名字,叫 细粒度锁保护有关联关系的多个资源如果多个资源是有关联关系的,那这个问题就有点复杂了。例如银行业务里面的转账操作,账户 A 减少 100 元,账户 B 增加 100 元。这两个账户就是有关联关系的。那对于像转账这种有关联关系的操作,我们应该怎么去解决呢?先把这个问题代码化。我们声明了个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作 transfer() 没有并发问题呢?class Account { private int balance; // 转账 void transfer(Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }相信你的直觉会告诉你这样的解决方案:用户 synchronized 关键字修饰一下 transfer() 方法就可以了,于是你很快就完成了相关的代码,如下所示。class Account { private int balance; // 转账 synchronized void transfer(Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把锁this,符合我们前面提到的,多个资源可以用一把锁来保护,这看上去完全正确呀。真的是这样吗?可惜,这个方案仅仅是看似正确,为什么呢?问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。下面我们具体分析一下,假设有 A、B、C 三个账户,余额都是 200 元,我们用两个线程分别执行两个转账操作:账户 A 转给账户 B 100 元,账户 B 转给账户 C 100 元,最后我们期望的结果应该是账户 A 的余额是 100 元,账户 B 的余额是 200 元, 账户 C 的余额是 300 元。我们假设线程 1 执行账户 A 转账户 B 的操作,线程 2 执行账户 B 转账户 C 的操作。这两个线程分别在两颗 CPU 上同时执行,那它们是互斥的吗?我们期望是,但实际上并不是。因为线程 1 锁定的是账户 A 的实例(A.this),而线程 2 锁定的是账户 B 的实例(B.this),所以这两个线程可以同时进入临界区 transfer()。同时进入临界区的结果是什么呢?线程 1 和线程 2 都会读到账户 B 的余额为 200,导致最终账户 B 的余额可能是 300(线程 1 后于线程 2 写 B.balance,线程 2 写的 B.balance 值被线程 1 覆盖),可能是 100(线程 1 先于线程 2 写 B.balance,线程 1 写的 B.balance 值被线程 2 覆盖),就是不可能是 200。使用锁的正确知识在上一篇文章中,我们提到用同一把锁来保护多个资源,也就是现实世界的“包场”,那在编程领域应该怎么“包场”呢?很简单,只要我们的 锁能覆盖所有受保护资源 就可以了。这里我们用 Account.class· 作为共享的锁。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。class Account { private int balance; // 转账 void transfer(Account target, int amt){ synchronized(Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }下面这幅图很直观地展示了我们是如何使用共享的锁 Account.class 来保护不同对象的临界区的。图思考下:上面的写法不是最佳实践,锁是可变的。小结对如何保护多个资源已经很有心得了,关键是要分析多个资源之间的关系。如果资源之间没有关系,很好处理,每个资源一把锁就可以了。如果资源之间有关联关系,就要选择一个粒度更大的锁,这个锁应该能够覆盖所有相关的资源。除此之外,还要梳理出有哪些访问路径,所有的访问路径都要设置合适的锁。思路:在第一个示例程序里,我们用了两把不同的锁来分别保护账户余额、账户密码,创建锁的时候,我们用的是:private final Object xxxLock = new Object();如果账户余额用 this.balance 作为互斥锁,账户密码用 this.password 作为互斥锁,你觉得是否可以呢? ...

April 10, 2019 · 3 min · jiezi

一文彻底搞懂面试中常问的各种“锁”

前言锁,顾名思义就是锁住一些资源,当只有我们拿到钥匙的时候,才能操作锁住的资源。在我们的Java,数据库,还有一些分布式的环境中,总是充斥着各种各样的锁让人头疼,例如“公平锁”、“自旋锁”、“读写锁”、“分布式锁”等等。其实真实的情况是,锁并没有那么多,很多概念只是从不同的功能特性,设计,以及锁的状态这些不同的侧重点来说明的,因此我们可以根据不同的分类来搞明白为什么会有这些“锁”?坐稳扶好了,准备开车。正文“公平锁”与“非公平锁”公平锁:指线程在等待获取同一个锁的时候,是严格按照申请锁的时间顺序来进行的,这就意味着在程序正常运作的时候,不会有线程执行不到,而被“饿死”,但是也需要额外的机制来维护这种顺序,所以效率相对于非公平锁会差点。非公平锁:概念跟“公平锁”恰恰相反,随机线程获取锁,相率相对高。new ReentrantLock(); //默认非公平锁new ReentrantLock(true); //公平锁“重入锁(递归锁)”与“不可重入锁(自旋锁)”这里要注意了,重入/递归,不可重入/自旋,虽然名字不同,但是确实是同一种锁,只是从锁的表现跟实现方式的角度来命名而已。重入锁:当一个线程获取了A锁以后,若后续方法运行被A锁锁住的话,当前线程也是可以直接进入的。public class Demo { private Lock lockA; public Demo(Lock Lock) { this.lockA = lock; } public void methodA() { lockA.lock(); methodB(); lockA.unlock(); } public void methodB() { lockA.lock(); //dosm lockA.unlock(); } }当我们运行methodA()的时候,线程获取了lockA,然后调用methodB()的时候发现也需要lockA,由于这是一个可重入锁,所以当前线程也是可以直接进入的。在java中,synchronized跟ReetrantLock都是可重入锁。不可重入锁:以上面的代码实例来说明,就是methodA进入methodB的时候不能直接获取锁,必须先调用unLock释放锁。才能执行下去,那实现不可重入锁有什么方式呢?那就是自旋,所以会有一个小名叫做自旋锁。public class SpinLock { private AtomicReference<Thread> sign =new AtomicReference<>(); public void lock(){ Thread current = Thread.currentThread(); while(!sign .compareAndSet(null, current)){ } } public void unlock (){ Thread current = Thread.currentThread(); sign .compareAndSet(current, null); }}“悲观锁”与“乐观锁”这两种锁呢,其实是一个很宏观的分类,它不是一种具体的锁,而是泛指看待并发的程度。悲观锁:有一个“悲观”的心态,既每次取数据的时候,都会认为该数据会被修改,所以必须加一把锁才安心。乐观锁:乐观的孩子,认为同一个数据不会发生并发操作的行为,所以取的时候不会加锁,只有在更新的时候,会通过例如版本号之类的来判断是否数据被修改了。Java中各种锁其实都是悲观锁的实现,既操作的数据的都会被获取锁的线程锁住,而乐观锁的话,一般是通过cas(compare and swap)的思想来实现,例如一些原子类AtomicInteger使用自旋来原子更新。“共享锁”与“排他锁”这两种锁的概念比较多的出现在数据库的事务当中。共享锁:也称读锁或S锁。如果事务对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。在java中的ReetrantReadWriteLock()也是如此。排它锁:也称独占锁、写锁或X锁。如果事务对数据A加上排它锁后,则其他事务不能再对A加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。分布式锁我们上面聊的这些锁,都是在单个程序上面的不同线程之间来实现的,那么当我们的不同程序需要去竞争同一块资源的时候,这就需要分布式锁了,我们可以通过redis、zookeeper等中间件来实现分布式锁。对于锁来说,其实还有偏向锁,轻量级锁等,但是这里涉及到的内容就比较多,这里就不在展开篇幅介绍了,有兴趣的同学可自行研究,如果你能搞懂上面介绍的这些锁,那基本上在绝大部分的公司关于“锁”的问题都可以迎刃而解。文章首发于微信公众号《深夜里的程序猿》,转载务必注明出处,侵权必究。 ...

April 9, 2019 · 1 min · jiezi

多线程、锁和线程同步方案

多线程多线程技术大家都很了解,而且在项目中也比较常用。比如开启一个子线程来处理一些耗时的计算,然后返回主线程刷新UI等。首先我们先简单的梳理一下常用到的多线程方案。具体的用法这里我就不说了,每一种方案大家可以去查一下,网上教程很多。常见的多线程方案我们比较常用的是GCD和NSOperation,当然还有NSThread,pthread。他们的具体区别我们不详细说,给出下面这一个表格,大家自行对比一下。容易混淆的术语提到多线程,有一个术语是经常能听到的,同步,异步,串行,并发。同步和异步的区别,就是是否有开启新的线程的能力。异步具备开启线程的能力,同步不具备开启线程的能力。注意,异步只是具备开始新线程的能力,具体开启与否还要跟队列的属性有关系。串行和并发,是指的任务的执行方式。并发是任务可以多个同时执行,串行之能是一个执行完成后在执行下一个。在面试的过程中可能被问到什么网情况下会出现死锁的问题,总结一下就是使用sync函数(同步)往当前的串行对列中添加任务时,会出现死锁。锁多线程的安全隐患多线程和安全问题是分不开的,因为在使用多个线程访问同一块数据的时候,如果同时有读写操作,就可能产生数据安全问题。所以这时候我们就用到了锁这个东西。其实使用锁也是为了在使用多线程的过程中保障数据安全,除了锁,然后一些其他的实现线程同步来保证数据安全的方案,我们一起来了解一下。线程同步方案下面这些是我们常用来实现线程同步方案的。OSSpinLockos_unfair_lockpthread_mutexNSLockNSRecursiveLockNSConditionNSConditinLockdispatch_semaphoredispatch_queue(DISPATCH_QUEUE_SERIAL)@synchronized可以看出来,实现线程同步的方案包括各种锁,还有信号量,串行队列。我们只挑其中不常用的来说一下使用方法。下面是我们模拟了存钱取钱的场景,下面是加锁之前的代码,运行之后肯定是有数据问题的。/** 存钱、取钱演示 /- (void)moneyTest { self.money = 100; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __saveMoney]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __drawMoney]; } });}/* 存钱 /- (void)__saveMoney { int oldMoney = self.money; sleep(.2); oldMoney += 50; self.money = oldMoney; NSLog(@“存50,还剩%d元 - %@”, oldMoney, [NSThread currentThread]); }/* 取钱 /- (void)__drawMoney { int oldMoney = self.money; sleep(.2); oldMoney -= 20; self.money = oldMoney; NSLog(@“取20,还剩%d元 - %@”, oldMoney, [NSThread currentThread]); }加锁的代码,涉及到锁的初始化、加锁、解锁这么三部分。我们从OSSpinLock开始说。OSSpinLock自旋锁OSSpinLock叫做自旋锁。那什么叫自旋锁呢?其实我们可以从大类上面把锁分为两类,一类是自旋锁,一类是互斥锁。我们通过一个例子来区分这两类锁。如果线程A率先到达加锁的部分,并成功加锁,线程B到达的时候会因为已经被A加锁而等待。如果是自旋锁,线程B会通过执行一个循环来实现等待,我们不用管它循环执行了什么,只要知道他在那"转圈圈"等着就行。如果是互斥锁,那线程B在等待的时候会休眠。使用OSSpinLock需要导入头文件#import <libkern/OSAtomic.h>//声明一个锁@property (nonatomic, assign) OSSpinLock lock;// 锁的初始化self.lock = OS_SPINLOCK_INIT;在我们这个例子中,存钱取钱都是访问了money,所以我们要在存和取的操作中使用同一个锁。/* 存钱 /- (void)__saveMoney { OSSpinLockLock(&_lock); //….省去中间的逻辑代码 OSSpinLockUnlock(&_lock);}/* 取钱 */- (void)__drawMoney { OSSpinLockLock(&_lock); //….省去中间的逻辑代码 OSSpinLockUnlock(&_lock);}这就是简单的自旋锁的使用,我们发现在使用的过程中,Xcode一直提醒我们这个OSSpinLock被废弃了,让我们使用os_unfair_lock代替。OSSpinLock之所以会被废弃是因为它可能会产生一个优先级反转的问题。具体来说,如果一个低优先级的线程获得了锁并访问共享资源,那高优先级的线程只能忙等,从而占用大量的CPU。低优先级的线程无法和高优先级的线程竞争(CPU会给高优先级的线程分配更多的时间片),所以会导致低优先级的线程的任务一直完不成,从而无法释放锁。os_unfair_lock的用法跟OSSpinLock很像,就不单独说了。pthread_mutexDefault一看到这个pthread我们应该就能知道这是一种跨平台的方案了。首先还是来看用法。//声明一个锁@property (nonatomic, assign) pthread_mutex_t lock;//初始化pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable)我们可以看到在初始化锁的时候,第一个参数是锁的地址,第二个参数是一个pthread_mutexattr_t类型的地址,如果我们不传pthread_mutexattr_t,直接传一个NULL,相当于创建一个默认的互斥锁。//方式一pthread_mutex_init(mutex, NULL);//方式二// - 创建attrpthread_mutexattr_t attr;// - 初始化attrpthread_mutexattr_init(&attr);// - 设置attr类型pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_DEFAULT);// - 使用attr初始化锁pthread_mutex_init(&_lock, &attr);// - 销毁attrpthread_mutexattr_destroy(&attr);上面两个方式是一个效果,那为什么使用attr,那就说明除了default类型的还有其他类型,我们后面再说。在使用的时候用pthread_mutex_lock(&_lock); 和 pthread_mutex_unlock(&_lock);加锁解锁。NSLock就是对这种普通互斥锁的OC层面的封装。RECURSIVE 递归锁调用pthread_mutexattr_settype的时候如果类型传入PTHREAD_MUTEX_RECURSIVE,会创建一个递归锁。举个例子吧。// 伪代码-(void)test { lock; [self test]; unlock;}如果是普通的锁,当我们在test方法中,递归调用test,应该会出现死锁,因为被lock,在递归调用时无法调用,一直等待。但是如果锁是递归锁,他会允许同一个线程多次加锁和解锁,就可以解决这个问题了。NSRecursiveLock是对递归锁的封装。Condition 条件锁我们直接上这种锁的使用方法,- (void)otherTest{ [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];}// 线程1// 删除数组中的元素- (void)__remove { pthread_mutex_lock(&_mutex); NSLog(@"__remove - begin"); if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex); } [self.data removeLastObject]; NSLog(@“删除了元素”); pthread_mutex_unlock(&_mutex);}// 线程2// 往数组中添加元素- (void)__add { pthread_mutex_lock(&_mutex); sleep(1); [self.data addObject:@“Test”]; NSLog(@“添加了元素”); // 信号 pthread_cond_signal(&_cond); // 广播// pthread_cond_broadcast(&_cond); pthread_mutex_unlock(&_mutex);}我们创建了两个线程,一个往数组中添加数据,一个删除数据,我们通过这个条件锁实现的效果就是在数组中还没有数据的时候等待,数组中添加了一个数据之后在进行删除。条件锁就是互斥锁+条件。我们声明一个条件并初始化。@property (assign, nonatomic) pthread_cond_t cond;//使用完后也要pthread_cond_destroy(&_cond);pthread_cond_init(&_cond, NULL);在__remove方法中if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex);}如果线程1率先拿到所并加锁,执行到上面代码这里发现数组中还没有数据,就执行pthread_cond_wait,此时线程1会暂时放开_mutex这个锁,并在这休眠等待。线程2在__add方法中最开始因为拿不到锁,所以等待,在线程1休眠放开锁之后拿到锁,加锁,并执行为数组添加数据的代码。添加完了之后会发个信号通知等待条件的线程,并解锁。 pthread_cond_signal(&_cond); pthread_mutex_unlock(&_mutex);线程2执行了pthread_cond_signal之后,线程1就收到了通知,退出休眠状态,继续执行下面的代码。这个地方可能有人会有疑问,是不是线程2应该先unlock再cond_dingnal,其实这个地方顺序没有太大差别,因为线程2执行了pthread_cond_signal之后,会继续执行unlock代码,线程1收到signal通知后会推出休眠状态,同时线程1需要再一次持有这个锁,就算此时线程2还没有unlock,线程1等到线程2 unlock 的时间间隔很短,等到线程2 unlock 后线程1会再去持有这个锁,并加锁。NSCondition就是OC层面的条件锁,内部把mutex互斥锁和条件封装到了一起。NSConditionLock其实也差不多,NSConditionLock可以指定具体的条件,这两个OC层面的类的用法大家可以自行上网搜索。dispatch_semaphore 信号量@property (strong, nonatomic) dispatch_semaphore_t semaphore;//初始化self.semaphore = dispatch_semaphore_create(5);在初始化一个信号的的过程中传入dispatch_semaphore_create的值,其实就代表了允许几个线程同时访问。再回到之前我们存钱取钱这个例子。self.moneySemaphore = dispatch_semaphore_create(1);我们一次只允许一个线程访问,所以在初始化的时候传1。下面就是使用方法。- (void)__drawMoney{ dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // … 省略代码 dispatch_semaphore_signal(self.moneySemaphore);}- (void)__saveMoney{ dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // … 省略代码 dispatch_semaphore_signal(self.moneySemaphore);}dispatch_semaphore_wait是怎么上锁的呢?如果信号量>0的时候,让信号量-1,并继续往下执行。如果信号量<=0的时候,休眠等待。就这么简单。dispatch_semaphore_signal让信号量+1。小提示在我们平时使用这种方法的时候,可以把信号量的代码提取出来定义一个宏。#define SemaphoreBegin \static dispatch_semaphore_t semaphore; \static dispatch_once_t onceToken; \dispatch_once(&onceToken, ^{ \ semaphore = dispatch_semaphore_create(1); }); \dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);#define SemaphoreEnd \dispatch_semaphore_signal(semaphore);读写安全方案上面我们讲到的线程同步方案都是每次只允许一个线程访问,在实际的情况中,读写的同步方案应该下面这样:每次只能有一个线程写可以有多个线程同时读读和写不能同时进行这就是多读单写,用于文件读写的操作。在我们的iOS中可以用下面这两种解决方案。pthread_rwlock 读写锁这个读写锁的用法很简单,跟之前的普通互斥锁都差不多,大家随便搜一下应该就能搜到,我就不拿出来写了,这里主要是提一下这种锁,大家以后有需要的时候可以用。dispatch_barrier_async 异步栅栏首先在使用这个函数的时候,我们要用自己创建的并发队列。如果传入的是一个串行队列或者全局的并发队列,那dispatch_barrier_async等同于dispatch_async的效果。self.queue = dispatch_queue_create(“rw_queue”, DISPATCH_QUEUE_CONCURRENT);dispatch_async(self.queue, ^{ [self read];}); dispatch_barrier_async(self.queue, ^{ [self write];});在读取数据的时候,使用dispatch_async往对列中添加任务,在写数据时,用dispatch_barrier_async添加任务。dispatch_barrier_async添加的任务会等前面所有的任务都执行完,他再执行,而且他执行的时候,不允许有别的任务同时执行。atomic我们都知道这个atomic是原子性的意思。他保证了属性setter和getter的原子性操作,相当于在set和get方法内部加锁。atomic修饰的属性是读/写安全的,但不是线程安全。假设有一个 atomic 的属性 “name”,如果线程 A 调用 [self setName:@“A”],线程 B 调用 [self setName:@“B”],线程 C 调用 [self name],那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。但是,如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。 ...

April 1, 2019 · 2 min · jiezi

一个游戏拨账系统的数据库结算设计

假设现存在一个简单的猜大小游戏,由用户下注大或者小,扣除手续费3%后的钱全部放入奖池中,赢的一方按投注比例平分整个奖池。使用mysql作为数据库,系统精度精确到1位小数。 本文将会讲解其中会出现的业务结算导致的数据问题,以及解决方法。数据库逻辑设计系统内应该存在一个用户钱包表,其中指定两条记录为系统收入账户和系统拨出账户。这样可以将投注的时候,对系统账户余额增加操作,和发奖的时候,对系统账户余额的减去操作分离。可以避免上一期游戏的结算,对下一期游戏的投注发生锁等待的问题。业务加锁考虑到高并发的情况下,推荐使用mysql自带的排他锁,不推荐乐观锁,因为乐观锁需要重试机制,而队列结算暂时不考虑。 当一名用户发起投注的时候,检查顺序应该如下检查系统游戏开关(冗余) 查询一次用户余额是否大于这次下注金额开启事务对系统收入账户加排他锁对用户收入账户加排他锁检查用户余额是否足够对用户进行扣款对系统进行收款为奖池加入97%的投注额度事务提交这里之所以要冗余检查用户的额度,是否了避免开启事务的消耗,防止恶意攻击消耗系统资源,用来开启无意义事务。奖池额度的97%这里计算需要保持一位精度,如果用户投注是98,按照计算得到的值应该是95.06,我们应该取95.0而不是95.1,否则你最后存到奖池里面的数就会大于97%,这样系统抽取就不会达到3%,用户少分点没关系,要保证系统一定能分到3%。简单一句话就是:精度位后都舍弃发奖过程设计假设按照投注比例,瓜分出的奖金总数是22.1,A用户的份额是55.5%,A用户拿到12.2655,B用户的份额是45%,B用户拿到9.8345。这种情况下,你会发现,按照舍弃,原则,分别是12.2和9.8,结果是只发放了22,如果你按照四舍五入原则,才能发放到22.1那为什么还要坚持舍弃原则呢?因为,假设出一个极端情况,当你碰到A的值是12.05,B的值是9.05,按照舍弃原则,总数的确还是22.1。但是按照四舍五入原则,发放的总值就是22.2了。结语在计算机系统内,浮点数的计算本身就是不可靠的,在业务内应该用整形去避免,当设计到百分比操作的时候,请尽量使用舍弃原则,保证不多发。按照舍弃原则,给用户少发0.05这种精度外的值,对业务来说无关紧要。如果超发了,会导致系统内账目混乱,后果将不堪设想。

January 28, 2019 · 1 min · jiezi

BlockingQueue与Condition原理解析

我在前段时间写了一篇关于AQS源码解析的文章AbstractQueuedSynchronizer超详细原理解析,在文章里边我说JUC包中的大部分多线程相关的类都和AQS相关,今天我们就学习一下依赖于AQS来实现的阻塞队列BlockingQueue的实现原理。本文中的源码未加说明即来自于以ArrayBlockingQueue。阻塞队列 相信大多数同学在学习线程池时会了解阻塞队列的概念,熟记各种类型的阻塞队列对线程池初始化的影响。当从阻塞队列获取元素但是队列为空时,当前线程会阻塞直到另一个线程向阻塞队列中添加一个元素;类似的,当向一个阻塞队列加入元素时,如果队列已经满了,当前线程也会阻塞直到另外一个线程从队列中读取一个元素。阻塞队列一般都是先进先出的,用来实现生产者和消费者模式。当发生上述两种情况时,阻塞队列有四种不同的处理方式,这四种方式分别为抛出异常,返回特殊值(null或在是false),阻塞当前线程直到执行结束,最后一种是只阻塞固定时间,到时后还无法执行成功就放弃操作。这些方法都总结在下边这种表中了。 我们就只分析put和take方法。put和take函数 我们都知道,使用同步队列可以很轻松的实现生产者-消费者模式,其实,同步队列就是按照生产者-消费者的模式来实现的,我们可以将put函数看作生产者的操作,take是消费者的操作。 我们首先看一下ArrayListBlock的构造函数。它初始化了put和take函数中使用到的关键成员变量,分别是ReentrantLock和Condition。public ArrayBlockingQueue(int capacity, boolean fair) { this.items = new Object[capacity]; lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition();} ReentrantLock是AQS的子类,其newCondition函数返回的Condition接口实例是定义在AQS类内部的ConditionObject实现类。它可以直接调用AQS相关的函数。 put函数会在队列末尾添加元素,如果队列已经满了,无法添加元素的话,就一直阻塞等待到可以加入为止。函数的源码如下所示。public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); //先获得锁 try { while (count == items.length) //如果队列满了,就NotFull这个Condition对象上进行等待 notFull.await(); enqueue(e); } finally { lock.unlock(); }}private void enqueue(E x) { final Object[] items = this.items; items[putIndex] = x; //这里可以注意的是ArrayBlockingList实际上使用Array实现了一个环形数组, //当putIndex达到最大时,就返回到起点,继续插入, //当然,如果此时0位置的元素还没有被取走, //下次put时,就会因为cout == item.length未被阻塞。 if (++putIndex == items.length) putIndex = 0; count++; //因为插入了元素,通知等待notEmpty事件的线程。 notEmpty.signal();} 我们会发现put函数使用了wait/notify的机制。与一般生产者-消费者的实现方式不同,同步队列使用ReentrantLock和Condition相结合的先获得锁,再等待的机制;而不是Synchronized和Object.wait的机制。这里的区别我们下一节再详细讲解。 看完了生产者相关的put函数,我们再来看一下消费者调用的take函数。take函数在队列为空时会被阻塞,一直到阻塞队列加入了新的元素。public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) //如果队列为空,那么在notEmpty对象上等待, //当put函数调用时,会调用notEmpty的notify进行通知。 notEmpty.await(); return dequeue(); } finally { lock.unlock(); }}private E dequeue() { E x = (E) items[takeIndex]; items[takeIndex] = null; //取出takeIndex位置的元素 if (++takeIndex == items.length) //如果到了尾部,将指针重新调整到头部 takeIndex = 0; count–; …. //通知notFull对象上等待的线程 notFull.signal(); return x;}await操作 我们发现ArrayBlockingList并没有使用Object.wait,而是使用的Condition.await,这是为什么呢?其中又有哪些原因呢? Condition对象可以提供和Object的wait和notify一样的行为,但是后者必须先获取synchronized这个内置的monitor锁,才能调用;而Condition则必须先获取ReentrantLock。这两种方式在阻塞等待时都会将相应的锁释放掉,但是Condition的等待可以中断,这是二者唯一的区别。 我们先来看一下Condition的wait函数,wait函数的流程大致如下图所示。 wait函数主要有三个步骤。一是调用addConditionWaiter 函数,在condition wait queue队列中添加一个节点,代表当前线程在等待一个消息。然后调用fullyRelease函数,将持有的锁释放掉,调用的是AQS的函数,不清楚的同学可以查看本篇开头的介绍的文章。最后一直调用isOnSyncQueue函数判断节点是否被转移到sync queue队列上,也就是AQS中等待获取锁的队列。如果没有,则进入阻塞状态,如果已经在队列上,则调用acquireQueued函数重新获取锁。public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); //在condition wait队列上添加新的节点 Node node = addConditionWaiter(); //释放当前持有的锁 int savedState = fullyRelease(node); int interruptMode = 0; //由于node在之前是添加到condition wait queue上的,现在判断这个node //是否被添加到Sync的获得锁的等待队列上,Sync就是AQS的子类 //node在condition queue上说明还在等待事件的notify, //notify函数会将condition queue 上的node转化到Sync的队列上。 while (!isOnSyncQueue(node)) { //node还没有被添加到Sync Queue上,说明还在等待事件通知 //所以调用park函数来停止线程执行 LockSupport.park(this); //判断是否被中断,线程从park函数返回有两种情况,一种是 //其他线程调用了unpark,另外一种是线程被中断 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //代码执行到这里,已经有其他线程调用notify函数,或则被中断,该线程可以继续执行,但是必须先 //再次获得调用await函数时的锁.acquireQueued函数在AQS文章中做了介绍. if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; ....}final int fullyRelease(Node node) { //AQS的方法,当前已经在锁中了,所以直接操作 boolean failed = true; try { int savedState = getState(); //获取state当前的值,然后保存,以待以后恢复 // release函数是AQS的函数,不清楚的同学请看开头介绍的文章。 if (release(savedState)) { failed = false; return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; }}private int checkInterruptWhileWaiting(Node node) { //中断可能发生在两个阶段中,一是在等待signa时,另外一个是在获得signal之后 return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;}final boolean transferAfterCancelledWait(Node node) { //这里要和下边的transferForSignal对应着看,这是线程中断进入的逻辑.那边是signal的逻辑 //两边可能有并发冲突,但是成功的一方必须调用enq来进入acquire lock queue中. if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { enq(node); return true; } //如果失败了,说明transferForSignal那边成功了,等待node 进入acquire lock queue while (!isOnSyncQueue(node)) Thread.yield(); return false;}signal操作 signal函数将condition wait queue队列中队首的线程节点转移等待获取锁的sync queue队列中。这样的话,wait函数中调用isOnSyncQueue函数就会返回true,导致wait函数进入最后一步重新获取锁的状态。 我们这里来详细解析一下condition wait queue和sync queue两个队列的设计原理。condition wait queue是等待消息的队列,因为阻塞队列为空而进入阻塞状态的take函数操作就是在等待阻塞队列不为空的消息。而sync queue队列则是等待获取锁的队列,take函数获得了消息,就可以运行了,但是它还必须等待获取锁之后才能真正进行运行状态。 signal函数的示意图如下所示。 signal函数其实就做了一件事情,就是不断尝试调用transferForSignal 函数,将condition wait queue队首的一个节点转移到sync queue队列中,直到转移成功。因为一次转移成功,就代表这个消息被成功通知到了等待消息的节点。public final void signal() { if (!isHeldExclusively()) //如果当前线程没有获得锁,抛出异常 throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) //将Condition wait queue中的第一个node转移到acquire lock queue中. doSignal(first);}private void doSignal(Node first) { do { //由于生产者的signal在有消费者等待的情况下,必须要通知 //一个消费者,所以这里有一个循环,直到队列为空 //把first 这个node从condition queue中删除掉 //condition queue的头指针指向node的后继节点,如果node后续节点为null,那么也将尾指针也置为null if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); //transferForSignal将node转而添加到Sync的acquire lock 队列}final boolean transferForSignal(Node node) { //如果设置失败,说明该node已经被取消了,所以返回false,让doSignal继续向下通知其他未被取消的node if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; //将node添加到acquire lock queue中. Node p = enq(node); int ws = p.waitStatus; //需要注意的是这里的node进行了转化 //ws>0代表canceled的含义所以直接unpark线程 //如果compareAndSetWaitStatus失败,所以直接unpark,让线程继续执行await中的 //进行isOnSyncQueue判断的while循环,然后进入acquireQueue函数. //这里失败的原因可能是Lock其他线程释放掉了锁,同步设置p的waitStatus //如果compareAndSetWaitStatus成功了呢?那么该node就一直在acquire lock queue中 //等待锁被释放掉再次抢夺锁,然后再unpark if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true;}后记 后边一篇文章主要讲解如何自己使用AQS来创建符合自己业务需求的锁,请大家继续关注我的文章啦.一起进步偶。 ...

January 16, 2019 · 2 min · jiezi

Golang并发:再也不愁选channel还是选锁

周末又到了,为大家准备了一份实用干货:如何使用channel和Mutex解决并发问题,利用周末的好时光,配上音乐,思考一下吧????。来,问自己个问题:面对并发问题,是用channel解决,还是用Mutex解决?如果自己心里还没有清晰的答案,那就读下这篇文章,你会了解到:使用channel解决并发问题的核心思路和示例channel擅长解决什么样的并发问题,Mutex擅长解决什么样的并发问题一个并发问题该怎么入手解解决一个重要的plus思维前戏前面很多篇的文章都在围绕channel介绍,而只有前一篇sync的文章介绍到了Mutex,不是我偏心,而是channel在Golang是first class级别的,设计在语言特性中的,而Mutex只是一个包中的。这就注定了一个是主角,一个是配角。并且Golang还有一个并发座右铭,在《Effective Go》的channel介绍中写到:Share memory by communicating, don’t communicate by sharing memory.通过通信共享内存,而不是通过共享内存而通信。Golang以如此明显的方式告诉我们:面对并发问题,你首先想到的应该是channel,因为channel是线程安全的并且不会有数据冲突,比锁好用多了。既生瑜,何生亮。既然有channel了,为啥还提供sync.Mutex呢?主角不是万能的,他也需要配角。在Golang里,channel也不是万能的,这是由channel的特性和局限造成的。下面就给大家介绍channel的特点、核心方法和缺点。channel解决并发问题的思路和示例channel的核心是数据流动,关注到并发问题中的数据流动,把流动的数据放到channel中,就能使用channel解决这个并发问题。这个思路是从Go语言的核心开发者的演讲中学来的,然而视频我已经找不到了,不然直接共享给大家,他提到了Golang并发的核心实践的4个点:DataFlow -> Drawing -> Pipieline -> ExitingDataFlow指数据流动,Drawing指把数据流动画出来,Pipeline指的是流水线,Exit指协程的退出。DataFlow + Drawing就是我提到到channel解决并发问题的思路,Pipeline和Exit是具体的实践模式,Pipeline和Exit我都写过文章,有需要自取:Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出下面我使用例子具体解释DataFlow + Drawing。借用《Golang并发的次优选择:sync包》中银行的例子,介绍如何使用channel解决例子中银行的并发问题:银行支持多个用户的同时操作。顺便看下同一个并发问题,使用channel和Mutex解决是什么差别。一起分析下多个用户同时操作银行的数据流动:每个人都可以向银行发起请求,请求可以是存、取、查3种操作,并且包含操作时必要的数据,包含的数据只和自身相关。银行处理请求后给用户发送响应,包含的数据只和操作用户相关。你一定发现了上面的数据流动:请求数据:个人请求数据流向银行。响应数据:银行处理结果数据流向用户。channel是数据流动的通道/管道,为流动的数据建立通道,这里需要建立2类channel:reqCh:传送请求的channel,把请求从个人发送给银行。retCh:传送响应的channel,把响应从银行发给个人。我们把channel添加到上图中,得到下面的图:以上就是从数据流动的角度,发现如何使用channel解决并发问题。思路有了,再思考下代码层面需要怎么做:银行:定义银行,只保存1个map即可银行操作:接收和解析请求,并把请求分发给存、取、查函数实现存、取、查函数:处理请求,并把结果写入到用户提供的响应通道定义请求和响应用户:创建请求和接收响应的通道,发送请求后等待响应,提取响应结果mian函数:创建银行和用户间的请求通道,创建银行、用户等协程,并等待操作完成以上,我们这个并发问题的逻辑实现和各块工作就清晰了,写起来也方便、简单。代码实现有200多行,公众号不方便查看,可以点阅读原文,一键直达。代码不能贴了,运行结果还是可以的,为了方便理解结果,介绍下示例代码做了什么。main函数创建了银行、小明、小刚3个并发协程:银行:从reqCh接收请求,依次处理每个请求,直到通道关闭,把请求交给处理函数,处理函数把结果写入到请求中的retCh。用户小明:创建了存100、取20、查余额的3个请求,每个请求得到响应后,再把下一个请求写入到reqCh。用户小刚:流程和小明相同,但存100取200,造成取钱操作失败,他查询下自己又多少钱,得到100。main函数最后使用WaitGroup等待小明、小刚结束后退出。下面是运行结果:$ go run channel_map.goxiaogang deposite 100 successxiaoming deposite 100 successxiaogang withdraw 200 failedxiaoming withdraw 20 successxiaogang has 100xiaoming has 80Bank exit这一遭搞完,发现啥没有?用Mutex直接加锁、解锁完事了,但channel搞出来一坨,是不是用channel解决这个问题不太适合?是的。对于当前这个问题,和Mutex的方案相比,channel的方案显的有点“重”,不够简洁、高效、易用。但这个例子展示了3点:使用channel解决并发问题的核心在于关注数据的流动channel不一定是某个并发问题最好的解决方案map在并发中,可以不用锁进行保护,而是使用channel现在,回到了开篇的问题:同一个并发问题,你是用channel解决,还是用mutex解决?下面,一起看看怎么选择。channel和mutex的选择面对一个并发问题的时候,应当选择合适的并发方式:channel还是mutex。选择的依据是他们的能力/特性:channel的能力是让数据流动起来,擅长的是数据流动的场景,《Channel or Mutex》中给了3个数据流动的场景:传递数据的所有权,即把某个数据发送给其他协程分发任务,每个任务都是一个数据交流异步结果,结果是一个数据mutex的能力是数据不动,某段时间只给一个协程访问数据的权限擅长数据位置固定的场景,《Channel or Mutex》中给了2个数据不动场景:缓存状态,我们银行例子中的map就是一种状态提供解决并发问题的一个思路:先找到数据的流动,并且还要画出来,数据流动的路径换成channel,channel的两端设计成协程基于画出来的图设计简要的channel方案,代码需要做什么这个方案是不是有点复杂,是不是用Mutex更好一点?设计一个简要的Mutex方案,对比&选择易做的、高效的channel + mutex思维面对并发问题,除了channel or mutex,你还有另外一个选择:channel plus mutex。一个大并发问题,可以分解成很多小的并发问题,每个小的并发都可以单独选型:channel or mutex。但对于整个大的问题,通常不是channel or mutex,而是channel plus mutex。如果你是认为是channel and mutex也行,但我更喜欢plus,体现相互配合。总结读到这里,感觉这篇文章头重脚轻,channel的讲了很多,而channel和mutex的选择却讲的很少。在channel和mutex的选择,实际并没有一个固定答案,也没有固定的方法,但提供了一个简单的思路:设计出channel和Mutex的简单方案,然后选择最适合当前业务、问题的那个。思考比结论更重要,希望你有所收获:关注数据的流动,就可以使用channel解决并发问题。不流动的数据,如果存在并发访问,尝试使用sync.Mutex保护数据。channel不一定某个并发问题的最优解。不要害怕、拒绝使用mutex,如果mutex是问题的最优解,那就大胆使用。对于大问题,channel plus mutex也许才是更好的方案。参考资料《Effective Go》,https://golang.org/doc/effect…《Mutex Or Channel》,https://github.com/golang/go/…文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发的次优选择:sync包如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/01/14/golang-channel-and-mutex/

January 14, 2019 · 1 min · jiezi

AbstractQueuedSynchronizer超详细原理解析

今天我们来研究学习一下AbstractQueuedSynchronizer类的相关原理,java.util.concurrent包中很多类都依赖于这个类所提供队列式同步器,比如说常用的ReentranLock,Semaphore和CountDownLatch等。 为了方便理解,我们以一段使用ReentranLock的代码为例,讲解ReentranLock每个方法中有关AQS的使用。ReentranLock示例 我们都知道ReentranLock的加锁行为和Synchronized类似,都是可重入的锁,但是二者的实现方式确实完全不同的,我们之后也会讲解Synchronized的原理。除此之外,Synchronized的阻塞无法被中断,而ReentrantLock则提供了可中断的阻塞。下面的代码是ReentranLock的函数,我们就以此为顺序,依次讲解这些函数背后的实现原理。ReentrantLock lock = new ReentrantLock();lock.lock();lock.unlock();公平锁和非公平锁 ReentranLock分为公平锁和非公平锁,二者的区别就在获取锁机会是否和排队顺序相关。我们都知道,如果锁被另一个线程持有,那么申请锁的其他线程会被挂起等待,加入等待队列。理论上,先调用lock函数被挂起等待的线程应该排在等待队列的前端,后调用的就排在后边。如果此时,锁被释放,需要通知等待线程再次尝试获取锁,公平锁会让最先进入队列的线程获得锁。而非公平锁则会唤醒所有线程,让它们再次尝试获取锁,所以可能会导致后来的线程先获得了锁,则就是非公平。public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();} 我们会发现FairSync和NonfairSync都继承了Sync类,而Sync的父类就是AbstractQueuedSynchronizer(后续简称AQS)。但是AQS的构造函数是空的,并没有任何操作。 之后的源码分析,如果没有特别说明,就是指公平锁。lock操作 ReentranLock的lock函数如下所示,直接调用了sync的lock函数。也就是调用了FairSync的lock函数。 //ReentranLock public void lock() { sync.lock(); } //FairSync final void lock() { //调用了AQS的acquire函数,这是关键函数之一 acquire(1); } 我们接下来就正式开始AQS相关的源码分析了,acquire函数的作用是获取同一时间段内只能被一个线程获取的量,这个量就是抽象化的锁概念。我们先分析代码,你慢慢就会明白其中的含义。public final void acquire(int arg) { // tryAcquire先尝试获取"锁",获取了就不进入后续流程 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //addWaiter是给当前线程创建一个节点,并将其加入等待队列 //acquireQueued是当线程已经加入等待队列之后继续尝试获取锁. selfInterrupt();} tryAcquire,addWaiter和acquireQueued都是十分重要的函数,我们接下来依次学习一下这些函数,理解它们的作用。//AQS类中的变量.private volatile int state;//这是FairSync的实现,AQS中未实现,子类按照自己的需要实现该函数protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); //获取AQS中的state变量,代表抽象概念的锁. int c = getState(); if (c == 0) { //值为0,那么当前独占性变量还未被线程占有 //如果当前阻塞队列上没有先来的线程在等待,UnfairSync这里的实现就不一致 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //成功cas,那么代表当前线程获得该变量的所有权,也就是说成功获得锁 setExclusiveOwnerThread(current); // setExclusiveOwnerThread将本线程设置为独占性变量所有者线程 return true; } } else if (current == getExclusiveOwnerThread()) { //如果该线程已经获取了独占性变量的所有权,那么根据重入性 //原理,将state值进行加1,表示多次lock //由于已经获得锁,该段代码只会被一个线程同时执行,所以不需要 //进行任何并行处理 int nextc = c + acquires; if (nextc < 0) throw new Error(“Maximum lock count exceeded”); setState(nextc); return true; } //上述情况都不符合,说明获取锁失败 return false;} 由上述代码我们可以发现,tryAcquire就是尝试获取那个线程独占的变量state。state的值表示其状态:如果是0,那么当前还没有线程独占此变量;否在就是已经有线程独占了这个变量,也就是代表已经有线程获得了锁。但是这个时候要再进行一次判断,看是否是当前线程自己获得的这个锁,如果是,就增加state的值。 这里有几点需要说明一下,首先是compareAndSetState函数,这是使用CAS操作来设置state的值,而且state值设置了volatile修饰符,通过这两点来确保修改state的值不会出现多线程问题。然后是公平锁和非公平锁的区别问题,在UnfairSync的nonfairTryAcquire函数中不会在相同的位置上调用hasQueuedPredecessors来判断当前是否已经有线程在排队等待获得锁。 如果tryAcquire返回true,那么就是获取锁成功;如果返回false,那么就是未获得锁,需要加入阻塞等待队列。我们下面就来看一下addWaiter的相关操作。等待锁的阻塞队列 将保存当前线程信息的节点加入到等待队列的相关函数中涉及到了无锁队列的相关算法,由于在AQS中只是将节点添加到队尾,使用到的无锁算法也相对简单。真正的无锁队列的算法我们等到分析ConcurrentSkippedListMap时在进行讲解。private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); //先使用快速入列法来尝试一下,如果失败,则进行更加完备的入列算法. //只有在必要的情况下才会使用更加复杂耗时的算法,也就是乐观的态度 Node pred = tail; //列尾指针 if (pred != null) { node.prev = pred; //步骤1:该节点的前趋指针指向tail if (compareAndSetTail(pred, node)){ //步骤二:cas将尾指针指向该节点 pred.next = node;//步骤三:如果成果,让旧列尾节点的next指针指向该节点. return node; } } //cas失败,或在pred == null时调用enq enq(node); return node;}private Node enq(final Node node) { for (;;) { //cas无锁算法的标准for循环,不停的尝试 Node t = tail; if (t == null) { //初始化 if (compareAndSetHead(new Node())) //需要注意的是head是一个哨兵的作用,并不代表某个要获取锁的线程节点 tail = head; } else { //和addWaiter中一致,不过有了外侧的无限循环,不停的尝试,自旋锁 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } }} 通过调用addWaiter函数,AQS将当前线程加入到了等待队列,但是还没有阻塞当前线程的执行,接下来我们就来分析一下acquireQueued函数。等待队列节点的操作 由于进入阻塞状态的操作会降低执行效率,所以,AQS会尽力避免试图获取独占性变量的线程进入阻塞状态。所以,当线程加入等待队列之后,acquireQueued会执行一个for循环,每次都判断当前节点是否应该获得这个变量(在队首了)。如果不应该获取或在再次尝试获取失败,那么就调用shouldParkAfterFailedAcquire判断是否应该进入阻塞状态。如果当前节点之前的节点已经进入阻塞状态了,那么就可以判定当前节点不可能获取到锁,为了防止CPU不停的执行for循环,消耗CPU资源,调用parkAndCheckInterrupt函数来进入阻塞状态。final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { //一直执行,直到获取锁,返回. final Node p = node.predecessor(); //node的前驱是head,就说明,node是将要获取锁的下一个节点. if (p == head && tryAcquire(arg)) { //所以再次尝试获取独占性变量 setHead(node); //如果成果,那么就将自己设置为head p.next = null; // help GC failed = false; return interrupted; //此时,还没有进入阻塞状态,所以直接返回false,表示不需要中断调用selfInterrupt函数 } //判断是否要进入阻塞状态.如果shouldParkAfterFailedAcquire //返回true,表示需要进入阻塞 //调用parkAndCheckInterrupt;否则表示还可以再次尝试获取锁,继续进行for循环 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //调用parkAndCheckInterrupt进行阻塞,然后返回是否为中断状态 interrupted = true; } } finally { if (failed) cancelAcquire(node); }}private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //前一个节点在等待独占性变量释放的通知,所以,当前节点可以阻塞 return true; if (ws > 0) { //前一个节点处于取消获取独占性变量的状态,所以,可以跳过去 //返回false do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //将上一个节点的状态设置为signal,返回false, compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false;}private final boolean parkAndCheckInterrupt() { LockSupport.park(this); //将AQS对象自己传入 return Thread.interrupted();}阻塞和中断 由上述分析,我们知道了AQS通过调用LockSupport的park方法来执行阻塞当前进程的操作。其实,这里的阻塞就是线程不再执行的含义,通过调用这个函数,线程进入阻塞状态,上述的lock操作也就阻塞了,等待中断或在独占性变量被释放。public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker);//设置阻塞对象,用来记录线程被谁阻塞的,用于线程监控和分析工具来定位 UNSAFE.park(false, 0L);//让当前线程不再被线程调度,就是当前线程不再执行. setBlocker(t, null);} 关于中断的相关知识,我们以后再说,就继续沿着AQS的主线,看一下释放独占性变量的相关操作吧。unlock操作 与lock操作类似,unlock操作调用了AQS的relase方法,参数和调用acquire时一样,都是1。public final boolean release(int arg) { if (tryRelease(arg)) { //释放独占性变量,起始就是将status的值减1,因为acquire时是加1 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h);//唤醒head的后继节点 return true; } return false;} 由上述代码可知,release就是先调用tryRelease来释放独占性变量。如果成功,那么就看一下是否有等待锁的阻塞线程,如果有,就调用unparkSuccessor来唤醒他们。protected final boolean tryRelease(int releases) { //由于只有一个线程可以获得独占先变量,所以,所有操作不需要考虑多线程 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //如果等于0,那么说明锁应该被释放了,否则表示当前线程有多次lock操作. free = true; setExclusiveOwnerThread(null); } setState(c); return free;} 我们可以看到tryRelease中的逻辑也体现了可重入锁的概念,只有等到state的值为0时,才代表锁真正被释放了。所以独占性变量state的值就代表锁的有无。当state=0时,表示锁未被占有,否在表示当前锁已经被占有。private void unparkSuccessor(Node node) { ….. //一般来说,需要唤醒的线程就是head的下一个节点,但是如果它获取锁的操作被取消,或在节点为null时 //就直接继续往后遍历,找到第一个未取消的后继节点. Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread);} 调用了unpark方法后,进行lock操作被阻塞的线程就恢复到运行状态,就会再次执行acquireQueued中的无限for循环中的操作,再次尝试获取锁。后记 有关AQS和ReentrantLock的分析就差不多结束了。不得不说,我第一次看到AQS的实现时真是震惊,以前都认为Synchronized和ReentrantLock的实现原理是一致的,都是依靠java虚拟机的功能实现的。没有想到还有AQS这样一个背后大Boss在提供帮助啊。学习了这个类的原理,我们对JUC的很多类的分析就简单了很多。此外,AQS涉及的CAS操作和无锁队列的算法也为我们学习其他无锁算法提供了基础。知识的海洋是无限的啊! ...

January 13, 2019 · 3 min · jiezi

Golang并发:除了channle,你还有其他选择

我们都知道Golang并发优选channel,但channel不是万能的,Golang为我们提供了另一种选择:sync。通过这篇文章,你会了解sync包最基础、最常用的方法,至于sync和channel之争留给下一篇文章。sync包提供了基础的异步操作方法,比如互斥锁(Mutex)、单次执行(Once)和等待组(WaitGroup),这些异步操作主要是为低级库提供,上层的异步/并发操作最好选用通道和通信。sync包提供了:Mutex:互斥锁RWMutex:读写锁WaitGroup:等待组Once:单次执行Cond:信号量Pool:临时对象池Map:自带锁的map这篇文章是sync包的入门文章,所以只介绍常用的结构和方法:Mutex、RWMutex、WaitGroup、Once,而Cond、Pool和Map留给大家自行探索,或有需求再介绍。互斥锁常做并发工作的朋友对互斥锁应该不陌生,Golang里互斥锁需要确保的是某段时间内,不能有多个协程同时访问一段代码(临界区)。互斥锁被称为Mutex,它有2个函数,Lock()和Unlock()分别是获取锁和释放锁,如下:type Mutexfunc (m *Mutex) Lock(){}func (m *Mutex) Unlock(){}Mutex的初始值为未锁的状态,并且Mutex通常作为结构体的匿名成员存在。经过了上面这么“官方”的介绍,举个例子:你在工商银行有100元存款,这张卡绑定了支付宝和微信,在中午12点你用支付宝支付外卖30元,你在微信发红包,抢到10块。银行需要按顺序执行上面两件事,先减30再加10或者先加10再减30,结果都是80,但如果同时执行,结果可能是,只减了30或者只加了10,即你有70元或者你有110元。前一个结果是你赔了,后一个结果是银行赔了,银行可不希望把这种事算错。看看实际使用吧:创建一个银行,银行里存每个账户的钱,存储查询都加了锁操作,这样银行就不会算错账了。银行的定义:type Bank struct { sync.Mutex saving map[string]int // 每账户的存款金额}func NewBank() *Bank { b := &Bank{ saving: make(map[string]int), } return b}银行的存取钱:// Deposit 存款func (b *Bank) Deposit(name string, amount int) { b.Lock() defer b.Unlock() if _, ok := b.saving[name]; !ok { b.saving[name] = 0 } b.saving[name] += amount}// Withdraw 取款,返回实际取到的金额func (b *Bank) Withdraw(name string, amount int) int { b.Lock() defer b.Unlock() if _, ok := b.saving[name]; !ok { return 0 } if b.saving[name] < amount { amount = b.saving[name] } b.saving[name] -= amount return amount}// Query 查询余额func (b *Bank) Query(name string) int { b.Lock() defer b.Unlock() if _, ok := b.saving[name]; !ok { return 0 } return b.saving[name]}模拟操作:小米支付宝存了100,并且同时花了20。func main() { b := NewBank() go b.Deposit(“xiaoming”, 100) go b.Withdraw(“xiaoming”, 20) go b.Deposit(“xiaogang”, 2000) time.Sleep(time.Second) fmt.Printf(“xiaoming has: %d\n”, b.Query(“xiaoming”)) fmt.Printf(“xiaogang has: %d\n”, b.Query(“xiaogang”))}结果:先存后花。➜ sync_pkg git:(master) ✗ go run mutex.goxiaoming has: 80xiaogang has: 2000也可能是:先花后存,因为先花20,因为小明没钱,所以没花出去。➜ sync_pkg git:(master) ✗ go run mutex.goxiaoming has: 100xiaogang has: 2000这个例子只是介绍了mutex的基本使用,如果你想多研究下mutex,那就去我的Github(阅读原文)下载下来代码,自己修改测试。Github中还提供了没有锁的例子,运行多次总能碰到错误:fatal error: concurrent map writes这是由于并发访问map造成的。读写锁读写锁是互斥锁的特殊变种,如果是计算机基本知识扎实的朋友会知道,读写锁来自于读者和写者的问题,这个问题就不介绍了,介绍下我们的重点:读写锁要达到的效果是同一时间可以允许多个协程读数据,但只能有且只有1个协程写数据。也就是说,读和写是互斥的,写和写也是互斥的,但读和读并不互斥。具体讲,当有至少1个协程读时,如果需要进行写,就必须等待所有已经在读的协程结束读操作,写操作的协程才获得锁进行写数据。当写数据的协程已经在进行时,有其他协程需要进行读或者写,就必须等待已经在写的协程结束写操作。读写锁是RWMutex,它有5个函数,它需要为读操作和写操作分别提供锁操作,这样就4个了:Lock()和Unlock()是给写操作用的。RLock()和RUnlock()是给读操作用的。RLocker()能获取读锁,然后传递给其他协程使用。使用较少。type RWMutexfunc (rw *RWMutex) Lock(){}func (rw *RWMutex) RLock(){}func (rw *RWMutex) RLocker() Locker{}func (rw *RWMutex) RUnlock(){}func (rw *RWMutex) Unlock(){}上面的银行实现不合理:大家都是拿手机APP查余额,可以同时几个人一起查呀,这根本不影响,银行的锁可以换成读写锁。存、取钱是写操作,查询金额是读操作,代码修改如下,其他不变:type Bank struct { sync.RWMutex saving map[string]int // 每账户的存款金额}// Query 查询余额func (b *Bank) Query(name string) int { b.RLock() defer b.RUnlock() if _, ok := b.saving[name]; !ok { return 0 } return b.saving[name]}func main() { b := NewBank() go b.Deposit(“xiaoming”, 100) go b.Withdraw(“xiaoming”, 20) go b.Deposit(“xiaogang”, 2000) time.Sleep(time.Second) print := func(name string) { fmt.Printf("%s has: %d\n", name, b.Query(name)) } nameList := []string{“xiaoming”, “xiaogang”, “xiaohong”, “xiaozhang”} for _, name := range nameList { go print(name) } time.Sleep(time.Second)}结果,可能不一样,因为协程都是并发执行的,执行顺序不固定:➜ sync_pkg git:(master) ✗ go run rwmutex.goxiaohong has: 0xiaozhang has: 0xiaogang has: 2000xiaoming has: 100等待组互斥锁和读写锁大多数人可能比较熟悉,而对等待组(WaitGroup)可能就不那么熟悉,甚至有点陌生,所以先来介绍下等待组在现实中的例子。你们团队有5个人,你作为队长要带领大家打开藏有宝藏的箱子,但这个箱子需要4把钥匙才能同时打开,你把寻找4把钥匙的任务,分配给4个队员,让他们分别去寻找,而你则守着宝箱,在这等待,等他们都找到回来后,一起插进钥匙打开宝箱。这其中有个很重要的过程叫等待:等待一些工作完成后,再进行下一步的工作。如果使用Golang实现,就得使用等待组。等待组是WaitGroup,它有3个函数:Add():在被等待的协程启动前加1,代表要等待1个协程。Done():被等待的协程执行Done,代表该协程已经完成任务,通知等待协程。Wait(): 等待其他协程的协程,使用Wait进行等待。type WaitGroupfunc (wg *WaitGroup) Add(delta int){}func (wg *WaitGroup) Done(){}func (wg *WaitGroup) Wait(){}来,一起看下怎么用WaitGroup实现上面的问题。队长先创建一个WaitGroup对象wg,每个队员都是1个协程, 队长让队员出发前,使用wg.Add(),队员出发寻找钥匙,队长使用wg.Wait()等待(阻塞)所有队员完成,某个队员完成时执行wg.Done(),等所有队员找到钥匙,wg.Wait()则返回,完成了等待的过程,接下来就是开箱。结合之前的协程池的例子,修改成WG等待协程池协程退出,实例代码:func leader() { var wg sync.WaitGroup wg.Add(4) for i := 0; i < 4; i++ { go follower(&wg, i) } wg.Wait() fmt.Println(“open the box together”)}func follower(wg *sync.WaitGroup, id int) { fmt.Printf(“follwer %d find key\n”, id) wg.Done()}结果:➜ sync_pkg git:(master) ✗ go run waitgroup.gofollwer 3 find keyfollwer 1 find keyfollwer 0 find keyfollwer 2 find keyopen the box togetherWaitGroup也常用在协程池的处理上,协程池等待所有协程退出,把上篇文章《Golang并发模型:轻松入门协程池》的例子改下:package mainimport ( “fmt” “sync”)func main() { var once sync.Once onceBody := func() { fmt.Println(“Only once”) } done := make(chan bool) for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } for i := 0; i < 10; i++ { <-done }}单次执行在程序执行前,通常需要做一些初始化操作,但触发初始化操作的地方是有多处的,但是这个初始化又只能执行1次,怎么办呢?使用Once就能轻松解决,once对象是用来存放1个无入参无返回值的函数,once可以确保这个函数只被执行1次。type Oncefunc (o *Once) Do(f func()){}直接把官方代码给大家搬过来看下,once在10个协程中调用,但once中的函数onceBody()只执行了1次:package mainimport ( “fmt” “sync”)func main() { var once sync.Once onceBody := func() { fmt.Println(“Only once”) } done := make(chan bool) for i := 0; i < 10; i++ { go func() { once.Do(onceBody) done <- true }() } for i := 0; i < 10; i++ { <-done }}结果:➜ sync_pkg git:(master) ✗ go run once.goOnly once示例源码本文所有示例源码,及历史文章、代码都存储在Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/sync_pkg下期预告这次先介绍入门的知识,下次再介绍一些深入思考、最佳实践,不能一口吃个胖子,咱们慢慢来,顺序渐进。下一篇我以这些主题进行介绍,欢迎关注:哪个协程先获取锁一定要用锁吗锁与通道的选择文章推荐Golang并发模型:轻松入门流水线模型Golang并发模型:轻松入门流水线FAN模式Golang并发模型:并发协程的优雅退出Golang并发模型:轻松入门selectGolang并发模型:select进阶Golang并发模型:轻松入门协程池Golang并发的次优选择:sync包如果这篇文章对你有帮助,请点个赞/喜欢,感谢。本文作者:大彬如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/01/04/golang-pkg-sync/ ...

January 5, 2019 · 3 min · jiezi

不可不说的Java“锁”事

前言Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本文旨在对锁相关源码(本文中的源码来自JDK 8)、使用场景进行举例,为读者介绍主流锁的知识点,以及不同的锁的适用场景。Java中往往是按照是否含有某一特性来定义锁,我们通过特性将锁进行分组归类,再使用对比的方式进行介绍,帮助大家更快捷的理解相关知识。下面给出本文内容的总体分类目录:1. 乐观锁 VS 悲观锁乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。在Java和数据库中都有此概念对应的实际应用。先说概念。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。根据从上面的概念描述我们可以发现:悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。光说概念有些抽象,我们来看下乐观锁和悲观锁的调用方式示例:// ————————- 悲观锁的调用方式 ————————-// synchronizedpublic synchronized void testMethod() { // 操作同步资源}// ReentrantLockprivate ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁public void modifyPublicResources() { lock.lock(); // 操作同步资源 lock.unlock();}// ————————- 乐观锁的调用方式 ————————-private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicIntegeratomicInteger.incrementAndGet(); //执行自增1通过调用方式示例,我们可以发现悲观锁基本都是在显式的锁定之后再操作同步资源,而乐观锁则直接去操作同步资源。那么,为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式 “CAS” 的技术原理来为大家解惑。CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。CAS算法涉及到三个操作数:需要读写的内存值 V。进行比较的值 A。要写入的新值 B。当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。之前提到java.util.concurrent包中的原子类,就是通过CAS来实现了乐观锁,那么我们进入原子类AtomicInteger的源码,看一下AtomicInteger的定义:根据定义我们可以看出各属性的作用:unsafe: 获取并操作内存的数据。valueOffset: 存储value在AtomicInteger中的偏移量。value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。接下来,我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:// ————————- JDK 8 ————————-// AtomicInteger 自增方法public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}// Unsafe.classpublic final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5;}// ————————- OpenJDK 8 ————————-// Unsafe.javapublic final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v;}根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:1.ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。2.循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。3.只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。2. 自旋锁 VS 适应性自旋锁在介绍自旋锁前,我们需要介绍一些前提知识来帮助大家明白自旋锁的概念。阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock,本文中仅做名词介绍,不做深入讲解,感兴趣的同学可以自行查阅相关资料。3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁这四种锁是指锁的状态,专门针对synchronized的。在介绍这四种锁状态之前还需要介绍一些额外的知识。首先为什么Synchronized能实现线程同步?在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。Java对象头synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。MonitorMonitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:锁状态存储内容存储内容无锁对象的hashCode、对象分代年龄、是否是偏向锁(0)01偏向锁偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)01轻量级锁指向栈中锁记录的指针00重量级锁指向互斥量(重量级锁)的指针10无锁无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。偏向锁偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。轻量级锁是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。重量级锁升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。整体的锁状态升级流程如下:综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。4. 公平锁 VS 非公平锁公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。直接用语言描述可能有点抽象,这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁。如上图所示,假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。如下图所示:接下来我们通过ReentrantLock的源码来讲解公平锁和非公平锁。根据代码可知,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。下面我们来看一下公平锁与非公平锁的加锁方法的源码:通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。5. 可重入锁 VS 非可重入锁可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:public class Widget { public synchronized void doSomething() { System.out.println(“方法1执行…”); doOthers(); } public synchronized void doOthers() { System.out.println(“方法2执行…”); }}在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。而为什么可重入锁就可以在嵌套调用时可以自动获得锁呢?我们通过图示和源码来分别解析一下。还是打水的例子,有多个人在排队打水,此时管理员允许锁和同一个人的多个水桶绑定。这个人用多个水桶打水时,第一个水桶和锁绑定并打完水之后,第二个水桶也可以直接和锁绑定并开始打水,所有的水桶都打完水之后打水人才会将锁还给管理员。这个人的所有打水流程都能够成功执行,后续等待的人也能够打到水。这就是可重入锁。但如果是非可重入锁的话,此时管理员只允许锁和同一个人的一个水桶绑定。第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和锁绑定也无法打水。当前线程出现死锁,整个等待队列中的所有线程都无法被唤醒。之前我们说过ReentrantLock和synchronized都是重入锁,那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁。首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。6. 独享锁 VS 共享锁独享锁和共享锁同样是一种概念。我们先介绍一下具体的概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁和共享锁。独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。下图为ReentrantReadWriteLock的部分源码:我们看到ReentrantReadWriteLock有两把锁:ReadLock和WriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步观察可以发现ReadLock和WriteLock是靠内部类Sync实现的锁。Sync是AQS的一个子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。在ReentrantReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。那读锁和写锁的具体加锁方式有什么区别呢?在了解源码之前我们需要回顾一下其他知识。在最开始提及AQS的时候我们也提到了state字段(int类型,32位),该字段用来描述有多少线程获持有锁。在独享锁中这个值通常是0或者1(如果是重入锁的话state值就是重入的次数),在共享锁中state就是持有锁的数量。但是在ReentrantReadWriteLock中有读、写两把锁,所以需要在一个整型变量state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。如下图所示:了解了概念之后我们再来看代码,先看写锁的加锁源码:protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); // 取到当前锁的个数 int w = exclusiveCount(c); // 取写锁的个数w if (c != 0) { // 如果已经有线程持有了锁(c!=0) // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) // 如果写线程数(w)为0(换言之存在读锁) 或者持有锁的线程不是当前线程就返回失败 return false; if (w + exclusiveCount(acquires) > MAX_COUNT) // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。 throw new Error(“Maximum lock count exceeded”); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。 return false; setExclusiveOwnerThread(current); // 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者 return true;}这段代码首先取到当前锁的个数c,然后再通过c来获取写锁的个数w。因为写锁是低16位,所以取低16位的最大值与当前的c做与运算( int w = exclusiveCount(c); ),高16位和0与运算后是0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。在取到写锁线程的数目后,首先判断是否已经有线程持有了锁。如果已经有线程持有了锁(c!=0),则查看当前写锁线程的数目,如果写线程数为0(即此时存在读锁)或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。如果当且写线程数为0(那么读线程也应该为0,因为上面已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;如果通过CAS增加写线程数失败也返回失败。如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功!tryAcquire()除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:必须确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。接着是读锁的代码:protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态 int r = sharedCount(c); if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current);}可以看到在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。此时,我们再回头看一下互斥锁ReentrantLock中公平锁和非公平锁的加锁源码:我们发现在ReentrantLock虽然有公平锁和非公平锁两种,但是它们添加的都是独享锁。根据源码所示,当某一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定ReentrantLock无论读操作还是写操作,添加的锁都是都是独享锁。结语本文Java中常用的锁以及常见的锁的概念进行了基本介绍,并从源码以及实际应用的角度进行了对比分析。限于篇幅以及个人水平,没有在本篇文章中对所有内容进行深层次的讲解。其实Java本身已经对锁本身进行了良好的封装,降低了研发同学在平时工作中的使用难度。但是研发同学也需要熟悉锁的底层原理,不同场景下选择最适合的锁。而且源码中的思路都是非常好的思路,也是值得大家去学习和借鉴的。参考资料1.《Java并发编程艺术》2.Java中的锁3.Java CAS 原理剖析4.Java并发——关键字synchronized解析5.Java synchronized原理总结6.聊聊并发(二)——Java SE1.6中的Synchronized7.深入理解读写锁—ReadWriteLock源码分析8.【JUC】JDK1.8源码分析之ReentrantReadWriteLock9.Java多线程(十)之ReentrantReadWriteLock深入分析10.Java–读写锁的实现原理作者简介家琪,美团点评后端工程师。2017 年加入美团点评,负责美团点评境内度假的业务开发。 ...

November 16, 2018 · 2 min · jiezi