线程

线程调度和执行的计时是不确定的。如果两个线程同时运行,而且都不等待,您必须假设在任何两个指令之间,其它线程都可以运行并修改程序变量。

 

不仅不同机器之间的结果可能不同,而且在同一机器上多次运行同一程序也可能生成不同结果。永远不要假设一个线程会在另一个线程之前执行某些操作

 

以下示例使用两个线程,一个用于计时,一个用于执行实际工作。主线程使用非常简单的算法计算素数。

在它启动之前,它创建并启动一个计时器线程,这个线程会休眠十秒钟,然后设置一个主线程要检查的标志。十秒钟之后,主线程将停止。请注意,共享标志被声明成 volatile


/**
 * CalculatePrimes -- calculate as many primes as we can in ten seconds 
 */ 

public class CalculatePrimes extends Thread {

    public static final int MAX_PRIMES = 1000000;
    public static final int TEN_SECONDS = 10000;

    public volatile boolean finished = false;

    public void run() {
        int[] primes = new int[MAX_PRIMES];
        int count = 0;

        for (int i=2; count<MAX_PRIMES; i++) {

            // Check to see if the timer has expired
            if (finished) {
                break;
            }

            boolean prime = true;
            for (int j=0; j<count; j++) {
                if (i % primes[j] == 0) {
                    prime = false;
                    break;
                }
            }

            if (prime) {
                primes[count++] = i;
                System.out.println("Found prime: " + i);
            }
        }
    }

    public static void main(String[] args) {
        CalculatePrimes calculator = new CalculatePrimes();
        calculator.start();
        try {
            Thread.sleep(TEN_SECONDS);
        }
        catch (InterruptedException e) {
            // fall through
        }

        calculator.finished = true;
    }
}

线程与进程有许多共同点,不同的是线程与同一进程中的其它线程共享相同的进程上下文,包括内存。这非常便利,但也有重大责任。只要访问共享变量(静态或实例字段),线程就可以方便地互相交换数据,但线程还必须确保它们以受控的方式访问共享变量,以免它们互相干扰对方的更改。 任何线程可以访问所有其作用域内的变量,就象主线程可以访问该变量一样。素数示例使用了一个公用实例字段,叫做finished用于表示已经过了指定的时间。当计时器过期时,一个线程会写这个字段;另一个线程会定期读取这个字段,以检查它是否应该停止。注:这个字段被声明成 volatile,这对于这个程序的正确运行非常重要。

 

 

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

Synchronized 有两个重要含义:它确保了一次只有一个线程可以执行代码的受保护部分(互斥,mutual exclusion 或者说 mutex

,而且它确保了一个线程更改的数据对于其它线程是可见的(更改的可见性)。

如果没有同步,数据很容易就处于不一致状态。例如,如果一个线程正在更新两个相关值(比如,粒子的位置和速率),而另一个线程正在

读取这两个值,有可能在第一个线程只写了一个值,还没有写另一个值的时候,调度第二个线程运行,这样它就会看到一个旧值和一个新值。

同步让我们可以定义必须原子地运行的代码块,这样对于其他线程而言,它们要么都执行,要么都不执行。

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

 

Volatile比同步更简单,只适合于控制对基本变量(整数、布尔变量等)的单个实例的访问。当一个变量被声明成 volatile任何

对该变量的写操作都会绕过高速缓存,直接写入主内存,而任何对该变量的读取也都绕过高速缓存,直接取自主内存。这表示所有线程在任

何时候看到的 volatile 变量值都相同。

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

 

 

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

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

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

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

根本不执行。

 

 

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

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

 

 

 

创建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 方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章