何时使用volatile和synchronized

Str*_*ies 31 java multithreading

我知道有很多问题,但我仍然不太明白.我知道这两个关键字的作用,但我无法确定在某些情况下使用哪个.以下是一些我正在尝试确定最适合使用的示例.

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

}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

任何帮助是极大的赞赏.

Ste*_*n C 43

一个简单的答案如下:

  • synchronized 可以随时用于为您提供线程安全/正确的解决方案,

  • volatile 可能会更快,但只能用于在有限的情况下为您提供线程安全/正确.

如有疑问,请使用synchronized.正确性比表现更重要.

表征volatile可以安全使用的情况包括确定每个更新操作是否可以作为单个volatile变量的单个原子更新来执行.如果操作涉及访问其他(非最终)状态或更新多个共享变量,则只能使用volatile进行安全操作.你还需要记住:

  • 更新为非易失性longdouble可能不是原子的,和
  • Java运营商喜欢++并且+=不是原子的.

术语:如果操作完全发生,或者根本不发生,则操作是"原子的".术语"不可分割的"是同义词.

当我们谈论原子性时,我们通常从外部观察者的角度来看原子性; 例如,与执行操作的线程不同的线程.例如,++从另一个线程的角度看,它不是原子的,因为该线程可能能够观察到在操作过程中递增的字段的状态.实际上,如果场是a long或a double,甚至可能观察到既不是初始状态也不是最终状态的状态!


Ada*_*iss 22

synchronized关键字

synchronized表示变量将在多个线程之间共享.它用于通过"锁定"对变量的访问来确保一致性,这样一个线程就无法修改它而另一个线程使用它.

经典示例:更新指示当前时间
全局变量incrementSeconds()函数必须能够不间断地完成,因为在运行时,它会在全局变量的值中创建临时不一致time.如果没有同步,另一个功能可能会看到time"12:60:00",或者在标记为的评论中>>>,当时间真的是"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;
      }
    }
  }
Run Code Online (Sandbox Code Playgroud)

volatile关键字

volatile只是告诉编译器不要对变量的常量做出假设,因为它可能会在编译器通常不期望它时发生变化.例如,数字恒温器中的软件可能具有指示温度的变量,其值由硬件直接更新.它可能会在正常变量不会发生变化的地方发生变化.

如果degreesCelsius未声明volatile,编译器可以自由优化:

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

进入这个:

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

  while (tempInFahrenheit < COMFY_TEMP_IN_FAHRENHEIT) {
    setHeater(ON);
    sleep(10);
  }
}
Run Code Online (Sandbox Code Playgroud)

通过声明degreesCelsiusvolatile,你告诉它有每次通过循环运行的时间来检查它的价值的编译器.

摘要

简而言之,synchronized允许您控制对变量的访问,因此您可以保证更新是原子的(即,一组更改将作为一个单元应用;没有其他线程可以在半更新时访问该变量).您可以使用它,以确保数据的一致性.另一方面,volatile承认变量的内容超出了您的控制范围,因此代码必须假设它可以随时更改.


Tud*_*dor 10

您的帖子中没有足够的信息来确定发生了什么,这就是为什么您获得的所有建议都是关于volatile和的一般信息synchronized.

所以,这是我的一般建议:

在编写 - 编译 - 运行程序的循环期间,有两个优化点:

  • 在编译时,编译器可能会尝试重新排序指令或优化数据缓存.
  • 在运行时,当CPU有自己的优化时,比如缓存和乱序执行.

所有这些意味着指令很可能不会按照您编写它们的顺序执行,无论是否必须维护此顺序以确保多线程环境中的程序正确性.您将在文献中经常发现的一个典型例子是:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

根据编译器优化和CPU架构,上述代码可能永远不会在多处理器系统上终止.这是因为值stop将被缓存在CPU运行线程的寄存器中t,这样线程将永远不会再次从主内存中读取值,即使主线程已经同时更新了它.

为了对抗这种情况,引入了记忆围栏.这些是特殊说明,不允许在围栏之后使用围栏后的说明重新排序围栏之前的常规指令.一个这样的机制是volatile关键字.标记volatile的变量未由编译器/ CPU优化,并且将始终直接写入/读取主存储器.简而言之,volatile确保跨CPU核心的变量值的可见性.

可见性很重要,但不应与原子性相混淆.即使声明了变量,递增相同共享变量的两个线程也可能产生不一致的结果volatile.这是因为在某些系统上,增量实际上被转换为可以在任何点上中断的汇编指令序列.对于这种情况,synchronized需要使用关键部分,例如关键字.这意味着只有一个线程可以访问synchronized块中包含的代码.关键部分的其他常见用途是对共享集合的原子更新,当通常迭代集合而另一个线程正在添加/删除项目时将导致抛出异常.

最后两点有趣:

  • synchronized以及其他一些构造如Thread.join隐含地引入内存栅栏.因此,增加synchronized块内的变量也不需要变量volatile,假设它是唯一被读/写的地方.
  • 对于简单的更新,如价值交换,递增,递减,你可以使用非阻塞原子的方法,如发现的那些AtomicInteger,AtomicLong等等,这些都是比快很多synchronized,因为他们没有触发的情况下,锁已经采取的上下文切换另一个线程.它们在使用时也会引入内存栅栏.


Pét*_*rök 1

注意:在您的第一个示例中,该字段serverSocket实际上从未在您显示的代码中初始化。

关于同步,取决于该类是否ServerSocket是线程安全的。(我认为是,但我从未使用过它。)如果是,则无需围绕它进行同步。

在第二个示例中,int变量可以自动更新,因此volatile可能就足够了。