JUC多并发编程-->多线程锁

1. 乐观锁、悲观锁

乐观锁

认为自己在使用数据时,不会有别的资源修改数据或资源,所以不会添加锁。
在java中是通过无锁编程来实现,只是在更新数据的时候去判断,之前有没有线程更新了这个数据。
如果没有被更新,当前线程将自己修改的数据写入
如果已经被其他线程更新了,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等。

判断是否更新的规则:
1. 版本号机制
2. CAS(Compare-and-Swap,比较交换)算法,java原子类中的递增操作就通过CAS自旋实现的(常用)

  • 适合读操作多的场景,不加锁的特点就是读操作的性能大幅度提升
  • 乐观锁直接去操作同步资源,是一种无锁算法

悲观锁

认为一定有其他线程来修改自己使用的资源,因此在获取数据时会先加锁,确保数据不会被其他线程修改。
synchronized关键字和Lock的实现类都是悲观锁。

  • 适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 显示的锁定之后再操作同步资源

2. synchronized锁相关

  1. 作用于实例方法:当前实例加锁,进入同步代码前要获得当前实例的锁
    对象锁
    调用指令将会检查方法的ACC_AYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor锁,再执行方法,最后再方 法完成后释放monitor。
  2. 作用于代码块,对括号内的配置对象加锁
    底层使用monitorenter、monitorexit实现锁的获取和释放。一般情况是一个enter俩个eixt(防止出现异常,保证异常时可以退出代码块)
  3. 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
    类锁
    ACC_AYNCHRONIZEDACC_STATIC防蚊标志区分该方法是否是静态同步方法

高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法(alibaba手册)

  1. 俩个普通同步方法,标准访问有ab 两个线程,请问先打印邮件还是短信

  2. sendEmal方法中加入暂停3 秒钟,网先打印邮件还是短信

    一个对象中如果有多个synchronized方法,某一时刻,只要一个线程去调用了其中的一个synchronized方法,其他线程都只能等待。锁的对象是this,被锁定后,其他线程都不能进入到当前对象的其他synchronized方法。

  3. 添加一个普通的hello方法,请向先打印邮件还是hello

    普通方法和同步锁无关。

  4. 有两部手机,请问先打印邮件还是短信

    俩部手机是俩个类,不会有争抢。

  5. 有两个静态同步方法,有1部手机,请问先打印邮件还是短信static synchronized

  6. 有两个静态同步方法,有2 部手机,请问先打印邮件还是短信

    加了static之后,就是静态方法。Phone phone = new Phone() new出来的是Phone类。此时是类锁

  7. 有1个静态同步方法,有1个普通同步方。有部手机,请先打印邮件还是短信

  8. 有1个静态同步方法,有1个普通同步方法有2部手机,请问先打印邮件还是短信


  • 对于普通同步方法,锁得是当前实例对象,通常是this,具体是一部部的手机,所有普通同步方法用的都是同一把锁–> 实例对象本身
  • 对于静态同步方法,锁得是当前类的Class对象,如Phone.class唯一的一个模板
  • 对于同步方法块,锁得是synchronized括号内的对象

3. 为什么任何一个对象都可以成为一个锁

管程(monitor)、锁、监视器是一个概念。是一种程序结构,结构内的多个子程序(对象或者模块)形成的多个工作线程互斥访问共享资源。

在这里插入图片描述
因为c++底层源码中,monitor采用ObjectMonitor实现。 每个Object都有一个ObjectMonitor

  • 其中_owner关键属性是指向持有ObjectMonitor对象的线程。
  • _WaitSet 存放处于wait状态的线程队列
  • EntryList存放处于等待锁block状态的线程队列
  • _recursions锁的重入次数
  • _count 记录该线程获取锁的次数

公平锁和非公平锁

// 什么都没有默认为非公平锁
ReentrantLock lock = new ReentrantLock ();
// fair==true;公平锁
ReentrantLock lock = new ReentrantLock (true);

公平锁:多个线程按照申请锁的顺序来获取锁(排队买票,先来先得)
非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的环境下,有可能造成优先级翻转或者饥饿的状态

为什么默认为非公平锁?

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

什么时候用公平、不公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;
否则那就用公平锁,大家公平使用。

AQS

可重入锁(递归锁)

一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入自己可以获取自己的内部锁。
自己可以获取自己内部的锁。

  • 指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
  • 如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
  • 所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

分类:

  1. 隐式锁(synchronized 关键字 修饰的锁) 默认是可重入锁 --> 同步块和同步方法都有
  2. 显示锁(Lock),ReentrantLock
// 显示锁加锁几次就要解锁几次
ReentrantLock lock = new ReentrantLock ();
lock.lock();
lock.unlock();

可重入锁的实现原理

  1. 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
    • _owner关键属性是指向持有ObjectMonitor对象的线程。
    • _recursions锁的重入次数
    • _count 记录该线程获取锁的次数
  2. 当执行monitorenter时,如果目标锁对象的让数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设留为当前线程,并且将其计数器加1。
  3. 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
  4. 计数器为零代表锁已被释放当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。当计数器为0时,代表锁已经释放。

死锁及排查

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种瓦相等待的现象,若无外力干涉那它们都将无法推进下去,如果
系然资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

// 死锁
public class DeadLockDemo {
    public static void main(String[] args) {
        final Object objectA = new Object();
        final Object objectB = new Object();

        new Thread(()->{
            synchronized (objectA){
                System.out.println(Thread.currentThread().getName()+"\t 自己持有A锁,希望获得B锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (objectB){
                    System.out.println(Thread.currentThread().getName()+"\t 成功获得B锁");
                }
            }
            
        },"t1").start();

        new Thread(()->{
            synchronized (objectB){
                System.out.println(Thread.currentThread().getName()+"\t 自己持有B锁,希望获得A锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (objectA){
                    System.out.println(Thread.currentThread().getName()+"\t 成功获得A锁");
                }
            }
        },"t2").start();

    }

}

产生死锁的原因:

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

检查死锁

  • 命令行:
    jps-l 当前程序的进程编号。–> jstack 进程编号(打印出栈信息)
  • jconsole
  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值