简体   繁体   English

何时使用volatile和synchronized

[英]When to use volatile and synchronized

I know there are many questions about this, but I still don't quite understand. 我知道有很多问题,但我仍然不太明白。 I know what both of these keywords do, but I can't determine which to use in certain scenarios. 我知道这两个关键字的作用,但我无法确定在某些情况下使用哪个。 Here are a couple of examples that I'm trying to determine which is the best to use. 以下是一些我正在尝试确定最适合使用的示例。

Example 1: 例1:

import java.net.ServerSocket;

public class Something extends Thread {

    private ServerSocket serverSocket;

    public void run() {
        while (true) {
            if (serverSocket.isClosed()) {
                ...
            } else { //Should this block use synchronized (serverSocket)?
                //Do stuff with serverSocket
            }
        }
    }

    public ServerSocket getServerSocket() {
        return serverSocket;
    }

}

public class SomethingElse {

    Something something = new Something();

    public void doSomething() {
        something.getServerSocket().close();
    }

}

Example 2: 例2:

public class Server {

    private int port;//Should it be volatile or the threads accessing it use synchronized (server)?

    //getPort() and setPort(int) are accessed from multiple threads
    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

}

Any help is greatly appreciated. 任何帮助是极大的赞赏。

A simple answer is as follows: 一个简单的答案如下:

  • synchronized can always be used to give you a thread-safe / correct solution, synchronized可以始终用于为您提供线程安全/正确的解决方案,

  • volatile will probably be faster, but can only be used to give you a thread-safe / correct in limited situations. volatile可能会更快,但只能用于在有限的情况下为您提供线程安全/正确。

If in doubt, use synchronized . 如有疑问,请使用synchronized Correctness is more important than performance. 正确性比表现更重要。

Characterizing the situations under which volatile can be used safely involves determining whether each update operation can be performed as a single atomic update to a single volatile variable. 表征可以安全使用volatile的情况涉及确定每个更新操作是否可以作为单个volatile变量的单个原子更新来执行。 If the operation involves accessing other (non-final) state or updating more than one shared variable, it cannot be done safely with just volatile. 如果操作涉及访问其他(非最终)状态或更新多个共享变量,则只能使用volatile进行安全操作。 You also need to remember that: 你还需要记住:

  • updates to non-volatile long or a double may not be atomic, and 对非易失性longdouble可能不是原子的,并且
  • Java operators like ++ and += are not atomic. +++=这样的Java运算符不是原子的。

Terminology: an operation is "atomic" if the operation either happens entirely, or it does not happen at all. 术语:如果操作完全发生,或者根本不发生,则操作是“原子的”。 The term "indivisible" is a synonym. 术语“不可分割的”是同义词。

When we talk about atomicity, we usually mean atomicity from the perspective of an outside observer; 当我们谈论原子性时,我们通常从外部观察者的角度来看原子性; eg a different thread to the one that is performing the operation. 例如,与执行操作的线程不同的线程。 For instance, ++ is not atomic from the perspective of another thread, because that thread may be able to observe state of the field being incremented in the middle of the operation . 例如,从另一个线程的角度来看, ++不是原子的,因为该线程可能能够观察到在操作过程中递增的字段的状态。 Indeed, if the field is a long or a double , it may even be possible to observe a state that is neither the initial state or the final state! 实际上,如果场是longdouble ,甚至可能观察到既不是初始状态也不是最终状态的状态!

The synchronized keyword synchronized关键字

synchronized indicates that a variable will be shared among several threads. synchronized表示变量将在多个线程之间共享。 It's used to ensure consistency by "locking" access to the variable, so that one thread can't modify it while another is using it. 它用于通过“锁定”对变量的访问来确保一致性,这样一个线程就无法修改它而另一个线程使用它。

Classic Example: updating a global variable that indicates the current time 经典示例:更新指示当前时间的全局变量
The incrementSeconds() function must be able to complete uninterrupted because, as it runs, it creates temporary inconsistencies in the value of the global variable time . incrementSeconds()函数必须能够不间断地完成,因为它在运行时会在全局变量time的值中创建临时不一致。 Without synchronization, another function might see a time of "12:60:00" or, at the comment marked with >>> , it would see "11:00:00" when the time is really "12:00:00" because the hours haven't incremented yet. 如果没有同步,另一个函数可能会看到“12:60:00”的time ,或者在标有>>>的注释中,当时间真的是“12:00:00”时,它会看到“11:00:00”因为小时数还没有增加。

void incrementSeconds() {
  if (++time.seconds > 59) {      // time might be 1:00:60
    time.seconds = 0;             // time is invalid here: minutes are wrong
    if (++time.minutes > 59) {    // time might be 1:60:00
      time.minutes = 0;           // >>> time is invalid here: hours are wrong
      if (++time.hours > 23) {    // time might be 24:00:00
        time.hours = 0;
      }
    }
  }

The volatile keyword volatile关键字

volatile simply tells the compiler not to make assumptions about the constant-ness of a variable, because it may change when the compiler wouldn't normally expect it. volatile只是告诉编译器不要对变量的常量做出假设,因为它可能会在编译器通常不期望它时发生变化。 For example, the software in a digital thermostat might have a variable that indicates the temperature, and whose value is updated directly by the hardware. 例如,数字恒温器中的软件可能具有指示温度的变量,其值由硬件直接更新。 It may change in places that a normal variable wouldn't. 它可能会在正常变量不会发生变化的地方发生变化。

If degreesCelsius is not declared to be volatile , the compiler is free to optimize this: 如果未将degreesCelsius声明为volatile ,则编译器可以自由优化:

void controlHeater() {
  while ((degreesCelsius * 9.0/5.0 + 32) < COMFY_TEMP_IN_FAHRENHEIT) {
    setHeater(ON);
    sleep(10);
  }
}

into this: 进入这个:

void controlHeater() {
  float tempInFahrenheit = degreesCelsius * 9.0/5.0 + 32;

  while (tempInFahrenheit < COMFY_TEMP_IN_FAHRENHEIT) {
    setHeater(ON);
    sleep(10);
  }
}

By declaring degreesCelsius to be volatile , you're telling the compiler that it has to check its value each time it runs through the loop. 通过将degreesCelsius声明为volatile ,您告诉编译器每次运行循环时都必须检查其值。

Summary 摘要

In short, synchronized lets you control access to a variable, so you can guarantee that updates are atomic (that is, a set of changes will be applied as a unit; no other thread can access the variable when it's half-updated). 简而言之, synchronized允许您控制对变量的访问,因此您可以保证更新是原子的(即,一组更改将作为一个单元应用;没有其他线程可以在半更新时访问该变量)。 You can use it so ensure consistency of your data. 您可以使用它,以确保数据的一致性。 On the other hand, volatile is an admission that the contents of a variable are beyond your control, so the code must assume it can change at any time. 另一方面, volatile是承认变量的内容超出了你的控制范围,所以代码必须假设它可以随时改变。

There is insufficient information in your post to determine what is going on, which is why all the advice you are getting is general information about volatile and synchronized . 您的帖子中没有足够的信息来确定发生了什么,这就是为什么您获得的所有建议都是关于volatilesynchronized一般信息。

So, here's my general advice: 所以,这是我的一般建议:

During the cycle of writing-compiling-running a program, there are two optimization points: 在编写 - 编译 - 运行程序的循环期间,有两个优化点:

  • at compile time, when the compiler might try to reorder instructions or optimize data caching. 在编译时,编译器可能会尝试重新排序指令或优化数据缓存。
  • at runtime, when the CPU has its own optimizations, like caching and out-of-order execution. 在运行时,当CPU有自己的优化时,比如缓存和乱序执行。

All this means that instructions will most likely not execute in the order that you wrote them, regardless if this order must be maintained in order to ensure program correctness in a multithreaded environment. 所有这些意味着指令很可能不会按照您编写它们的顺序执行,无论是否必须维护此顺序以确保多线程环境中的程序正确性。 A classic example you will often find in the literature is this: 您将在文献中经常发现的一个典型例子是:

class ThreadTask implements Runnable {
    private boolean stop = false;
    private boolean work;

    public void run() {
        while(!stop) {
           work = !work; // simulate some work
        } 
    }

    public void stopWork() {
        stop = true; // signal thread to stop
    }

    public static void main(String[] args) {
        ThreadTask task = new ThreadTask();
        Thread t = new Thread(task);
        t.start();
        Thread.sleep(1000);
        task.stopWork();
        t.join();
    }
}

Depending on compiler optimizations and CPU architecture, the above code may never terminate on a multi-processor system. 根据编译器优化和CPU架构,上述代码可能永远不会在多处理器系统上终止。 This is because the value of stop will be cached in a register of the CPU running thread t , such that the thread will never again read the value from main memory, even thought the main thread has updated it in the meantime. 这是因为stop的值将被缓存在CPU运行线程t的寄存器中,这样线程将永远不会再次从主内存读取值,即使主线程已经同时更新了它。

To combat this kind of situation, memory fences were introduced. 为了对抗这种情况,引入了记忆围栏 These are special instructions that do not allow regular instructions before the fence to be reordered with instructions after the fence. 这些是特殊说明,不允许在围栏之后使用围栏后的说明重新排序围栏之前的常规指令。 One such mechanism is the volatile keyword. 一个这样的机制是volatile关键字。 Variables marked volatile are not optimized by the compiler/CPU and will always be written/read directly to/from main memory. 标记为volatile变量未由编译器/ CPU优化,并且将始终直接写入/读取主存储器。 In short, volatile ensures visibility of a variable's value across CPU cores . 简而言之, volatile确保了跨CPU核心的变量值的可见性

Visibility is important, but should not be confused with atomicity . 可见性很重要,但不应与原子性相混淆。 Two threads incrementing the same shared variable may produce inconsistent results even though the variable is declared volatile . 即使变量声明为volatile两个递增相同共享变量的线程也可能产生不一致的结果。 This is due to the fact that on some systems the increment is actually translated into a sequence of assembler instructions that can be interrupted at any point. 这是因为在某些系统上,增量实际上被转换为可在任何点上中断的汇编指令序列。 For such cases, critical sections such as the synchronized keyword need to be used. 对于这种情况,需要使用关键部分,例如synchronized关键字。 This means that only a single thread can access the code enclosed in the synchronized block. 这意味着只有一个线程可以访问synchronized块中包含的代码。 Other common uses of critical sections are atomic updates to a shared collection, when usually iterating over a collection while another thread is adding/removing items will cause an exception to be thrown. 关键部分的其他常见用途是对共享集合的原子更新,当通常迭代集合而另一个线程正在添加/删除项目时将导致抛出异常。

Finally two interesting points: 最后两点有趣:

  • synchronized and a few other constructs such as Thread.join will introduce memory fences implicitly. synchronized和一些其他结构如Thread.join将隐式引入内存栅栏。 Hence, incrementing a variable inside a synchronized block does not require the variable to also be volatile , assuming that's the only place it's being read/written. 因此,在synchronized块内增加变量不要求变量也是volatile ,假设它是唯一被读/写的地方。
  • For simple updates such as value swap, increment, decrement, you can use non-blocking atomic methods like the ones found in AtomicInteger , AtomicLong , etc. These are much faster than synchronized because they do not trigger a context switch in case the lock is already taken by another thread. 对于诸如值交换,递增,递减之类的简单更新,您可以使用非阻塞原子方法,如AtomicIntegerAtomicLong等中的那些。这些方法比synchronized快得多,因为它们不会在锁定时触发上下文切换已经被另一个线程占用了。 They also introduce memory fences when used. 它们在使用时也会引入内存栅栏。

volatile solves “visibility” problem across CPU cores. volatile解决了CPU内核的“可见性”问题。 Therefore, value from local registers is flushed and synced with RAM. 因此,本地寄存器的值被刷新并与RAM同步。 However, if we need consistent value and atomic op, we need a mechanism to defend the critical data. 但是,如果我们需要一致的值和原子操作,我们需要一种机制来保护关键数据。 That can be achieved by either synchronized block or explicit lock. 这可以通过synchronized块或显式锁来实现。

Note: In your first example, the field serverSocket is actually never initialized in the code you show. 注意:在第一个示例中,字段serverSocket实际上从未在您显示的代码中初始化。

Regarding synchronization, it depends on whether or not the ServerSocket class is thread safe. 关于同步,它取决于ServerSocket类是否是线程安全的。 (I assume it is, but I have never used it.) If it is, you don't need to synchronize around it. (我假设它是,但我从未使用它。)如果是,你不需要围绕它同步。

In the second example, int variables can be atomically updated so volatile may suffice. 在第二个示例中, int变量可以原子更新,因此volatile可能就足够了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM