JUC——8锁问题

发布时间:2024年01月16日


八锁现象是在多线程编程中经常遇到的一种情况,指的是在使用synchronized关键字进行同步时可能出现的不同的执行顺序。下面对问题一到问题八进行总结:

问题一和问题二:

  • 两个线程分别调用Phone类的senMesg()和call()方法。
  • 根据synchronized关键字的特性,锁的是方法的调用者,这里锁的是phone对象。
  • 在标准情况下,两个线程交替执行,先输出发短信,然后输出打电话。
  • 如果senMesg()方法延迟了4秒,那么先输出发短信,再输出打电话。
public class Test1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            phone.senMesg();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone{
    public synchronized void senMesg(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

这两个问题涉及同一个对象的同步方法之间的互相调用。当线程执行一个对象的同步方法时,它就持有了这个对象的锁。当它需要再次调用这个对象的另一个同步方法时,如果这个方法也是同步的,那么这个线程就可以直接进入该方法,而无需重新获取锁。这就是“可重入锁”的体现。
在这里插入图片描述

问题三:

  • 添加了一个普通方法hello(),与同步方法senMesg()同时执行。
  • hello()方法不受锁的影响,所以先输出hello,然后输出发短信。
public class Test2 {
    public static void main(String[] args) {
        Phone1 phone = new Phone1();
        new Thread(()->{
            phone.senMesg();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone.hello();
        },"B").start();
    }
}
class Phone1{
    public synchronized void senMesg(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
    public void hello(){
        System.out.println("hello");
    }
}

这个问题涉及父子类继承关系中使用同步方法。父类中的同步方法在被子类继承后,子类同样可以使用这个同步方法,并且可以在这个同步方法中调用父类的同步方法,这也是“可重入锁”的体现。

问题四:

  • 使用两个不同的Phone对象phone1和phone2,分别执行senMesg()和call()方法。
  • 两个对象的锁是不同的,所以先输出发短信,然后输出打电话。
public class Test3 {
    public static void main(String[] args) {
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();
        new Thread(()->{
            phone1.senMesg();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone2{
    public synchronized void senMesg(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

这个问题涉及不同对象的同步方法之间的互相调用。当线程执行一个对象的同步方法时,它就持有了这个对象的锁。如果它需要调用另一个对象的同步方法,那么这个线程就需要获取另一个对象的锁才能进入该方法,因此这两个同步方法之间没有“可重入锁”的关系。
在这里插入图片描述

问题五和问题六:

  • 两个静态同步方法,只有一个Phone对象phone1。
  • 根据synchronized关键字的特性,锁的是类模板,即Phone类。
  • 所以无论是调用senMesg()还是call()方法,都会先输出发短信,然后输出打电话。
public class Test4 {
    public static void main(String[] args) {
        Phone3 phone1 = new Phone3();
        new Thread(()->{
            phone1.senMesg();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone1.call();
        },"B").start();
    }
}
class Phone3{
    public static synchronized void senMesg(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public static synchronized void call(){
        System.out.println("打电话");
    }
}

这两个问题涉及静态同步方法和非静态同步方法之间的互相调用。静态同步方法持有的是类锁,而非静态同步方法持有的是对象锁,因此它们持有的锁是不同的,没有“可重入锁”的关系。
在这里插入图片描述

问题七和问题八:

  • 一个静态同步方法senMesg()和一个普通同步方法call(),执行顺序为先打电话再发短信。
  • 两个Phone对象phone1和phone2,分别执行静态同步方法和普通同步方法。
  • 静态同步方法锁的是类模板,普通同步方法锁的是调用者对象。
  • 所以无论是phone1还是phone2,都会先输出打电话,然后输出发短信。
public class Test5 {
    public static void main(String[] args) {
        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();
        new Thread(()->{
            phone1.senMesg();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone4{
    public static synchronized void senMesg(){
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

总结

synchronized关键字用于方法或代码块时,它可以用来实现对象级别的锁和类级别的锁。

  1. 对象级别锁:

    • synchronized用于非静态方法时,锁对象是调用该方法的实例对象。即每个对象实例都有自己的锁。
    • synchronized用于代码块时,锁对象可以是任意的Java对象。通常情况下,我们使用某个共享资源作为锁对象。
    • 无论是方法级别的锁还是代码块级别的锁,同一时间只能有一个线程获得锁并执行同步代码,其他线程需要等待锁释放。
  2. 类级别锁:

    • synchronized用于静态方法时,锁对象是当前方法所属的类对象(Class对象)。
    • 类级别锁是针对整个类的所有对象实例生效的,不同实例之间共享这个锁。
    • 同一时间只能有一个线程获得类级别锁并执行同步代码,其他线程需要等待锁释放。

总结:

  • 对象级别的锁是基于对象实例的,每个对象实例有自己的锁,不同实例之间互不影响。
  • 类级别的锁是基于类对象的,所有类的实例共享同一个锁。
  • synchronized关键字通过获取锁来保证同一时间只有一个线程可以执行同步代码块或方法。
  • 锁的作用是保护共享资源,避免多个线程同时访问导致的数据竞争和并发问题。
文章来源:https://blog.csdn.net/pengjun_ge/article/details/133149540
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。