Java多线程--锁详解

一 锁是什么?

/**
 * 现象描述:
 * 1 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信     共用一个对象锁
 * 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?---------先邮件,后短信  共用一个对象锁
 * 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件
 * 4. 有两部手机,请问先打印邮件还是短信? ----先短信后邮件  资源没有争抢,不是同一个对象锁
 * 5. 有两个静态同步方法,一步手机, 请问先打印邮件还是短信?---------先邮件后短信  共用一个类锁
 * 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? ----------先邮件后短信 共用一个类锁
 * 7. 有一个静态同步方法 一个普通同步方法,请问先打印邮件还是短信? ---------先短信后邮件   一个用类锁一个用对象锁
 * 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? -------先短信后邮件 一个类锁一个对象锁
 *
 * 总结:
 * 1-2:普通同步方法-加对象锁 一个类里面有多个synchronized方法,只要访问其中一个synchronized方法,那么其他线程就不能访问这个资源类(this),其他线程不能进入这个类对象访问它的其他方法
 * 3:不产生争抢锁
 * 4:不是同一个对象锁
 * 5-6:静态同步方法---加类锁
 * 7-8:
 */
class Phone{
    public synchronized void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }
    public synchronized void sendSMS(){
        System.out.println("------sendSMS");
    }
    public void Hello(){
        System.out.println("------Hello");
    }
}



public class test01 {
    public static void main(String[] args) {
        Phone phone=new Phone();
        Phone phone2=new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone2.sendSMS();
        }, "b").start();
    }
}

结论:

对于普通同步方法,锁的是当前实例对象,通常指this,所有的同步方法用的都是同一把锁-->实例对象本身

对于静态同步方法,锁的时当前类的Class对象

对于同步方法块,锁的时synchronized括号内的对象

synchronized的三种用法

  • 作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁;
  • 作用于代码块,对括号里配置的对象加锁
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

从字节码角度分析synchronized实现

  • synchronized同步代码块
    • 实现使用的是monitorenter和monitorexit指令

  • synchronized普通同步方法
    • 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将现持有monitor锁,然后再执行该方法,最后在方法完成(无论是否正常结束)时释放monitor

  • synchronized静态同步方法
    • ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法

反编译synchronized锁的是什么

为什么每一个对象都可以称为锁?

那是因为每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来

C++源码:ObjectMonitor.java--->ObjectMonitor.cpp--->ObjectMonitor.hpp

总结:指针指向Monitor对象(也称为管程或监视器)的真实地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要的数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现)

 1.乐观锁和悲观锁

乐观锁:认为在使用数据的过程中一定会有别的线程来修改数据,因此在获取数据的时候会加锁,确保数据不会被别的线程所修改,synchronized和Lock类的实现都是悲观锁,适合写操作多的场景。先加锁可以保证数据只会被一个线程所修改,保证写操作的数据的正确性

悲观锁:认为在使用数据的过程中不会有别的线程修改数据或者资源,因此不会添加锁,Java中使用无锁编程实现,只是在数据进行更新的时候进行判断,之前有没有线程修改了这个数据,如果没有线程修改这个数据,那么自己的修改数据的操作成功,如果已经其他线程对数据进行了修改,那么根据不同的实现方式执行不同的操作,比如:放弃修改,重试抢锁等,判断规则有:版本号机制Version,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。-----适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法,

2.公平锁和非公平锁

公平锁:是指多个线程按照申请锁的顺序获取锁,这里类似于排队买票,先来的人先买,后来的人在队尾排着,这样看着是公平的  Lock lock = new ReentrantLock(true)---表示公平锁,先来先得。

非公平锁:指多个线程获取锁的顺序不是按照申请锁的顺序获取的,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)---- Lock lock = new ReentrantLock(false)---表示非公平锁,后来的也可能先获得锁,默认为非公平锁。

公平锁和非公平锁的利弊:

  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分地利用CPU的时间片,尽量减少CPU空间状态时间。
  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得很大,所以就减少了线程的开销。
  • 什么时候用公平?什么时候用非公平?
  • 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换的时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用。

3.可重入锁(递归锁)

可重入锁:是指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞---------优点之一就是可一定程度避免死锁。

可重入锁的种类:

  • 隐式锁(即synchronized关键字使用的锁),默认是可重入锁
  • 在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码块时,是永远可以得到锁。
  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁

4.死锁及排查

死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。

产生原因:

  • 系统资源不足
  • 进程运行推进顺序不合适
  • 系统资源分配不当

获得一个死锁

/*
获得一个死锁
 */
public class DeadLock {
    public static void main(String[] args) {
        Object objA=new Object();
        Object objB=new Object();
        new Thread(() -> {
            synchronized(objA){
                System.out.println(Thread.currentThread().getName()+"持有A锁,希望获得B锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(objB){
                    System.out.println(Thread.currentThread().getName()+"成功获得B锁");
                }
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized(objB){
                System.out.println(Thread.currentThread().getName()+"持有B锁,希望获得A锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(objA){
                    System.out.println(Thread.currentThread().getName()+"成功获得A锁");
                }
            }
        }, "t2").start();

    }
}

如何排查死锁

  • 纯命令
    • jps -l
    • jstack 进程编号
  • 图形化
    • jconsole

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力学习的小飞侠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值