一个线程对主内存的修改可以及时的被其他线程观察到
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);
});
}
}
从上面类的执行结果可知,同一个类
的不同对象
执行同步修饰的方法
,执行的顺序是同步的