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关键字修饰后,自增操作分为三步:

  1. 读,线程从内存中读取变量值,保存一个变量值副本在自己的工作内存
  2. 改,线程在自己的工作内存中修改这个变量值
  3. 写,线程将修改后的值写回内存。

        我们假设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条 同步访问共享的可变数据

Effective Java2读书笔记-类和接口

Effective Java2读书笔记-对于所有对象都通用的方法

Effective Java2读书笔记-类和接口

《Effective Java中文版第二版》读书笔记

[读书笔记]《Effective Java》第10章并发