Effective Java 读书笔记第78条 同步访问共享的可变数据
Posted 舒泱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Effective Java 读书笔记第78条 同步访问共享的可变数据相关的知识,希望对你有一定的参考价值。
书上的一个例子:
public class StopThread
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException
Thread backgroundThread = new Thread(new Runnable()
@Override
public void run()
int i=0;
while(!stopRequested)
i++;
);
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
StopRequested =true;
你可能期待这个程序运行大约一秒钟左右,之后主线程将 stopRequested 设置为 true ,致使后台线程的循环终止。但是在我的机器上,这个程序永远不会终止:后台线程永远在循环!
这是为什么呢?
每个线程都有自己的工作内存,上述例子中,变量stopRequested的值保存在内存中,没有赋初值则初值默认为false,子线程一开始的时候会到内存中把stopRequested的值复制一份到自己的工作内存,然后主线程在子线程开启大约一秒钟后,在自己的工作内存里把stopRequested的值改成了true并写回内存,子线程并不能“看到”这个修改,子线程工作内存中的stopRequested的值仍然是false,所以循环不会停下。
书上给了两种修改方案。
修改方法一、synchronized
public class StopThread
private static boolean stopRequested;
// 写方法
private static synchronized void requestStop()
stopRequested = true;
// 读方法
private static synchronized boolean stopRequested()
return stopRequested;
public static void main(String[] args) throws InterruptedException
Thread backgroundThread = new Thread(new Runnable()
@Override
public void run()
int i=0;
while(!stopRequested())
i++;
);
backgroundThread.start();
TimeUnit.SECONDS.sleep(5);
requestStop();
某个线程在访问临界资源的时候,给临界资源上锁,访问完后释放锁,这样其他线程就能访问该临界资源了,达到互斥访问的目的。在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
在以上代码中,变量stopRequested相当于临界资源,主线程和子线程都要去访问它,主线程会写这个变量,子线程会读这个变量。主线程调用requestStop()方法修改stopRequested的值,修改后的值会被更新到内存,修改完后释放锁,然后子线程获得锁,读变量stopRequested的值,读到的是被主线程修改后的新值。
修改方法二、volatile
// Cooperative thread termination with a volatile field
public class StopThread
private static volatile Boolean stopRequested;
public static void main(String[] args)
throws InterruptedException
Thread backgroundThread = new Thread(() ->
int i = 0;
while (!stopRequested)
i++;
);
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
在这个场景中,其实我们只是想让子线程“看到”主线程对变量stopRequested的修改,而不是为了实现多个线程同步访问变量stopRequested,因此没必要用加锁的方式,毕竟不停加锁解锁也是一笔性能开销。 因此比较好的改法是使用volatile关键字。
我们知道,线程对变量的读写可以分为以下几步:去内存读变量值;将变量的值复制一份到自己的工作内存;在修改了变量值后,将修改的值写回内存。在书上的例子中,第一段代码,主线程修改了stopRequested的值,但没有谁会通知子线程重新去读stopRequested的值。
当变量stopRequested被volatile关键字修饰之后,子线程每次读取stopRequested的值的时候,都会被强制去内存读最新的值,所以当主线程修改了stopRequested的值并写回内存后,子线程被强制去内存读值,就能“看到”主线程对stopRequested的修改了。
书上的另一个例子:
// Broken - requires synchronization!
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber()
return nextSerialNumber++;
假设现在有thread1和thread2在访问generateSerialNumber()函数,它们每次都能拿到不一样的nextSerialNumber值吗?
不能。自增操作(++)并不是原子的,当变量被volatile关键字修饰后,自增操作分为三步:
- 读,线程从内存中读取变量值,保存一个变量值副本在自己的工作内存
- 改,线程在自己的工作内存中修改这个变量值
- 写,线程将修改后的值写回内存。
我们假设thread1从内存中读了nextSerialNumber的值为0,在自己的工作内存中将nextSerialNumber修改成了1,但还没来得及将1写回内存,此时thread2去内存读nextSerialNumber的值,读到的仍是0。然后thread1在内存中写nextSerialNumber=1,thread2也会在内存中写nextSerialNumber=1。这两个线程调用generateSerialNumber()可能得到的是同样的值。
因此,volatile的作用仅仅是
- 当线程修改了某个变量的值,强制线程及时地将修改写回内存。
- 当线程要读某个变量,强制线程去内存读,而不是继续使用自己工作内存中变量副本的值。
使用volatile不能实现互斥访问。
以上例子中,多个线程会同时读写一个变量的值。我们修正 generateSerialNumber 方法的一种方法是在它的声明中增加 synchronized 修饰符,synchronized能保证多个线程互斥地访问变量nextSerialNumber。另一个修改方法是使用 AtomicLong 类,它是 java.util.concurrent.atomic 的组成部分,这个包为在单个变量上进行免锁定、线程安全的编程提供了基本类型,能保证变量的原子操作。
// Lock-free synchronization with java.util.concurrent.atomic
private static final Atomiclong nextSerialNum = new Atomiclong();
public static long generateSerialNumber()
return nextSerialNum.getAndIncrement();
synchronized的用法参考了:
volatile的用法参考了:
以上是关于Effective Java 读书笔记第78条 同步访问共享的可变数据的主要内容,如果未能解决你的问题,请参考以下文章
Effective Java 读书笔记第78条 同步访问共享的可变数据