无锁意味着方法未加锁,直观表现为线程之间存在着交叉执行
非原子操作例如 number++; number = number + 1; 这些操作实际上分为好几步执行
Atomic 包介绍
- Atomic 包是 JDK5 之后提供的,这个包提供了一系列原子类,这些类允许在多线程环境下,无锁的进行原子操作(当某个线程在执行 Atomic 方法时不会被其它线程打断,其它线程会一直等到该方法执行完成)
- Atomic 底层是借助 CPU 的 CAS 指令来实现的
Atomic 的作用
为了更好的让大家明白 Atomic 的作用,下面通过两个案例来进行分析
AtomicInteger 实现多线程安全加法
- Code 1 不能保证共享变量在多线程中的可见性、原子性
- Code 2 只能保证共享变量在多线程中的可见性、不能保证原子性
- Code 3 能保证共享变量在多线程中的可见性、原子性
Code 1(非线程安全)
- 下面代码在多线程中不安全,理想的输出结果为 1000,但多运行几次输出结果可能为 990、980,这是因为共享变量 number 是非线程安全的,而产生的可见性、原子性问题导致的
public class Code1 {
public static void main(String[] args) {
Test test = new Test(0);
for (int j = 0; j < 100; j++)
new Thread(test).start();
System.out.println(test.number);
}
public static class Test implements Runnable {
public int number;
public Test(int number) {
this.number = number;
}
@Override
public void run() {
number = number + 1;
number = number + 2;
number = number + 3;
number = number + 4;
}
}
}
Code 2 (使用 volatile,仍非线程安全)
- 下面使用 volatile 修饰变量 number,volatile 可以保证共享变量 number 在多线程中的可见性,但不能保证原子性,因此 number 结果仍可能不正确(不为 1000)
public class Code2 {
public static void main(String[] args) {
Test test = new Test();
for (int j = 0; j < 100; j++)
new Thread(test).start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("number------------"+test.number);
}
public static class Test implements Runnable {
volatile public int number;
public Test() {
this.number = 0;
}
@Override
public void run() {
number = number + 1;
System.out.println("ThreadId-" + Thread.currentThread().getId() + " : " + number);
number = number + 2;
System.out.println("ThreadId-" + Thread.currentThread().getId() + " : " + number);
number = number + 3;
System.out.println("ThreadId-" + Thread.currentThread().getId() + " : " + number);
number = number + 4;
System.out.println("ThreadId-" + Thread.currentThread().getId() + " : " + number);
}
}
}
- 输出结果可能如下
.....
ThreadId-108 : 974
ThreadId-108 : 980
ThreadId-108 : 983
ThreadId-109 : 984
ThreadId-107 : 978
ThreadId-109 : 990
ThreadId-109 : 993
ThreadId-108 : 988
ThreadId-109 : 997
number------------997
Code 3 (使用 AtomicInteger,线程安全)
- 使用 AtomicInteger 可以保证共享变量的可见性、操作原子性,因此 number 结果总为正确值(1000)
public class Code3 {
public static void main(String[] args) {
Test test = new Test(0);
for (int j = 0; j < 100; j++)
new Thread(test).start();
}
public static class Test implements Runnable {
public AtomicInteger number;
public Test(int number) {
this.number = new AtomicInteger(number);
}
@Override
public void run() {
number.addAndGet(1);
System.out.println("ThreadId-" + Thread.currentThread().getId() + " : " + number);
number.addAndGet(2);
System.out.println("ThreadId-" + Thread.currentThread().getId() + " : " + number);
number.addAndGet(3);
System.out.println("ThreadId-" + Thread.currentThread().getId() + " : " + number);
number.addAndGet(4);
System.out.println("ThreadId-" + Thread.currentThread().getId() + " : " + number);
}
}
}
- 输出结果可能如下
......
ThreadId-108 : 977
ThreadId-108 : 983
ThreadId-108 : 986
ThreadId-109 : 987
ThreadId-108 : 991
ThreadId-109 : 993
ThreadId-109 : 996
ThreadId-109 : 1000
AtomicBoolean 解决并发多次初始化问题
-
AtomicBoolean 的 compareAndSet(boolean expect, boolean update) 方法讲解:
- 比较 AtomicBoolean 和 expect 的值,如果一致,执行方法内的语句(其实就是一个if语句),并把 AtomicBoolean 的值设成 update
- 这两个动作之间不会被打断,任何内部或者外部的语句都不可能在两个动作之间运行,属于原子操作
Code 1(非并发安全)
- 下面代码在多线程并发会存在多次初始化问题,原因可能是共享变量的可见性与线程争夺交叉执行导致的
public class Code1 {
public static void main(String[] args) {
Test test = new Test(true);
for (int j = 0; j < 100; j++)
new Thread(test).start();
}
public static class Test implements Runnable {
public boolean flag;
public Test(boolean flag) {
this.flag = flag;
}
public void init() {
if (flag) {
System.out.println("初始化中1");
System.out.println("初始化中2");
System.out.println("初始化完毕");
flag = false;
}
}
@Override
public void run() {
init();
}
}
}
- 输出结果可能如下,这是因为当线程 1 在执行 init() 初始化时线程 2 抢占到了 CPU 资源,并且也执行了 init() 方法,导致多次初始化
初始化中1
初始化中2
初始化完毕
初始化中1
初始化中2
初始化完毕
Code 2(使用 AtomicBoolean)
- 使用 AtomicBoolean 的 compareAndSet 方法保证 compare 与 set 是原子级操作,可以保证 init() 只被调用一次
public class Code2 {
public static void main(String[] args) {
Test test = new Test(true);
for (int j = 0; j < 100; j++)
new Thread(test).start();
}
public static class Test implements Runnable {
AtomicBoolean atomicBoolean;
public Test(boolean flag) {
this.atomicBoolean = new AtomicBoolean(flag);
}
public void init() {
if (atomicBoolean.compareAndSet(true, false)) {
System.out.println("初始化中1");
System.out.println("初始化中2");
System.out.println("初始化完毕");
}
}
@Override
public void run() {
init();
}
}
}
- 因此输出结果总是如下
初始化中1
初始化中2
初始化完毕