Java多线程初学者指南(6):慎重使用volatile关键字

来源:互联网 发布:淘宝类目分析 编辑:程序博客网 时间:2024/05/16 09:25

volatile关键字相信了解Java多线程的读者都很清楚它的作用。volatile关键字用于声明简单类型变量,如int、float、boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的。但这有一定的限制。例如,下面的例子中的n就不是原子级别的:


Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->package mythread;

public class JoinThread extends Thread
{
    
public staticvolatile int n = 0;
    public void run()
    {
        
for (int i = 0; i < 10; i++)
            
try
        {
                n 
= n + 1;
                sleep(
3); // 为了使运行结果更随机,延迟3毫秒

            }
            
catch (Exception e)
            {
            }
    }

    
public static void main(String[] args) throws Exception
    {

        Thread threads[] 
= new Thread[100];
        
for (int i = 0; i < threads.length; i++)
            
// 建立100个线程
            threads[i] = new JoinThread();
        
for (int i = 0; i < threads.length; i++)
            
// 运行刚才建立的100个线程
            threads[i].start();
        
for (int i = 0; i < threads.length; i++)
            
// 100个线程都执行完后继续
            threads[i].join();
        System.out.println(
"n=" + JoinThread.n);
    }
}


     如果对n的操作是原子级别的,最后输出的结果应该为n=1000,而在执行上面积代码时,很多时侯输出的n都小于1000,这说明n=n+1不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:


Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->= n + 1;
n
++;


      如果要想使这种情况变成原子操作,需要使用synchronized关键字,如上的代码可以改成如下的形式:


Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->package mythread;

public class JoinThread extends Thread
{
    
public staticint n = 0;

    
public static synchronized void inc()
    {
        n
++;
    }
    
public void run()
    {
        
for (int i = 0; i < 10; i++)
            
try
            {
                inc(); 
// n = n + 1 改成了 inc();
                sleep(3); // 为了使运行结果更随机,延迟3毫秒

            }
            
catch (Exception e)
            {
            }
    }

    
public static void main(String[] args) throws Exception
    {

        Thread threads[] 
= new Thread[100];
        
for (int i = 0; i < threads.length; i++)
            
// 建立100个线程
            threads[i] = new JoinThread();
        
for (int i = 0; i < threads.length; i++)
            
// 运行刚才建立的100个线程
            threads[i].start();
        
for (int i = 0; i < threads.length; i++)
            
// 100个线程都执行完后继续
            threads[i].join();
        System.out.println(
"n=" + JoinThread.n);
    }
}


    上面的代码将n=n+1改成了inc(),其中inc方法使用了synchronized关键字进行方法同步。因此,在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1、n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。


Synchronized和volatile的区别:


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

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

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

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


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

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

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

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

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

 简单的同步示例第 6 页(共12 页)

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

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

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

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

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

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

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


原创粉丝点击