Java同步(Synchronization)
前言
线程间的通信主要通过共享对字段的访问和对象引用字段的引用,可能会产生两种错误,线程干扰和内存一致性错误。Java的同步就是防止这些错误,但当多个线程访问同一资源会导致线程执行缓慢,甚至暂停执行。
线程干扰(Thread Interference)
例子
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }
如果现在有两个线程,线程A执行increment,线程B执行decrement,它们都使用了相同的变量c,这时会发生干扰。即便是执行非常简单的语句,但简单语句也可以转化为Java虚拟机的多个步骤,例如c++可以分解为三个步骤:
1、检索c的当前值。
2、将检索值加1。
3、将值存储回c中。
PS:c—也可以分解为相同步骤,只是第二步是减1。
假如线程A和线程B几乎同时调用,c的初始值为0,则它们交错的步骤可能是以下顺序:
线程A:检索c。 线程B:检索c。 线程A:增加检索值;结果是1。 线程B:减少检索值;结果是-1。 线程A:将结果存储在c中; c现在是1。 线程B:将结果存储在c中; c现在是-1。
线程A的结果丢失了,被线程B覆盖了。这种交错只是一种可能性,也可能是B的结果丢失,也可能不会出错。也就是因为顺序是不可预测的,所以线程干扰的错误可能难以发现和修复。
内存一致性错误
不同线程看到的内存中的同一份数据却有不同的“视图”,原因很复杂,作为程序员最需要做的是处理好“happens-before”的关系,避免问题。
同步方法
Java提供了两种基本的同步方法:同步方法和同步语句。同步的行为是依赖锁来构建的,每一个对象都有与之相关的固定锁,想要独占访问对象的线程必须先获取对象的锁(拿不到阻塞线程),完成后释放锁,同时与请求同一锁的线程建立一个happens-before关系。
同步方法的作用:
1、当一个线程正在执行一个对象的同步方法时,所有其他调用该对象的同步方法的线程将被阻塞,直到第一个线程执行完成。
2、当一个同步方法退出后,会自动建立与后续调用的同步方法(相同对象)的一个happens-before关系,保证所有线程对对象状态的修改可见。
注意:
1、在构造函数中使用synchronized关键字是语法错误。同步构造函数没有意义,因为只有创建对象的线程在构建时才能访问它。
2、如果一个对象对多个线程可见,则通过同步方法完成对该对象变量的所有读取或写入操作,不然还是会出现线程干扰和内存一致性错误。
public class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } }
同步语句
另一种创建同步代码的方法,与同步方法不同,同步语句必须指定提供内部锁的对象。同步语句有利于通过细粒度同步来提高并发性。
例如,假设类MsLunch具有两个实例字段c1和c2,它们从不一起使用(很重要的前提条件!)。这些字段的所有更新都必须同步,但没有理由阻止c1的更新与c2的更新交错,这样做会创建不必要的阻塞来降低并发性。我们不使用同步方法,而是创建两个对象来提供锁。
public class MsLunch { private long c1 = 0; private long c2 = 0; private Object lock1 = new Object(); private Object lock2 = new Object(); public void inc1() { synchronized(lock1) { c1++; } } public void inc2() { synchronized(lock2) { c2++; } } }
volatile关键字
原子操作:一个原子操作,要么发生,要么不发生。比如 c=0;(非long和double类型) 这个操作是执行了就会发生。再比如:c++;可分割成三个操作步骤,执行时可能会丢失某些步骤,就不是一个原子操作。非原子操作都会存在线程安全问题,同步方法和同步语句可以让它变成一个原子操作。
volatile变量是一种稍弱的同步机制,所有声明为volatile的变量(包括long和double),读取和写入都是原子的。
volatile变量特性:
1、当一个线程读取一个volatile变量时,它看到总是最新的值。
2、原子动作不能交错,使用volatile变量不用担心线程干扰。
PS:java.util.concurrent包下提供了一些原子类。
参考文献
https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html