共享数据的访问,其实就是协调同步

为了确保可以在线程之间以受控方式共享数据,Java 语言提供了两个关键字: synchronizedvolatile

Synchronized 有两个重要含义:它确保了一次只有一个线程可以执行代码的受保护部分(互斥,mutual exclusion 或者说 mutex),而且它确保了一个线程更改的数据对于其它线程是可见的(更改的可见性)。

如果没有同步,数据很容易就处于不一致状态。例如, 如果一个线程正在更新两个相关值(比如,粒子的位置和速率),而另一个线程正在读取这两个值,有可能在第一个线程只写了一个值,还没有写另一个值的时候,调度第二个线程运行,这样它就会看到一个旧值和一个新值。同步让我们可以定义必须原子地运行的代码块,这样对于其他线程而言,它们要么都执行,要么都不执行

同步的原子执行或互斥方面类似于其它操作环境中的临界段的概念


确保共享数据更改的可见性

同步可以让我们确保线程看到一致的内存视图

处理器可以使用高速缓存加速对内存的访问(或者编译器可以将值存储到寄存器中以便进行更快的访问)。在一些多处理器体系结构上,如果在一个处理器的高速缓存中修改了内存位置,没有必要让其它处理器看到这一修改,直到刷新了写入器的高速缓存并且使读取器的高速缓存无效。

这表示在这样的系统上,对于同一变量,在两个不同处理器上执行的两个线程可能会看到两个不同的值!这听起来很吓人,但它却很常见。它只是表示在访问其它线程使用或修改的数据时,必须遵循某些规则。

Volatile 比同步更简单,只适合于控制对基本变量(整数、布尔变量等)的单个实例的访问。当一个变量被声明成 volatile,任何对该变量的写操作都会绕过高速缓存,直接写入主内存,而任何对该变量的读取也都绕过高速缓存,直接取自主内存。这表示所有线程在任何时候看到的 volatile 变量值都相同

如果没有正确的同步,线程可能会看到旧的变量值,或者引起其它形式的数据损坏


用锁保护的原子代码块
Volatile 对于确保每个线程看到最新的变量值非常有用,但有时我们需要保护比较大的代码片段,如涉及更新多个变量的片段。

同步使用监控器(monitor)或锁的概念,以协调对特定代码块的访问。

每个 Java 对象都有一个相关的锁。 同一时间只能有一个线程持有 Java 锁当线程进入 synchronized 代码块时,线程会阻塞并等待,直到锁可用,当它可用时,就会获得这个锁,然后执行代码块。当控制退出受保护的代码块时,即到达了代码块末尾或者抛出了没有在 synchronized 块中捕获的异常时,它就会释放该锁

这样,每次只有一个线程可以执行受给定监控器保护的代码块。从其它线程的角度看,该代码块可以看作是原子的,它要么全部执行,要么根本不执行


简单的同步示例

使用 synchronized 块可以让您将一组相关更新作为一个集合来执行,而不必担心其它线程中断或看到计算的中间结果。以下示例代码将打印“1 0”或“0 1”。如果没有同步,它还会打印“1 1”(或“0 0”,随便您信不信)。


[b]public class SyncExample { 
  private static lockObject = new Object();
  private static class Thread1 extends Thread { 
    public void run() { 
      synchronized (lockObject) {
        x = y = 0;
        System.out.println(x);
      }
    }
  }

  private static class Thread2 extends Thread { 
    public void run() { 
      synchronized (lockObject) {
        x = y = 1;
        System.out.println(y);
      }
    }
  }

  public static void main(String[] args) {
    new Thread1().run();
    new Thread2().run();
  }[/b]}


这个例子要看到内部中去:当执行其中任何一个方法时候,x和y被赋1或者0,如果这时候另一个线程抢去资源,使用system。out。print(x)时,将和原来的一样

         

在这两个线程中都必须使用同步,以便使这个程序正确工作

Java 锁定J
ava 锁定合并了一种互斥形式。每次只有一个线程可以持有锁。锁用于保护代码块或整个方法,必须记住是锁的身份保护了代码块,而不是代码块本身,这一点很重要。一个锁可以保护许多代码块或方法。

反之,仅仅因为代码块由锁保护并不表示两个线程不能同时执行该代码块。它只表示如果两个线程正在等待相同的锁,则它们不能同时执行该代码。

在以下示例中,两个线程可以同时不受限制地执行 setLastAccess() 中的 synchronized 块,因为每个线程有一个不同的 thingie 值。因此,synchronized 代码块受到两个正在执行的线程中不同锁的保护。


public class SyncExample {
  public static class Thingie {

    private Date lastAccess;

    public synchronized void setLastAccess(Date date) {
      this.lastAccess = date;
    }
  }

  public static class MyThread extends Thread { 
    private Thingie thingie;

    public MyThread(Thingie thingie) {
      this.thingie = thingie;
    }

    public void run() {
      thingie.setLastAccess(new Date());
    }
  }

  public static void main() { 
    Thingie thingie1 = new Thingie(), 
      thingie2 = new Thingie();

    new MyThread(thingie1).start();
    new MyThread(thingie2).start();
  }
}


同步的方法

创建 synchronized 块的最简单方法是将方法声明成 synchronized。这表示在进入方法主体之前,调用者必须获得锁:


public class Point {
  public synchronized void setXY(int x, int y) {
    this.x = x;
    this.y = y;
  }
}
    
     

对于普通的 synchronized方法, 这个锁是一个对象,将针对它调用方法。 对于静态 synchronized 方法,这个锁是与 Class 对象相关的监控器,在该对象中声明了方法。

仅仅因为 setXY() 被声明成 synchronized 并不表示两个不同的线程不能同时执行 setXY(),只要它们调用不同的 Point 实例的 setXY() 就可同时执行。对于一个 Point 实例,一次只能有一个线程执行 setXY(),或 Point 的任何其它 synchronized 方法。

大多数类并没有同步
因为同步会带来小小的性能损失,大多数通用类,如 java.util 中的 Collection 类,不在内部使用同步。这表示在没有附加同步的情况下,不能在多个线程中使用诸如 HashMap 这样的类。

通过每次访问共享集合中的方法时使用同步,可以在多线程应用程序中使用 Collection 类。对于任何给定的集合,每次必须用同一个锁进行同步。通常可以选择集合对象本身作为锁。

下一页中的示例类 SimpleCache 显示了如何使用 HashMap 以线程安全的方式提供高速缓存。但是,通常适当的同步并不只是意味着同步每个方法。

Collections 类提供了一组便利的用于 List、Map 和 Set 接口的封装器。您可以用 Collections.synchronizedMap 封装 Map,它将确保所有对该映射的访问都被正确同步。

如果类的文档没有说明它是线程安全的,那么您必须假设它不是


示例:简单的线程安全的高速缓存
如以下代码样本所示,SimpleCache.java 使用 HashMap 为对象装入器提供了一个简单的高速缓存。load() 方法知道怎样按对象的键装入对象。在一次装入对象之后,该对象就被存储到高速缓存中,这样以后的访问就会从高速缓存中检索它,而不是每次都全部地装入它。对共享高速缓存的每个访问都受到 synchronized 块保护。由于它被正确同步,所以多个线程可以同时调用 getObject 和 clearCache 方法,而没有数据损坏的风险。


public class SimpleCache {
  private final Map cache = new HashMap();

  public Object load(String objectName) { 
    // load the object somehow
  }

  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;
  }
}







小结
就象程序一样,线程有生命周期:它们启动、执行,然后完成。一个程序或进程也许包含多个线程,而这些线程看来互相单独地执行。

线程是通过实例化 Thread 对象或实例化继承 Thread 的对象来创建的,但在对新的 Thread 对象调用 start() 方法之前,这个线程并没有开始执行。当线程运行到其 run() 方法的末尾或抛出未经处理的异常时,它们就结束了。

sleep() 方法可以用于等待一段特定时间;而 join() 方法可能用于等到另一个线程完成。

你可能感兴趣的:(多线程,thread,数据结构,cache,J#)