Java-synchronized关键字

本文详细解析了Java中`synchronized`关键字的使用,包括其悲观锁性质、对象/类锁的区分、代码块锁的灵活性以及死锁案例。通过实例演示了非静态方法与静态方法的不同竞态情况,以及如何避免因不当加锁导致的死锁问题。
摘要由CSDN通过智能技术生成

Java-synchronized关键字

  • synchronized关键字用于为Java对象、方法、代码块提供线程安全的操作
  • synchronized关键字是一种独占式的悲观锁,同时属于可重入锁。

上面的关键词:

  • 悲观锁:每次读取数据时都认为别人会修改数据,所以每次读写数据时都会加锁,这样导致其他线程想要读写这个数据时就会阻塞、等待直到获取锁。

Java中的每个对象都有个monitor对象,加锁就是在竞争monitor对象。对代码块加锁是通过在前后分别加上monitorenter和monitorexit指令实现的,对方法是否加锁是通过一个标记位来判断的。

synchronized的作用范围

了解作用范围能帮助我们知道多线程情况下何时才能获取到被加synchronized关键字的资源。

作用范围如下:

  • 作用于成员变量和非静态方法,锁住的是对象的实例,即this对象
  • 作用于静态方法,锁住的是Class实例,因为静态方法属于Class而不属于对象
  • 作用于代码块,锁住的是所有代码块中配置的对象

为了详细了解synchronized关键字作用于非静态方法和作用于静态方法的区别,我们定义如下的对象,其中有两个非静态方法和两个静态

package com.tianhao.luo.thread.concurrent;

/**
 * Synchronized关键字
 *
 * @author: tianhao.luo@hand-china.com 2021/5/4  16:44
 */
public class SynchronizedDemo {

    /**
     * synchronized作用于非静态方法
     */
    public synchronized void generalMethod1() {

        try {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ",exec generalMethod1: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public synchronized static void generalMethod3(){

        try {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + ",exec generalMethod3: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

作用于成员变量和非静态方法

实例一 不同线程竞争同一个对象

定义一个Junit测试类如下:

public class SynchronizedDemoTest extends TestCase {

    private final CountDownLatch countDownLatch = new CountDownLatch(2);

     public void testFieldAndNotStaticMethod1() throws Exception{
        // 测试synchronized作用于非静态方法generalMethod1
        // 两个线程中都是用了同一个synchronizedDemo对象调用方法generalMethod1
        // 此时被加锁的是synchronizedDemo对象,导致只能是先获取到synchronizedDemo对象的线程先跑完整个循环
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 1=========");
                synchronizedDemo.generalMethod1();
                countDownLatch.countDown();
            }
        }).start();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 2=========");
                synchronizedDemo.generalMethod1();
                countDownLatch.countDown();
            }
        }).start();

        countDownLatch.await();
        System.out.println("=========this is step 3=========");
    }
}

执行输出的结果如下:测试方法中启动了两个子线程,Thread-1的输出会等待Thread-0中输出完毕后才出现。

=========this is step 1=========
Thread-0,exec generalMethod1: 0
=========this is step 2=========
Thread-0,exec generalMethod1: 1
Thread-0,exec generalMethod1: 2
Thread-1,exec generalMethod1: 0
Thread-1,exec generalMethod1: 1
Thread-1,exec generalMethod1: 2
=========this is step 3=========

总结:step 2在Thread-0还没有输出完毕就输出了,表示已经进入了Thread-1线程;Thread-0和Thread-1中使用的都是同一个synchronizedDemo对象,因为synchronized是悲观锁,作用于非静态方法,锁住的是当前对象;导致当第二个线程中synchronizedDemo对象执行非静态方法还需要等待第一个线程中synchronizedDemo对象执行非静态方法完毕释放掉monitor。

实例二 不同线程使用不同的SynchronizedDemo对象

定义的测试方法与上面不同的是创建了两个SynchronizedDemo类对象,并且两个线程分别调用不同对象的加了synchronized方法。

public class SynchronizedDemoTest extends TestCase {

    private final CountDownLatch countDownLatch = new CountDownLatch(2);

    public void testFieldAndNotStaticMethod2() throws Exception{
        // 测试synchronized作用于非静态方法generalMethod1
        // 两个线程分别执行不同的synchronizedDemo对象的generalMethod方法
        // 此时第一个线程被加锁的是synchronizedDemo1对象,第二个线程被加锁的是synchronizedDemo2对象,这两个线程之间不存在对synchronizedDemo对象的竞争
        SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
        SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 1=========");
                synchronizedDemo1.generalMethod1();
                countDownLatch.countDown();
            }
        }).start();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 2=========");
                synchronizedDemo2.generalMethod1();
                countDownLatch.countDown();
            }
        }).start();

        countDownLatch.await();
        System.out.println("=========this is step 3=========");
    }
}

执行输出的结果如下:启动了两个线程,他们的输出是交替进行的。

=========this is step 1=========
Thread-0,exec generalMethod1: 0
=========this is step 2=========
Thread-1,exec generalMethod1: 0
Thread-0,exec generalMethod1: 1
Thread-1,exec generalMethod1: 1
Thread-1,exec generalMethod1: 2
Thread-0,exec generalMethod1: 2
=========this is step 3=========

总结:此处的输出恰好验证了synchronized关键字对非静态方法的加锁是针对对象的,不同线程对不同引用对象加的锁互相不会干扰。

作用于静态方法

实例一 不同线程竞争同一个Class对象

测试方法中两个线程都直接使用SynchronizedDemo类去调用静态方法

public class SynchronizedDemoTest extends TestCase {

    private final CountDownLatch countDownLatch = new CountDownLatch(2);

    public void testFieldAndStaticMethod1() throws Exception{
        // 测试synchronized作用于静态方法generalMethod3
        // 两个线程都是使用的SynchronizedDemo类去直接调用static方法
        // 此时被加锁的是SynchronizedDemo这个Class对象,导致只能是先获取到SynchronizedDemo的Class对象的线程先跑完整个循环
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 1=========");
                SynchronizedDemo.generalMethod3();
                countDownLatch.countDown();
            }
        }).start();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 2=========");
                SynchronizedDemo.generalMethod3();
                countDownLatch.countDown();
            }
        }).start();

        countDownLatch.await();
        System.out.println("=========this is step 3=========");
    }
}

执行的输出结果如下:Thread-0线程输出完毕后,Thread-1线程才开始输出;和两个线程中都使用同一个synchronizedDemo对象调用synchronized非静态方法的输出相似。

=========this is step 1=========
=========this is step 2=========
Thread-0,exec generalMethod3: 0
Thread-0,exec generalMethod3: 1
Thread-0,exec generalMethod3: 2
Thread-1,exec generalMethod3: 0
Thread-1,exec generalMethod3: 1
Thread-1,exec generalMethod3: 2
=========this is step 3=========

总结:synchronizaed作用于静态同步方法,锁住的是当前类的Class对象,也就是说当多个线程同时调用类的synchronized静态方法时,会形成阻塞。

实例二 不同线程使用SynchronizedDemo对象和Class对象

public class SynchronizedDemoTest extends TestCase {

    private final CountDownLatch countDownLatch = new CountDownLatch(2);

    public void testFieldAndStaticMethod2() throws Exception{
        // 测试synchronized作用于非静态方法generalMethod1和静态方法generalMethod3
        // 一个线程使用的SynchronizedDemo类去直接调用static方法,一个线程使用synchronizedDemo对象调用非静态方法
        // 第一个线程被加锁的是synchronizedDemo这个对象,第二个线程被加锁的是SynchronizedDemo这个Class对象;两个线程之间不存在竞争对象的关系
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 1=========");
                synchronizedDemo.generalMethod1();
                countDownLatch.countDown();
            }
        }).start();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 2=========");
                SynchronizedDemo.generalMethod3();
                countDownLatch.countDown();
            }
        }).start();

        countDownLatch.await();
        System.out.println("=========this is step 3=========");
    }
}

执行输出的结果如下:启动了两个线程,他们的输出是交替进行的。和不同线程使用不同的SynchronizaedDemo对象调用synchronized非静态方法的输出一致。

=========this is step 1=========
Thread-0,exec generalMethod1: 0
=========this is step 2=========
Thread-1,exec generalMethod3: 0
Thread-0,exec generalMethod1: 1
Thread-1,exec generalMethod3: 1
Thread-0,exec generalMethod1: 2
Thread-1,exec generalMethod3: 2
=========this is step 3=========

总结:SynchronizedDemo类对象的锁和SynchronizedDemo的Class对象的锁是互不干扰的。

总结

  • 不同线程(T1,T2)同时调用对象©被synchronized修饰的非静态方法(M)时,会形成对对象©锁的竞争关系
  • 不同线程(T1,T2)同时调用类中©被synchronized修饰的静态方法(M)时,会形成对类Class对象©锁的竞争关系
  • 不同线程(T1,T2)同时调用同类但不同对象(C1,C2)被synchronized修饰的非静态方法(M)时,不同对象(C1,C2)的锁互不干扰
  • 不同线程(T1,T2)同时调用对象(C1)被synchronized修饰的非静态方法(M1)和调用类中(C2)被synchronized修饰的静态方法(M2),类对象(C2)和Class对象(C1)的锁互不干扰

作用于代码块

作用于代码块时,锁住的是synchronized中的对象,这个被加锁的对象有多种情况。如:

  1. this对象
  2. 常量
  3. 变量/对象

定义一个类如下:其中四个方法都是synchronized加锁代码块,不过锁的对象不一样:

  1. generalMethod6方法中锁的是this,即当前对象引用
  2. generalMethod7方法中锁的是integer1,即常量
  3. generalMethod8方法中锁的是integer2,即变量
  4. generalMethod9方法中锁的是integer3,即对象
package com.tianhao.luo.thread.concurrent;

/**
 * Synchronized关键字
 *
 * @author: tianhao.luo@hand-china.com 2021/5/4  16:44
 */
public class SynchronizedDemo {
    //装箱动作是通过valueOf方法实现的。通过valueOf产生包装对象时,如果int参数在-128到127之间,则直接从整型池中获取对象,不在该范围的int类型则通过new生成包装对象。
    Integer integer1 = 127;
    Integer integer2 = 130;
    Integer integer3 = new Integer(124);

    public  void generalMethod6(){

        try {
            synchronized (this){
                for (int i = 0; i < 3; i++) {
                    System.out.println(Thread.currentThread().getName() + ",exec generalMethod6: " + i);
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public  void generalMethod7(){

        try {
            synchronized (integer1){
                for (int i = 0; i < 3; i++) {
                    System.out.println(Thread.currentThread().getName() + ",exec generalMethod7: " + i);
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public  void generalMethod8(){

        try {
            synchronized (integer2){
                for (int i = 0; i < 3; i++) {
                    System.out.println(Thread.currentThread().getName() + ",exec generalMethod8: " + i);
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public  void generalMethod9(){

        try {
            synchronized (integer3){
                for (int i = 0; i < 3; i++) {
                    System.out.println(Thread.currentThread().getName() + ",exec generalMethod9: " + i);
                    Thread.sleep(1000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

实例一 代码块加锁this

测试方法结构如下:

public class SynchronizedDemoTest extends TestCase {
	public void testCodeBlock3() throws Exception{
        // 测试synchronized作用于generalMethod5方法中的代码块
        // 两个线程用不同synchronizedDemo对象调用加synchronized修饰代码块的同一方法
        // 两个线程中被加锁的是synchronizedDemo这个对象的引用this;两个线程之间加锁对象不同,互不干扰
        SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
        SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 1=========");
                synchronizedDemo1.generalMethod6();
                countDownLatch.countDown();
            }
        }).start();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 2=========");
                synchronizedDemo2.generalMethod6();
                countDownLatch.countDown();
            }
        }).start();

        countDownLatch.await();
        System.out.println("=========this is step 3=========");
    }
}

执行输出的结果如下:启动了两个线程,他们的输出是交替进行的。

=========this is step 1=========
Thread-0,exec generalMethod6: 0
=========this is step 2=========
Thread-1,exec generalMethod6: 0
Thread-1,exec generalMethod6: 1
Thread-0,exec generalMethod6: 1
Thread-0,exec generalMethod6: 2
Thread-1,exec generalMethod6: 2
=========this is step 3=========

实例二 代码块加锁常量

将实例一中两个线程中不同synchronizedDemo对象调用的方法改为generalMethod7(),即加锁为定义的成员变量(Integer integer1 = 127;),实际上由于装箱的原理,当使用=创建范围为-128~127的Integer对象时,是取自常量池,所以这里可以理解为加锁常量。

执行输出的结果如下:Thread-1会等待Thread-0执行完毕,虽然是不同的对象调用方法,但是由于加锁的是常量,导致两个线程形成了对常量的竞争。

=========this is step 1=========
Thread-0,exec generalMethod7: 0
=========this is step 2=========
Thread-0,exec generalMethod7: 1
Thread-0,exec generalMethod7: 2
Thread-1,exec generalMethod7: 0
Thread-1,exec generalMethod7: 1
Thread-1,exec generalMethod7: 2
=========this is step 3=========

实例三 代码块加锁变量

将实例一中两个线程中不同synchronizedDemo对象调用的方法改为generalMethod8(),即加锁为定义的成员变量(Integer integer2 = 130;),实际上由于装箱的原理,当使用=创建范围不为-128~127的Integer对象时,使用了new生成,所以这里可以理解为加锁变量。

执行的输出如下:Thread-1和Thread-0交替输出,即两个线程中加锁的对象不同。

=========this is step 1=========
Thread-0,exec generalMethod8: 0
=========this is step 2=========
Thread-1,exec generalMethod8: 0
Thread-0,exec generalMethod8: 1
Thread-1,exec generalMethod8: 1
Thread-0,exec generalMethod8: 2
Thread-1,exec generalMethod8: 2
=========this is step 3=========

将实例一中两个线程中不同synchronizedDemo对象调用的方法改为generalMethod9(),即加锁为定义的成员变量(Integer integer3 = new Integer(124);),使用new关键字创建的对象会指向不同的引用,所以这里可以理解为加锁变量。

执行的输出如下::Thread-1和Thread-0交替输出,即两个线程中加锁的对象不同。

=========this is step 1=========
Thread-0,exec generalMethod9: 0
=========this is step 2=========
Thread-1,exec generalMethod9: 0
Thread-1,exec generalMethod9: 1
Thread-0,exec generalMethod9: 1
Thread-1,exec generalMethod9: 2
Thread-0,exec generalMethod9: 2
=========this is step 3=========

总结

synchronized作用于代码块时,加锁的对象不同,不同线程之间的竞争关系也不同

  • 加锁对象为this时,如果是多线程同时调用同一对象该方法时,多线程会竞争该对象。如果是多线程同时调用不同对象该方法时,由于加锁对象不同,则无竞争关系
  • 加锁对象为常量时,如果是多线程同时调用该方法时(无论是否是不同的对象),多线程都会竞争该常量对象,形成阻塞
  • 加锁对象为变量时,和this一致

死锁的形成

定义类如下:其中generalMethod20方法中不仅会先对integer1加锁,还会对integer4加锁。generalMethod21方法加锁顺序恰好和generalMethod20相反。

public class SynchronizedDemo {
    //装箱动作是通过valueOf方法实现的。通过valueOf产生包装对象时,如果int参数在-128到127之间,则直接从整型池中获取对象,不在该范围的int类型则通过new生成包装对象。
    Integer integer1 = 127;
    Integer integer4 = 126;
 
    public  void generalMethod20(){

        try {
            synchronized (integer1){
                for (int i = 0; i < 3; i++) {
                    System.out.println(Thread.currentThread().getName() + ",exec generalMethod20: " + i);
                    Thread.sleep(1000);
                }
                synchronized (integer4){

                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public  void generalMethod21(){

        try {
            synchronized (integer4) {
                for (int i = 0; i < 3; i++) {
                    System.out.println(Thread.currentThread().getName() + ",exec generalMethod21: " + i);
                    Thread.sleep(1000);
                }
                synchronized (integer1){

                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试方法如下:

public class SynchronizedDemoTest extends TestCase {
	public void testCodeBlock7() throws Exception{
        // 测试synchronized作用于generalMethod方法中的代码块
        // 两个线程用不同synchronizedDemo对象分别调用两个方法去获取对方还未释放的锁对象,因为互相等待形成死锁
        SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
        SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 1=========");
                synchronizedDemo1.generalMethod20();
                countDownLatch.countDown();
            }
        }).start();
        new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("=========this is step 2=========");
                synchronizedDemo2.generalMethod21();
                countDownLatch.countDown();
            }
        }).start();

        countDownLatch.await();
        System.out.println("=========this is step 3=========");
    }
}

测试输出结果如下:多线程中调用的不是相同的方法,方法中开始的加锁对象也不同,所以会先交替输出;但是当Thread-1中的方法执行到去获取integer3对象锁时,由于integer3还未被Thread-0释放,所以Thread-1线程会等待;Thread-0线程执行到去获取integer4对象锁时,由于Thread-1还在阻塞状态并未释放integer4对锁,Thread-0线程也会等待。此时由于没有超时机制,导致线程之间互相等待资源释放,而形成死锁。

=========this is step 1=========
Thread-0,exec generalMethod20: 0
=========this is step 2=========
Thread-1,exec generalMethod21: 0
Thread-1,exec generalMethod21: 1
Thread-0,exec generalMethod20: 1
Thread-1,exec generalMethod21: 2
Thread-0,exec generalMethod20: 2
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值