【并发编程】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
- 点赞
- 收藏
- 关注作者
评论(0)