【并发编程】synchronized底层原理及对象锁和类锁实践

简介: 【并发编程】synchronized底层原理及对象锁和类锁实践

1.什么是synchronzied

  • synchronized是解决线程安全的问题,常用在 同步普通方法、静态方法、代码块 中
  • 每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待
  • 锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性
  • 所以是非公平、可重入的悲观锁

2.synchronzied对象锁实践

(1)什么是对象锁

  • 也叫实例锁,对应synchronized关键字,当多个线程访问多个实例时,它们互不干扰,每个对象都拥有自己的锁
  • 但如果是多个线程访问同个对象的sychronized块,是同步的加锁,访问不同对象的话就是不同步的
  • synchronized(object){}的 效果和在实例方法上加锁一样,不同的是可以在()里添加不同的对象

(2)准备测试的方法

我们现在有一个场景,就是在火车站用售票机购买火车票,那么一个售票机是不是同一时刻只能有一个人去购买,后面的人只能排队等待,多个售票机可以实现多个人同时去购买,但是这个几个人同时购买取决于我们有多少个售票机。那么我们现在就来模拟下这个购买票的场景。
  • 定义售票机类,类中定义售票的方法。
public class TicketMachine {
  //我们这块先用同步方法进行测试
    public synchronized void buyTicket(){
        try {
            //假设购买一张票的时间是2s
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+",购买了一张火车票。");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
public class SyncTest {
    public static void main(String[] args) {
        //创建售票机对象
        TicketMachine ticketMachine = new TicketMachine();
        //创建李祥线程去 ticketMachine 这个售票机购买一张票
        new Thread(()->{
            //记录开始时间
            long start = new Date().getTime();
            ticketMachine.buyTicket();
            //记录结束时间
            long end = new Date().getTime();
            System.out.println("李祥购票耗时:"+(end-start)+" 毫秒");
        },"李祥").start();
        //创建张三线程去 ticketMachine 这个售票机购买一张票
        new Thread(()->{
            //记录开始时间
            long start = new Date().getTime();
            ticketMachine.buyTicket();
            //记录结束时间
            long end = new Date().getTime();
            System.out.println("张三购票耗时:"+(end-start)+" 毫秒");
        },"张三").start();
    }
}

首先我们分析这段代码一共起了多少线程,主线程、李祥线程、张三线程,主线程先启动,由于张三线程和李祥线程CPU调度不一定先分给谁,所以谁先运行都可能,但是他们在同一个售票机,也就是同一个锁资源,必然会出现,一个线程执行完成之后,才会执行另外一个线程。

dcc77ba761954abfaf7189cfc4c09dbf.png

  • 采用同步代码块操作也是一样的
public class TicketMachine {
    public void buyTicket(){
        //锁this,锁住当前对象
        synchronized(this){
            try {
                //假设购买一张票的时间是2s
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+",购买了一张火车票。");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

a1c624af43c64a9cb49fe1ed3e6f104f.png

  • 如果是两个售票机对象,张三调用售票机1对象,李祥调用售票机2对象
public class SyncTest {
    public static void main(String[] args) {
        //创建售票机对象
        TicketMachine ticketMachine1 = new TicketMachine();
        TicketMachine ticketMachine2 = new TicketMachine();
        //创建李祥线程去 ticketMachine1 这个售票机购买一张票
        new Thread(()->{
            //记录开始时间
            long start = new Date().getTime();
            ticketMachine1.buyTicket();
            //记录结束时间
            long end = new Date().getTime();
            System.out.println("李祥购票耗时:"+(end-start)+" 毫秒");
        },"李祥").start();
        //创建张三线程去 ticketMachine2 这个售票机购买一张票
        new Thread(()->{
            //记录开始时间
            long start = new Date().getTime();
            ticketMachine2.buyTicket();
            //记录结束时间
            long end = new Date().getTime();
            System.out.println("张三购票耗时:"+(end-start)+" 毫秒");
        },"张三").start();
    }
}

cfc3a5f798594d88bff4961a2a876909.png

3.synchronzied类锁实践

(1)什么是类锁(static sychronized method{})

  • 关键字是 static sychronized,是一个全局锁,不管创建多少个对象都共享同一个锁
  • 保障同一个时刻多个线程同时访问同一个synchronized块,当一个线程在访问时,其他的线程等待
  • 静态方法使用synchronized关键字后,无论是多线程访问单个对象还是多个对象的sychronized块,都是同步的
  • synchronized(xxx.class){}的 效果和在静态方法方法上加锁一样,不同的是可以在()里添加不同的类对象

(2)类锁实践

  • 还是刚刚的代码稍加改动就能实现类锁
  • 同步方法加上static关键字就能实现类锁
  • 同步代码块锁住 类.class 就能实现类锁
public class TicketMachine {
    public void buyTicket(){
        //锁this,锁住当前对象
        synchronized(TicketMachine.class){
            try {
                //假设购买一张票的时间是2s
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+",购买了一张火车票。");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class TicketMachine {
    public static synchronized void buyTicket(){
         try {
            //假设购买一张票的时间是2s
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+",购买了一张火车票。");
         } catch (InterruptedException e) {
            throw new RuntimeException(e);
         }
     }
}

41974d74032345528633c692afcd5248.png

4.synchronized底层锁原理分析

测试方法,生成字节码查看


0142d168b0c54b0297de84d46ec8bb60.png

javac TicketMachine.java

javap -v TicketMachine.class

两种形式:

  • 同步方法
  • 生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位
  • 当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识
  • 如果存在,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor
  • 在方法执行期间,其他任何线程都无法再获得同一个monitor对象,也叫隐式同步

afd35c6088a94902b89f79612b921b3a.png

同步方法

加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令

每个monitor维护着一个记录着拥有次数的计数器, 未被拥有的monitor的该计数器为0,

当一个线程获执行monitorenter后,该计数器自增1

当同一个线程执行monitorexit指令的时候,计数器再自减1。

当计数器为0的时候,monitor将被释放.也叫显式同步


1144a87be6564799a8bf912937e6cc48.png

  • 两种本质上没有区别,底层都是通过monitor来实现同步, 只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成

5.线上死锁排查案例

(1)什么是死锁

  • 两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象
  • 若无外力作用,它们都将无法让程序进行下去
  • 生产环境出现如何发现死锁,说说你的排查方式

(2)案例代码

模拟方法A获取locka锁后,线程挂起但不释放锁资源,再去尝试获取lockb锁,方法B获取lockb锁后,线程挂起但不释放锁资源,再去尝试获取locka锁,这样如果同时调用两个方法,则会出现死锁的现象。

public class DeadLockDemo {
    private static Object locka = new Object();
    private static Object lockb = new Object();
    public void methodA(){
        synchronized (locka){
            System.out.println("我是A方法中获得了锁A "+Thread.currentThread().getName() );
            //让出CPU执行权,不释放锁
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(lockb){
                System.out.println("我是A方法中获得了锁B "+Thread.currentThread().getName() );
            }
        }
    }
    public void methodB(){
        synchronized (lockb){
            System.out.println("我是B方法中获得了锁B "+Thread.currentThread().getName() );
            //让出CPU执行权,不释放锁
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(locka){
                System.out.println("我是B方法中获得了锁A "+Thread.currentThread().getName() );
            }
        }
    }
    public static void main(String [] args){
        System.out.println("主线程运行开始运行:"+Thread.currentThread().getName());
        DeadLockDemo deadLockDemo = new DeadLockDemo();
        new Thread(()->{
            deadLockDemo.methodA();
        }).start();
        new Thread(()->{
            deadLockDemo.methodB();
        }).start();
        System.out.println("主线程运行结束:"+Thread.currentThread().getName());
    }
}

9dc7d95dc5d141b9ba4a2c880b0eb35e.png

死锁的4个必要条件

互斥条件:资源不能共享,只能由一个线程使用

请求与保持条件:线程已经获得一些资源,但因请求其他资源发生阻塞,对已经获得的资源保持不释放

不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强行回收,只能由线程使用完自己释放

循环等待条件:多个线程形成环形链,每个都占用对方申请的下个资源

只要发生死锁,上面的条件都成立;只要一个不满足,就不会发生死锁

排查方式

方式一:Jps查到具体的某个进程, jstack查看死锁

1e5be1a3bc2246c4b964b0a93804c4d8.png

e4451884eea44e3a8bd4784551707380.png

  • 方式二:jconsole


2e702679445f475e80d1ec5f715123c8.png


相关文章
|
6月前
|
Java
【多线程系列】你先说说synchronized的实现原理
面试官:听说你精通多线程,那我就考考你吧面试官:不用慌尽管说,错了也没关系😊。。。❤️。
【多线程系列】你先说说synchronized的实现原理
|
6月前
|
安全 Java
Java并发编程:Synchronized及其实现原理
Java并发编程:Synchronized及其实现原理
53 4
|
2月前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
5月前
|
存储 Java
Java并发编程 Synchronized原理
Java并发编程 Synchronized原理
24 0
|
6月前
|
Java API
【并发编程】吃透Synchronized
【并发编程】吃透Synchronized
29 1
|
6月前
|
安全 Java
并发编程之synchronized的详细解析
并发编程之synchronized的详细解析
43 0
|
6月前
|
缓存 安全 Java
Java并发编程中的线程安全性探讨
【2月更文挑战第6天】在Java开发中,多线程编程是一种常见的方式,然而如何确保线程安全性却是一个复杂且关键的问题。本文将深入探讨Java并发编程中的线程安全性,包括线程安全性的概念、常见的线程安全性问题以及解决方法,旨在帮助开发者更好地理解和应对多线程环境下的挑战。
|
12月前
|
缓存 安全 Java
Java并发编程学习2-线程安全性
本篇介绍 线程安全性,竞态条件,加锁机制
85 1
Java并发编程学习2-线程安全性
|
6月前
|
存储 算法 Java
JUC并发编程之Synchronized锁优化
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
|
存储 缓存 安全
并发编程——synchronized
原子性、有序性、可见性
43 0