线程主要经过共享对字段和引用对象的引用字段的访问来进行通讯,这种通讯形式很是有效,但可能产生两种错误:线程干扰和内存一致性错误,防止这些错误所需的工具是同步。html
可是,同步可能会引入线程竞争,当两个或多个线程同时尝试访问同一资源并致使Java运行时更慢地执行一个或多个线程,甚至暂停它们执行,饥饿和活锁是线程竞争的形式。java
本节包括如下主题:c++
考虑一个名为Counter的简单类:git
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }
Counter
的设计为每次increment
的调用都会将c
加1,每次decrement
的调用都会从c
中减去1,可是,若是从多个线程引用Counter
对象,则线程之间的干扰可能会妨碍这种状况按预期发生。程序员
当两个操做在不一样的线程中运行但做用于相同的数据时,会发生干扰,这意味着这两个操做由多个步骤组成,而且步骤序列交叠。github
对于Counter
实例的操做彷佛不可能进行交错,由于对c
的两个操做都是单个简单的语句,可是,即便是简单的语句也能够由虚拟机转换为多个步骤,咱们不会检查虚拟机采起的具体步骤 — 只需知道单个表达式c++
能够分解为三个步骤:编程
c
的当前值。表达式c--
能够以相同的方式分解,除了第二步是递减而不是递增。segmentfault
假设在大约同一时间,线程A
调用increment
,线程B调用decrement
,若是c
的初始值为0
,则它们的交错操做可能遵循如下顺序:api
c
。c
。1
。-1
。c
中,c
如今是1
。c
中,c
如今是-1
。线程A的结果丢失,被线程B覆盖,这种特殊的交错只是一种可能性,在不一样的状况下,多是线程B的结果丢失,或者根本没有错误,由于它们是不可预测的,因此难以检测和修复线程干扰错误。安全
当不一样的线程具备应该是相同数据的不一致视图时,会发生内存一致性错误,内存一致性错误的缘由很复杂,超出了本教程的范围,幸运的是,程序员不须要详细了解这些缘由,所须要的只是避免它们的策略。
避免内存一致性错误的关键是理解先发生关系,这种关系只是保证一个特定语句的内存写入对另外一个特定语句可见,要了解这一点,请考虑如下示例,假设定义并初始化了一个简单的int
字段:
int counter = 0;
counter
字段在两个线程A和B之间共享,假设线程A递增counter
:
counter++;
而后,不久以后,线程B打印出counter
:
System.out.println(counter);
若是两个语句已在同一个线程中执行,则能够安全地假设打印出的值为“1
”,但若是两个语句在不一样的线程中执行,则打印出的值可能为“0
”,由于没法保证线程A对counter
的更改对线程B可见 — 除非程序员在这两条语句之间创建了先发生关系。
有几种操做能够建立先发生关系,其中之一是同步,咱们将在下面的部分中看到。
咱们已经看到了两种建立先发生关系的操做。
Thread.start
时,与该语句具备一个先发生关系的每一个语句也与新线程执行的每一个语句都有一个先发生关系,致使建立新线程的代码的效果对新线程可见。Thread.join
返回时,已终止的线程执行的全部语句与成功join
后的全部语句都有一个先发生关系,线程中代码的效果如今对执行join
的线程可见。有关建立先发生关系的操做列表,请参阅java.util.concurrent包的Summary页面。
Java编程语言提供了两种基本的同步语法:同步方法和同步语句,下两节将介绍两个同步语句中较为复杂的语句,本节介绍同步方法。
要使方法同步,只需将synchronized
关键字添加到其声明:
public class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; } public synchronized void decrement() { c--; } public synchronized int value() { return c; } }
若是count
是SynchronizedCounter
的一个实例,那么使这些方法同步有两个效果:
请注意,构造函数没法同步 — 将synchronized
关键字与构造函数一块儿使用是一种语法错误,同步构造函数没有意义,由于只有建立对象的线程在构造时才能访问它。
构造将在线程之间共享的对象时,要很是当心对对象的引用不会过早“泄漏”,例如,假设你要维护一个包含每一个类实例的名为instances
的List
,你可能想要将如下行添加到你的构造函数中:instances.add(this);
可是其余线程能够在构造对象完成以前使用
instances
来访问对象。
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:若是一个对象对多个线程可见,则对该对象的变量全部读取或写入都是经过synchronized
方法完成的(一个重要的例外:一旦构造了对象,就能够经过非同步方法安全地读取构造对象后没法修改的final
字段),这种策略颇有效,但可能会带来活性问题,咱们将在本课后面看到。
同步是围绕称为固有锁或监控锁的内部实体构建的(API规范一般将此实体简称为“监视器”。),固有锁在同步的两个方面都起做用:强制执行对对象状态的独占访问,并创建对可见性相当重要的先发生关系。
每一个对象都有一个与之关联的固有锁,按照约定,须要对对象字段进行独占和一致访问的线程必须在访问对象以前获取对象的固有锁,而后在完成它们时释放固有锁。线程在获取锁和释放锁期间被称为拥有固有锁,只要一个线程拥有固有锁,没有其余线程能够得到相同的锁,另外一个线程在尝试获取锁时将阻塞。
当线程释放固有锁时,在该操做与同一锁的任何后续获取之间创建先发生关系。
当线程调用同步方法时,它会自动获取该方法对象的固有锁,并在方法返回时释放它,即便返回是由未捕获的异常引发的,也会发生锁定释放。
你可能想知道调用静态同步方法时会发生什么,由于静态方法与类相关联,而不是与对象相关联,在这种状况下,线程获取与类关联的Class
对象的固有锁,所以,对类的静态字段的访问由一个锁控制,该锁与该类的任何实例的锁不一样。
建立同步代码的另外一种方法是使用同步语句,与同步方法不一样,同步语句必须指定提供固有锁的对象:
public void addName(String name) { synchronized(this) { lastName = name; nameCount++; } nameList.add(name); }
在此示例中,addName
方法须要同步更改lastName
和nameCount
,但还须要避免同步调用其余对象的方法(从同步代码中调用其余对象的方法可能会产生有关活性一节中描述的问题),若是没有同步语句,则必须有一个单独的、不一样步的方法,其惟一目的是调用nameList.add
。
同步语句对于经过细粒度同步提升并发性也颇有用,例如,假设类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++; } } }
谨慎使用这种用法,你必须绝对确保对受影响字段的交错访问是安全的。
回想一下,线程没法获取另外一个线程拥有的锁,可是一个线程能够得到它已经拥有的锁,容许线程屡次获取同一个锁可以使可重入同步。这描述了一种状况,其中同步代码直接或间接地调用也包含同步代码的方法,而且两组代码使用相同的锁,在没有可重入同步的状况下,同步代码必须采起许多额外的预防措施,以免线程致使自身阻塞。
在编程中,原子操做是一次有效地同时发生的操做,原子操做不能停在中间:它要么彻底发生,要么根本不发生,在操做完成以前,原子操做的反作用在完成以前是不可见的。
咱们已经看到增量表达式(如c++
),没有描述原子操做,即便很是简单的表达式也能够定义能够分解为其余操做的复杂操做,可是,你能够指定为原子操做:
long
和double
以外的全部类型),读取和写入都是原子的。volatile
的全部变量(包括long
和double
),读取和写入都是原子的。原子操做不能交错,所以可使用它们而不用担忧线程干扰,可是,这并不能消除全部同步原子操做的须要,由于仍然可能存在内存一致性错误。使用volatile
变量能够下降内存一致性错误的风险,由于对volatile
变量的任何写入都会创建与以后读取相同变量的先发生关系,这意味着对volatile
变量的更改始终对其余线程可见。更重要的是,它还意味着当线程读取volatile
变量时,它不只会看到volatile
的最新更改,还会看到致使更改的代码的反作用。
使用简单的原子变量访问比经过同步代码访问这些变量更有效,但程序员须要更加当心以免内存一致性错误,额外的功夫是否值得取决于应用程序的大小和复杂性。
java.util.concurrent包中的某些类提供了不依赖于同步的原子方法,咱们将在高级并发对象一节中讨论它们。