Java 中的并发锁大致分为隐式锁和显式锁两种。
隐式锁就是我们最常使用的 synchronized 关键字
显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和ReentrantReadWriteLock,这两个类都是基于 AQS(AbstractQueuedSynchronizer) 实现的。还有的地方将 CAS 也称为一种锁,在包括 AQS 在内的很多并发相关类中,CAS 都扮演了很重要的角色。
概念辨析
1. 悲观锁和乐观锁
在Java和数据库中都有此概念对应的实际应用。
悲观锁(独占锁) ,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。这么做的好处是简单安全,但是挂起线程和恢复线程都需要转入内核态进行,这样做会带来很大的性能开销。
悲观锁的代表: synchronized关键字和Lock的实现类。
然而在真实环境中,大部分时候都不会产生冲突。悲观锁会造成很大的浪费。
使用场景: 适合写操作多的场景
乐观锁 ,它假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试)。这种锁不会阻塞其他的线程,也不涉及上下文切换,性能开销小。
乐观锁会乐观的认为每次查询都不会造成更新丢失,利用版本字段控制
最常采用的是CAS算法
java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的
使用场景: 适用于多读的应用类型,这样可以提高吞吐量
2. 公平锁和非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指线程加锁前不考虑排队问题,直接尝试获取锁,获取不到再去队尾排队。
性能略高于公平锁
值得注意的是,在 AQS 的实现中,一旦线程进入排队队列,即使是非公平锁,线程也得乖乖排队。
⽐如synchronized、ReentrantLock()无参构造
3. 可重入锁和不可重入锁
如果某个线程试图获取一个已经由他自己持有的锁,这个请求可以成功,那么此时的锁就是可重入锁,重入锁的这种机制也说明了它是以“线程”为粒度获取锁,而不是以“调用”为粒度。
又叫递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
从名字上理解,ReentrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数 器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
4. 独享锁 和 共享锁
独享锁(互斥锁,排它锁,独占锁):该锁每一次只能被一个线程所持有。 ReeReentrantLock/synchronized
共享锁:该锁可被多个线程共有 ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占
5. 读写锁
相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写(译者注:也就是说:读-读能共存,读-写不能共存,写-写不能共存)。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。
6. 自旋锁 和 自适应自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在 JDK1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。
不会发⽣线程状态的切换,⼀直处于⽤户态,减少了线程上下⽂切换的消耗,缺点是循环会消耗CPU
常⻅的⾃旋锁:TicketLock,CLHLock,MSCLock
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
死锁
当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试
获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻
塞现象,我们称为死锁。
public class DeadLock {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args){
Thread a = new Thread(new Lock1());
//Thread b = new Thread(new Lock2());
a.start();
//b.start();
}
}
class Lock1 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock1 running");
while(true){
synchronized(DeadLock.obj1){
System.out.println("Lock1 lock obj1");
Thread.sleep(3000);
synchronized(DeadLock.obj2){
System.out.println("Lock1 lock obj2");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
class Lock2 implements Runnable{
@Override
public void run(){
try{
System.out.println("Lock2 running");
while(true){
synchronized(DeadLock.obj2){
System.out.println("Lock2 lock obj2");
Thread.sleep(3000);
synchronized(DeadLock.obj1){
System.out.println("Lock2 lock obj1");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}