同步非常非常重要,我们在处理多线程问题时,基本上都要使用Synchronized关键字。
因此必须重点理解。
首先,多线程会出现问题的根本原因是数据共享。
Threads communicate primarily by sharing access to fields and the objects reference fields refer to. This form of communication is extremely efficient, but makes two kinds of errors possible:thread interference and memory consistency errors . The tool needed to prevent these errors is synchronization .
这篇文章将要谈到的问题是,为什么多线程会有问题,以及怎么解决。
Thread interference 线程间干扰
一个简单的例子:一个class有一个成员变量,两个方法,一个加一个减,用成员变量保存运算结果,用get方法提供给外部代码。
Threads communicate primarily by sharing access to fields and the objects reference fields refer to。也就是说,多个线程操作的是同一个对象。
即使是最简单的c++, c--操作,实际上也不是原子操作(Atomic access)。在执行时,实际可能是以下步骤:
1, 取到c的值。
2, 加1。
3, 写回c的值。
而多任务操作系统的特点就是:线程会随时被挂起,cpu将时间slice交给另外一个线程执行。假如上述步骤中应执行3的时候被挂起,另一个线程修改了c的值。之后第一个线程又被唤醒,写回了c值。那样第二个线程的修改就丢失了。
Java的Object基类实现了一个monitor,本质上是一种内置锁,因此所有Object的子类都具有该功能,实例化出来的每一个对象都有这个锁。当一个线程需要操作对象的时候,需要先检查对象的锁是否被占用,如果被占用则需要等待,如果未被占用则取得这个锁。
我们可以看到之后将要谈到的同步synchronized,在本质上就是:在一个线程取得内置锁,执行代码段完毕并释放内置锁之前,该线程的操作绝对不会被干扰(线程一样可能在执行代码段时被挂起,但是由于该线程持有内置锁,因此其他线程绝对无法改变共享对象)。如此就保证了一个线程一定能完整的,不被干扰的执行完一段代码,就形成了类似原子操作。
Memory consistency error也会产生问题,但是原因复杂没有详述。
避免这种error的方法就是使用happens-before relationships. 希望一个线程一定在另一个线程前执行。
synchronized
解决以上两种问题的方法就是使用synchronized 关键字。
Java有两种synchronization idioms:synchronized method and synchronized statement。
- 同步方法 synchronized method
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
//仅仅需要简单的在方法前添加synchronized关键字
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
//这样在线程调用SynchronizedCounter 类的实例的方法时,只有先获得这个实例的锁,才能运行这几个方法。也就是说,在一个线程执行完同步方法,并释放该实例的内部锁前,其他线程都会被阻塞。
//注意只有多个线程试图操作同一个类的实例对象时,synchronized 才会发挥作用。
//在构造函数上使用synchronized关键字是没有意义的,因为synchronized对于每一个实例才有意义。
//final
成员变量也不需要同步方法. which cannot be modified after the object is constructed。
下面是关于使用同步方法要注意的两点:
- First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.
- Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.
进一步解释synchronized工作的原理是:
只有当一个线程取得了实例对象的锁(intrinsic lock)后,这个线程才能执行类中的synchronized方法。
当一个线程占有一个对象的锁时,其它线程无法取得该锁,直到第一个线程释放锁。
一个线程企图调用一个synchronized方法时,它会自动请求对象的锁,同时在使用完毕后释放。
不管该线程执行synchronized方法是否正常结束(例如遇到了异常),仍然会释放锁。
静态同步方法让人有些奇怪,因为静态方法是属于类,而不是实例的。使用静态同步方法会控制线程对该类的静态成员变量的访问。
- 同步语句 synchronized statement
上面取得对象锁的同步方案隐含了一个问题:一个对象上的所有同步方法都只能串行执行,即使其中一些同步方法并不是互斥的即使同时运行也没问题。另外如果一个同步方法操作了多个对象,第一个对象锁被占用会导致整个线程被挂起,block了对其他对象的操作。因此使用同步方法的本质是:方法中所有操作成为了一个原子操作,成为了一个整体,是以牺牲性能为代价的。
另外一种实现同步的方法就是同步语句。同步语句必须指明提供内部锁的对象:
public void addName(String name) {
synchronized(this) { //即就是当前实例
lastName = name;
nameCount++;
}
nameList.add(name);
}
在这个例子中,实现的是同步对当前实例的lastName 和nameCount的修改,但是没有限制对另外一个对象nameList.add调用。实现了更细粒度的控制。而如果是synchronized method,则无法实现,必须将原方法劈成两个。
所以同步语句很实用,因为一个方法操作的可能是多个对象实例。
使用同步语句的另一个好处就是可以提高并发度。比如下面的例子:c1和c2之间没有任何关系,我们需要的是不同线程不能同时修改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++;
//通过提供两个不用的对象lock1和lock2,线程修改两个变量就可以互相不影响了。
//这种解决办法的本质是:在同步方法中只能使用object的内置锁,因此无法实现对不同成员变量的分别控制
//而同步代码块则是自己指定要使用的锁,并实现控制分离。
}
}
}