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

深刻学习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、计算机系统根底、算法与数据结构)常识。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理