关于多线程访问并操作共享数据的解决方案概述
内存可见性
- 内存可见性(Memory Visibility)是指当某个线程正在使用对象状态 而另一个线程在同时修改该状态,需要确保当一个线程修改了对象 状态后,其他线程能够看到发生的状态变化。
可见性错误是指当读操作与写操作在不同的线程中执行时,我们无 法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚 至是根本不可能的事情。
image.png
- 上图展现了当内存不可见是可能发生的问题
原子性:
原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
有序性:
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
synchronized同步代码块
- 执行流程大约如下,它可以保证再同一时间只有一个线程操作数据,知道操作完成,别的线程才能够访问

- 缺点:效率不高,由于线程访问的时候加了隐式的同步锁,导致其他线程无法访问,可能会发生线程阻塞。
volatile变量
一种轻量级的同步机制,当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
image.png
- 在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
也正因为没有加锁这个操作,volatile 不具备“互斥性",不能保证变量的“原子性”,关于原子性,上面提到过a++的问题,当一个操作不是原子操作的时候,volatile就无法保证其内存可见性是否是正确的(一个线程没操作完,另一个线程就直接操作,导致了不同步,存在着线程安全问题)
CAS算法
CAS(Compare-And-Swap) 算法保证数据变量的原子性
CAS 算法是硬件对于并发操作的支持
CAS 包含了三个操作数:
①内存值 V
②预估值 A
③更新值 B
当且仅当 V == A 时, V = B; 否则,不会执行任何操作。

- 上图的什么都不做并不是什么都不做,而是进入下一次循环,重新执行一次操作,这样一来就不存在就没有同步锁,也就没有了线程阻塞的问题,线程不阻塞提高了CPU的利用率也就提升了速度(比synchronized快上不少)
Lock显示同步锁
- 引用一道编程题:
编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。 * 如:ABCABCABC…… 依次递归
package JUC;
import org.junit.Test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by CHEN on 2019/8/5.
*/
/*
* 编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。
* 如:ABCABCABC…… 依次递归
*/
public class Lock {
//普通写法
@Test
public void test01() {
Demo demo = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
demo.LoopA(i);
}
}
}, "ThreadA").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
demo.LoopB(i);
}
}
}, "ThreadB").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
demo.LoopC(i);
System.out.println("-----------------------------");
}
}
}, "ThreadC").start();
}
//Lambda表达式
@Test
public void test02() {
Demo demo = new Demo();
new Thread(() -> {
for (int i = 1; i <= 10; i++)
demo.LoopA(i);
},"ThreadAWriteByLambda").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++)
demo.LoopB(i);
},"ThreadAWriteByLambda").start();
new Thread(() -> {
for (int i = 1; i <= 10; i++){
demo.LoopC(i);
System.out.println("--------------------------");
}
},"ThreadAWriteByLambda").start();
}
}
class Demo {
private int number = 1;
private ReentrantLock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void LoopA(int totalLoop) {
//上锁
lock.lock();
try {
if (number != 1) {
//等待
condition1.await();
}
//打印
System.out.println(Thread.currentThread().getName() + "\t" + totalLoop);
//唤醒
number = 2;
condition2.signal();
} catch (Exception e) {
} finally {
//开锁
lock.unlock();
}
}
public void LoopB(int totalLoop) {
//上锁
lock.lock();
try {
if (number != 2) {
//等待
condition2.await();
}
//打印
System.out.println(Thread.currentThread().getName() + "\t" + totalLoop);
//唤醒
number = 3;
condition3.signal();
} catch (Exception e) {
} finally {
//开锁
lock.unlock();
}
}
public void LoopC(int totalLoop) {
//上锁
lock.lock();
try {
if (number != 3) {
//等待
condition3.await();
}
//打印
System.out.println(Thread.currentThread().getName() + "\t" + totalLoop);
//唤醒
number = 1;
condition1.signal();
} catch (Exception e) {
} finally {
//开锁
lock.unlock();
}
}
}
- 在主类中我写了两中写法,主要是为了练练Lambda表达式
- 这道题的实现思路很简单:顺序打印,线程又是同时执行的,因此需要加一个标记(number),来标记那个线程可以打印了,否则等待,打印完了唤醒下一个线程(唤醒等待机制)
- 最后重点是,这里用了Lock显式锁,观察代码可以发现其实我们可以在任何地方加锁(只要最后能打开),这种方法比较灵活,可以由我们决定的空间比较大。
- 去掉锁是必要的,因此加在finally这类必须执行的代码块中。