八种锁现象

43 阅读7分钟

什么是锁?

总所周知,锁是控制共享资源的访问机制,防止多线程访问共享资源的时候导致数据的不一致性。

那么在synchronized中锁是如何定义的?

synchronized

现象1: 实例锁竞争

2个同步方法,一个对象,先打印 发短信?打电话?

2个同步方法,两个对象,先打印发短信?打电话?

代码:

public class Test1 {
    public static void main(String[] args) throws InterruptedException {

        sample s = new sample();
        new Thread(() -> {
            s.sendSms();
        }).start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(() -> {
            s.codePhone();
        }).start();
    }
}
class sample {
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void codePhone() {
        System.out.println("打电话");
    }
}
public class Test1 {
    public static void main(String[] args) throws InterruptedException {

        sample s = new sample();
        sample s2 = new sample();
        new Thread(() -> {
            s.sendSms();
        }).start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(() -> {
            s2.codePhone();
        }).start();
    }
}

class sample {
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void codePhone() {
        System.out.println("打电话");
    }
}

结果

1:

2:

原因:

由于两个方法都使用了 synchronized,它们会互斥执行。第二个线程在 sendSms() 执行过程中被阻塞,直到 sendSms() 执行完并释放锁,才会开始执行 codePhone(),因此最终的输出顺序是 “发短信”“打电话”

代码分析:
  1. sendSms() 方法是同步方法
    1. 方法上有synchronized 关键字,当一个线程进入该方法时,他会对sample2实例对象锁定, 其他线程无法在此期间访问sendSms(),直到当前线程执行完成该方法。
  1. codePhone() 方法是同步方法
    1. codePhone() 方法也加了 synchronized关键字,因此它也是同步方法,需要等待同一个实例对象锁,如果锁先给别的线程拿去,需要等待。
线程执行顺序:
  • 第一个线程 启动并执行 sendSms() 方法,它将获得 s 对象的锁并开始执行该方法。
  • sendSms() 方法中有 sleep(3),这意味着线程会睡眠 3 秒。此时,该线程占用锁,其他线程无法进入任何同步方法(包括 codePhone())。
  • 第二个线程 启动并执行 codePhone() 方法,由于 sendSms() 已经持有了 s 对象的锁,codePhone() 将被阻塞,直到 sendSms() 执行完毕并释放锁。
  • 当第一个线程执行完 sendSms() ,它会释放锁,这时 第二个线程才能获取到锁并执行 codePhone() ,输出 “打电话”

现象2: 实例锁与非锁 并行

1个静态的同步方法,1个普通的同步方法,一个对象,先打印 发短信?打电话?

1个静态的同步方法,1个普通的同步方法,两个对象,先打印发短信?打电话?

代码

public class Test2 {
    public static void main(String[] args) throws InterruptedException {

        sample2 s = new sample2();
        new Thread(() -> {
            s.sendSms();
        }).start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(() -> {
            s.codePhone();
        }).start();
    }
}
class sample2 {
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public  void codePhone() {
        System.out.println("打电话");
    }
}

结果:

原因:

代码分析:

“打电话” 先输出的原因与 synchronized 的锁机制和线程的执行顺序有关。

  1. sendSms() 方法是同步方法
    1. 方法上有synchronized 关键字,当一个线程进入该方法时,他会对sample2实例对象锁定, 其他线程无法在此期间访问sendSms(),直到当前线程执行完成该方法。
  1. codePhone() 方法是普通方法
    1. codePhone() 方法没有加 synchronized,因此它是 非同步方法,不需要等待任何锁,线程可以直接执行。
线程执行顺序
  • main() 方法创建了两个线程,第一个线程执行 sendSms() 方法,第二个线程执行 codePhone() 方法。虽然 sendSms()synchronized 修饰,但由于 codePhone() 没有任何同步控制,它是可以立即执行的。
  • 由于线程的调度是异步的,第一个线程 (sendSms()) 被启动后,马上会尝试获取当前 sample2 实例的锁。此时第二个线程 (codePhone()) 虽然在等待 sendSms() 完成,但没有任何同步机制限制它的执行,它可以直接执行并输出 “打电话”
  • 由于 sendSms() 线程在执行时会睡眠 3 秒(TimeUnit.SECONDS.sleep(3)),而 codePhone() 是一个无锁的方法,它会 抢先执行,所以会 先输出 "打电话"
锁的细节:
  • sendSms() 方法被 synchronized 修饰,意味着它在执行时会锁住当前对象 sample2 的实例锁。当第一个线程执行 sendSms() 时,它会持有该锁,直到 sendSms() 执行完毕。
  • codePhone() 没有任何同步控制,因此它在没有任何限制的情况下, 可以在 sendSms() 执行过程中先行执行
线程调度:

线程的执行顺序是由操作系统的调度器决定的。这里,尽管 sendSms() 在第一个线程中被调用,但由于没有对 codePhone() 方法加锁,它会先执行,输出 “打电话”

现象3: 类锁的竞争

增加两个静态的同步方法,只有一个对象,先打印发短信?打电话

增加两个静态的同步方法,两个对象! 先打印发短信?打电话?

代码

public class Test3 {
    public static void main(String[] args) throws InterruptedException {

        sample3 s = new sample3();
        sample3 s2 = new sample3();
        new Thread(() -> {
            s.sendSms();
        }).start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(() -> {
            s2.codePhone();
        }).start();
    }
}
class sample3 {
    public static  synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public static synchronized void codePhone() {
        System.out.println("打电话");
    }
}

结果:

原因:

sendSms() codePhone() 方法都使用了 static synchronized 修饰符。由于这两个方法是静态的,它们会使用 类锁,而不是实例对象锁。

代码分析:

static synchronized 锁的工作原理

  • 当方法是静态的时,锁是基于 类级别的锁(即 sample3.class),而不是基于实例对象的锁。这意味着,所有调用 sendSms()codePhone() 方法的线程都必须争夺同一个 类锁
  • 由于 sendSms()codePhone() 都是静态同步方法,它们都需要 锁住 sample3.class 类锁。因此,一个线程正在执行 sendSms() 时,另一个线程无法执行 codePhone() ,直到第一个线程释放锁
线程执行顺序:
  • 第一个线程:启动后执行 sendSms(),该方法是静态同步的,因此会尝试获取 sample3.class 类锁,执行该方法。
  • 第二个线程:启动后执行 codePhone(),由于 sendSms() 方法已经持有了 sample3.class 类锁,所以第二个线程在获取类锁时会被阻塞,直到第一个线程释放锁。

现象4: 实例锁与类锁 并行

一个静态同步方法,一个同步方法,只有一个对象,先打印发短信?打电话

一个静态同步方法,一个同步方法,两个对象! 先打印发短信?打电话?

代码

public class Test3 {
    public static void main(String[] args) throws InterruptedException {

        sample4 s = new sample4();
        sample4 s2 = new sample4();
        new Thread(() -> {
            s.sendSms();
        }).start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(() -> {
            s2.codePhone();
        }).start();
    }
}
class sample4 {
    public static  synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public  synchronized void codePhone() {
        System.out.println("打电话");
    }
}

结果:

原因:

sendSms() 静态同步方法,而 codePhone() 实例同步方法。这意味着它们会使用 不同的锁,一个是基于 类锁,另一个是基于 实例锁

代码分析:
  • sendSms() 方法
    • sendSms()static synchronized,意味着它锁住的是 sample4.class 这个类锁,即所有对该类的静态同步方法的访问都需要先获得类的锁。
  • codePhone() 方法
    • codePhone()实例同步方法,它锁住的是 当前对象实例 的锁(即 this 锁)。因此,不同的实例可以并发执行这个方法,因为它们锁住的是不同的对象实例。
线程执行顺序:
  • 第一个线程 启动并执行 sendSms(),它会尝试获取 sample4.class 类锁,并在执行时会进行 3 秒的 sleep,即第一个线程持有 类锁sample4.class) 3 秒钟。
  • 第二个线程 启动并执行 codePhone(),它会尝试获取 s2 对象实例的锁,即 s2 实例的锁。由于 sendSms() 锁住的是 类锁,并不会阻塞 codePhone() 线程的执行。因此,第二个线程能够 立即执行 codePhone() 方法

小结

synchronized锁的粒度不同:

new this 具体的一个手机

static Class 唯一的一个模板