【Effective Java 读书笔记】第78条 同步访问共享的可变数据

本文探讨了《Effective Java》中关于同步访问共享可变数据的问题。通过书中的实例,解释了未同步访问导致的线程无法感知变量修改的现象,详细介绍了synchronized和volatile关键字的作用。synchronized提供互斥访问,保证了变量的原子性;volatile确保多线程间变量的可见性,但不保证原子性。文章还提到了AtomicLong作为线程安全的替代方案。
摘要由CSDN通过智能技术生成

书上的一个例子:

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的用法参考了:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值