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