Java多线程 - 线程同步 synchronized

synchronized是Java同步的基础,只有彻底了解synchronized的对象锁、可重入锁...特性,将来在使用高阶的ReentrantLock、ReentrantReadWriteLock才能得心应手

本文将透过大量的具体实例来了解synchronized的特性

  • 具体实例

    • 假设现在有一个MyObject对象myObject,他有methodA()和methodB()方法,创造两个线程Thread1 和 Thread2 ,使他们都去执行这个myObject对象的methodA方法,代码如下

      class MyObject {
          public void methodA() {
              System.out.println("begin method A, 谁执行我: " + Thread.currentThread().getName());
              System.out.println("end");
          }
      ​
          public void methodB() {
              System.out.println("begin method B, 谁执行我: " + Thread.currentThread().getName());
              System.out.println("end");
          }
      }
      ​
      class Thread1 extends Thread {
          private MyObject object;
      ​
          Thread1(MyObject object) {
              this.object = object;
          }
      ​
          @Override
          public void run() {
              object.methodA(); // thread 1 执行 myObject 对象的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          private MyObject object;
      ​
          Thread2(MyObject object) {
              this.object = object;
          }
      ​
          @Override
          public void run() {
              object.methodA();  // thread 2 也执行 myObject 对象的 methodA() 方法
          }
      }
      ​
      public class Main {
          public static void main(String[] args) {
              MyObject object = new MyObject();
              Thread1 th1 = new Thread1(object);
              th1.setName("thread 1");
              Thread2 th2 = new Thread2(object);
              th2.setName("thread 2");
              th1.start();
              th2.start();
          }
      }
    • 因为methodA()方法上没有加上synchronized关键字进行线程同步,所以thread1和thread2会争抢著执行methodA(),所以结果如下

      begin methodA, 谁执行我: thread 1
      begin methodA, 谁执行我: thread 2
      end
      end
    • 这时如果在methodA上加上synchronized关键字,thread1和thread2会一个一个排队执行methodA,结果如下

      class MyObject {
          synchronized public void methodA() { ... }
          public void methodB() { ... }
      }
      begin method A, 谁执行我: thread 1
      end
      begin method A, 谁执行我: thread 2
      end
    • 这时如果再新增一个对象MyObject对象myObject2,让Thread2改为执行myObject2对象的mehodA()方法,则结果如下

      • 会使得结果又变为不同步的原因是因为synchronized其实是一个对象锁,而不是代码锁

      • 意思是指假设当Thread1开始执行myObject的methodA()时,实际上是锁住了myObject这个对象的methodA()方法,而不是锁住了class MyObject们的methodA()方法

      • 所以在jvm中,若是创建了多个实例对象,则会产生多把锁,每个对象自己一把锁,对象间的锁彼此不干扰

      • 而在这个例子中,thread1取得的是myObject的对象锁,而thread2取得的是myObject2的对象锁,因为他们不是使用同一把锁,所以才会使得线程不同步

      class MyObject {
          synchronized public void methodA() { ... }
          public void methodB() { ... }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 执行的是 myObject 对象的methodA()方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodA(); //thread 2 执行的是 myObject2 对象的methodA()方法
          }
      }
      begin method A, 谁执行我: thread 1
      begin method A, 谁执行我: thread 2
      end
      end
    • 如果将myObject2删了,改回Thread1和Thread2都使用myObject这个对象,并且改成Thread1执行methodA(),Thread2执行methodB(),注意methodA有synchronized,但methodB没有,其结果如下

      • 会使得线程又不同步的原因是因为methodB是一个非同步的方法,即是methodB没有加上synchronized关键字

      • 因此虽然thread1已经先抢到synchonized锁了,但是因为methodB不是一个线程同步的方法,因此他根本不care什麽对象锁,直接就能够被调用了

      class MyObject {
          synchronized public void methodA() { ... }
          public void methodB() { ... }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 执行myObject对象的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 执行myObject对象的 methodB() 方法
          }
      }
      begin method A, 谁执行我: thread 1
      begin method B, 谁执行我: thread 2
      end
      end
    • 尝试将methodB()也加上synchronized关键字,而Thread1仍旧是执行myObject的methodA()方法,Thread2仍旧执行myObject的methodB()方法,结果如下

      • Thread1明明调用的是methodA,但是为什麽会使得同个对象的其他synchronized方法受到影响呢?为什麽会使得结果同步?

      • 原因是因为synchronized是一个对象锁,所以当Thread1取得锁开始执行methodA时,他实际上其实是取得了myObject这个对象的synchronized

      • 因此当Thread2想要开始执行methodB时,他会去看myObject这个对象的synchronized锁被取走了没,而他就会发现已经被Thread1取走了,因此他必须等待Thread1完成后,才能去取得myObject的对象锁,去执行methodB,所以结果才会是同步的

      class MyObject {
          synchronized public void methodA() { ... }
          synchronized public void methodB() { ... }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 执行myObject对象的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 执行myObject对象的 methodB() 方法
          }
      }
      begin method A, 谁执行我: thread 1
      end
      begin method B, 谁执行我: thread 2
      end
    • 如果把Thread1和Thread2都改成执行methodA方法,不过将methodA方法改成他还会去调用methodB方法,注意methodA和methodB方法都有加上synchronized关键字,结果如下

      • 可以看到当Thread1抢到myObject对象的锁执行methodA到一半,当他想要执行methodB时,他就必须去取得methodB的对象锁,也就是myObject对象锁,也就是他当初近来methodA时已经拿到的锁

      • 由于Thread1已经拿到了这个锁,所以当他再次取得这个对象锁时,能够再次取到,这就是锁重入性

      • 这也说明当一个synchronized方法methodA的内部调用本类的其他synchronized方法methodB时,是永远可以得到锁的,因为当初进入methodA时此线程就已经得过对象锁了

      class MyObject {
          synchronized public void methodA() {
              System.out.println("begin method A");
              methodB();  //methodA去调用同个对象的另一个synchronized方法methodB
              System.out.println("end");
          }
          synchronized public void methodB() {
              System.out.println("begin method B");
              System.out.println("end");
          }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 执行myObject对象的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodA(); //thread 2 执行myObject对象的 methodA() 方法
          }
      }
      begin method A, 谁执行我: thread 1
      begin method B, 谁执行我: thread 1
      end
      end
      begin method A, 谁执行我: thread 2
      begin method B, 谁执行我: thread 2
      end
      end
  • 根据以上实例,小结synchronized关键字的特性

    • synchronized是一个对象锁

      • 假设thread1线程已经持有某object对象的synchronized lock

      • 那麽thread2仍然可以调用此object对象中的非同步的方法,即是thread2可以直接调用那些没有加上sychronized关键字的方法,不需要等待

      • 但是thread2如果调用了加上synchronized关键字的方法,就必须要等thread1释放lock才行

    • synchronized对象锁具有锁重入性

      • 当一个线程取得某object对象的对象锁时,再次请求此对象锁是可以再次得到该对象锁的

      • 也就是说,当一个synchronized方法methodA的内部调用本类的其他synchronized方法methodB时,是永远可以得到锁的,因为当初进入methodA时此线程就已经得过对象锁了

    • 当线程在执行synchronized方法时出现异常,对象锁会自动释放

    • 同步不具有继承性

      • 假设Parent类中有一个synchronized public void hello()方法,那麽继承到Child类中时此方法会变成public void hello()

      • 如果Child也想要让此方法变成同步方法,那麽必须手动加上synchronized关键字才行

  • 具体实例二

    • 到刚才为止,我们都是将synchronized修饰在方法上,但是直接修饰方法可能会使得critical section范围设置过大,而真正要同步的区块其实没有这麽大,因此synchronized还支持修饰代码块,或是修饰一个变量

      • 使用synchronized (this)可以同步某一段代码块,而这个方式也是一个对象锁,此例就是会锁住myObject

      • 要注意只有在synchronized (this)裡的代码是同步的,其他地方都是异步的

        • 所以当thread1执行到methodA的运作很久的方法时,因为他没有被synchronized修饰,所以是异步的,所以他不会去管什麽锁,直接就能执行了

        • 但是当thread1执行到synchronized (this)时,他会想要去取得myObject的对象锁,而他会发现已经被thread2的methodB取走了,所以他要等thread2释放对象锁之后,才能开始执行synchronized (this)裡面的代码块

      class MyObject {
          public void methodA() {
              System.out.println("method A 运作很久的一个方法"); //这是异步的
              synchronized (this) {
                  System.out.println("begin method A");
                  System.out.println("end");
              }
          }
          synchronized public void methodB() {
              System.out.println("begin method B");
              System.out.println("end");
          }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 执行myObject对象的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 执行myObject对象的 methodB() 方法
          }
      }
      method A 运作很久的一个方法
      begin method B
      end
      begin method A
      end
    • 接续刚才的例子,但是将sychronized改成用来修饰一个变量,methodA去同步myObject对象裡的变量x,methodB去同步myObject对象裡的变量y

      • 可以看到结果是不同步的,原因是因为当synchonized修饰变量时,他的锁会变成这个变量对象的锁,而不是myObject这个对象的锁了

      • 因此methodA取得的是myObject中的x的这个对象锁,而methodB取得的是myObject中的y的对象锁,因为他们拿的是两个不同的锁,所以他们才不会被同步

      class MyObject {
          private String x = new String();
          private String y = new String();
      ​
          public void methodA() {
              synchronized (x) {
                  System.out.println("begin method A");
                  System.out.println("end");
              }
          }
          public void methodB() {
              synchronized (y) {
                  System.out.println("begin method B");
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 执行myObject对象的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 执行myObject对象的 methodB() 方法
          }
      }
      begin method A
      begin method B
      end
      end
    • 如果再新增一个myObject2,让thread1执行myObject的methodA,thread2执行myObject2的methodA方法,结果如下

      • 结果会是不同步的,原因是因为synchronized是一个对象锁,thread1取得的是myObject中的x的对象锁,thread2取得的是myObject2中的x对象锁,是两个锁,所以才会不同步

      class MyObject {
          private String x = new String();
      ​
          public void methodA() {
              synchronized (x) {
                  System.out.println("begin method A, 谁执行我: " + Thread.currentThread().getName());
                  System.out.println("end");
              }
          }
          public void methodB() { ... }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 执行 myObject 对象的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodA(); //thread 2 执行 myObject2 对象的 methodA() 方法
          }
      }
      begin method A, 谁执行我: thread 1
      end
      begin method A, 谁执行我: thread 2
      end
    • 如果将myObject2删了,改回Thread1和Thread2都使用myObject这个对象,并且改回Thread1执行methodA,Thread2执行methodB,然后methodA去同步一个变量对象x,但methodB去同步一个类对象myObject,结果如下

      • 结果会是不同步的,是因为他们其实取的锁是不同的锁,methodA取得的是myObject中的变量锁x,而methodB是取得myObject锁

      • 所以可以知道,就算取得了类对象myObject的锁,也不会影响其中变量x的锁,也就是说并非取得对象锁myObject就会把所有他的变量锁x也都全部锁起来,他们之间是独立的两个锁,分别是myObjectx in myObject

      • 所以假设又来了一个新的对象myObject2,那麽锁就有4把,分别是myObjectx in myObjectmyObject2x in myObject2

      class MyObject {
          private String x = new String();
      ​
          public void methodA() {
              synchronized (x) {
                  System.out.println("begin method A");
                  System.out.println("end");
              }
          }
          public void methodB() {
              synchronized (this) {
                  System.out.println("begin method B");
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          public void run(){
              object.methodA(); //thread 1 执行myObject对象的 methodA() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          public void run(){
              object.methodB(); //thread 2 执行myObject对象的 methodB() 方法
          }
      }
      begin method A
      begin method B
      end
      end
    • 使用synchronized修饰String变量时,要小心jvm中的String常量池特性

      • jvm中的String常量池缓存会导致String指向同个对象,像是a、b预期中应该是两个"1"对象,但是因为String常量池的缓存影响,所以a、b储存的是同个对象,才导致a == b 输出是true

        public class Test {
            public static void main() {
                String a = "1";
                String b = "1";
                System.out.println(a == b); //输出true
            }
        }
      • 将methodA改成使用synchornized修饰传进来的input对象,只有当传进来的对象一样,那麽他们取得的锁才会是同个对象锁,由于因为String常量池的关系,Thread1和Thread2传进来的"xx"会被视为视同个对象,所以才会使得结果是同步的

        class MyObject {
            public void methodA(String input) {
                synchronized (input) {
                    System.out.println("begin method A, 谁执行我: " + Thread.currentThread().getName());
                    System.out.println("end");
                }
            }
            public void methodB() { ... }
        }
        ​
        class Thread1 extends Thread {
            public void run(){
                object.methodA("xx"); //thread 1 执行myObject对象的 methodA() 方法
            }
        }
        ​
        class Thread2 extends Thread {
            public void run(){
                object.methodA("xx"); //thread 2 执行myObject对象的 methodA() 方法
            }
        }
        begin method A, 谁执行我: thread 1
        end
        begin method A, 谁执行我: thread 2
        end
      • 如果传进来的对象改成别的不是String类型的对象,那麽就不会有因为缓存而带来的这个问题,因此Thread1和Thread2传进来的对象不会是同一个对象,因此他们取得的锁也不是同把锁,所以才会使得结果是不同步的

        class MyObject {
            public void methodA(MyType type) {
                synchronized (type) {
                    System.out.println("begin method A, 谁执行我: " + Thread.currentThread().getName());
                    System.out.println("end");
                }
            }
            public void methodB() { ... }
        }
        ​
        class Thread1 extends Thread {
            public void run(){
                object.methodA(new MyType());
            }
        }
        ​
        class Thread2 extends Thread {
            public void run(){
                object.methodA(new MyType());
            }
        }
        begin method A, 谁执行我: thread 1
        begin method A, 谁执行我: thread 2
        end
        end
    • synchronized除了修饰方法、变量、代码块,也可以用来修饰静态方法static,如果synchronized加在static方法上,那就是加上了一个class锁,和使用synchronized(MyObject.class)是一样的,都是要去取得class锁

      • 运行结果是同步的,原因就是因为Thread1和Thread2都想要去取得MyObject的class锁,所以才会使得结果同步

      class MyObject {
          synchronized public static void methodA() {
              System.out.println("begin static method A");
              System.out.println("end");
          }
          public void methodB() {
              synchronized (MyObject.class) {
                  System.out.println("begin method B");
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          @Override
          public void run() {
              MyObject.methodA(); //thread 1 执行 MyObject.methodA() 静态方法
          }
      }
      ​
      class Thread2 extends Thread {
          @Override
          public void run() {
              object.methodB(); //thread 2 执行myObject对象的 methodB() 方法
          }
      }
      begin static method A
      end
      begin method B
      end
    • 假设新增一个对象myObject2,并且改成Thread1执行myObject的methodB,Thread2执行myObject2的methodB,结果如下

      • 结果会是同步的,原因是因为class锁可以对所有对象的起作用,也就是所有这个类的对象们myObject、myObject2...只有一份class锁MyObject.class,因此不管生成再多对象,他们使用的都是同一个class锁,所以结果才会是同步的

      class MyObject {
          synchronized public static void methodA() { ... }
          public void methodB() {
              synchronized (MyObject.class) {
                  System.out.println("begin method B, 谁执行我: " + Thread.currentThread().getName());
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          @Override
          public void run() {
              object.methodA(); //thread 1 执行 myObject 对象的 methodB() 方法
          }
      }
      ​
      class Thread2 extends Thread {
          @Override
          public void run() {
              object.methodB(); //thread 2 执行 myObject2 对象的 methodB() 方法
          }
      }
      begin method B, 谁执行我: thread 1
      end
      begin method B, 谁执行我: thread 2
      end
    • 不过要注意,class锁、myObject对象锁、变量x对象锁,他们彼此之间都是互相独立的,所以取得了class锁不代表可以畅行无阻myObject对象锁和变量x对象锁

      • 把刚刚的myObject2删掉,让Thread1执行myObject的methodA,Thread2执行myObject的methodB,Thread3执行myObject的methodC

      • 结果是不同步的是因为methodA、methodB、methodC各自用了3种不同的锁

        • methodA是静态方法,所以他使用的是class锁

        • methodB使用的是myObject对象锁

        • methodC使用的是变量x in myObject对象锁

      class MyObject {
          private String x = new String();
      ​
          synchronized public static void methodA() {
              System.out.println("begin static method A");
              System.out.println("end");
          }
          synchronized public void methodB() {
              System.out.println("begin method B");
              System.out.println("end");
          }
          public void methodC() {
              synchronized (x) {
                  System.out.println("begin method C");
                  System.out.println("end");
              }
          }
      }
      ​
      class Thread1 extends Thread {
          @Override
          public void run() {
              MyObject.methodA(); //thread 1 执行 MyObject.methodA() 静态方法
          }
      }
      ​
      class Thread2 extends Thread {
          @Override
          public void run() {
              object.methodB(); //thread 2 执行myObject对象的 methodB() 方法
          }
      }
      ​
      class Thread3 extends Thread {
          @Override
          public void run() {
              object.methodC(); //thread 3 执行myObject对象的 methodC() 方法
          }
      }
      begin static method A
      begin method B
      begin method C
      end
      end
      end
  • 根据以上实例,总结synchronized关键字的特性

    • synchronized对象锁具有锁重入性

      • 也就是当一个线程取得某object对象的对象锁时,再次请求此对象锁是可以再次得到该对象锁的

    • 当线程在执行synchronized方法时出现异常,对象锁会自动释放

    • 同步不具有继承性

    • synchronized不仅可以用在方法上,也能用在变量、代码块、静态方法上

      • class锁、myObject对象锁、x变量 in myObject的对象锁、y变量 in myObject的对象锁,彼此间都是独立的,并非取得myObject对象锁就能够取得所有他其中的变量的对象锁,也并非取得class锁就能取得所有对象myObject、myObject2...的对象锁

      • class锁可以对所有对象的起作用,也就是所有这个类的对像们myObject、myObject2...只有一份class锁

    • 使用synchronized在String变量上时,要注意String的常量池特性

    • 就算改了某个对象中的某个属性,像是myObject.setInfo("123),只要myObject对象不改变,那麽synchronized运行的结果仍然会是同步的

猜你喜欢

转载自blog.csdn.net/weixin_40341116/article/details/81261169