锁的分类从不同的角度去看,它们就会衍生出不同的名字。
乐观锁、悲观锁
宏观上锁的定义只有两种:乐观锁和悲观锁,
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。、
悲观锁机制存在以下问题:
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 一个线程持有锁会导致其它所有需要此锁的线程挂起。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
乐观锁:
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.ato mic 包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测。总结就是:冲突检测和数据更新。
实现案例:CAS
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
具体步骤:CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。
缺点:
- ABA问题
- 循环时间开销大:未成功的线程会一直循环,直到更改数据成功
- 只能保证一个共享变量的操作。但是可以将多个变量放入一个对象中进行操作。
对象锁、类锁
根据锁添加的位置进行分类。
-
类锁:使用**字节码文件(即.class)**作为锁。如静态同步函数(使用本类的.class),同步代码块中使用.class。
-
对象锁:使用对象作为锁。如同步函数(使用本类实例,即 this),同步代码块中使用同一个对象。
公平锁、非公平锁
-
公平锁:多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。
-
非公平锁:多个线程在等待同一个锁时,不用申请锁的先后顺序来一次获得锁。也就是不用排队。
非公平锁是可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
公平锁
-
等待锁的线程不会饿死
-
效率低
非公平锁
-
效率高
-
有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。
公平锁可以通过 new ReentrantLock (true) 来实现;非公平锁可以通过 new ReentrantLock (false) 或者默认构造函数 new ReentrantLock () 实现。
synchronized 是非公平锁,并且它无法实现公平锁。
可重入锁、不可重入锁
根据线程能否获得自己的锁
-
可重入锁:如果某个线程试图获取一个已经由他自己持有的锁,这个请求可以成功
-
不可重入锁:当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
共享锁、排它锁
共享锁和排它锁多用于数据库中的事物操作,主要针对读和写的操作。而在 Java 中,对这组概念通过 ReentrantReadWriteLock 进行了实现,它的理念和数据库中共享锁与排它锁的理念几乎一致,即一条线程进行读的时候,允许其他线程进入上锁的区域中进行读操作;当一条线程进行写操作的时候,不允许其他线程进入进行任何操作。即读 + 读可以存在,读 + 写、写 + 写均不允许存在。
-
共享锁:也称读锁或 S 锁。如果事务 T 对数据 A 加上共享锁后,则其他事务只能对 A 再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
-
排它锁:也称独占锁、写锁或 X 锁。如果事务 T 对数据 A 加上排它锁后,则其他事务不能再对 A 加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。
自旋锁、偏向锁、轻量级锁
死锁、活锁
1.死锁
所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
死锁形成的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生:
-
互斥:进程要求对所分配的资源进行排他性控制,此时若有其他进程请求该资源,则请求进程只能等待。
-
不剥夺:资源只能由获得该资源的进程自己来释放(只能是主动释放)。
-
请求和保持:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
-
循环等待:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
1.活锁
活锁和死锁在表现上是一样的两个线程都没有任何进展,但是区别在于:死锁,两个线程都处于阻塞状态,说白了就是它不会再做任何动作,我们通过查看线程状态是可以分辨出来的。而活锁呢,并不会阻塞,而是一直尝试去获取需要的锁,不断的 try,这种情况下线程并没有阻塞所以是活的状态,我们查看线程的状态也会发现线程是正常的,但重要的是整个程序却不能继续执行了,一直在做无用功。
锁消除、锁粗化(锁膨胀)
1.锁消除
对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据是来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而能被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步加锁自然就无需进行。
例子:StringBuffer 对象中的append()方法,
2.锁粗化
原则上,要将同步块的作用范围限制的尽量小 —— 只在共享数据的实际作用域中才进行同步,使得需要同步的操作数量尽可能变小,如果存在锁禁止,那等待的线程也能尽快拿到锁。大部分情况下,这些都是正确的。
但是,如果一系列的操作都是同一个对象****反复加锁和解锁,甚至加锁操作是出现在循环体中的,那么即使没有线程竞争,频繁地进行****互斥同步操作也导致不必要的性能损耗。
类似上面锁消除的 concatString () 方法。如果 StringBuffer sb = new StringBuffer (); 定义在方法体之外,那么就会有线程竞争,但是每个 append () 操作都对同一个对象反复加锁解锁,那么虚拟机探测到有这样的情况的话,会把加锁同步的范围扩展到整个操作序列的外部,即扩展到第一个 append () 操作之前和最后一个 append () 操作之后,这样的一个锁范围扩展的操作就称之为锁粗化。