JUC多并发编程 锁
乐观锁:
- 版本号机制 Version
- 常采用 CAS算法, Java 原子类中的递增操作就是通过 CAS 自旋实现的
- 认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁,在Java 中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据
- 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
- 如果这个数据已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等
- 适合读操作的场景,不加锁的特点能够使其读操作的性能大幅度提高
// 保证多个线程使用的是同一个 AtomicInterprivate AtomicInteger atomicInteger = new AtomicInteger();public void m3() {atomicInteger.incrementAndGet();}
悲观锁:
- synchronized 关键字和 Lock 的实现类都是悲观锁
- 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
- 适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源
public synchronized void m1() {// 加锁后的业务逻辑}// 保证多个线程使用的同一个 lock对象的前提下ReentrantLock lock = new ReentrantLock();public void m2() {lock.lock();try {// 操作同步资源}finally {lock.unlock();}}
8中锁的案例
import java.util.concurrent.TimeUnit;class Phone {public static synchronized void sendEmail() {try{ TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("----- sendEmail");}public synchronized void sendSMS() {System.out.println("----- sendSMS");}public void hello() {System.out.println("----- hello");}
}/*** 1. 标准访问 两个线程,先打印邮件还是短信* 2. sendEmail 方法中加入暂停 3 秒钟,先打印邮件还是短信* 3. 添加一个普通的 hello 方法,先打印邮件还是 hello* 4. 有两部手机,先打印邮件还是短信* 5. 有两个静态同步方法,先打印邮件还是短信* 6. 有两个静态同步方法,有两部手机,先打印邮件还是短信* 7. 有一个静态同步方法,有一个普通同步方法,先打印邮件还是短信* 8. 有一个静态同步方法,有一个普通同步方法,两部手机,先打印邮件还是短信*/
public class Lock8Demo {public static void main(String[] args) {Phone phone = new Phone();Phone phone2 = new Phone();new Thread(()-> {phone.sendEmail();},"t1").start();try{ TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }new Thread(()-> {phone2.sendSMS();},"t2").start();}
}
情景一 和 二:
一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个 synchronized 方法了,其他的线程都只能等待,换句话说,某一个时刻,只能有唯一的一个线程去访问这些synchronized 方法,锁的是当前对象 this, 被锁定后,其它的线程都不能进入到当前目前对象的其他的 synchronized 方法
情景三和四:
- 普通的方法,没有 synchronized ,并不会竞争资源
- 换成两个对象后,不是同一把锁,不会竞争资源
情景五和六:
- 对于普通同步方法,锁的是当前实例对象,通常指 this
- 对于静态同步方法,锁的是当前类的 Class 对象
- 对于同步方法块,锁的是 synchronized 括号内的对象
情景七和八:
- 当一个线程视图访问同步代码时,它首先必须得到锁,正常退出或抛出异常时必须释放锁
- 所有的普通同步方法用的都是同一把锁-实例对象本身,即 new 出来的具体实例对象本身,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获得锁的方法释放锁后才能获得锁
- 所有的静态同步方法用的也是同一把锁,类对象本身,一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁
- 实例对象本身和类对象本身 是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
反编译 synchronized 同步代码块:
public class LockSyncDemo {Object object = new Object();public void m1() {synchronized (object) {System.out.println("----- hello synchronized code block");}}public static void main(String[] args) {}
}
javap -c .\\LockSyncDemo.class:
Compiled from "LockSyncDemo.java"
public class com.bxtech.juc.LockSyncDemo {java.lang.Object object; public com.bxtech.juc.LockSyncDemo();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: aload_05: new #2 // class java/lang/Object8: dup9: invokespecial #1 // Method java/lang/Object."<init>":()V12: putfield #3 // Field object:Ljava/lang/Object;15: returnpublic void m1();Code:0: aload_01: getfield #3 // Field object:Ljava/lang/Object;4: dup5: astore_16: monitorenter21: aload_122: monitorexit23: aload_224: athrow25: returnException table:from to target type7 17 20 any20 23 20 anypublic static void main(java.lang.String[]);Code:0: return
}
- 实现使用的是 monitorenter 和 monitorexit 指令
- 一般情况就是 1个 enter 对应的 2个 exit(一个正常退出,一个异常退出)
反编译 synchronized 普通同步方法:
public class LockSyncDemo {public synchronized void m2() {System.out.println("----- hello synchronized code m2");}public static void main(String[] args) {}
}
javap -v .\\LockSyncDemo.class :
public synchronized void m2();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String ----- hello synchronized code m25: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 11: 0line 12: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 this Lcom/bxtech/juc/LockSyncDemo;
- 调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置。如果设置了,执行线程会将先持有 monitor 锁,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor
反编译 synchronized 静态同步方法:
public class LockSyncDemo {public static synchronized void m3() {System.out.println("----- hello synchronized code m3");}public static void main(String[] args) {}
}
javap -v .\\LockSyncDemo.class :
public static synchronized void m3();descriptor: ()Vflags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZEDCode:stack=2, locals=0, args_size=00: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;stack=0, locals=1, args_size=10: returnLineNumberTable:line 19: 0LocalVariableTable:Start Length Slot Name Signature0 1 0 args [Ljava/lang/String;
- ACC_STATIC, ACC_SYNCHRONIZED 访问标志区分该方法是否静态同步方法
管程
- 管程(Monitors, 也称为监视器): 是一种程序结构,结构内的多个子程序(对象或模块) 形成的多个工作线程互斥访问共享资源。
- 这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作及中在一个模块中。(把信号量以及操作原语封装在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看作一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制
为什么任何一个对象都可以成为一个锁:
- 每个对象天生都带着一个对象监视器
- 每一个被锁住的对象都会和 Monitor 关联起来
ObjectMonitor() {_header = NULL;_count = 0; // 用来记录该线程获得锁的次数_waiters = 0,_recursions = 0; // 锁的重入次数_object = NULL;_owner = NULL; // 指向持有 ObjectMonitor对象线程_WaitSet = NULL; // 存放处于 wait 状态的线程队列_WaitSetLock = 0 ; _Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ; // 存放处于等待锁 block 状态的线程队列_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;}
公平锁和非公平锁
类型 | 说明 |
公平锁 | 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的 Lock lock = new ReentrantLock(true) |
非公平锁 |
是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿状态(某个线程一直得不到锁) Lock lock = new ReentrantLock(false) 或 Lock lock = new ReentrantLock() |
import java.util.concurrent.locks.ReentrantLock;class Ticket{private int number = 50;ReentrantLock lock = new ReentrantLock(true);public void sale() {lock.lock();try {if(number > 0) {System.out.println(Thread.currentThread().getName() + "卖出第:\\t" + (number--) + "\\t 还剩下:" + number );}}finally {lock.unlock();}}
}
public class SaleTicketDemo {public static void main(String[] args) {Ticket ticket = new Ticket();new Thread(()-> {for(int i = 0; i < 55; i++) ticket.sale(); },"a").start();new Thread(()-> {for(int i = 0; i < 55; i++) ticket.sale(); },"b").start();new Thread(()-> {for(int i = 0; i < 55; i++) ticket.sale(); },"c").start();}
}
为什么有公平锁和非公平锁:
- 恢复挂起的线程到真正锁的获取还是有时间差,从开发人员来看这个时间微乎其微,但是从 CPU 的角度来看,这个时间差存在的还是很明显。所以非公平锁能更充分的利用 CPU 的时间, 尽量减少 CPU 空闲状态时间
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此次再次获取同步状态的概率变得份长达,所以就减少了线程的开销
可重复锁(递归锁)
- 在同一个线程的外层方法获取锁的时候,在进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象), 不会因为之前已经获取过还没释放而阻塞。
- Java 中 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可以一定程度避免死锁。
隐式锁(即 synchronized 关键字使用的锁):
- 指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫可重入锁
- 简单来说就是:在一个 synchronized 修饰的方法或代码块的内部调用本来的其他 synchronized 修饰的方法或代码块时,是永远可以得到锁的
public class ReEntryLockDemo {public synchronized void m2() {System.out.println(Thread.currentThread().getName() + "\\t ---- come in");m3();System.out.println(Thread.currentThread().getName() + "\\t ---- end m1");}public synchronized void m3() {System.out.println(Thread.currentThread().getName() + "\\t ---- come in");m4();}public synchronized void m4() {System.out.println(Thread.currentThread().getName() + "\\t ---- come in");}public static void main(String[] args) {ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();new Thread(()->{reEntryLockDemo.m2();},"t1").start();}public static void m1() {final Object object= new Object();new Thread(()->{synchronized (object){System.out.println(Thread.currentThread().getName() + "\\t ---- 外层调用");synchronized (object) {System.out.println(Thread.currentThread().getName() + "\\t ---- 中层调用");synchronized (object) {System.out.println(Thread.currentThread().getName() + "\\t ---- 内层调用");}}}},"t1").start();}
}
synchronized 重入的实现机理:
- 每个锁对象有一个锁计数器 和一个指向持有该锁的线程的指针
- 当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有, Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1.
- 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待直至持有线程释放该锁
- 当执行 monitorexit 时, Java 虚拟机则需要将锁对象的计数器减1。计数器为零代表锁已被释放。
显式锁(即 Lock):
- lock 和 unlock 要一一匹配
static Lock lock = new ReentrantLock();public static void main(String[] args) {new Thread(()->{lock.lock();try{System.out.println(Thread.currentThread().getName() + "\\t ---- 外层调用");lock.lock();try {System.out.println(Thread.currentThread().getName() + "\\t ---- 内层调用");}finally {lock.unlock();}}finally {lock.unlock();}},"t1").start();}
死锁和排查
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
产生死锁的主要原因:
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
死锁代码:
import java.util.concurrent.TimeUnit;public class DeadLockDemo {public static void main(String[] args) {final Object objectA = new Object();final Object objectB = new Object();new Thread(()-> {synchronized (objectA){System.out.println(Thread.currentThread().getName()+"\\t 自己持有A锁,希望获得B锁");try{ TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }synchronized (objectB) {System.out.println(Thread.currentThread().getName()+"\\t 成功获得B锁");}}},"A").start();new Thread(()-> {synchronized (objectB){System.out.println(Thread.currentThread().getName()+"\\t 自己持有B锁,希望获得A锁");try{ TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }synchronized (objectA) {System.out.println(Thread.currentThread().getName()+"\\t 成功获得A锁");}}},"B").start();}
}
死锁排查:
- jstack 进程编号(可用 jps 查询)
- jconsole 检测死锁