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

举报
互联网小阿祥 发表于 2023/05/30 21:44:42 2023/05/30
【摘要】 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调度不一定先分给谁,所以谁先运行都可能,但是他们在同一个售票机,也就是同一个锁资源,必然会出现,一个线程执行完成之后,才会执行另外一个线程。

在这里插入图片描述

  • 采用同步代码块操作也是一样的
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);
            }
        }
    }
}

在这里插入图片描述

  • 如果是两个售票机对象,张三调用售票机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();
    }
}

在这里插入图片描述

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);
         }
     }
}

在这里插入图片描述

4.synchronized底层锁原理分析

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

在这里插入图片描述

javac TicketMachine.java

javap -v TicketMachine.class

两种形式:

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

在这里插入图片描述

  • 同步方法
    • 加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令
    • 每个monitor维护着一个记录着拥有次数的计数器, 未被拥有的monitor的该计数器为0,
    • 当一个线程获执行monitorenter后,该计数器自增1
    • 当同一个线程执行monitorexit指令的时候,计数器再自减1。
    • 当计数器为0的时候,monitor将被释放.也叫显式同步

在这里插入图片描述

  • 两种本质上没有区别,底层都是通过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());
    }
}

在这里插入图片描述

  • 死锁的4个必要条件
    • 互斥条件:资源不能共享,只能由一个线程使用
    • 请求与保持条件:线程已经获得一些资源,但因请求其他资源发生阻塞,对已经获得的资源保持不释放
    • 不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强行回收,只能由线程使用完自己释放
    • 循环等待条件:多个线程形成环形链,每个都占用对方申请的下个资源
  • 只要发生死锁,上面的条件都成立;只要一个不满足,就不会发生死锁
  • 排查方式
  • 方式一:Jps查到具体的某个进程, jstack查看死锁

在这里插入图片描述

在这里插入图片描述

  • 方式二:jconsole

在这里插入图片描述

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。