四、说说Java“锁”事

182 阅读11分钟

面试题

  1. java加锁有哪几种锁?
  2. 怎么处理并发?线程池有哪些核心参数?
  3. 简单说下lock

1.乐观锁和悲观锁

悲观锁:

认为自己在使用数据的时候一定有别的线程来修改线程,因此在获取数据的时候先加锁,确保数据不会被别的线程修改。

Synchronized关键字Lock的实现类 都是悲观锁

适合写操作多的场景,先加锁可以保证写操作时数据正确。

显示的锁定之后在操作同步资源

一句话: 狼性锁

乐观锁:

认为自己在使用数据时 不会有别的线程修改数据 或 资源,所有不会添加锁。

在Java中通过使用 无锁编程 来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。

如果这个数据已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等

判断规则

  1. 版本号机制Version
  2. 最常采用的是CAS算法,Java原子类中递增操作就通过CAS自旋实现的

2.八锁案例

8种锁的案例体现在3个地方

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

1.标准访问有ab两个线程,请问先打印邮件还是短信

class Phone{
    public synchronized  void sendEmail(){
        System.out.println("------sendEmail");
    }

    public synchronized void sendSMS(){
        System.out.println("---------sendMessage");
    }
}

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

------sendEmail

---------sendMessage

2. SendEmail方法种加入了暂停3秒钟,请问先打印邮件还是短信

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("---------sendMessage");
    }
}

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

------sendEmail

--------sendMessage

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

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("---------sendMessage");
    }

    public void  hello(){
        System.out.println("hello");
    }
}

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

------Hello

------sendEmail

4.有两部手机后,请问先打印哪一个

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

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("---------sendMessage");
    }

    public void  hello(){
        System.out.println("hello");
    }
}

---------sendMessage

------sendEmail

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

class Phone{
    public static synchronized  void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }

    public static synchronized void sendSMS(){
        System.out.println("---------sendMessage");
    }

    public void  hello(){
        System.out.println("hello");
    }
}
public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.sendSMS();
            //phone.hello();
            //phone2.sendSMS();
        },"b").start();
    }
}

------sendEmail

---------sendMessage

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

class Phone{
    public static synchronized  void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }

    public static synchronized void sendSMS(){
        System.out.println("---------sendMessage");
    }

    public void  hello(){
        System.out.println("hello");
    }
}
public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            //phone.sendSMS();
            //phone.hello();
            phone2.sendSMS();
        },"b").start();
    }
}

------sendEmail

---------sendMessage

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

class Phone{
    public synchronized  void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }

    public static synchronized void sendSMS(){
        System.out.println("---------sendMessage");
    }

    public void  hello(){
        System.out.println("hello");
    }
}

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

---------sendMessage

------sendEmail

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

class Phone{
    public synchronized  void sendEmail(){
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }

    public static synchronized void sendSMS(){
        System.out.println("---------sendMessage");
    }

    public void  hello(){
        System.out.println("hello");
    }
}
public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            phone.sendEmail();
        },"a").start();
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            //phone.sendSMS();
            //phone.hello();
            phone2.sendSMS();
        },"b").start();
    }
}

---------sendMessage

------sendEmail

八锁总结

  • 1-2 :一个对象里面的如果有多个Synchronized 方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程就只能等待,换句话说,某一时刻内,只能有唯一的一个线程去访问这些synchronized方法 ,锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其它的synchronized方法
  • 3-4: 加个普通方法后发现和同步锁无关,换成两个对象后,不是同一把锁了 情况立刻变化
  • 5-6:都换成静态同步方法后,情况又变化,三种 synchronized 锁的内容有一些差别:1. 对于普通的同步方法,锁的是当前实例对象 ,通常指this,具体的一部手机,所有的普通同步方法用的都是同一把锁--》实例对象本身,2. 对于静态同步方法,锁的是当前类的class对象 ,如phone,class唯一的一个模板,3. 对于同步方法块,锁的是synchronized括号内的对象
  • 7-8:对象锁和类锁是不同的锁,锁的东西不一样, 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。.

3.从字节码角度分析synchronized实现

1.JDK源码notify说明

锁的三个地方:

  1. 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
  2. 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁
  3. 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁

2.syschronized实现

javap -c ***.class 文件 反编译

1.同步代码块

public class LockSynDemo {
    Object object = new Object();

    public LockSynDemo() {
    }

    public void m1() {
        synchronized(this.object) {
            System.out.println("----hello synchronized");
        }
    }

    public static void main(String[] args) {
    }
}

monitorenter 进入锁 monitorexit 退出锁

为什么两个monitorexit: 保证异常情况也能退出

一定时一个enter 两个exit吗?

一般情况就是一个enter 对应两个exit

极端,当同步代码块里面出现了 抛出异常

2.普通同步方法

javap -v ***.class

调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。

如果设置了,执行线程会将现持有monitor锁,然后在执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor

3.静态同步方法

javap -v ***.class

3.反编译synchronized锁的是什么

面试题:为什么每一个对象都可以成为一个锁

所有对象都继承Object

在HotSpot虚拟机中,monitor采用ObjectMonitor实现

  1. c++ 源码解读 : ObjectMonitor.java ->ObjectMonitor.cp-->ObjectMonitor.hpp
  2. ObjectMonitor.hpp
  3. 每个对象都天生带着一个对象监视器
  4. 每一个被锁住的对象都会和Monitor关联起来

4.公平锁和非公平锁

1.恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在还是很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。

2.使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销

5.可重入锁

可重入锁又名递归锁

是指在同一个线程在外层方法获取锁的时候,在进入该线程的内层方法会自动获取锁(前提,锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有synchronized修饰的递归调用方法,程序第二次进入被自己阻塞了岂不是天大的笑话。

所以Java中的ReentrantLock 和 synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

可:可以

重:再次

入:进入

锁:同步锁

进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码)

一句话: 1.一个线程中的多个流程可以获取同一把锁,持有这把锁的可以再次进入

2.自己可以获取自己的内部锁


1.可重入锁种类

隐式锁

Synchronized关键字使用的锁默认是可重入锁

  1. 同步块
  2. 同步方法

Synchronized的重入的实现机理

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

objectMonitor.hpp(监视器)

_owner指向持有ObjectMonitor对象的线程
_WaitSet存放处理wait状态的线程队列
_EntryList存放处于等待锁BLOCK状态的线程队列
_recursions锁的重入次数
_count用来记录该线程获取锁的次数

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针

当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,java虚拟器会将该锁对象的持有线程设置为当前线程,并且将计数器加1。

在目前锁对象的计数器不为零的情况下,如果锁对象的持有对象是当前线程,那么java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit,Java虚拟器则需将锁对象的计数器减1计数器为零代表锁已经被释放

显式锁(即Lock)

6.死锁及排查

1.死锁是什么

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。

  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。

  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

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

2.死锁代码

package com.lyj.sc.duoThread.lockStudy;

import java.util.concurrent.TimeUnit;

/**
 * @Author: liyangjing
 * @Date: 2022/08/11/22:49
 * @Description:
 */
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) {
                    e.printStackTrace();
                }
                synchronized (objectB){
                    System.out.println(Thread.currentThread().getName()+"\t 成功获取B锁");
                }
            }
        },"A").start();

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

A 自己持有A锁,希望获取B锁

B 自己持有b锁,希望获取a锁

系统资源不足,资源分配不合理

3.如何排查死锁

jps 类似于 ps ef|

jstack + 进程编号 打印堆栈信息可以查看到死锁

图形控制台 jconsole

win+R 打开

检测死锁

小总结

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