java—当一个线程初始化值而其他线程只是读取它时,是否存在并发性问题

mqxuamgl  于 2021-07-09  发布在  Java
关注(0)|答案(4)|浏览(335)

假设我有一个这样的共享对象:

class Shared {

   private Value val;

   //synchronized set
   public synchronized void setValue(Value val) {
      if (this.val == null) {
         this.val = val;
      } else {
          throw new IllegalStateException();
      }
   }

   //unsynchronized get
   public Value getValue() {
        return this.val;
   }
}

如果我让一个线程在应用程序初始化期间设置了值,在其他线程有机会读取之前,并且没有任何内容再次更改该值,那么不同步地读取该值是安全的,还是我冒着其他线程永远看不到该设置值的风险(因为变量不存在) volatile 而且不保证会被刷新到主内存)?想象一下,在一个web应用程序的上下文中,设置发生在servlet初始化期间。我不知道此时是否创建了其他线程,因为这是容器的工作。但是我假设到那时已经创建了一个线程拉取来处理将来的请求。
如果不安全,有没有一种方法可以安全地初始化值而不必为每次读取付出代价,即使值永远不会改变?i、 e.有没有办法只冲一次值?
另外,这不正是spring一直在做的吗?在初始化容器时,单例上的各种不同步设置正在发生:通过setters注入bean, @PostConstruct 初始化等一旦完成,请求被接受,没有修改发生。如果这是不安全的,难道不是每个单例方法都需要同步吗?

e5nszbig

e5nszbig1#

视情况而定。
在编写值和读取值的线程之间有许多操作,这些操作可能会创建“发生在”关系,您需要确保值存在。
我将讨论springmvc(以及任何servlet应用程序)的情况。springmvc通常创建两个 ApplicationContext 示例:一个 ContextLoaderListener 一个在一个 DispatcherServlet .
在典型的servlet容器上 ContextLoaderListener 以及 DispatcherServlet 将在同一线程上顺序初始化。然后,容器将启动线程来侦听连接和服务请求。
在非典型容器上,您仍然可以依赖以下事实(servlet规范,参见2.3.2初始化一章): ContextLoaderListener 以及 DispatcherServlet 在接收请求之前,必须完全初始化它(因此您的spring上下文)。
初始化完成后,容器将启动服务线程,然后
打电话给 start() 在开始的线程中的任何操作之前发生。
你的价值将会显现出来。否则,初始化线程将通过其他机制(可能是 CountDownLatch )它提供了自己的发生前关系。
假设您只从一个线程设置了该值,并且不再更改它,那么您甚至不需要 synchronized 在setter上。
看看这些发生之前的关系,你会没事的。显然,如果你不想,那么 volatile 解决方法很好。如果您的应用程序逻辑类似于servlet容器(或者是springmvc应用程序),则不需要这样做 synchronized 或者额外的 volatile . 关键是
在任何其他线程有机会读取它之前,不会再改变值
为了防止其他线程读取该值,您可能已经有了一个通知机制,该机制已经添加了将正确发布您的值的before关系。

a0x5cqrl

a0x5cqrl2#

如果有方法初始化构造函数中的元素,并使其同时 final 并且是不可变的,则构造函数的结尾与在构造对象之后启动的所有线程建立“发生在”关系,并且读取将是线程安全的。如果在构造之后的任何时候初始化元素,或者 final 或者不是不变的,则必须同步访问。 volatile 将保护您不受指针不可见更改的影响,但这不足以使引用的对象线程安全。

r6hnlfcb

r6hnlfcb3#

这不安全。
你需要进入这个领域 volatile :

private volatile Value val;

其他线程可以缓存自己的非易失性字段副本,因此一个线程对它们所做的更改可能不会被其他线程“看到”。 volatile 强制所有线程立即“查看”对字段的更改(实际上,它强制线程不使用缓存副本)。

ckocjqey

ckocjqey4#

这是一场数据竞赛,所以不安全。
这是一个数据竞争,因为有两个线程,一个写一个值,另一个读一个值,它们之间没有同步。当你使用 synchronized 关键字,当一个线程在第一个线程释放锁2之后获取锁时,同步就开始了。
自从 getValue() 方法不同步(因此永远不会获取锁),它和任何 setValue(...) 打电话。还可以通过将字段标记为 volatile :从易失性字段的读取与之前对它的任何写入3以及其他技术同步。对线程安全出版物的全面描述太宽泛,这里无法给出答案。
因为您有一个数据竞争,所以值是以非原子方式从一个线程发布到另一个线程的。假设你做到了:

Value v = new Value();
v.setName("Bond");
v.setId(7);
Shared.setValue(v);

可能有人会打电话来 getValue() 并查看一个名为“bond”但其id仍然是默认字段值(0)的对象。它甚至有可能看到一个空名称和一个7的id,尽管代码似乎暗示如果id被设置了,那么名称一定已经被设置了。数据竞赛会让你面对许多微妙的、不直观的错误。
1.jls 17.4.5:“当程序包含两个冲突的访问时(§17.4.1)不按“发生在”关系排序的,称为包含数据竞争
2.jls 17.4.4“监视器m上的解锁操作与m上的所有后续锁定操作同步”。
3.也是17.4.4

相关问题