关于synchronized修饰静态方法和非静态方法

284 阅读11分钟

介绍

一直没有深究过synchronized修饰方法的区别,这个知识点了解的也总是模模糊糊。网上看到的资料大部分也是只有说明,有点雾里看花的意思。很多还都是复制粘贴。今天整体梳理一次。

知识点描述

synchronized可以用来修饰类和方法以及代码块,用于实现同步互斥锁功能。本次针对的是修饰类和修饰方法方面。所以可以分为下列几种情况:

情况列举

一、用synchronized修饰类

1)两个线程是否可以调用同一个对象SynchronizedTest的method1方法。

public class SynchronizedTest {

    public void method1() {
        synchronized(SynchronizedTest.class) {
            System.out.println(Thread.currentThread().getName() + "抢占1方法,时间:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "释放1方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        }
    }
}
public class Test2 {

    public static void main(String[] args) throws ParseException {
        SynchronizedTest s1 = new SynchronizedTest();
        new Thread(s1::method1).start();
        new Thread(s1::method1).start();
    }
}
  • 打印结果:
Thread-0抢占1方法,时间:2020-08-03 17:30:42
Thread-0释放1方法2020-08-03 17:30:47
Thread-1抢占1方法,时间:2020-08-03 17:30:47
Thread-1释放1方法2020-08-03 17:30:52

2)两个线程是否可以调用不同对象SynchronizedTest的method1方法。

public class SynchronizedTest {

    public void method1() {
        synchronized(SynchronizedTest.class) {
            System.out.println(Thread.currentThread().getName() + "抢占1方法,时间:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "释放1方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        }
    }
}
public class Test2 {

    public static void main(String[] args) throws ParseException {
        SynchronizedTest s1 = new SynchronizedTest();
        SynchronizedTest s2 = new SynchronizedTest();
        new Thread(s1::method1).start();
        new Thread(s2::method1).start();
    }
}
  • 打印结果:
Thread-0抢占1方法,时间:2020-08-03 17:44:19
Thread-0释放1方法2020-08-03 17:44:24
Thread-1抢占1方法,时间:2020-08-03 17:44:24
Thread-1释放1方法2020-08-03 17:44:29

3)一个线程对象SynchronizedTest的method1方法,另一个线程可以访问同个对象的其他同步方法和非同步方法吗。

public class SynchronizedTest {

    public void method1() {
        try {
            synchronized (SynchronizedTest.class) {
                System.out.println(Thread.currentThread().getName() + "抢占1方法,时间:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + "释放1方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void method2() {
        try {
            synchronized (SynchronizedTest.class) {
                System.out.println(Thread.currentThread().getName() + "抢占2方法,时间:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + "释放2方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void method3() {
        try {
            System.out.println(Thread.currentThread().getName() + "抢占3方法,时间:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + "释放3方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Test2 {
    public static void main(String[] args) throws ParseException, InterruptedException {
        SynchronizedTest s1 = new SynchronizedTest();
        Thread thread1 = new Thread(s1::method1);
        thread1.setName("线程1");
        thread1.start();
        Thread thread2 = new Thread(s1::method2);
        thread2.setName("线程2");
        thread2.start();
        Thread thread3 = new Thread(s1::method3);
        thread3.setName("线程3");
        thread3.start();
    }
}
  • 打印结果
线程3抢占3方法,时间:2020-08-07 16:26:16
线程1抢占1方法,时间:2020-08-07 16:26:16
线程3释放3方法2020-08-07 16:26:21
线程1释放1方法2020-08-07 16:26:21
线程2抢占2方法,时间:2020-08-07 16:26:21
线程2释放2方法2020-08-07 16:26:26
  • 说明
    当synchronized修饰类的时候,实际上锁住的是.class,类和对象之间的关系有点像是图纸和实物。所有的SynchronizedTest对象使用的是同一个.class.所以直接产生了互斥。和是否是不同对象无关。而且类和类内部方法的关系有点像是房子的关系,一个小区(.class)有很多长得一样的房子(对象),每个房子有一个大门,内部有很多个房间(方法)。当synchronized修饰类的时候,类似这个小区被锁住了,钥匙只有一把,所以当这个小区有人进去以后,小区会重新关闭,所有在外面等着的人(线程)都只能等着。

二、用synchronized修饰非静态方法

public class SynchronizedTest {

    public synchronized void method1() {
        System.out.println(Thread.currentThread().getName() + "抢占1方法,时间:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "释放1方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
    }

    public void method2() {
        System.out.println(Thread.currentThread().getName() + "抢占2方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "释放2方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
    }
}

1)一个线程占用了SynchronizedTest的method1(上锁)方法,另一个线程是否可以访问同个对象的SynchronizedTest的method1方法。

public class Test2 {

    public static void main(String[] args) throws ParseException {
        SynchronizedTest s1 = new SynchronizedTest();
        new Thread(s1::method1).start();
        new Thread(s1::method1).start();
    }
}
  • 打印结果:
Thread-0抢占1方法,时间:2020-08-03 19:08:00
Thread-0释放1方法2020-08-03 19:08:05
Thread-1抢占1方法,时间:2020-08-03 19:08:05
Thread-1释放1方法2020-08-03 19:08:10
  • 说明
    当synchronized修饰非静态方法的时候,实际上锁住的是对应的对象,准确的说是分配的内存。而上述情况是同一个对象,也就是内存是一个。打比方的话,类似这个小区(.class)里面有个房子(对象),房子中,两个人(线程)都想进入这个房子的房间A,但是要说明的是房子中凡是上锁的房间其实共用了一把钥匙就是房间大门对应的钥匙,钥匙只有一把,所以当张三进入了上锁的房间A后,李四还想进入房间A就只能在房间A外面等张三出来。

2)一个线程占用了SynchronizedTest的method1(上锁)方法,另一个线程是否可以访问同个对象的SynchronizedTest的method2方法。

public class Test2 {

    public static void main(String[] args) throws ParseException {
         SynchronizedTest s1 = new SynchronizedTest();
        new Thread(s1::method1).start();
        new Thread(s1::method2).start();
    }
}
  • 打印结果:
Thread-1抢占2方法2020-08-03 19:21:36
Thread-0抢占1方法,时间:2020-08-03 19:21:36
Thread-1释放2方法2020-08-03 19:21:41
Thread-0释放1方法2020-08-03 19:21:41
  • 说明
    和1)类似还是同一个对象。还是上面的比喻,区别只是这个房子中只有房间A上了锁,房间B没上锁,所以等于房间B是不设防的,可以随意进出。和钥匙没关系了。但是假如房间B也上了锁,就是method2也被synchronized修饰了之后,李四还想在张三进入房间A的时候去房间B看看的话就不行了,因为上面说过了,房子中的房间共用了同一把钥匙,而这把钥匙被张三带到房间A里面了。所以还是要等张三出来。

3)一个线程占用了SynchronizedTest的method1(上锁)方法,另一个线程是否可以访问不同对象的SynchronizedTest的method1方法。

public class Test2 {

    public static void main(String[] args) throws ParseException {
        SynchronizedTest s1 = new SynchronizedTest();
        SynchronizedTest s2 = new SynchronizedTest();
        new Thread(s1::method1).start();
        new Thread(s2::method1).start();
    }
}
  • 打印结果:
Thread-1抢占1方法,时间:2020-08-03 19:15:57
Thread-0抢占1方法,时间:2020-08-03 19:15:57
Thread-1释放1方法2020-08-03 19:16:02
Thread-0释放1方法2020-08-03 19:16:02
  • 说明
    和1)不同的是这里是new了两个对象,也就是内存是不同的。还是上面的比喻,类似这个小区(.class)里面有个房子A(对象A),房子中房间A被锁住了,张三(线程)拿钥匙进入了这个房子,但是旁边又盖了一个一模一样的房子,李四进入的是第二个房子B(对象B),这个新房子还没人进去,所以第二个人依然可以进入这个房子的上锁房间A。

4)一个线程占用了SynchronizedTest的method1(上锁)方法,另一个线程是否可以访问不同对象的SynchronizedTest的method2方法。

public class Test2 {
​
    public static void main(String[] args) throws ParseException {
        SynchronizedTest s1 = new SynchronizedTest();
        SynchronizedTest s2 = new SynchronizedTest();
        new Thread(s1::method1).start();
        new Thread(s2::method2).start();
    }
}
  • 打印结果:
Thread-1抢占2方法2020-08-03 19:21:36
Thread-0抢占1方法,时间:2020-08-03 19:21:36
Thread-1释放2方法2020-08-03 19:21:41
Thread-0释放1方法2020-08-03 19:21:41
  • 说明
    道理和上面3)一样,因为对象不一样,所以可以调用各自对象的内部方法,和3的区别只是第二个人去了房子B的第二个房间而已。而且这个房间还没上锁。

三、用synchronized修饰静态方法

public class SynchronizedTest {
​
    public synchronized static void method1() {
        System.out.println(Thread.currentThread().getName() + "抢占1方法,时间:" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "释放1方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
    }
​
    public void method2() {
        System.out.println(Thread.currentThread().getName() + "抢占2方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "释放2方法" + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
    }
}

1)一个线程占用了SynchronizedTest的method1(上锁)方法,另一个线程是否可以同时访问这个method1方法。

public class Test2 {

    public static void main(String[] args) throws ParseException {
        new Thread(SynchronizedTest::method1).start();
        new Thread(SynchronizedTest::method1).start();
    }
}
  • 打印结果:
Thread-0抢占1方法,时间:2020-08-03 19:08:00
Thread-0释放1方法2020-08-03 19:08:05
Thread-1抢占1方法,时间:2020-08-03 19:08:05
Thread-1释放1方法2020-08-03 19:08:10
  • 说明
    首先说一下静态方法和费静态方法的区别,只说几个点:
    静态方法:和静态变量一样,是属于这个类(.class)的,在初始化类的时候机就会加载到内存中,通过类调用,即使是一直不用,jvm也不会清除。
    非静态方法:是属于这个对象实例的,当对象实例化的时候才会分配内存,也是通过实例去调用方法的,在这个实例被jvm回收的时候也会被回收。
    根据上述介绍,当synchronized修饰静态方法的时候,因为这个静态方法和类绑定,也就是说所有这个类产生的实例对象都是公用这个方法的。打个比方,还是上面的小区房子,这个静态方法好像是所有小区的房子公用的停车场一样。所以当张三和李四都想去停车场停车的时候,发现停车场被锁住了,钥匙和小区的钥匙是同一把。虽然小区没上锁,但是张三占用了这个钥匙,那么李四没办法,还是只能等在外面。

2)一个线程占用了SynchronizedTest的method1(上锁)方法,另一个线程是否可以访问method2方法。

public class Test2 {

    public static void main(String[] args) throws ParseException {
       SynchronizedTest s1 = new SynchronizedTest();
        new Thread(SynchronizedTest::method1).start();
        new Thread(s1::method2).start();
    }
}
  • 打印结果:
Thread-0抢占1方法,时间:2020-08-03 20:16:56
Thread-1抢占2方法2020-08-03 20:16:56
Thread-0释放1方法2020-08-03 20:17:01
Thread-1释放2方法2020-08-03 20:17:01
  • 说明
    张三拿到钥匙了进入了停车场,而因为其他人进入的是房间B没上锁,所以任何人都可以将进入。假如房间B这个非静态的方法上锁了,就是method2也被synchronized修饰了之后呢。李四再进去可以吗?答案是可以的。因为首先要明确一点,就是静态方法上锁,可以类比成是类锁,非静态方法上锁实际上是对象锁,类锁和对象锁不是一个东西。也就是说小区大门、停车场和其他的房子大门以及房子内的房间的锁不一样,不能共用钥匙,简单来说,张三拿了小区大门的钥匙打开了停车场的锁进去停车了,而李四则拿着房子A的钥匙去打开了上锁的房间B。互不影响的。

总结

  1. synchronized修饰静态方法的时候,可以理解成类锁,所有对象共用。
  2. synchronized修饰非静态方法的时候,可以理解成对象锁,当前对象使用,而且是这个对象内部所有上锁方法共用。
  3. 类锁和对象锁不是一个意思,是可以共存的。