关于java:深入学习Synchronized各种使用方法

42次阅读

共计 7880 个字符,预计需要花费 20 分钟才能阅读完成。

深刻学习 Synchronized 各种应用办法

在 Java 当中 synchronized 通常是用来标记一个办法或者代码块。在 Java 当中被 synchronized 标记的代码或者办法在同一个时刻只可能有一个线程执行被 synchronized 润饰的办法或者代码块。因而被 synchronized 润饰的办法或者代码块不会呈现 数据竞争 的状况,也就是说被 synchronized 润饰的代码块是并发平安的。

Synchronized 关键字

synchronized 关键字通常应用在上面四个中央:

  • synchronized 润饰实例办法。
  • synchronized 润饰静态方法。
  • synchronized 润饰实例办法的代码块。
  • synchronized 润饰静态方法的代码块。

在理论状况当中咱们须要仔细分析咱们的需要抉择适合的应用 synchronized 办法,在保障程序正确的状况下晋升程序执行的效率。

Synchronized 润饰实例办法

上面是一个用 Synchronized 润饰实例办法的代码示例:

public class SyncDemo {

  private int count;

  public synchronized void add() {count++;}

  public static void main(String[] args) throws InterruptedException {SyncDemo syncDemo = new SyncDemo();
    Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {syncDemo.add();
      }
    });

    Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {syncDemo.add();
      }
    });
    t1.start();
    t2.start();
    t1.join(); // 阻塞住线程期待线程 t1 执行实现
    t2.join(); // 阻塞住线程期待线程 t2 执行实现
    System.out.println(syncDemo.count);// 输入后果为 20000
  }
}

在下面的代码当中的 add 办法只有一个简略的 count++ 操作,因为这个办法是应用 synchronized 润饰的因而每一个时刻只能有一个线程执行 add 办法,因而下面打印的后果是 20000。如果 add 办法没有应用 synchronized 润饰的话,那么线程 t1 和线程 t2 就能够同时执行 add 办法,这可能会导致最终 count 的后果小于 20000,因为 count++ 操作不具备原子性。

下面的剖析还是比拟明确的,然而咱们还须要晓得的是 synchronized 润饰的 add 办法一个时刻只能有一个线程执行的意思是对于一个 SyncDemo 类的对象来说一个时刻只能有一个线程进入。比方当初有两个 SyncDemo 的对象 s1s2,一个时刻只能有一个线程进行 s1add办法,一个时刻只能有一个线程进入 s2add办法,然而同一个时刻能够有两个不同的线程执行 s1s2add 办法,也就说 s1add办法和 s2add是没有关系的,一个线程进入 s1add办法并不会阻止另外的线程进入 s2add办法,也就是说 synchronized 在润饰一个非静态方法的时候“锁”住的只是一个实例对象,并不会“锁”住其它的对象。其实这也很容易了解,一个实例对象是一个独立的个体别的对象不会影响他,他也不会影响别的对象。

Synchronized 润饰静态方法

Synchronized 润饰静态方法:

public class SyncDemo {

  private static int count;

  public static synchronized void add() {count++; // 留神 count 也要用 static 润饰 否则编译通过不了}

  public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {SyncDemo.add();
      }
    });

    Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {SyncDemo.add();
      }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(SyncDemo.count); // 输入后果为 20000
  }
}

下面的代码最终输入的后果也是 20000,然而与前一个程序不同的是。这里的 add 办法用 static 润饰的,在这种状况下真正的只能有一个线程进入到 add 代码块,因为用 static 润饰的话是所有对象公共的,因而和后面的那种状况不同,不存在两个不同的线程同一时刻执行 add 办法。

你认真想想如果可能让两个不同的线程执行 add 代码块,那么 count++ 的执行就不是原子的了。那为什么没有用 static 润饰的代码为什么能够呢?因为当没有用 static 润饰时,每一个对象的 count 都是不同的,内存地址不一样,因而在这种状况下 count++ 这个操作依然是原子的!

Sychronized 润饰多个办法

synchronized 润饰多个办法示例:

public class AddMinus {
  public static int ans;

  public static synchronized void add() {ans++;}

  public static synchronized void minus() {ans--;}

  public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {AddMinus.add();
      }
    });

    Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {AddMinus.minus();
      }
    });

    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(AddMinus.ans); // 输入后果为 0
  }
}

在下面的代码当中咱们用 synchronized 润饰了两个办法,addminus。这意味着在同一个时刻这两个函数只可能有一个被一个线程执行,也正是因为addminus函数在同一个时刻只能有一个函数被一个线程执行,这才会导致 ans 最终输入的后果等于 0。

对于一个实例对象来说:

public class AddMinus {
  public int ans;

  public synchronized void add() {ans++;}

  public synchronized void minus() {ans--;}

  public static void main(String[] args) throws InterruptedException {AddMinus addMinus = new AddMinus();
    Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {addMinus.add();
      }
    });

    Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {addMinus.minus();
      }
    });

    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(addMinus.ans);
  }
}

下面的代码没有应用 static 关键字,因而咱们须要 new 出一个实例对象才可能调用 addminus办法,然而同样对于 AddMinus 的实例对象来说同一个时刻只能有一个线程在执行 add 或者 minus 办法,因而下面代码的输入同样是 0。

Synchronized 润饰实例办法代码块

Synchronized 润饰实例办法代码块

public class CodeBlock {

  private int count;

  public void add() {System.out.println("进入了 add 办法");
    synchronized (this) {count++;}
  }

  public void minus() {System.out.println("进入了 minus 办法");
    synchronized (this) {count--;}
  }

  public static void main(String[] args) throws InterruptedException {CodeBlock codeBlock = new CodeBlock();
    Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {codeBlock.add();
      }
    });

    Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {codeBlock.minus();
      }
    });

    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(codeBlock.count); // 输入后果为 0
  }
}

有时候咱们并不需要用 synchronized 去润饰代码块,因为这样并发度就比拟低了,一个办法一个时刻只能有一个线程在执行。因而咱们能够抉择用 synchronized 去润饰代码块,只让某个代码块一个时刻只能有一个线程执行,除了这个代码块之外的代码还是能够并行的。

比方下面的代码当中 addminus办法没有应用 synchronized 进行润饰,因而一个时刻能够有多个线程执行这个两个办法。在下面的 synchronized 代码块当中咱们应用了 this 对象作为锁对象,只有拿到这个锁对象的线程才可能进入代码块执行,而在同一个时刻只能有一个线程可能取得锁对象。也就是说 add 函数和 minus 函数用 synchronized 润饰的两个代码块同一个时刻只能有一个代码块的代码可能被一个线程执行,因而下面的后果同样是 0。

这里说的锁对象是 this 也就 CodeBlock 类的一个实例对象,因为它锁住的是一个实例对象,因而当实例对象不一样的时候他们之间是没有关系的,也就是说不同实例用 synchronized 润饰的代码块是没有关系的,他们之间是能够并发的。

Synchronized 润饰动态代码块

public class CodeBlock {

  private static int count;

  public static void add() {System.out.println("进入了 add 办法");
    synchronized (CodeBlock.class) {count++;}
  }

  public static void minus() {System.out.println("进入了 minus 办法");
    synchronized (CodeBlock.class) {count--;}
  }

  public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {CodeBlock.add();
      }
    });

    Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {CodeBlock.minus();
      }
    });

    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(CodeBlock.count);
  }
}

下面的代码是应用 synchronized 润饰动态代码块,下面代码的锁对象是 CodeBlock.class,这个时候他不再是锁住一个对象了,而是一个类了,这个时候的并发度就变小了,上一份代码当锁对象是CodeBlock 的实例对象时并发度更大一些,因为当锁对象是实例对象的时候,只有实例对象外部是不可能并发的,实例之间是能够并发的。然而当锁对象是 CodeBlock.class 的时候,实例对象之间时不可能并发的,因为这个时候的锁对象是一个类。

应该用什么对象作为锁对象

在后面的代码当中咱们别离应用了实例对象和类的 class 对象作为锁对象,事实上你能够应用任何对象作为锁对象,然而不举荐应用字符串和根本类型的包装类作为锁对象,这是因为字符串对象和根本类型的包装对象会有缓存的问题。字符串有字符串常量池,整数有小整数池。因而在应用这些对象的时候他们可能最终都指向同一个对象,因为指向的都是同一个对象,线程取得锁对象的难度就会减少,程序的并发度就会升高。

比方在上面的示例代码当中就是因为锁对象是同一个对象而导致并发度降落:

import java.util.concurrent.TimeUnit;

public class Test {public void testFunction() throws InterruptedException {synchronized ("HELLO WORLD") {System.out.println(Thread.currentThread().getName() + "\tI am in synchronized code block");
      TimeUnit.SECONDS.sleep(5);
    }
  }

  public static void main(String[] args) {Test t1 = new Test();
    Test t2 = new Test();
    Thread thread1 = new Thread(() -> {
      try {t1.testFunction();
      } catch (InterruptedException e) {e.printStackTrace();
      }
    });

    Thread thread2 = new Thread(() -> {
      try {t2.testFunction();
      } catch (InterruptedException e) {e.printStackTrace();
      }
    });
    thread1.start();
    thread2.start();}
}

在下面的代码当中咱们应用两个不同的线程执行两个不同的对象外部的 testFunction 函数,按情理来说这两个线程是能够同时执行的,因为执行的是两个不同的实例对象的同步代码块。然而下面代码的执行首先一个线程会进入同步代码块而后打印输出,期待 5 秒之后,这个线程退出同步代码块另外一个线程才会再进入同步代码块,这就阐明了两个线程不是同时执行的,其中一个线程须要期待另外一个线程执行实现才执行。这正是因为两个 Test 对象当中应用的 "HELLO WORLD" 字符串在内存当中是同一个对象,是存储在字符串常量池中的对象,这才导致了锁对象的竞争。

上面的代码执行的后果也是一样的,一个线程须要期待另外一个线程执行实现才可能继续执行,这是因为在 Java 当中如果整数数据在 [-128, 127] 之间的话应用的是小整数池当中的对象,应用的也是同一个对象,这样能够缩小频繁的内存申请和回收,对内存更加敌对。

import java.util.concurrent.TimeUnit;

public class Test {public void testFunction() throws InterruptedException {synchronized (Integer.valueOf(1)) {System.out.println(Thread.currentThread().getName() + "\tI am in synchronized code block");
      TimeUnit.SECONDS.sleep(5);
    }
  }

  public static void main(String[] args) {Test t1 = new Test();
    Test t2 = new Test();
    Thread thread1 = new Thread(() -> {
      try {t1.testFunction();
      } catch (InterruptedException e) {e.printStackTrace();
      }
    });

    Thread thread2 = new Thread(() -> {
      try {t2.testFunction();
      } catch (InterruptedException e) {e.printStackTrace();
      }
    });
    thread1.start();
    thread2.start();}
}

Synchronized 与可见性和重排序

可见性

  • 当一个线程进入到 synchronized 同步代码块的时候,将会刷新所有对该线程的可见的变量,也就是说如果其余线程批改了某个变量,而且线程须要在 Synchronized 代码块当中应用,那就会从新刷新这个变量到内存当中,保障这个变量对于执行同步代码块的线程是可见的。
  • 当一个线程从同步代码块退出的时候,也会将线程的工作内存同步到内存当中,保障在同步代码块当中批改的变量对其余线程可见。

重排序

Java 编译器和 JVM 当发现可能让程序执行的更快的时候是可能对程序的指令进行重排序解决的,也就是通过调换程序指令执行的程序让程序执行的更快。

然而重排序很可能让并发程序产生问题,比如说当一个在 synchronized 代码块当中的写操作被重排序到 synchronized 同步代码块内部了这显然是有问题的。

在 JVM 的实现当中是不容许 synchronized 代码块外部的指令和他后面和前面的指令进行重排序的,然而在 synchronized 外部的指令是可能与 synchronized 外部的指令进行重排序的,比拟驰名的就是 DCL 单例模式,他就是在synchronized 代码块当中存在重排序的,如果你对 DCL 单例模式 还不是很相熟,你能够浏览这篇文章的 DCL 单例 模式局部。

总结

在本篇文章当中次要介绍了各种 synchronized 的应用办法,总结如下:

  • Synchronized 润饰实例办法,这种状况不同的对象之间是能够并发的。
  • Synchronized 润饰实例办法,这种状况下不同的对象是不能并发的,然而不同的类之间能够进行并发。
  • Sychronized 润饰多个办法,这多个办法在对立时刻只能有一个办法被执行,而且只能有一个线程可能执行。
  • Synchronized 润饰实例办法代码块,同一个时刻只能有一个线程执行代码块。
  • Synchronized 润饰动态代码块,同一个时刻只能有一个线程执行这个代码块,而且不同的对象之间不可能进行并发。
  • 应该用什么对象作为锁对象,倡议不要应用字符串和根本类型的包装类作为锁对象,因为 Java 对这些进行优化,很可能多个对象应用的是同一个锁对象,这会大大降低程序的并发度。
  • 程序在进入和来到 Synchronized 代码块的时候都会将线程的工作内存刷新到内存当中,以保证数据的可见性,这一点和 volatile 关键字很像,同时 Synchronized 代码块中的指令不会和 Synchronized 代码块之间和之后的指令进行重排序,然而 Synchronized 代码块外部可能进行重排序。

更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHu…

关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

正文完
 0