在一个应用程序中使用多线程,一般情况下,这多个线程之间是要互相通信并共享结果的。让多个线程共享结果的最简单方法是使用共享变量,线程与同一个进程中的其他线程共享相同的进程上下文,包括内存,因此只要访问共享变量(静态或实例字段),线程就可以互相交换数据。要保证这些共享变量值从一个线程正确传播到另一个线程,保证一个线程在更新一些相关数据时其他线程看到一致的中间结果,要使用同步。即通过使用同步来保护对共享变量的访问,这样就可以确保线程以可预料的方式与程序变量进行交互。访问局部变量(基于堆栈的)不需要同步,因为它们只能被自己所属的线程访问。
Java提供的用于同步的关键字有两个:synchronized和volatile。synchronized有两个重要含义:它确保一次只有一个线程可以执行代码的synchronized代码段部分,同时确保一个线程更改的数据对于其他线程是可见的。volatile只适用于控制对基本变量(如整数、布尔型变量等)的单个实例的访问。当一个变量被声明为volatile,任何对该变量的写操作都会绕过高速缓存直接写入主内存,而任何对该变量的读操作也都绕过高速缓存直接取主内存。这样来保证所有线程在任何时候看到的volatile变量值都相同。
每个java对象都有一个相关的锁,即每个java对象都可以充当锁。同一时间只有一个线程可以持有锁。锁用于保护代码块或整个方法。当线程要进入synchronized代码块时,线程会被阻塞并等待直到锁可用。当锁可用时就会获得这个锁,才能执行synchronized代码块。当控制退出synchronized代码块时,即到达了代码块末尾,或者抛出了没有在synchronized代码块中捕获的异常时,它才会释放锁。反之,仅仅因为代码块由锁保护并不表示两个线程不能同时执行该代码块,它只表示如果两个线程正在等待相同的锁的情况下它们是不能同时拥有锁来执行代码块的。
使用synchronized的方法有2种,其中相对简单的一种是将方法声明synchronized。表示在进入方法主体之前,调用者必须获得锁,这个锁是一个对象,将针对它调用方法。对于静态synchronized方法,这个锁是与Class对象相关的监控器,在该对象中声明了方法。另一种采用synchronized块的语法比synchronized方法稍微复杂一点,因为还需要显式地指定锁以及锁要保护哪个块。很多时候我们使用this引用作为锁,这表示该代码块将与这个类中的synchronized方法使用同一个锁。由于同步防止了多个线程同时执行一个代码块,因此性能上会有些损耗,即使是在单处理器系统上,最好在尽可能小的代码块上使用同步。
示例代码如下:
import java.util.HashMap;
import java.util.Map;public class SimpleCache {
private Map cache = new HashMap();
public Object load(String objectName){
//load the object somehow
Object obj = new Object();
return obj;
}
public void clearCache(){
synchronized(cache){
cache.clear();
}
}
public Object getObject(String objectName){
synchronized(cache){
Object o = cache.get(objectName);
if(o==null){
o = load(objectName);
cache.put(objectName, o);
}
return o;
}
}
}
什么时候必须同步?
只要是在几个线程之间共享非final的变量,就必须使用synchronized或volatile。在以下情况中必须同步:
- 读取上一次可能由另一个线程写入的变量
- 写入下一次可能由另一个线程读取的变量
什么时候不需要同步?
- 有静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
- 在创建线程之前创建对象时
- 线程可以看见它将要处理的对象时
- 访问final字段时
同步准则:
- 不要阻塞。不要在synchronized块或者方法中调用可能引起阻塞的方法,如InputStream.read()。
- 在持有锁的时候不要对其它对象调用方法。这样可以消除最常见的死锁。要避免死锁,应该确保在获取多个锁时,在所有的线程中都以相同的顺序获取锁。
- 使同步块尽可能的简洁。在保证相关数据操作的完整性的同时,把不随线程变化的预处理和后处理都移出synchronized块。
注:
死锁
当一个线程需要一个资源而另一个线程持有该资源的锁时,就会发生死锁。这种情况通常可以用下面2种方法解决:将多个锁组成一组并放到同一个锁下,或在所有的线程中按相同的次序获取所有资源锁。例如,如果有四个资源 ―A、B、C 和 D ― 并且一个线程可能要获取四个资源中任何一个资源的锁,则请确保在获取对 B 的锁之前首先获取对 A 的锁,依此类推。如果“线程 1”希望获取对 B 和 C 的锁,而“线程 2”获取了 A、C 和 D 的锁,则这一技术可能导致阻塞,但它永远不会在这四个锁上造成死锁。
活锁
当一个线程忙于接受新任务以致它永远没有机会完成任何任务时,就会发生活锁。这个线程最终将超出缓冲区并导致程序崩溃。试想一个秘书需要录入一封信,但她一直在忙于接电话,所以这封信永远不会被录入。