21. 各种锁的理解
1)公平锁,非公平锁
在Java中,锁(Lock)是一种用于多线程同步的机制。公平锁和非公平锁是两种不同类型的锁。
公平锁(Fair Lock)是指线程获取锁的顺序与线程请求锁的顺序保持一致
。换句话说,当多个线程同时请求一个公平锁时,锁会按照线程请求锁的顺序逐一分配锁。因此,公平锁保证了线程获取锁的公平性,在一定程度上避免了线程饥饿现象(某些线程一直无法获取到锁)。公平锁的实现通常会有比较大的性能开销。
非公平锁(Unfair Lock)是指线程获取锁的顺序与线程请求锁的顺序没有必然关系
。多个线程同时请求一个非公平锁时,锁会先尝试将锁分配给先到达的线程,如果未成功,则进入队列等待。非公平锁的实现通常比公平锁更加高效,但可能会导致某些线程长时间等待。
ReentrantLock是Java中提供的一个可重入锁(Reentrant Lock)
,可以作为公平锁或非公平锁使用。在构造ReentrantLock对象时,可以传入一个boolean参数fair,用于指定是否使用公平锁。默认情况下,ReentrantLock是非公平锁。当fair为true时,ReentrantLock会以公平锁的方式工作;当fair为false时,ReentrantLock会以非公平锁的方式工作。
使用ReentrantLock时,可以通过lock()方法获取锁,通过unlock()方法释放锁。对于公平锁,锁会按照线程请求锁的顺序分配;而对于非公平锁,会尽可能地将锁分配给已经等待较久的线程
。无论是公平锁还是非公平锁,都可以通过tryLock()方法尝试获取锁,并返回获取结果。
总结起来,公平锁保证了线程获取锁的公平性,避免了线程饥饿现象;非公平锁在一定程度上提高了性能,但可能导致线程长时间等待。通过ReentrantLock可以灵活地选择使用公平锁或非公平锁
。
1.公平锁:非常公平,不能插队,必须先来后到
/*** Creates an instance of {@code ReentrantLock}.* This is equivalent to using {@code ReentrantLock(false)}.*/
public ReentrantLock() {sync = new NonfairSync();
}
2.非公平锁:非常不公平,允许插队,可以改变顺序
/*** Creates an instance of {@code ReentrantLock} with the* given fairness policy.** @param fair {@code true} if this lock should use a fair ordering policy*/
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}
2)可重入锁
可重入锁(Reentrant Lock)是指同一个线程在获取锁之后可以再次获取该锁而不会被阻塞
。在Java中,Lock和Synchronized都是可重入锁的实现方式。
-
Lock(显式锁):
Lock是Java.util.concurrent.locks包下的接口,它提供了与Synchronized类似的功能
,但更灵活。Lock实现了一个可重入的互斥锁
。下面是一个使用Lock的示例:import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;public class LockExample {private Lock lock = new ReentrantLock();public void method1() {lock.lock();try {// 代码块1method2();} finally {lock.unlock();}}public void method2() {lock.lock();try {// 代码块2} finally {lock.unlock();}} }
上述代码中,method1()和method2()都使用了Lock来保护临界区,而且两个方法均可以重入。
-
Synchronized(隐式锁):
Synchronized是Java语言内置的锁机制,它使用起来简单方便,但相对于Lock缺少了一些灵活性。Synchronized关键字可修饰方法或代码块
,它的特点是同一时间只能有一个线程获得锁,其他线程将被阻塞
。下面是一个使用Synchronized的示例:public class SynchronizedExample {public synchronized void method1() {// 代码块1method2();}public synchronized void method2() {// 代码块2} }
上述代码中,method1()和method2()都使用了Synchronized关键字修饰,因此它们都是可重入的。
无论是Lock还是Synchronized,都支持线程的重入
。当一个线程进入一个使用Lock或Synchronized修饰的方法或代码块时,如果该线程已经获得了锁,它可以再次获得相同的锁而不会被阻塞。这种特性使得线程可以在同一个线程内递归地调用同步方法或访问使用同步块保护的数据,可以有效地避免死锁等问题。
Synchonized 锁(同一把锁)
public class Demo01 {public static void main(String[] args) {Phone phone = new Phone();new Thread(()->{phone.sms();},"A").start();new Thread(()->{phone.sms();},"B").start();}}class Phone{public synchronized void sms(){System.out.println(Thread.currentThread().getName()+"=> sms");call();//这里也有一把锁}public synchronized void call(){System.out.println(Thread.currentThread().getName()+"=> call");}
}
Lock 锁(不一样的锁)
//lock
public class Demo02 {public static void main(String[] args) {Phone2 phone = new Phone2();new Thread(()->{phone.sms();},"A").start();new Thread(()->{phone.sms();},"B").start();}}
class Phone2{Lock lock=new ReentrantLock();public void sms(){lock.lock(); //细节:这个是两把锁,两个钥匙//lock锁必须配对,否则就会死锁在里面try {System.out.println(Thread.currentThread().getName()+"=> sms");call();//这里也有一把锁} catch (Exception e) {e.printStackTrace();}finally {lock.unlock();}}public void call(){lock.lock();try {System.out.println(Thread.currentThread().getName() + "=> call");}catch (Exception e){e.printStackTrace();}finally {lock.unlock();}}
}
注意:
- lock锁必须配对,相当于lock和 unlock 必须数量相同;
- 在外面加的锁,也可以在里面解锁;在里面加的锁,在外面也可以解锁;
3)自旋锁
自动不断地去尝试,直到成功为止
自旋锁是一种基于忙等待的锁机制
,它不会让线程休眠,而是在获取锁失败时,线程会不断尝试获取锁,直到获取到锁为止,这种方式避免了线程切换的开销。
Java中的自旋锁可以使用AtomicBoolean类来实现
,通过设置和获取AtomicBoolean对象的值来表示锁的状态。当一个线程需要获取锁时,会不断循环尝试获取锁,直到成功获取锁为止。
下面是一个简单的示例,展示了使用自旋锁实现的互斥访问临界区的代码:
import java.util.concurrent.atomic.AtomicBoolean;public class SpinLock {private AtomicBoolean locked = new AtomicBoolean(false);public void lock() {while (!locked.compareAndSet(false, true)) {// 自旋等待获取锁}}public void unlock() {locked.set(false);}
}
在上述代码中,locked
是一个AtomicBoolean对象,用于表示锁的状态。lock()
方法会不断循环尝试将locked
的值由false设置为true,直到成功获取锁为止。unlock()
方法则将locked
的值设置为false,释放锁。
下面是一个使用自旋锁的示例:
public class SpinLockExample {private SpinLock lock = new SpinLock();private int count = 0;public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}public int getCount() {return count;}
}
在上述代码中,increment()
方法会获取自旋锁,然后对count
进行+1操作,最后释放锁。getCount()
方法则返回count
的值。
值得注意的是,自旋锁适用于临界区的代码执行时间短的情况下,如果临界区的代码执行时间长,自旋锁可能会造成CPU资源的浪费。因此,在使用自旋锁时需要根据具体的场景来确定是否适合使用自旋锁。
范例
自己设计的锁
使用CAS设计的自旋锁
public class SpinlockDemo {// 默认// int 0//thread nullAtomicReference<Thread> atomicReference=new AtomicReference<>();//加锁public void myLock(){Thread thread = Thread.currentThread();System.out.println(thread.getName()+"===> mylock");//自旋锁while (!atomicReference.compareAndSet(null,thread)){System.out.println(Thread.currentThread().getName()+" ==> 自旋中~");}}//解锁public void myUnlock(){Thread thread=Thread.currentThread();System.out.println(thread.getName()+"===> myUnlock");atomicReference.compareAndSet(thread,null);}}
测试:
public class TestSpinLock {public static void main(String[] args) throws InterruptedException {ReentrantLock reentrantLock = new ReentrantLock();reentrantLock.lock();reentrantLock.unlock();//使用CAS实现自旋锁SpinlockDemo spinlockDemo=new SpinlockDemo();new Thread(()->{spinlockDemo.myLock();try {TimeUnit.SECONDS.sleep(3);} catch (Exception e) {e.printStackTrace();} finally {spinlockDemo.myUnlock();}},"t1").start();TimeUnit.SECONDS.sleep(1);new Thread(()->{spinlockDemo.myLock();try {TimeUnit.SECONDS.sleep(3);} catch (Exception e) {e.printStackTrace();} finally {spinlockDemo.myUnlock();}},"t2").start();}
}
结果:
t2必须等到t1释放锁以后,才执行
4)偏向锁
Java的偏向锁是一种针对线程同步优化的机制
。它通过在对象头中的mark word中存储线程ID来表示该对象是否被某个线程独占,从而避免线程竞争引起的锁争用以提高程序性能。
偏向锁有以下的特点:
偏向锁只针对线程的同步问题,而不是对所有对象的同步进行优化
。偏向锁只有在对象的mark word为空的时候才会被启用
。偏向锁是一种悲观锁,即默认认为同步资源会存在竞争,需要进行加锁操作
。但在实际运行过程中,由于大部分情况下锁都不会存在竞争,因此偏向锁可以大大减少资源竞争。偏向锁会在加锁之后,将线程ID写入对象的mark word中,并将对象的线程ID设置为当前线程的ID,表示该对象已经被锁定
。当其他线程尝试获取同一个对象的锁时,会检查对象的mark word中的线程ID与当前线程的ID是否相等,如果相等,则表示可以获取锁,否则需要进行锁撤销操作,升级为轻量级锁
。
以下是一个使用偏向锁的示例:
public class BiasLockExample {private static Object lock = new Object();public static void main(String[] args) {synchronized (lock) {System.out.println("Main thread has acquired the lock");// Logic here}}
}
在上面的示例中,只有主线程能够获取到lock对象的偏向锁,其他线程在尝试获取lock对象的锁时会失败,因为偏向锁只有在mark word为空时才会被启用。
总结来说,偏向锁是用来优化同步操作的机制,在不存在线程竞争的情况下,可以大大减少同步操作的开销。这在单线程的情况下特别有效,但在多线程竞争的情况下效果可能会降低。因此,偏向锁适用于大多数情况下都是单线程访问的场景
。
5)轻量级锁
轻量级锁是Java中一种基于自旋的同步机制
,主要用于提高多线程并发执行的性能。轻量级锁的概念是在对象头中的Mark Word中添加额外的标志位,用于表示对象是否被锁定。当一个线程尝试获取一个被轻量级锁标记的对象时,它会通过CAS原子操作尝试将对象的Mark Word替换为指向线程自身的指针,如果成功,说明该线程成功获取了锁,可以继续执行。如果CAS操作失败,说明有其他线程正在持有该对象的锁,那么当前线程就会进入自旋状态,一直在循环中等待其他线程释放锁。
举个例子来解释轻量级锁的工作原理:
class Counter {private int count;public synchronized void increment() {count++;}
}public class Main {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread thread1 = new Thread(() -> {for (int i = 0; i < 100000; i++) {counter.increment();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 100000; i++) {counter.increment();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("Final count: " + counter.getCount());}
}
在这个例子中,我们定义了一个Counter类,其中有一个用synchronized修饰的increment方法。我们创建了两个线程并分别调用increment方法对计数器进行100000次增加操作。
当线程1尝试获取Counter对象的锁时,它会发现对象的Mark Word标记为可用状态,于是通过CAS操作将Mark Word更新为指向线程1的指针。线程2也会进行类似的尝试,但是由于线程1已经获取了锁并且没有释放,所以线程2的CAS操作失败,线程2进入自旋状态。
在自旋状态中,线程2会不断检查对象的Mark Word,看是否被线程1持有锁。一旦线程1释放锁,线程2就可以通过CAS操作成功获取锁,并继续执行。这种自旋等待的方式避免了线程的阻塞和唤醒,提高了多线程并发执行的效率。
需要注意的是,轻量级锁适用于对锁的竞争不激烈的情况
。如果锁的竞争非常激烈,那么线程在自旋状态中消耗的CPU资源会增加,反而影响性能。此时,JVM会将轻量级锁升级为重量级锁,即使用传统的互斥量来实现锁的功能
。
6)重量级锁
重量级锁(Heavyweight Lock)是一种用于多线程环境下的同步机制
。它可以保证同一时刻只有一个线程能够访问被锁定的资源,从而防止多线程并发访问时可能导致的数据不一致问题。
重量级锁的实现基于操作系统提供的底层同步原语
,如互斥量(Mutex)或信号量(Semaphore)。它通常涉及操作系统级别的上下文切换和线程阻塞/唤醒操作,因此被称为“重量级”。
重量级锁的使用场景一般是在多线程并发访问共享资源时,要保证数据的一致性和线程安全性。举例来说,考虑一个银行账户的转账操作,多个线程并发执行转账业务时,需要确保每次只有一个线程能够访问该账户,避免出现并发问题。这时可以使用重量级锁对账户对象进行加锁,以保证线程安全。
下面是一个简单的Java代码示例,演示了如何使用重量级锁来保护共享资源:
public class BankAccount {private int balance;public void deposit(int amount) {// 获取重量级锁synchronized (this) {balance += amount;}}public void withdraw(int amount) {// 获取重量级锁synchronized (this) {if (balance >= amount) {balance -= amount;}}}
}
在上面的代码中,deposit()
和 withdraw()
方法都使用了 synchronized
关键字来获取重量级锁。这样,在多个线程并发执行这些方法时,每次只有一个线程能够获取锁,并且安全地访问 balance
变量。
需要注意的是,重量级锁的使用可能会导致性能问题,因为它涉及到线程的上下文切换和阻塞/唤醒操作。因此,在设计多线程程序时,应该尽量避免过多地使用重量级锁,并考虑使用更轻量级的同步机制,如乐观锁、无锁编程等。
7)锁升级顺序
在Java中,锁的升级顺序是从无锁状态到偏向锁状态
,再到轻量级锁状态
,最后到重量级锁状态
。具体的升级过程如下:
-
无锁状态
:当多个线程访问同一个资源时,没有任何线程持有锁,所有线程都可以自由地访问该资源。 -
偏向锁状态
:当一个线程访问资源时,会在对象头中记录该线程的标识,表示该线程已经获取了锁。如果其他线程想要访问该资源,会发现该资源已经被偏向锁占用,但是由于占用锁的线程是唯一的,所以其他线程不需要竞争锁,直接进入偏向锁模式。 -
轻量级锁状态
:当多个线程竞争同一个资源时,会进入轻量级锁状态。在这种状态下,会先尝试使用CAS操作来获取锁,如果成功则说明获取锁的线程可以继续执行,如果失败则表示锁被其他线程持有,那么当前线程会进入自旋等待状态,不会阻塞。 -
重量级锁状态
:当自旋等待一定次数后还没有成功获取锁时,线程会进入阻塞状态,此时会将锁升级为重量级锁。在重量级锁状态下,线程会被阻塞,进入等待队列,直到获取到锁的线程释放锁。
锁的升级是为了提高锁的竞争效率
。一开始使用偏向锁来避免多线程竞争,如果有竞争则进入轻量级锁状态进行自旋等待,如果自旋等待一定次数还没有成功获取锁,则进入重量级锁状态阻塞线程。这样的锁升级机制可以根据不同场景下的线程竞争情况来选择最适合的锁状态,以提高并发性能。
8)死锁
死锁是指两个或多个线程在互斥地持有一些资源,并且同时等待其他线程释放自己持有的资源,从而导致所有线程都无法继续执行的情况
。通常情况下,死锁发生时,线程将无法继续执行,除非外部干预或者有一些特殊的机制来打破死锁。
死锁通常发生在多线程环境下,具有以下四个必要条件:
-
互斥条件
:至少有一个资源被线程独占,其他线程无法同时访问; -
请求与保持条件
:线程至少持有一个资源,并且请求额外的资源,但是不释放已经持有的资源; -
不可剥夺条件
:线程已经获得的资源只能由自己来释放,其他线程无法剥夺; -
循环等待条件
:存在循环等待的资源链,每个线程都在等待下一个资源,使得无法被其他线程释放。
下面是一个死锁的示例:
class Resource {public void doSomething() {// 这里是资源的具体操作}
}class ThreadA implements Runnable {private Resource resourceA;private Resource resourceB;public ThreadA(Resource resourceA, Resource resourceB) {this.resourceA = resourceA;this.resourceB = resourceB;}public void run() {synchronized (resourceA) {System.out.println("ThreadA got resourceA");try {Thread.sleep(1000);} catch (InterruptedException e) {}synchronized (resourceB) {System.out.println("ThreadA got resourceB");// 使用资源进行操作resourceA.doSomething();resourceB.doSomething();}}}
}class ThreadB implements Runnable {private Resource resourceA;private Resource resourceB;public ThreadB(Resource resourceA, Resource resourceB) {this.resourceA = resourceA;this.resourceB = resourceB;}public void run() {synchronized (resourceB) {System.out.println("ThreadB got resourceB");try {Thread.sleep(1000);} catch (InterruptedException e) {}synchronized (resourceA) {System.out.println("ThreadB got resourceA");// 使用资源进行操作resourceA.doSomething();resourceB.doSomething();}}}
}public class DeadlockExample {public static void main(String[] args) {Resource resourceA = new Resource();Resource resourceB = new Resource();Thread threadA = new Thread(new ThreadA(resourceA, resourceB));Thread threadB = new Thread(new ThreadB(resourceA, resourceB));threadA.start();threadB.start();}
}
在上面的例子中,有两个线程ThreadA和ThreadB分别持有资源A和资源B,并且互相等待对方释放资源。当ThreadA获取到resourceA后,需要获取resourceB才能继续执行,而ThreadB也在等待获取resourceA才能继续执行。这样就形成了一个死锁,导致两个线程都无法继续执行下去。
为了避免死锁,可以使用一些技术手段,比如避免循环等待、按照固定的顺序获取资源、使用超时机制等。另外,也可以使用工具来检测和解决死锁问题,如使用Java的线程监视工具(jstack)来查看线程的状态和调用关系,或者使用嵌入式锁检查工具(javax.management)进行死锁检测和解决。
package com.ogj.lock;import java.util.concurrent.TimeUnit;public class DeadLock {public static void main(String[] args) {String lockA= "lockA";String lockB= "lockB";new Thread(new MyThread(lockA,lockB),"t1").start();new Thread(new MyThread(lockB,lockA),"t2").start();}
}class MyThread implements Runnable{private String lockA;private String lockB;public MyThread(String lockA, String lockB) {this.lockA = lockA;this.lockB = lockB;}@Overridepublic void run() {synchronized (lockA){System.out.println(Thread.currentThread().getName()+" lock"+lockA+"===>get"+lockB);try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB){System.out.println(Thread.currentThread().getName()+" lock"+lockB+"===>get"+lockA);}}}
}
陷入死锁局面
问题1:
死锁避免和死锁预防的区别
:
死锁预防是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,而死锁避免则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。死锁避免是在系统运行过程中注意避免死锁的最终发生。
排查解决
找到项目程序的进程号
1、使用jps定位进程号,jdk的bin目录下: 有一个jps
命令:jps -l
2、使用jstack
进程进程号 找到死锁信息
查看对应进程的堆栈信息
JUC并发编程-各种锁:公平锁,非公平锁、可重入锁、自旋锁、偏向锁、轻量级锁、重量级锁、锁升级顺序、死锁、死锁排查 到此完结,笔者归纳、创作不易,大佬们给个3连再起飞吧