synchronized(作用范围)、volatile的用法

本文深入探讨了Java并发编程中的核心概念,包括线程间的数据可见性问题、volatile关键字的作用及使用场景,以及synchronized关键字的多种应用方式。通过具体示例解释了volatile为何不能保证原子性,并详细解析了synchronized的同步机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一个线程对主内存的修改可以及时的被其他线程观察到

1、导致共享变量在线程间不可见的原因

  • 线程交叉执行
  • 重排序结合线程交叉执行
  • 共享变量更新后的值没有在工作内存与主存间及时更新

1.1 可见性之synchronized

JMM关于synchronized的规定

  • 线程解锁前,必须把共享变量的最新值刷新到主内存
  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁是同一把锁)

1.2 可见性之volatile

通过加入内存屏障禁止指令重排序优化来实现

  • 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
  • 对volatile变量读操作时,会在读操作前加入一条load屏障指令从主内存中读取共享变量
    在这里插入图片描述
    在这里插入图片描述

用volatile做计数操作,看是否是线程安全的

package com.mmall.concurrency.example.count;

import com.mmall.concurrency.annoations.NotThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

@Slf4j
@NotThreadSafe
public class CountExample4 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static volatile int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
        /*
        运行结果:
         5000
        4992
        4998
        因此volatile也是线程不安全的
        原因是:假设开始时count=5,线程1和线程2同时做count++操作时,线程1和线程2都从内存中读取到了count=5,然后线程1和线程2同时对count做加一操作,然后线程1把count的结果6由工作内存存回内存,线程2也把count的结果6由工作内存存回内存,因此运行结果小于等于5000
        */
    }

    private static void add() {
        count++;
        // 1、count
        // 2、+1
        // 3、count
    }
}

运行结果:

5000
4992
4998

因此volatile也是线程不安全的
原因是:假设开始时count=5,线程1和线程2同时做count++操作时,线程1和线程2都从内存中读取到了count=5,然后线程1和线程2同时对count做加一操作,然后线程1把count的结果6由工作内存存回内存,线程2也把count的结果6由工作内存存回内存,因此运行结果小于等于5000。

2、volatile使用场景

volatile适合使用在状态标识的场景中,如下实例:(volatile还可以用于检查2次的场景中)

volatile boolean inited = false;//全局变量

//线程1:
context = loadContext();
inited= true;

// 线程2:
while( !inited ){
    sleep();
}
doSomethingWithConfig(context)

2.1 理解volatile的可见性:实例

import java.util.Date;

/**
 * <p>Volatile不是坑</p>
 *
 * @author wuyidi
 * @version v2.0.1 2018年07月05日 10:33 wuyidi
 */
public class VolatileTestSample {

    /**
     * 非volatile变量,当前线程如果不刷新句柄,则永远不可见
     * 也就是说,o的句柄一直是在Cache中
     */
    //private Object o = null;

    /**
     * 当变量为volatile时,另一根线程对其改动,会立即可见
     */
	private volatile Object o = null;

    private boolean flag = false;

    public void methodA() {
        while (true) {
            if (o != null && flag == false) {
                System.out.println("Handler o detected changed."+new Date());
                flag = true;
                System.exit(0);
            }
        }
    }

    public void methodB() {
        o = new Object();
        System.out.println("Handler o changed!"+new Date());
    }

    public static void main(String[] args) {
        System.out.println("当前时间是:"+new Date());
        VolatileTestSample volatileTestSample = new VolatileTestSample();
        Thread threadHandlerListener = new Thread(new Runnable() {
            @Override
            public void run() {
                volatileTestSample.methodA();
            }
        });
        threadHandlerListener.start();
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        volatileTestSample.methodB();
    }

}

运行结果:
这里写图片描述

这里写图片描述下面内容转自:
https://www.jianshu.com/p/895950290179

3、原子性 - Synchronize

  • 修饰一个代码块:作用区域是调用方法的对象 synchronized (this)不同对象之间互不影响
  • 修饰一个方法:作用区域是调用方法的对象不同对象之间互不影响
  • 修饰一个类synchronized (SynchronizedTest2.class)作用区域是是所有对象
  • 修饰一个静态方法作用区域是是所有对象

3.1 Synchronize的用法

同步代码块与同步方法演示与解析

@Slf4j
public class SynchronizedExample1 {

    // 修饰一个代码块
    public void test1(int j) {
        //同步代码块 作用于调用的对象
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j, i);
            }
        }
    }

    // 修饰一个方法 同步方法 作用于调用的对象
    public synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample1 example1 = new SynchronizedExample1();
        SynchronizedExample1 example2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test2(1);
        });
        executorService.execute(() -> {
            example2.test2(2);
        });
    }
}

若线程池中开启两个线程:

  • 两个线程中都使用同一个对象进行操作,那么他们是同步的,输出的结果都是先执行test2-1 0-9的输出后执行test2-2 0-9的输出,或先执行test2-2 0-9的输出后执行test2-1 0-9的输出
executorService.execute(() -> {
    example1.test2(1);
});
executorService.execute(() -> {
    example1.test2(2)
});
  • 两个线程不使用同一个对象进行操作,那么他们输出即为交叉执行
executorService.execute(() -> {
    example1.test2(1);
});
executorService.execute(() -> {
    example2.test2(2);
});

注意:如果某个类为父类,并且存在同步方法,子类在继承这个类后,如果子类调用该父类的同步方法后,该方法是没有synchronized关键字的,原因是synchronized不属于方法声明的一部分

修饰静态方法与修饰类演示与解析

@Slf4j
//作用于类的所有对象
public class SynchronizedExample2 {

    // 修饰一个类
    public static void test1(int j) {
        synchronized (SynchronizedExample2.class) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j, i);
            }
        }
    }

    // 修饰一个静态方法
    public static synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample2 example1 = new SynchronizedExample2();
        SynchronizedExample2 example2 = new SynchronizedExample2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test1(1);
        });
        executorService.execute(() -> {
            example2.test1(2);
        });
    }
}

从上面类的执行结果可知,同一个类不同对象执行同步修饰的方法执行的顺序是同步的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值