Java内置锁是一个互斥锁,这就意味着最多只有一个线程就能够获得该锁,当线程B尝试去获得线程A持有的内置锁时,线程B必须等待或者阻塞,直到线程A释放这个锁,如果线程A不释放这个锁,那么线程B将永远必须等待下去。
Java中每个对象都可以用作锁,这些锁称成为内置锁。线程进入同步代码块或方法时会自动获得该锁,在退出同步代码或方法时会释放该锁。获得内置锁的,唯一途径就是进入这个锁保护的同步代码块或方法。
先聊聊线程安全问题,为大家揭秘Java内置锁的核心原理。
什么是线程安全呢?当多个线程并发访问某个Java对象(Object)时,无论系统如何调度这些线程,也无论这些线程将如何交替操作,这个对象都能表现出一致的,正确的行为,那么对这个对象的操作是线程安全的。如果这个对象表现出不一致的、错误的行为,那么对这个对象的操作不是线程安全的,发生了线程的安全问题。
自增运算不是线程安全的
粗看上去,感觉这是一件不可思议的事情:对一个整数进行自增运算(++),怎么可能不是线程安全的呢?这可是只有一个完整的操作,看上去是那么的不可分割。
先来看看安全小实验
为了讲清楚问题,这里先提供一下实验代码,10个线程并行运行,对一个共享数据进行自增运算,每个线程自增运算1000次,具体代码如下:
NotSafePlusTest
package com.bilibili.itwolf;
public class NotSafePlusTest {
//定义初始化
private static Integer amount = 0;
public void selfPlus() {
amount++;
}
public Integer getAmount(){
return this.amount;
}
}
PlusTest
package com.bilibili.itwolf;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
@Slf4j
public class PlusTest {
final int MAX_TREAD = 10;
final int MAX_TURN = 1000;
/**
* 测试用例:测试不安全的累加器
*/
@Test
public void testNotSafePlus() throws InterruptedException {
//倒数 MAX_TREAD 次
CountDownLatch latch = new CountDownLatch(MAX_TREAD);
NotSafePlusTest counter = new NotSafePlusTest();
Runnable runnable = () -> {
for (int i = 0; i < MAX_TURN; i++) {
counter.selfPlus();
}
latch.countDown();
};
for (int i = 0; i < MAX_TREAD; i++) {
new Thread(runnable).start();
}
//等待倒数次数为0 所有线程执行完成
latch.await();
log.info("理论结果:{}", MAX_TREAD*MAX_TURN);
log.info("实际结果:{}", counter.getAmount());
log.info("差距结果:{}", MAX_TREAD*MAX_TURN-counter.getAmount());
}
}
运行结果:
09:50:58.053 [main] INFO com.bilibili.itwolf.PlusTest - 理论结果:10000 09:50:58.057 [main] INFO com.bilibili.itwolf.PlusTest - 实际结果:6167 09:50:58.057 [main] INFO com.bilibili.itwolf.PlusTest - 差距结果:3833
Process finished with exit code 0
通过结果可以看出:总计自增10000次,结果少了3833次,差距在36%左右。当然,这只是一次结果,每一次运行,差距都不同的,大家可以动手运行体验一下。总之,从结果可以看出,对NotSafePlus的amount成员的“++”,运算在多线程并发执行场景下出现了不一致、错误的行为,自增运算符“++”不是线程安全的。
以上代码中,为了获得10个线程的结果,主线程通过CountDownLatch工具类进行了并发线程的等待。
CountDownLatch是一个非常实用的等待多线程并发的工具类。调用线程可以在倒数上进行等待,一直等待倒数次数减少到0,才继续往下执行。每一个被等待的线程执行完成之后进行一次倒数。所有被等待的线程执行完成之后,倒数器的次数减少到0,调用线程可以往下执行,从而达到并发等待的效果。
在使用CountDownLatch时,先创建了一个CountDownLatch实例,设置其倒数的总数,例子中值为10,表示等待10个线程执行完成。主线程通过调用latch.await()在倒数实例上执行等待,等到latch实例倒数到0才能继续执行。
原因分析:自增运算符不是线程安全的
为什么自增运算符不是线程安全的呢?实际上,一个自增运算符是一个复合操作,至少包括三个JVM指令:"内存取值","寄存器增加1"和"存值到内存"。这三个指令在JVM内部是独立进行的,中间完全可能会出现多个线程并发进行。
比如在amount=100时,假设有三个线程同一时间读取amount值,读到的都是100,增加1后结果为101,三个线程都将结果存入amount的内存,amount的结果是101,而不是103
"内存取值","寄存器增加1"和"存值到内存"这三个JVM指令本身是不可再分的,它们都具备原子性,是线程安全的,也叫原子操作。但是,两个或者两个以上的原子操作合在一起进行操作就不再具备原子性了。比如先读后写,就有可能在读之后,其实这个变量被修改了,出现读和写数据不一致的情况。
|