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

本节书摘来异步社区《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;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值