目录
4、Volatile关键字和synchronized关键字的比较
一、有序性、可见性、原子性
1、有序性
多线程指令重排序:
指令重排分为硬件层面和虚拟机层面,此处只讲虚拟机层面
目的:解决内存操作速度慢于CPU操作速度所带来的CPU空置问题,尽可能充分利用CPU
表现形式:虚拟机会按照特定的规则将程序打乱,使得写在后面的程序可能会先执行,写在前边的程序可能会后执行。
2、可见性
所谓的可见性,是线程A可以读取到线程B修改后的变量的值,改变线程从私有工作内存取变量值的方式,强制线程从主内存取值。
3、原子性
原子是世界上最小的单位,具有不可分割的特性
二、volatile关键字的作用——可见性
关键字volatile的主要作用是使变量在多个线程中可见。通过使用volatile关键字,强制从公共内存中读取变量的值,不允许线程内部对变量进行缓存和重排序、不加锁、不阻塞,实现更轻量级的线程同步。
内存结构图:
1、Volatile关键字使用条件
(1)对变量的写操作不依赖于当前值
(2)该变量没有包含在具有其他变量的不变式中
2、Volatile关键字使用场景
(1)用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机
(2)用作状态标志
状态标记的公有特性,通常只有一种状态转换:比如标志从false转换为true,然后停止
3、Volatile关键字使用缺点
只有在状态真正独立于程序内其他内容时才能使用Volatile,但大多数编程环境都不满足上述条件,因此使用volatile比使用锁更加容易出错(谨慎)
4、Volatile关键字和synchronized关键字的比较
1、多线程访问volatile不会发生阻塞,而synchronized会出现阻塞
2、volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性和可见性。
注:线程安全包括原子性和可见性两个方面,java的同步机制都是围绕这两个方面来确保线程安全的。
5、volatile的非原子性
关键字volatile虽然实现变量在线程之间的可见性,但是不能保障同步性(原子性),所以执行的结果也是存在问题的。
测试代码如下:
public class MyThread extends Thread {
public volatile static int count;
private static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("打印相加结果:" + count);
}
@Override
public void run() {
addCount();
}
public static void main(String[] args) {
// 定义100个线程
MyThread[] myThreads = new MyThread[100];
for (int i = 0; i < 100; i++) {
myThreads[i] = new MyThread();
}
for (int i = 0; i < 100; i++) {
myThreads[i].start();
}
}
}
执行结果:
这是因为,多个线程执行相加时,虽然可以相互读到其他线程修改后的值,但是,如果有多个线程同时修改count值为100,假如10个线程同时修改,那么最终只有一个线程修改的值有效,这个时候另外的9个线程相加效果就失效了,所以,我们可以看到,打印的结果总是小于正确数:10000。
原理:i++操作不是同步的
给addCount方法加锁:
private synchronized static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("打印相加结果:" + count);
}
加锁后的执行结果:
结论:
volatile关键字解决的是变量读取时的可见性问题,但无法保证原子性,对于多线程访问同一个实例变量还是需要加锁
三、原子类——原子性
原子类基于CAS操作,不使用锁或同步机制来保护对其值的并发访问,避免死锁,提高性能。但一次只能同步一个值。
1、原子类使用场景
(1)多线程环境中统计次数和递增操作
public class ExecuteMethod {
public static void main(String[] args) throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
ExecutorService ex = Executors.newFixedThreadPool(10);
for (int j = 0; j < 10; j++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
atomicInteger.incrementAndGet();
System.out.println("atomicInteger的值" + atomicInteger);
}
};
ex.execute(runnable);
}
ex.shutdown();
}
}
执行结果:10个线程每个线程执行一次递增操作,一次值操作了一个值,但是如果在循环外再加一层循环,原子类操作就会出现线程安全问题
(2)限制接口数量,超过数量进行熔断
public class MyService {
private AtomicInteger atomicInteger = new AtomicInteger();
private void method() {
int number = atomicInteger.incrementAndGet();// 递增
int limit = 2;
try {
if (number > limit) {
System.out.println("拒绝访问!超过线程允许访问数!");
} else {
System.out.println("允许访问");
}
} finally {
atomicInteger.decrementAndGet();// 递减
}
}
public static void main(String[] args) {
MyService myService = new MyService();
ExecutorService ex = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
ex.execute(() -> myService.method());
}
}
}
执行结果:
2、CAS(比较并替换)操作和ABA问题
(1)CAS操作
一种基于硬件的处理方式,每个CAS操作都涉及到三个值:内存地址V,期望值A和新值B,如果给的值和期望值相等就赋值为B,不相等就不执行任何操作
(2)ABA问题
执行CAS操作时,其他线程将变量A改成了B又改成了A,当本线程用期望值和该变量进行比较时,发现A变量的值没有变就进行了数据的修改操作,这样与设计的初衷不相符。
举个栗子:
假如执行一次为20元以下的账户增加10元的存款操作。
A线程去扫描账户,发现账户的余额为15元,便执行加10元的操作,但是碰巧此时,该账户又消费了10元,因此账户余额又回到了15元;当B线程第二次去扫描账户的时候,发现账户值仍旧为15元,便又给账户进行增加10元的操作。
ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成casd多次执行的问题。
解决方法:每次变量更新时,赋值给变量一个版本号并加1递增,这样就避免了ABA问题。
3、原子类Atomic的不安全性
原子操作是不可分割的整体,它可以在没有锁的情况下做到线程安全,但是如果在具有逻辑性的多线程环境下(方法之间的调用不是同步的),原子类的操作也会存在不安全性。
如下同样是执行一个相加到10000的操作
测试代码:
public class MyThread extends Thread {
AtomicInteger atomicInteger = new AtomicInteger();
private void addCount() {
for (int i = 0; i < 100; i++) {
atomicInteger.incrementAndGet();
}
System.out.println("打印相加结果:" + atomicInteger);
}
@Override
public void run() {
addCount();
}
public static void main(String[] args) {
// 定义100个线程
MyThread myThread = new MyThread();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(myThread);
thread.start();
}
}
}
执行结果1:
执行结果2:
上述结果可以看到,原子类在方法中一旦被多个线程调用(且方法不是同步方法),中间的执行逻辑是混乱的,执行中数据的正确性也得不到保障。
因此,解决这个问题的办法同样还是在方法上加同步锁!!!
给方法进行加锁操作:
private synchronized void addCount() {
for (int i = 0; i < 100; i++) {
atomicInteger.incrementAndGet();
}
System.out.println("打印相加结果:" + atomicInteger);
}
执行结果: