3.1 线程同步机制简介
线程同步机制是一套用于协调线程之间的数据访问的机制,该机制可以保障线程安全。
Java平台提供的线程同步机制包括:锁、volatile、final、static以及相关的API,如Object.wait()、Object.notify()等。
3.2 锁概述
将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问。锁可以理解为对共享数据进行保护的一个许可证,任何线程想要访问共享数据必须持有许可证,此许可证只能被一个线程持有,并且在访问结束后,线程必须释放持有的许可证。
锁的持有线程在获得锁之后和释放锁之前这段时间执行的代码称为临界区(Critical Section)。一个锁一次只能被一个线程持有的锁称为排它锁(Exclusive)或者互斥锁(Mutex)。JVM把锁分为内部锁和显示锁两种。
- 内部锁通过synchronized关键字实现;
显示锁通过java.concurent.locks.Lock接口的实现类实现。
3.2.1 锁的作用
锁可以实现对共享数据的安全访问,保障线程的原子性、可见性与有序性。
锁是通过互斥保障原子性,一个锁只能被一个线程持有,这就保证临界区的代码一次只能被一个线程执行。
- 锁通过写线程处理器的缓存和读线程刷新处理器缓存这两个动作保障可见性。在Java平台中,锁的获得隐含着刷新处理器缓存动作,锁的释放隐含着冲刷处理器缓存的动作。
- 锁能够保障有序性,写线程在临界区所执行的在读线程所执行的临界区看起来像是完全按照源码顺序执行的。
使用锁保障线程的安全性,必须满足以下条件:
- 这些线程在访问共享数据时必须使用同一个锁;
- 即使是读共享数据的线程也需要使用同步锁,避免脏读,例如:在写线程未完成任务时,读线程读取了这些不完整信息。
3.2.2 锁相关的概念
- 可重入性(Reentrancy):一个线程持有该锁时能再次(多次)申请该锁。
- 锁的争用与调度:Java平台中内部锁属于非公平锁,显示Lock锁既支持公平锁,又支持非公平锁。
锁的粒度:一个锁可以保护的共享数据的数量大小。保护共享数据量大,称该锁的粒度粗,反之为粒度细。过粗会导致不必要的等待,过细会增加开销。
3.3 内部锁:synchronized关键字
Java中的每个对象都有一个与之关联的内部锁(Intrinsic lock)。这种锁也称为监视器(Monitor),这种内部锁是一种排他锁,可以保障原子性、可见性与有序性。
内部锁通过synchronized关键字实现,它可以修饰代码块、方法。修饰方法就称为同步实例方法,修饰静态方法称为同步静态方法。
同步过程中,线程出现异常,线程会释放锁。3.3.1 修饰代码块
public class Test01 {
public static void main(String[] args) {
Test01 obj = new Test01();
// 当前的锁对象this就是obj对象
new Thread(obj::doSomething).start();
// 当前的锁对象this也是obj对象
new Thread(obj::doSomething).start();
}
public void doSomething() {
// 使用this当前对象作为锁对象
synchronized (this) {
int num = 100;
for (int i = 1; i <= num; i++) {
System.out.println(Thread.currentThread().getName() + " ->" + i);
}
}
}
}
经常使用
this
作为锁对象,也可以定义一个final
常量作为锁对象。3.3.2 修饰实例方法
public class Test02 {
public static void main(String[] args) {
Test02 obj = new Test02();
// 当前的锁对象this就是obj对象
new Thread(obj::doSomething).start();
new Thread(obj::doSomething).start();
}
/**
* 使用synchronized修饰实例方法同步实例方法,默认this作为锁对象
*/
public synchronized void doSomething() {
int num = 100;
for (int i = 1; i <= num; i++) {
System.out.println(Thread.currentThread().getName() + " ->" + i);
}
}
}
3.3.3 修饰静态方法
public class Test03 {
public static void main(String[] args) {
// 当前的锁对象是Test03.class
new Thread(Test03::doSomething).start();
new Thread(Test03::doSomething).start();
}
/**
* 使用synchronized修饰静态方法,默认当前类的运行时类对象为锁对象,可以理解为以下代码:
* public static void doSomething(){
* synchronized(Test03.class){
* ......
* }
* }
*/
public synchronized static void doSomething() {
int num = 100;
for (int i = 1; i <= num; i++) {
System.out.println(Thread.currentThread().getName() + " ->" + i);
}
}
}
3.3.4 死锁
```java /**
死锁:在多线程中,同步时可能需要使用多个锁,如果获得锁的顺序不一致,可能导致死锁 */ public class Test04 {
public static void main(String[] args) { SubThread t1 = new SubThread(); t1.setName(SubThread.thread1); t1.start();
SubThread t2 = new SubThread(); t2.setName(SubThread.thread2); t2.start();
}
static class SubThread extends Thread{ private static final Object lock1 = new Object(); private static final Object lock2 = new Object();
private static String thread1 = “a”; private static String thread2 = “b”;
@Override public void run() {
if (thread1.equals(Thread.currentThread().getName())){
synchronized (lock1) {
System.out.println("a线程获得了lock1锁,还需要获得lock2锁");
synchronized (lock2){
System.out.println("a线程获得lock1后又获得了lock2,可以想干任何想干的事");
}
}
}
if (thread2.equals(Thread.currentThread().getName())){
synchronized (lock2) {
System.out.println("b线程获得了lock2锁,还需要获得lock1锁");
synchronized (lock1){
System.out.println("b线程获得lock2后又获得了lock1,可以想干任何想干的事");
}
}
}
} } }
<a name="7nnNQ"></a>
## 3.4 轻量级同步机制:volatile关键字
<a name="D1iDt"></a>
### 3.4.1 volatile的作用
使变量在多个线程之间可见。
```java
public class Test01 {
public static void main(String[] args) {
PrintString printString = new PrintString();
// 在子线程中打印
new Thread(printString::doSomething).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("在main线程中修改打印标志");
printString.setContinuePrint(false);
/*
现象:子线程依然未结束(未添加volatile)
原因:main线程修改了printString对象的打印标志后,子线程读不到
解决办法:使用volatile关键字修饰printString对象的打印标志,
volatile的作用可以强制线程从公共内存读取变量的值,而不是从工作内存中读取。
*/
}
static class PrintString{
private volatile boolean continuePrint = true;
public PrintString setContinuePrint(boolean continuePrint) {
this.continuePrint = continuePrint;
return this;
}
public void doSomething(){
System.out.println("开始……");
while (continuePrint){
}
System.out.println("结束……");
}
}
}
volatile与synchronized比较
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好;
- volatile只能修饰变量,而synchronized可以修饰方法、代码块;
- 多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞;
- volatile保证数据的可见性,不能保证原子性,而synchronized可以保证原子性,也可以保证可见性;
volatile解决的是变量在多个线程之间的可见性,synchronized解决的是多个线程之间访问公共资源的同步性。
3.4.2 volatile非原子特性
volatile增加了实例变量在多个线程的可见性,但不具备原子性。
public class Test02 {
public static void main(String[] args) {
int threadNum = 100;
for (int i = 0; i < threadNum; i++){
// count != threadNum * sum;
new MyThread().start();
}
}
static class MyThread extends Thread {
// volatile关键字仅仅表示所有线程从主内存中读取count变量的值
volatile public static int count;
// 这段代码不是线程安全的,需要添加synchronized关键字,它保证原子性的同时,还保证了可见性,也就不需要volatile
public static void addCount(){
int sum = 1000;
for (int i = 0; i < sum; i++){
// count++不是原子操作
count++;
}
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
@Override
public void run() {
addCount();
}
}
}
3.4.3 利用原子类进行自增自减
由于
i++
不是原子操作,除了使用synchronized进行同步外,也可以使用AtomicInteger/AtomicLong
原子类进行实现。public class Test03 {
public static void main(String[] args) throws InterruptedException {
int threadNum = 100;
for (int i = 0; i < threadNum; i++){
new MyThread().start();
}
Thread.sleep(1000);
System.out.println(MyThread.count.get());
}
static class MyThread extends Thread {
private static AtomicInteger count = new AtomicInteger();
public static void addCount(){
int sum = 1000;
for (int i = 0; i < sum; i++){
count.getAndIncrement();
}
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
@Override
public void run() {
addCount();
}
}
}
3.5 CAS
CAS(Compare and Swap)是由硬件实现的。CAS将read-modify-write这类操作转换为原子操作。
i++
自增包括三个子操作:从主内存读取
i
变量值;- 对
i
的值加1
; - 再把加
1
之后的值保存到主内存。
CAS原理:在把数据更新到主内存时,再次读取主内存变量的值,如现在变量的值与期望值(操作起始时读取的值)一样就更新。
public class CASTest {
public static void main(String[] args) {
CASCounter casCounter = new CASCounter();
long sum = 10000;
for (int i = 0; i < sum; i++){
new Thread(()->System.out.println(casCounter.incrementAndGet())).start();
}
}
}
class CASCounter {
/**
* 使用volatile修饰value值,使线程可见
*/
volatile private long value;
public long getValue() {
return value;
}
/**
* 定义compare and swap方法
*/
private boolean compareAndSwap(long expectedValue, long newValue) {
// 如果当前value的值与期望的expectedValue值一样,就把当前的value字段替换为newValue值
synchronized (this) {
if (value == expectedValue) {
value = newValue;
return true;
} else {
return false;
}
}
}
/**
* 定义自增方法
*/
public long incrementAndGet() {
long oldValue;
long newValue;
do {
oldValue = value;
newValue = oldValue + 1;
} while (!compareAndSwap(oldValue, newValue));
return newValue;
}
}
CAS实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过。
实际上这种假设不一定总是成立,可能存在 ABA
问题。
有共享变量count=0 A线程对count值修改为10 B线程对count值修改为20 C线程对count值修改为0 当前线程看到count变量的值,现在是0,现在是否认为count变量的值没有被其他线程更新呢?
如果要规避ABA
问题,可以为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会增加1
,ABA变量更新过程:[A, 0] -> [B, 1] -> [A, 2]
,通过修改号可以准确判断变量是否被其他线程修改,AtomicStampedReference
类就是基于这种思想产生的。
3.6 原子变量类
原子变量类基于CAS实现的,当前共享变量进行read-modify-write
更新操作时,通过原子变量类可以保障操作的原子性与可见性,对变量的read-modify-write
更新操作是指当前操作不是一个简单的赋值,而是变量的新值依赖变量的旧值,如自增操作。由于volatile
能保证可见性,原子变量类内部就是借助一个volatile
变量保证可见性的同时,借助CAS保证了原子性,有时把原子变量类看作是增强的volatile
变量,原子变量类有12个。
分组 | 原子变量类 |
---|---|
基础数据型 | AtomicInteger、AtomicLong、AtomicBoolean |
数组型 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
字段更新器 | AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater |
引用型 | AtomicReference、AtomicStampedReference、AtomicMarkableReference |
3.6.1 AtomicLong
/**
* 使用原子变量类定义一个计数器
* 该计数器在整个程序中都能使用,并且所有的地方都使用这一个计算器,这个计算器可以设计为单例
* @author 王游
* @date 2021/3/2 19:55
*/
public class Indicator {
/**
* 构造方法私有化
*/
private Indicator(){}
/**
* 定义一个私有本类静态对象
*/
private static final Indicator INSTANCE = new Indicator();
/**
* 提供一个公共静态方法返回该类唯一实例
*/
public static Indicator getInstance(){
return INSTANCE;
}
/**
* 使用原子变量类保存请求总数、成功数、失败娄
*/
private final AtomicLong requestCount = new AtomicLong(0);
private final AtomicLong successCount = new AtomicLong(0);
private final AtomicLong failureCount = new AtomicLong(0);
/**
* 有新的请求
*/
public void newRequestReceive(){
requestCount.incrementAndGet();
}
/**
* 处理成功
*/
public void requestProcessSuccess(){
successCount.incrementAndGet();
}
/**
* 处理失败
*/
public void requestProcessFailure(){
failureCount.incrementAndGet();
}
/**
* 查看请求量
*/
public long getRequestCount(){
return requestCount.get();
}
/**
* 查看成功量
*/
public long getSuccessCount(){
return successCount.get();
}
/**
* 查看失败量
*/
public long getFailureCount(){
return failureCount.get();
}
}
/**
* 模拟服务器的请求总数,处理成功/失败的数量
*/
public class Test {
public static void main(String[] args) {
int threadNum = 10000;
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
Indicator.getInstance().newRequestReceive();
int num = new Random().nextInt();
if (num % 2 == 0) {
Indicator.getInstance().requestProcessSuccess();
} else {
Indicator.getInstance().requestProcessFailure();
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("总数:" + Indicator.getInstance().getRequestCount());
System.out.println("成功:" + Indicator.getInstance().getSuccessCount());
System.out.println("失败:" + Indicator.getInstance().getFailureCount());
}
}
3.6.2 AtomicIntegerArray
/**
* AtomicIntegerArray的基本操作
*/
public class Test {
public static void main(String[] args) {
// 1) 创建一个指定长度的原子数组:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
System.out.println("1:" + atomicIntegerArray);
// 2) 返回指定位置的元素:0
System.out.println("2:" + atomicIntegerArray.get(0));
// 3) 设置指定位置的元素
atomicIntegerArray.set(0, 10);
// 4) 设置新值时,返回数组元素的旧值:10
System.out.println("4:" + atomicIntegerArray.getAndSet(0, 20));
// 5) 修改数组元素的值,把数组元素加上某个值:42、42
System.out.println("5:" + atomicIntegerArray.addAndGet(0, 22));
System.out.println("5:" + atomicIntegerArray.getAndAdd(0, 33));
// 6) CAS操作,如果数组中索引值为0的元素的值是32,就修改为222:true
System.out.println("6:" + atomicIntegerArray.compareAndSet(0, 75, 222));
// 6) 自增自减:1、1、1
System.out.println("6:" + atomicIntegerArray.incrementAndGet(1));
System.out.println("6:" + atomicIntegerArray.getAndIncrement(1));
System.out.println("6:" + atomicIntegerArray.decrementAndGet(1));
}
}
3.6.3 AtomicIntegerFieldUpdater
AtomicIntegerFieldUpdater可以对原子整数字段进行更新,要求:
- 字符必须使用volatile修饰,使线程之间可见;
只有是实例变量,不能是静态变量,也不能是使用final修饰的字段。 ```java public class User { int id; /**
使用AtomicIntegerFieldUpdater更新的字段必须使用volatile修饰 */ volatile int age;
public User(int id, int age) { this.id = id; this.age = age; }
@Override public String toString() { return “User{“ +
"id=" + id +
", age=" + age +
'}';
} }
public class SubThread extends Thread{ private User user; /**
* 创建age字段更新器
*/
private AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public SubThread(User user){
this.user = user;
}
@Override
public void run() {
int age = 10;
for (int i = 0; i < age; i++){
System.out.println(updater.getAndIncrement(user));
}
}
}
public class Test { public static void main(String[] args) { User user = new User(1234, 10);
int threadNum = 10;
for (int i = 0; i < threadNum; i++){
new SubThread(user).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(user);
}
}
<a name="EjVFK"></a>
### 3.6.4 AtomicReference
```java
public class Test {
/**
* static AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("abc", 0);
* atomicStampedReference.compareAndSet("abc", "def", oldStamp(取值时的版本号), newStamp);
*/
static AtomicReference<String> atomicReference = new AtomicReference<>("abc");
public static void main(String[] args) {
int threadNum = 100;
for(int i = 0; i < threadNum; i++){
new Thread(()->{
if (atomicReference.compareAndSet("abc","def")){
System.out.println(Thread.currentThread().getName() + "把字符串abc更改为def");
}
}).start();
}
for (int i = 0; i < threadNum; i++){
new Thread(()->{
if (atomicReference.compareAndSet("def","abc")){
System.out.println(Thread.currentThread().getName() + "把字符串def更改为abc");
}
}).start();
}
}
}
AtomicReference
可能导致ABA问题。AtomicStampedReference
能通过时间戳解决ABA问题,AtomicMarkableReference
能通过标志位解决ABA问题。