【并发编程】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
|
Java
Java并发编程和多线程的区别
Java并发编程和多线程的区别
77 0
|
2月前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
3月前
|
安全 Java
Java并发编程实战:使用synchronized和ReentrantLock实现线程安全
【8月更文挑战第31天】在Java并发编程中,保证线程安全是至关重要的。本文将通过对比synchronized和ReentrantLock两种锁机制,深入探讨它们在实现线程安全方面的优缺点,并通过代码示例展示如何使用这两种锁来保护共享资源。
|
5月前
|
存储 安全 Java
Java多线程中线程安全问题
Java多线程中的线程安全问题主要涉及多线程环境下对共享资源的访问可能导致的数据损坏或不一致。线程安全的核心在于确保在多线程调度顺序不确定的情况下,代码的执行结果始终正确。常见原因包括线程调度随机性、共享数据修改以及原子性问题。解决线程安全问题通常需要采用同步机制,如使用synchronized关键字或Lock接口,以确保同一时间只有一个线程能够访问特定资源,从而保持数据的一致性和正确性。
5684 1
|
5月前
|
存储 Java
Java并发编程 Synchronized原理
Java并发编程 Synchronized原理
24 0
|
6月前
|
安全 Java
并发编程之synchronized的详细解析
并发编程之synchronized的详细解析
44 0
|
6月前
|
缓存 安全 Java
Java并发编程中的线程安全性探讨
【2月更文挑战第6天】在Java开发中,多线程编程是一种常见的方式,然而如何确保线程安全性却是一个复杂且关键的问题。本文将深入探讨Java并发编程中的线程安全性,包括线程安全性的概念、常见的线程安全性问题以及解决方法,旨在帮助开发者更好地理解和应对多线程环境下的挑战。
|
6月前
|
存储 算法 Java
JUC并发编程之Synchronized锁优化
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。