1. 乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读比较-写的操作。java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
2. 悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会bock直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁侧是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
3. 自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗Cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
3.1 自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用Cpu做无用功,占着XX不XX, 同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;
3.2 自旋锁的时间阈值
自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!
JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时M还针对当前CPU的负荷情况做了较多的优化,如果平均负载小于CPUs则一直自旋,如果有超过(CPUs/2)
个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现Ownr发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果CPU处于节电模式则停止自旋,自旋时间的最坏情况是CPU的存储延迟(CPUA存储了一个数据,到CPUB得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。
自旋锁的开启
JDK1.6中-XX:+UseSpinning开启;
XX:PreBlockSpin=10为自旋次数;
JDK1.7后,去掉此参数,由jvm控制;
4. Synchronized 同步锁
synchronized它可以把任意一个非NU儿L的对象当作锁。他属于独占式的惑观锁,同时属于可重入锁。
4.1 Synchronized 作用范围
- 作用于方法时,锁住的是对象实例。
- 作用于静态方法时,锁住的是Class实例,因为 Class 实例数据在永久代,永久代又是全局共享的,因此静态方法相当于一个类的全局锁,所有调用这个加锁的静态方法的线程都会被锁住。
- 作用于对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象的监视器时,对象监视器会将这些线程存储在不同的容器中。
4.2 Synchronized 核心组件
- Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;
- Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
- Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;
- OnDeck:任意时刻,最多只有一个线程正在亮争锁资源,该线程被成为OnDeck;
- Owner:当前已经获取到所资源的线程被称为Owner;
- Owner:当前释放锁的线程。
4.3 Synchronized 实现
- JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。
- Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList ,并指定某个线程位 OnDeck 线程(一般是最先进去的那个线程)。
- Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,OnDeck 需要重新竞争锁。这样虽然会牺牲一些公平性,但是能极大的提升系统的吞吐量,在 JVM中把这种行为称为"竞争切换"。
- OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList 中。如果 Owner 线程被 Wait 方法阻塞,则转移到WaiteSet队列中,直到某个时刻通过 notify 唤醒才会重新进入 EntryList。
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的。
- Synchronized 是非公平锁。Synchronized 在线程进入 ContentionList 时,等待线程会尝试自旋获取锁,如果获取不到才会进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的是,自旋获取锁的还可能抢占 OnDeck 线程的锁资源。
- 每个对象都有个 Monitor 对象,加锁就是在竞争 Monitor 对象,代码块加锁在前后分别使用 MonitorEnter 和 MonitorExit 指令实现的,方法加锁是通过一个标记位判断的。
- Synchronized 是一个重量级操作,需要调用操作系统相关的接口,性能是低效的,有可能给线程加锁消耗的时间比操作消耗的时间更多。
- 1.6 版本后,Synchronized 进行了很多的优化,有适应自旋,锁消除,锁粗化,轻量级锁及偏向锁。效率上有了本质的提高。在之后推出的Java 1.7 与 1.8 中,均对该关键字的实现机理做了优化,引入了偏向锁和轻量级锁,都是在对象头中有标记位,不需要经过操作系统加锁。
- 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫锁膨胀。
- JDK 1.6 后都是默认开启偏向锁和轻量级锁,可以通过 -XX:UseBiasedLocking 来禁用偏向锁。
5.ReentrantLock
ReentrantLock 继承接口 Lock 并实现了接口中定义的方法,它是一种可重入锁,除了能完成 Synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法。
5.1 Lock的主要接口
- void lock() :执行此方法时,如果锁处于空闲状态,当前线程将获取到锁。相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁。
- boolean tryLock():如果锁可用,则获取锁,并立即返回true,否则返回false。该方法和 lock() 的区别是,tryLock() 只是试图获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码。而lock() 方法则是一定要获取到锁,如果锁不可用就一直等待,在未获取到锁之前,当前线程并不继续向下执行。
- void unLock():执行此方法,当前线程将释放锁。锁只能由持有者释放。如果该线程并不持有锁,却执行该方法,可能导致异常。
- Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await() 方法,而调用后,当前线程将释放锁。
- getHoldCount():查询当前线程保持此锁的次数,也就是执行lock方法的次数。
- getQueueLength():返回正在等待此锁的线程计数,比如启动了10个线程,1个线程获取了锁,此时返回的就是9。
- getWaitQueueLength(Condition condition):返回等待与此锁相关的给定条件的线程计数。比如10个线程,用同一个 condition 对象,并且此时1欧哥线程都执行了condition 对象的 await 方法,那么此时执行的方法返回就是 10
- hasWaiters(Condition conditon):查询是否有线程等待与此锁有关的给定条件(condition),对于指定 condition 对象,有多少线程执行了 condition 的 await 方法。
- hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁。
- hasQueuedThreads():是否有线程在等待此锁
- isFair():该锁是否公平
- isHeldByCurrentThread():当前线程是否锁定,线程的执行 Lock 方法的前后分别是 false 和 true
- isLock():此锁师傅有任意线程占用
- lockIterruptibly():如果当前线程未被中断,获取锁
- tryLock():尝试获取锁,仅在调用时锁未被线程占用,获得锁
- tryLock(long timeOut, TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。
5.2 非公平锁
JVM 按随机,就近原则分配锁的机制称为非公平锁,ReentrantLock 在构造函数中提供了师傅公平锁的初始化方式,默认非公平锁。非公平锁实际执行效率远高于公平锁,除非程序有特殊的需求。
5.3 公平锁
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
5.4 ReentranLock 与 Synchronized
-
- ReentrantLock 通过方法 Lock() 与 unLock() 来进行加锁与解锁的操作,与 Synchronized 会被 JVM 自动解锁机制不同,Reetrantlock 加锁后需要手动的释放锁。为了避免程序没法正常解锁,ReentrantLock 使用时必须在 finally 控制块中进行解锁操作。
- ReentrantLock 相比 Synchronied 的优势是可中断、可公平、多个锁。这种情况下需要用 ReentrantLock。
5.5 ReetranLcok 的实现
5.6 Condition 类和 Object 类锁方法区别
- Condition 类的 await 方法和 Object 类的 wait 方法等效
- Conditong 类的 signal 方法和 Object 类的 notify 方法等效
- Condition 类的 signalAll 方法和 Object累的 notifyAll 方法等效
- ReentrantLock 类可以唤醒指定条件的线程,而 Object 的唤醒是随机的
5.7 tryLock 和 Lock 和 LockInterruptibly 的区别
- tryLock 获取锁时能获取到就是 true 不能获取到 就是false。tryLcok(Long timeOut, TimeUnit unit),可以增加获取时间限制,如果超过该时间获取不到就返回false。
- lock 能获取到锁就返回true,不能的话就一直等待直到获取到锁。
- lock 和 lockInterruptibly ,如果两个线程分别执行这两个方法,但此时中断这两个线程,lock 不会跑出异常,而lockInterruptibly 会抛出异常
6. Semaphore 信号量
Semaphore 是一种基于计数的信号量。它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池。
6.1 实现互斥锁
我们可以创建计数为 1 的 Semaphore ,将其作为一种类似的互斥锁机制,这也叫二元信号量,表示两种互斥状态。
/**
* 测试内容: 使用 Semaphore 实现互斥锁完成 I++
* 测试结果: i = 10
*/
@Test
public void test1() throws Exception {
Semaphore semaphore = new Semaphore(1);
ThreadGroup work = new ThreadGroup("work");
Runnable runnable = new Runnable() {
private int i = 0;
@Override
public void run() {
try {
semaphore.acquire();
i++;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
@Override
public String toString() {
return "$classname{" +
"i=" + i +
'}';
}
};
for (int i = 0; i < 10; i++) {
new Thread(work, runnable).start();
}
while (true) {
if (work.activeCount() == 0) {
System.out.println(runnable.toString());
break;
}
}
}
6.2 Semaphore 和 ReentrantLock
Semaphore 基本能完成 ReentrantLock 的所以工作,使用方法也与之类似,通过 acquire() 与 release() 方法来获得和释放临界区资源。经实测。Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中被 Thread.interrupt() 方法中断。
此外,Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法吗 tryAcquire 与 tryLock 不同,使用方法与 ReenTrantLock 几乎一致。Semaphore 也提供了公平与非公平锁的机制,也可在构造函数中进行设定。
Semaphore 的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而无法正常的释放锁的情况,释放操作也必须在finally中完成。
7.AtomicIteger
首先说明,此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有 AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference等。他们实现的原理相同,区别在于运算的对象类型不同。令人兴奋的,还可以通过 AtomicReference 将一个对象的所有操作转化为原子操作。
我们知道,在多线程程序中,诸如++或++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用synchronized将该操作变成一个原子操作,但JVM为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常Atomiclnteger的性能是ReentantLock的好几倍。
8. 可重入锁(递归锁)
本文里面讲的是广义上的可重入锁,而不是单指JAVA下的ReentrantLock。可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下ReentrantLock和synchronized都是可重入锁。
9. 公平锁与非公平锁
9.1 公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。
9.2 非公平锁(NonFair)
加锁时不考虑排队等待的问题,直接尝试获取锁,获取不到自动到队尾等待。
-
- 非公平锁性能比公平锁高5-7倍,因为公平锁需要在多核情况下维护一个队列
- java 中的Synchronized 是非公平锁,ReentrantLock 默认也是非公平锁。
10. ReadWriteLock 读写锁
为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制。如果在没有写锁的情况下读是无阻塞的,在一定的程度上提高了程序的执行效率。读写锁分为读锁和写锁,多个读锁不互斥,这是由JVM控制的,只需要我们加好对应的锁即可。
10.1 读锁
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁。
10.2 写锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
Java中读写锁有个接口java.util.concurrent.locks..ReadWriteLock,也有具体的实现ReentrantReadWriteLock.
11. 共享锁和独占锁
java 并发包提供的加锁模式分为独占锁和共享锁。
11.1 独占锁
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
11.2 共享锁
共享锁侧允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
- AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,他们分别标识AQS队列中等待线程的锁获取模式。
- java的并发包中提供了ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
12. 重量级锁
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的我们称之为"重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和"偏向锁”。
13. 轻量级锁
锁的状态总共有四种:无锁,偏向锁,轻量级锁和重量级锁。
锁升级:随着锁的竞争,锁可以从偏向升级到轻量级,再升级就是重量级(但锁的升级是单向的,也就是说只能从低到高的升级,不会出现降级)
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
14. 偏向锁
HotSpot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是有同一个线程多次获得。
偏向锁的目的是在某个线程获得锁之后,消除这个线程重入(CAS)的开销,看起来让这个线程的到了偏护。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS 原子指令。而偏向锁只需要在置换 ThreadID 的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况下就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步代码时提高性能。而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
15. 分段锁
分段锁也并非一种实际的锁,而是一种思想,以减小和隔离锁的粒度,来达到减少锁并发竞争,提高多线程的效率。例如ConcurrentHashMap。
17.锁优化
17.1 减少锁的持有时间
只用在有线程安全要求的程序上加锁
17.2 减小锁的粒度
将打对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最典型的减小粒度的案例就是ConcurrentHashMap。
17.3 锁分离
最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能,具体也请查看[高并发Jva五]DK并发包1。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如LinkedBlockingQueue从头部取出,从尾部放数据。
17.4 锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
17.5 锁消除
锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。
产考网上大佬整理出来的,侵删