ps:这连天看锁看了好多资料,资料也不分前后顺序,理顺概念,搞定关联脉络着是废了一番劲,总算是基本搞清楚了,真是不容易啊,这个时刻我想起一句话:越往深里学,越得看书,权威书籍的资料更全面,连贯
老规矩,妹子镇楼,抚慰心灵
锁涉及到的点
锁涉及到的点很多,这里从底层向上列举出来:
AQS(抽象队列同步器)、非阻塞数据结构和原子变量类等基础类都是基于 volatile 变量的读/写和 CAS 实现,是 Java 并发包里实现锁、同步的一个重要的基础框架,而像 Lock 、同步器、阻塞队列、Executor 和并发容器等高层类又是基于基础类实现
其实每个部分都可以讲很多,这里列出来,大家心里有个数
java 线程阻塞的代价
- 如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作
- 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间
- 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的
- 另外线程的切换还涉及到线程自身的上下文切换,原理和户态 -> 核心态切换一样
Synchronize 原理
简单来说 Synchronize 是通过 JVM 的对象监视器 Monitor 进入和退出命令来实现对方法、同步块的同步的,在进入同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令,本质就是获取一个 Monitor,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的,而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。
然后我们来看看线程在进入同步队列之后是怎么竞争资源的
别的不用关系,我们熟悉 Contention List 、EntryList 就行
- 首先竞争锁的线程第一次会加入到 Contention List 这个队列里
- 然后在 Owner unlock 也就是当前线程完事释放锁之后,从 Contention List 里面挑选由资格竞争锁的线程放到 EntryList 里面来
- EntryList 里面所有的线程被唤醒,做一次非公平的 CAS 竞争
- WaitSet 里面放的是被 obj.wait 的线程,只有 notify 、nitifyAll 时才会重新参与竞争
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,每次唤醒 EntryList 里面的线程是很大的性能开销
- 更坑爹的是 Synchronized 是非公平锁,外面等待的线程会先尝试自旋获取锁,如果获取不到才进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接获得锁资源
锁的特性
这里介绍 java 中并发核心锁的特性,不是具体的锁,具体的锁一般都是多种特性符合存在的,但是的确有些锁只具有一种特性,但是这种锁一般都是有专门应用场景的,不具有普遍适用性
乐观锁 / 悲观锁
乐观锁 - 既无锁机制,好比做人,这个人很乐观,每次访问数据的时候都认为别人不会修改,也就不加锁。适用于多并发读的场景,CAS算法就是最典型的乐观锁
悲观锁 - 看名字自然也知道悲观锁是乐观锁的对立面,悲观锁在获取资源时不管有没有来竞争,都先加一把锁,这也是并发中最常用也普通的做法,保险不会出错,但是缺点是性能不高,使用于多并发写的场景
公平锁 / 非公平锁
公平锁 - 这个也好理解,还是好比做人,公平锁做人很规矩,在希望申请资源时,先看看有没有其他线程排队等着,有的话就去后面排队去。缺点同样是性能低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大
非公平锁 - 非公平锁自然也是公平锁对立的,非公平锁做人就不地道了,这丫的不排队,非公平锁来了才不管你有没有排队等着,直接竞争资源,没竞争才取排队。优点是比公平锁性能好些,新的线程有可能直接获取到 cpu 资源从而减少唤醒阻塞线程的开销
典型应用 - 就是 ReentrantLock 这个锁,ReentrantLock(true) 构造参数有一个 boolean 值,true = 公平锁 ,false = 非公平锁(默认)
独享锁 / 共享锁
- 独享锁 - 锁只能被一个线程持有, 绝大部分锁都是独享锁,比如我们常用的 synchronized、ReentrantLock 都是独享锁
- 共享锁 - 锁可以被多个线程持有,但是共享锁的应用非常狭窄,比如 ReadWriteLock 就是共享锁, 其写锁是独享锁,读锁是共享锁
分段锁
- 分段锁 - 是特定领域的锁,一般数据结构上多见,比如 ConcurrentHashMap 用的就是这种锁。分段锁核心思路细化锁的粒度,把整体分成多段,每段有一把锁,这样锁住的只是一段,不影响其他段的读写。在 ConcurrentHashMap 中就是给每个分段链表加一把锁,这样可以大大提高 map 在多线程下整体的读写效率。但是在统计 size 的时候,可就是获取 hashmap 全局信息的时候,就需要获取所有的分段锁才能统计
可重入锁
- 可重入锁 - 指的是同一个方法若是已经获取了锁,但是在方法内还有地方需要这个锁的话,可以不受锁的影响,无阻碍的获得锁,这点在递归函数中是至关重要的,所以也叫可重入锁递归锁。ReentrantLock、synchronized 都是可重入锁,但是自旋锁就不是可重入锁
自旋锁
自旋锁是互斥锁的进步,自旋锁在有其他线程竞争同步代码时,我们可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁,为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。这种优化思路就是基于前面说的锁阻塞的时间总是很短的考量
自旋锁在 JDK 1.4 中就已经引入,只不过默认是关闭的,在 JDK 1.6 中就已经改为默认开启了,但是自旋锁也有自身的局限
java 中锁的 API
- synchronized - 由 JVM 实现的内置锁,其特征是:非公平,悲观,独享,互斥,可重入锁,在 JDL 1.6 对其优化之后,可以根据竞争激烈程度,从 无锁 -> 偏向锁 -> 轻量级所 -> 重量级锁升级
- ReentrantLock - 基于 AQS 开发的可重入锁,自旋锁,由 JDK 实现,有公平和非公平之分,也可以感知到中断。ReenTrantLock的自旋,是通过循环调用 CAS 操作来实现加锁的,它的性能好是因为避免了使线程进入内核态的阻塞状态
- Condition - 条件锁,用在阻塞队列中,可控制消费者和生产者的读取进度
- ReentrantReadWriteLock - 读写锁,读和写拆开,可以极大的提高读多写少的场景下的性能
- CyclicBarrier - 同样是基于 AQS 开发的锁,也叫栅栏锁,有换代的操作,可重置(想象成多个栅栏一般)
- CountDownLatch - 基于AQS开发的锁,也叫计时器锁,当计数器减为0后,就可以执行其他任务了,不可重置
- - 3.Semaphore
- AtomicInteger -
上述两种锁机制类型都是“互斥锁”,学过操作系统的都知道,互斥是进程同步关系的一种特殊情况,相当于只存在一个临界资源,因此同时最多只能给一个线程提供服务。但是,在实际复杂的多线程应用程序中,可能存在多个临界资源,这时候我们可以借助Semaphore信号量来完成多个临界资源的访问。
Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire()与release()方法来获得和释放临界资源。
经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。
此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire与tryLock不同,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。
Semaphore的锁释放操作也由手动进行,因此与ReentrantLock一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成
性能对比:
- synchronized 经过 JDK 1.6 的优化,在低并发时 ReentrantLock 和 synchronized 性能相差无几,但在高并发时 synchronized 性能会迅速下降几十倍,而 ReentrantLock 的性能却能依然维持一个水准,因为 ReentrantLock 采取的是循环调用 CAS 避免了线程切换到阻塞,所以在高并发时建议使用ReentrantLock
- 非公平锁实际执行效率要远远超出公平锁,一般推荐使用非公平锁
- 在高并发时 Atomic 性能会优于 ReentrantLock 一倍左右,但是 Atomic 在一段同步代码中只能出现一个 Atomic 的变量,多于一个同步无效,因为他不能在多个Atomic之间同步
锁使用策略:
一般来说同步我们优先考虑 synchronized ,synchronized 是 JVM 层面的,自动加锁取消锁,不会出 bug,如果有特殊需要再进一步优化。ReentrantLock 和 Atomic 如果用的不好,不仅不能提高性能,还可能带来灾难