《Java线程与并发编程实践》—— 2.4 volatile和final变量

简介: 你之前学到的同步展示了两种属性:互斥性和可见性。synchronized关键字与两者都有关系。Java同时也提供了一种更弱的、仅仅包含可见性的同步形式,并且只以volatile关键字关联。 假设你自己设计了一个停止线程的机制(因为无法使用Thread不安全的stop()方法))。

本节书摘来异步社区《Java线程与并发编程实践》一书中的第2章,第2.4节,作者: 【美】Jeff Friesen,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.4 volatile和final变量

你之前学到的同步展示了两种属性:互斥性和可见性。synchronized关键字与两者都有关系。Java同时也提供了一种更弱的、仅仅包含可见性的同步形式,并且只以volatile关键字关联。

假设你自己设计了一个停止线程的机制(因为无法使用Thread不安全的stop()方法))。清单2-2中ThreadStopping程序源码展示了该如何完成这项任务。

清单2-2 尝试停止一个线程

public class ThreadStopping
{
   public static void main(String[] args)
   {
      class StoppableThread extends Thread
      {
         private boolean stopped; // defaults to false

         @Override
         public void run()
         {
            while(!stopped)
               System.out.println("running");
         }

         void stopThread()
         {
            stopped = true;
         }
      }
      StoppableThread thd = new StoppableThread();
      thd.start();
      try
      {
         Thread.sleep(1000); // sleep for 1 second
      }
      catch (InterruptedException ie)
      {
      }
      thd.stopThread();
   } 
}```
清单2-2中的main()方法声明了一个叫做StoppableThread的本地类,它继承自Thread。在初始化完StoppableThread之后,默认的主线程启动和这个 Thread对象关联的线程。之后它睡眠 1 秒,并且在死亡之前调用StoppableThread的stop()方法。

StoppableThread声明了一个被初始化为false的stopped实例变量,stopThread()方法会将该变量设置为true,同时run()方法中的while循环会在每次迭代中检查stopped的值是否已经修改为true。

照下面编译清单2-2:

javac ThreadStopping.java
运行程序:

java ThreadStopping`
你应该能观测到一系列运行时的消息。

当你在单处理器/单核的机器上运行这个程序的时候,很可能会观测到程序停止。但是在一个多处理器的机器或多核单处理器的机器上,可能就看不到程序停止,因为每个处理器或者核心很可能有自己的一份stopped的拷贝,当一条线程修改了自己的拷贝,其他线程的拷贝并没有被改变。

你或许决定使用synchronized关键字以确保只能访问主存中的stopped变量。然后经过一番思考,你决定在清单2-3中使用同步访问一对临界区的方式来解决这个问题。

清单2-3 尝试使用synchronized来停止一个线程

public class ThreadStopping
{
   public static void main(String[] args)
   {
      class StoppableThread extends Thread
      {
         private boolean stopped; // defaults to false

         @Override
         public void run()
         {
            synchronized(this)
            {
               while(!stopped)
                  System.out.println("running");
            } 
         }

         synchronized void stopThread()
         {
            stopped = true;
         }
      }
      StoppableThread thd = new StoppableThread();
      thd.start();
      try
      {
         Thread.sleep(1000); // sleep for 1 second
      }
      catch (InterruptedException ie)
      {
      }
      thd.stopThread();
   }
}```
出于两个因素考虑,清单2-3不是一个好主意。尽管你只需解决可见性的问题,synchronized却同时解决了互斥的问题(在该程序中不是个问题)。更重要的是,你还往程序中引进了另一个更严重的问题。

你已经正确地对stopped进行了同步访问,但是进一步观察run()方法中的同步块,尤其是这个while循环。由于正在执行循环的这个线程已经获取了当前StoppableThread对象(通过synchronized(this))的锁,这个循环不会终止。因为默认的主线程需要获取相同的锁,所以它在该对象上调用stopThread()方法的任意尝试都会导致自己被阻塞住。

你可以使用局部变量并在同步块中将stopped的值赋给这个变量来解决这一问题,如下所示:

public void run()
{
boolean _stopped = false;
while (!_stopped)
{

  synchronized(this)
  {
     _stopped = stopped;
  }
  System.out.println("running");

}
}`
不过,每次循环迭代都要尝试获取锁的方式会存在性能开销(还不如以前),所以这个解决方式是得不偿失的。清单2-4展示了一个更为高效且整洁的方法。

清单2-4 尝试通过volatile关键字来停止一个线程

public class ThreadStopping
{
   public static void main(String[] args)
   {
      class StoppableThread extends Thread
      {
         private #####volatile boolean stopped; // defaults to false

         @Override
         public void run()
         {
            while(!stopped)
               System.out.println("running");
         }

         void stopThread()
         {
            stopped = true;
         }
      }
      StoppableThread thd = new StoppableThread();
      thd.start();
      try
      {
         Thread.sleep(1000); // sleep for 1 second
      }
      catch (InterruptedException ie)
      {
      }
      thd.stopThread();
   } 
}```
由于stopped已经标记为volatile,每条线程都会访问主存中该变量的拷贝而不会访问缓存中的拷贝。这样,即使在多处理器或者多核的机器上,该程序也会停止。

警告:
  >只有可见性导致问题时,才应该使用volatile。而且,你也只能在属性声明处才能使用这个保留字(如果你尝试将局部变量声明成volatitle``,会收到一个错误)。最后,你可以将double和long型的属性声明成volatile,但是应该避免在32位的JVM上这样做,原因是此时访问一个double或者long型的变量值需要进行两步操作,若要安全地访问它们的值,互斥(通过synchronized)是必要的。
当一个属性变量声明成volatile,就不能同时被声明final的。不过,由于Java可以让你安全地访问final的属性而无需同步,这也就不能称之为一个问题了。为了克服DeadlockDemo中的缓存变量问题,我把lock1和lock2都标记成final,尽管也能将它们标记成volatile的。

以后,你会经常使用final关键字来确保在不可变(不会发生改变)类的上下文中线程的安全性。参考清单2-5。

清单2-5 借助于final创建一个不可变且线程安全的类

import java.util.Set;
import java.util.TreeSet;

public final class Planets
{
private final Set planets = new TreeSet<>();

public Planets()
{

  planets.add("Mercury");
  planets.add("Venus");
  planets.add("Earth");
  planets.add("Mars");
  planets.add("Jupiter");
  planets.add("Saturn");
  planets.add("Uranus");
  planets.add("Neptune");

}

public boolean isPlanet(String planetName)
{

  return planets.contains(planetName);

}
}`
清单2-5展示了一个不可变类Planets,其对象存储着星球名字的集合。尽管集合是可变的,但这个类的设计却保证在构造函数退出之后,集合不会再被改变。通过声明planet``s为final,这个属性的引用不能被更改。而且,该引用也不能被缓存,所以缓存变量的问题也不复存在。

关于不可变对象,Java提供了一种特殊的线程安全的保证。即便没有用同步来发布(暴露)这些对象的引用,它们依然可以被多条线程安全地访问。不可变对象提供了下列易于识别的规则:

不可变对象绝对不允许状态变更。
所有的属性必须声明成final。
对象必须被恰当地构造出来以防this引用脱离构造函数。
最后一点很让人迷惑,所以这里给出一个this显式地脱离构造函数的简单例子:

public class ThisEscapeDemo
{
   private static ThisEscapeDemo lastCreatedInstance;

   public ThisEscapeDemo()
   {
      lastCreatedInstance = this;
   }
相关文章
|
10月前
|
人工智能 安全 调度
Python并发编程之线程同步详解
并发编程在Python中至关重要,线程同步确保多线程程序正确运行。本文详解线程同步机制,包括互斥锁、信号量、事件、条件变量和队列,探讨全局解释器锁(GIL)的影响及解决线程同步问题的最佳实践,如避免全局变量、使用线程安全数据结构、精细化锁的使用等。通过示例代码帮助开发者理解并提升多线程程序的性能与可靠性。
306 0
|
12月前
|
Java 开发者 Kotlin
华为仓颉语言初识:并发编程之线程的基本使用
本文详细介绍了仓颉语言中线程的基本使用,包括线程创建(通过`spawn`关键字)、线程名称设置、线程执行控制(使用`get`方法阻塞主线程以获取子线程结果)以及线程取消(通过`cancel()`方法)。文章还指出仓颉线程与Java等语言的差异,例如默认不提供线程名称。掌握这些内容有助于开发者高效处理并发任务,提升程序性能。
373 2
|
12月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
348 0
|
存储 Java
# 【Java全栈学习笔记-U1-day02】变量+数据类型+运算符
本篇笔记主要围绕Java全栈学习的第二天内容展开,涵盖了变量、数据类型、运算符以及Scanner类的应用。首先介绍了变量的概念与命名规范,以及如何定义和使用变量;接着详细讲解了Java中的基本数据类型,包括整型、浮点型、字符型、布尔型等,并通过实例演示了数据类型的运用。随后,深入探讨了各类运算符(赋值、算术、关系、逻辑)及其优先级,帮助理解表达式的构成。最后,介绍了如何利用Scanner类实现用户输入功能,并通过多个综合示例(如计算圆面积、购物打折、变量交换及银行利息计算)巩固所学知识。完成相关作业将进一步加深对这些基础概念的理解与实践能力。
262 13
|
存储 缓存 安全
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是写出高端的CRUD应用。2025年,我正在沉淀自己,博客更新速度也在加快。在这里,我会分享关于Java并发编程的深入理解,尤其是volatile关键字的底层原理。 本文将带你深入了解Java内存模型(JMM),解释volatile如何通过内存屏障和缓存一致性协议确保可见性和有序性,同时探讨其局限性及优化方案。欢迎订阅专栏《在2B工作中寻求并发是否搞错了什么》,一起探索并发编程的奥秘! 关注我,点赞、收藏、评论,跟上更新节奏,让我们共同进步!
492 8
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
|
存储 传感器 缓存
java变量与数据类型:整型、浮点型与字符类型
### Java数据类型全景表简介 本文详细介绍了Java的基本数据类型和引用数据类型,涵盖每种类型的存储空间、默认值、取值范围及使用场景。特别强调了`byte`、`int`、`long`、`float`、`double`等基本类型在不同应用场景中的选择与优化,如文件流处理、金融计算等。引用数据类型部分则解析了`String`、数组、类对象、接口和枚举的内存分配机制。
516 15
|
设计模式 存储 SQL
【Java并发】【volatile】适合初学者体质的volatile
当你阅读dalao的框架源码的时候,你是否会见到这样一个关键字 - - - volatie,诶,你是否会好奇,为什么要加它?加了它有什么作用?
335 14
【Java并发】【volatile】适合初学者体质的volatile
|
缓存 安全 Java
Volatile关键字与Java原子性的迷宫之旅
通过合理使用 `volatile`和原子操作,可以在提升程序性能的同时,确保程序的正确性和线程安全性。希望本文能帮助您更好地理解和应用这些并发编程中的关键概念。
391 21
|
Java Linux iOS开发
如何配置 Java 环境变量:设置 JAVA_HOME 和 PATH
本文详细介绍如何在Windows和Linux/macOS系统上配置Java环境变量。
19315 12
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
487 11

热门文章

最新文章