1. 线程安全概念
概念: 当多线程并发访问时,程序有可能得不到正确的数据结果,即为线程不安全。
- 同步与异步:比如你和赵四一起做同一套数学模拟题,异步就是你们将卷子复制一套,然后一人做半套题,最后拼成一份答案,同步就是你做几道,将卷子扔给赵四,赵四做几道再扔给你,循环交替,直到卷子做完。
- 异步:互不干扰,资源利用率高,因为整个过程中没有人会长时间处于等待状态,但是不安全,因为有可能两个人题目刷重。
- 同步:安全,不会刷重题目,但是效率相对而言会低一些,但有些时候,我们不得不牺牲一点效率因素,来提升安全因素。
- 线程不安全原因:线程安全问题多由异步造成:
- 比如赵四账户有1万元余额
- 今天别人说好要给他转账5000元(线程A)
- 他也要给别人转账2000元(线程B)
- 线程A执行,10000 + 5000 = 15000 但是还未来得及更新账户余额,线程就进入了等待状态
- 线程B执行,10000 - 2000 = 8000,但是还未来得及更新账户余额,线程就进入了等待状态
- 线程A继续执行,更新账户余额为15000
- 线程B继续执行,更新账户余额为8000
- 最终,经过赵四一顿操作,余额最终为8000元
- 线程安全建议:
- 尽量对共享资源使用同步的操作,如代码加锁等。
- 尽量对共享资源使用原子性的操作,如使用JDK提供的原子类等。
- 尽量少用共享资源,多用线程私有资源,如使用ThreadLocal等。
源码: /javase-advanced/
- src:
c.y.thread.sync.TicketSellTest
/**
* @author yap
*/
public class TicketSellTest {
private static class Ticket implements Runnable {
private int ticketNo;
@SneakyThrows
@Override
public void run() {
while (true) {
TimeUnit.SECONDS.sleep(1L);
sellTicket();
}
}
private void sellTicket() {
int maxNo = 100;
if (ticketNo < maxNo) {
ticketNo++;
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "卖票: " + ticketNo);
}
}
}
@Test
public void sellTicket() {
Ticket ticket = new Ticket();
new Thread(ticket, "thread-A").start();
new Thread(ticket, "thread-B").start();
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}
2. synchronized
概念: 关键字synchronized可以将代码进行线程同步隔离,处于同步隔离区的代码,只能被所有线程排队执行,牺牲效率以保证数据安全。
- 本质:synchronized的本质是非公平锁,在字节码指令级:
- 使用
monitorenter指令来表示进入和离开隔离区,即获取锁。 - 使用
monitorexit指令表示离开隔离区,即释放锁。
- 使用
- 用法:
- 可以在方法签名中添加
synchronized修饰,对整个方法加锁。 - 可以使用
synchronized(锁类型){}同步代码块对部分代码加锁。 - 除了String,Integer和Long之外,都可以作为同步锁的类型。
- 锁实例建议填写final,因为一旦运行中途锁实例被改变了,同步效果会立即消失。
- 只有多个线程使用的是同一把锁时才会发生同步现象。
- 可以在方法签名中添加
- 原则:粒度尽量小,数量尽量少,只在拥有共享数据的地方加锁。
synchronized也可以保证可见性,但不能禁止指令重排。
源码: /javase-advanced/
- src:
c.y.thread.sync.TicketSellProTest
2.1 同步方法的锁类型
概念:
- synchronized修饰成员方法时使用的是this锁。
- synchronized修饰静态方法时使用的是当前类的字节码锁。
源码: /javase-advanced/
- src:
c.y.thread.sync.LockTypeTest
/**
* @author yap
*/
public class TicketSellProTest {
private static class Ticket implements Runnable {
private int ticketNo;
@SneakyThrows
@Override
public void run() {
while (true) {
TimeUnit.SECONDS.sleep(1L);
sellTicket();
}
}
private /*synchronized*/ void sellTicket() {
int maxNo = 100;
synchronized (this) {
if (ticketNo < maxNo) {
ticketNo++;
String threadName = Thread.currentThread().getName();
System.out.println(threadName + "卖票: " + ticketNo);
}
}
}
}
@Test
public void sellTicket() {
Ticket ticket = new Ticket();
new Thread(ticket, "thread-A").start();
new Thread(ticket, "thread-B").start();
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}
2.2 锁的可重入性
概念: synchronized的锁具有重入性,即在 synchronized methodA() 中可以调用 synchronized methodA(),因为是同一个线程申请的这把锁,允许重入同步代码区。
- 锁的重入流程:如果因重入而获取了5次锁,则对应的必须要释放5次重入锁,所以锁的重入次数必须被记录下来:
- 在hotspot实现中,加非OS锁时会在线程栈中生成一个LR(Lock Record),将锁实例的mark-word信息进行备份(主要就是实例的hashcode)因为加锁后,锁的信息会覆盖掉这些信息。
- 每重入一次,则都会再次入栈一个空的LR,重入5次锁,则入栈5个空LR。
- 每释放一次锁,弹出1个空LR,5个空LR均被弹出,则表示全部重入锁释放完毕。
- 最后再释放最后一个带有备份信息的LR(释放前还原备份信息),完成完整的同步操作。
- OS锁也可以重入,但重入次数不通过LR记录,而是通过底层的一个变量来完成。
源码: /javase-advanced/
- src:
c.y.thread.sync.ReentryTest
/**yap
* @author JoeZhou
*/
public class ReentryTest {
private synchronized void methodA() {
System.out.println("methodA...");
// Found to be the same thread, allowing reentry
methodB();
}
private synchronized void methodB() {
System.out.println("methodB...");
}
@Test
public void reentry() {
new ReentryTest().methodA();
}
}
2.3 异常释放锁
概念: synchronized同步隔离区中的代码如果爆发了异常会释放锁,此时其他等待进入的线程则有可能获取到锁,进入到同步代码中。
源码: /javase-advanced/
- src:
c.y.thread.sync.ExceptionTest
/**
* @author yap
*/
public class ExceptionTest {
private static class ExceptionDemo implements Runnable {
private int count;
@Override
public synchronized void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + ":" + count);
try {
TimeUnit.SECONDS.sleep(1L);
if (count++ == 3) {
throw new ArithmeticException();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Test
public void exception() {
ExceptionDemo exceptionDemo = new ExceptionDemo();
new Thread(exceptionDemo, "threadA").start();
new Thread(exceptionDemo, "threadB").start();
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}
2.4 死锁
概念:
- 假设吃饭的时候,我有一根筷子,你有一根筷子,我需要你给我凑成一双,我吃饭,你需要我给你凑成一双,你吃饭,这时候就会僵持不下,发生死锁,线程也是一样,A线程持有一个B的锁,B线程持有一个A的锁,二者谁也不肯释放锁,就会发生死锁。
- 死锁的现象我们应该积极避免,应该按照锁的hashcode进行排列,hashcode大的先锁,hashcode小的后锁,所有线程都按照这个规矩办事,就不会出现死锁问题了。
源码: /javase-advanced/
- src:
c.y.thread.sync.DeadLockTest
/**
* @author yap
*/
public class DeadLockTest {
private static class DeadLockRunnable implements Runnable {
private final Object objA = new Object();
private final Object objB = new Object();
@SneakyThrows
@Override
public void run() {
String threadA = "threadA";
if (Thread.currentThread().getName().equals(threadA)) {
synchronized (objA) {
System.out.println("if: objA");
TimeUnit.SECONDS.sleep(1L);
synchronized (objB) {
System.out.println("if: objB");
}
}
} else {
synchronized (objB) {
System.out.println("else: objB");
TimeUnit.SECONDS.sleep(1L);
synchronized (objA) {
System.out.println("else: objA");
}
}
}
}
}
@Test
public void staticMethodLockType() {
Runnable runnable = new DeadLockRunnable();
new Thread(runnable, "threadA").start();
new Thread(runnable, "threadB").start();
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}
2.5 DCL饱汉单例模式
概念:
- 饿汉单例本身就是线程安全的,饱汉单例本身就是线程不安全的。
- 我们可以使用DCL(Double Check Lock)模式来优化饱汉单例模式,使其变为线程安全的。
- 必须添加volatile来禁止指令重排,否则可能会发生如下过程:
- 线程A进入隔离区,if判断通过,执行new指令(new不是原子性的指令)
- 线程A为实例分配空间,得到内存地址0x9527。
- 线程A赋属性初始值:name=null,age=0等。
- (重排后)线程A调用
astore关联变量:instance=0x9527。 - 线程B执行,第一个if处发现instance不为null,直接返回instance,此时instance中的属性均为初始值(错误)。
- (重排后)线程A赋属性真正值:name="赵四",age=58,但为时已晚,线程B已经拿到错误数据。
源码: /javase-advanced/
- src:
c.y.thread.sync.DclSingletonTest
/**
* @author yap
*/
public class DclSingletonTest {
private static class DclSingleton {
/**
* why use volatile?
*/
private volatile static DclSingleton singleton;
private DclSingleton() {
}
public static DclSingleton getInstance() {
// for improve efficiency
if (singleton == null) {
synchronized (DclSingleton.class) {
if (singleton == null) {
singleton = new DclSingleton();
}
}
}
return singleton;
}
}
@Test
public void dclSingleton() {
for (int i = 0, j = 10; i < j; i++) {
new Thread(() -> {
System.out.println(DclSingleton.getInstance());
}).start();
}
}
}
3. 原子类
概念: 在保证线程安全的手段中,除了加锁这种比较消耗性能的方法外,我们可以使用java并发包 java.util.concurrent.atomic 中提供的原子类来利用CAS自旋的方式完成更简单高效的原子性操作。
源码: /javase-advanced/
- src:
c.y.thread.sync.AtomicOperationTest
/**
* @author yap
*/
public class AtomicOperationTest {
private int num;
private AtomicInteger atomicNum = new AtomicInteger(0);
@Test
public /*synchronized*/ void nonAtomicOperation() {
for (int i = 0, j = 100; i < j; i++) {
new Thread(() -> {
for (int m = 0, n = 100; m < n; m++) {
num++;
}
System.out.println(num);
}).start();
}
}
@Test
public void atomicOperation() {
for (int i = 0, j = 100; i < j; i++) {
new Thread(() -> {
for (int m = 0, n = 100; m < n; m++) {
atomicNum.incrementAndGet();
}
System.out.println(atomicNum);
}).start();
}
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}
3.1 基本类型原子类
概念: 想要线程安全地操作基本类型可以使用对应的原子类,如 AtomicBoolean,AtomicInteger 和 AtomicLong。
- 构造:以
AtomicInteger为例:AtomicInteger():默认初始值0。AtomicInteger(int initialValue):指定初始值。
- 方法:以
AtomicInteger为例:int get():获取当前变量值。int incrementAndGet():自增1后返回。int decrementAndGet():自减1后返回。int addAndGet(int delta):自增delta后返回,支持负数。int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction):自定义计算过程- param1:函数式接口中的初始right值。
- param2:函数式接口,需要两个int型运算参数,返回一个int型结果。
源码: /javase-advanced/
- src:
c.y.thread.sync.BaseAtomicTest
/**
* @author yap
*/
public class BaseAtomicTest {
@Test
public void atomicInteger() {
AtomicInteger num = new AtomicInteger(0);
System.out.println("num:" + num.get());
System.out.println("++num:" + num.incrementAndGet());
System.out.println("--num:" + num.decrementAndGet());
System.out.println("num+=6 then return:" + num.addAndGet(6));
System.out.println("accumulate then return:" + num.accumulateAndGet(5, (left, right) -> {
System.out.print("left:" + left + "\t");
System.out.print("right:" + right + "\n");
return (left - 1) * (right - 9) / 3;
}
));
System.out.println("num:" + num.get());
}
@Test
public void atomicLong() {
AtomicLong num = new AtomicLong(0L);
System.out.println("num:" + num.get());
System.out.println("num++:" + num.getAndIncrement());
System.out.println("num--:" + num.getAndDecrement());
System.out.println("return then num-=6:" + num.getAndAdd(-6));
System.out.println("return then accumulate:" + num.getAndAccumulate(5, (left, right) -> {
System.out.print("left:" + left + "\t");
System.out.print("right:" + right + "\n");
return (left - 1) * (right - 9) / 3;
}
));
System.out.println("num:" + num.get());
}
@Test
public void atomicBoolean() {
AtomicBoolean flag = new AtomicBoolean(false);
System.out.println("flag:" + flag.get());
System.out.println("change to true:" + flag.getAndSet(true));
System.out.println("flag:" + flag.get());
}
}
3.2 数组原子类
概念: 如果想要对数组进行原子操作,则可以使用 AtomicIntegerArray,AtomicLongArray 和 AtomicReferenceArray。
- 构造:以
AtomicIntegerArray为例:AtomicIntegerArray():默认初始值0。AtomicIntegerArray(int initialValue):指定初始值。
- 方法:以
AtomicIntegerArray为例:int get(int i):通过角标获取元素。int incrementAndGet(int i):通过角标自增1后返回。int decrementAndGet(int i):通过角标自减1后返回。int addAndGet(int i, int delta):通过角标自增delta后返回,支持负数。int accumulateAndGet(int i, int x, IntBinaryOperator accumulatorFunction):自定义计算过程- param1:数组角标。
- param2:初始right值。
- param3:函数式接口,需要两个int型运算参数,返回一个int型结果。
源码: /javase-advanced/
- src:
c.y.thread.sync.ArrayAtomicTest
/**
* @author yap
*/
public class ArrayAtomicTest {
@Test
public void atomicIntegerArray() {
AtomicIntegerArray arr = new AtomicIntegerArray(new int[]{3, 2});
System.out.println("arr[0]:" + arr.get(0));
System.out.println("++(arr[0]):" + arr.incrementAndGet(0));
System.out.println("--(arr[0]):" + arr.decrementAndGet(0));
System.out.println("(arr[0])+=6 then return:" + arr.addAndGet(0, 6));
System.out.println("accumulate then return:" + arr.accumulateAndGet(0, 5, (left, right) -> {
System.out.print("left:" + left + "\t");
System.out.print("right:" + right + "\n");
return (left - 1) * (right + 9) / 3;
}
));
System.out.println("arr[0]:" + arr.get(0));
}
@Test
public void atomicLongArray() {
AtomicLongArray arr = new AtomicLongArray(new long[]{3, 2});
System.out.println("arr[0]:" + arr.get(0));
System.out.println("(arr[0])++:" + arr.getAndIncrement(0));
System.out.println("(arr[0])--:" + arr.getAndDecrement(0));
System.out.println("return then (arr[0])-=6:" + arr.getAndAdd(0, -6));
System.out.println("return then accumulate:" + arr.getAndAccumulate(0, 5, (left, right) -> {
System.out.print("left:" + left + "\t");
System.out.print("right:" + right + "\n");
return (left - 1) * (right + 9) / 3;
}
));
System.out.println("arr[0]:" + arr.get(0));
}
@Test
public void atomicReferenceArray() {
AtomicReferenceArray<String> arr = new AtomicReferenceArray<>(new String[]{"3", "2"});
System.out.println("arr[0]:" + arr.get(0));
System.out.println("update then return" + arr.updateAndGet(0, v -> v + "-"));
System.out.println("arr[0]:" + arr.get(0));
}
}
3.3 升级原子类
概念: DoubleAdder和LongAdder对Double和Long的原子更新性能进行了优化提升,但是它们只有简单的自增,添加和自减方法,底层使用了分段锁技术,在高并发情况下,效率会更高,比如假设有1000个线程,分成5段,则每段执行200个线程,最后将5段结果汇总并返回。
- 构造:以
LongAdder为例:LongAdder():默认初始值0。
- 方法:以
LongAdder为例:void add(long x):自增x,支持负数。void increment():自增1。void decrement():自减1。
源码: /javase-advanced/
- src:
c.y.thread.sync.AdderTest
/**
* @author yap
*/
public class AdderTest {
@Test
public void longAdder() {
LongAdder num = new LongAdder();
System.out.println("current value: " + num);
num.add(5);
System.out.println("after +5:" + num);
num.increment();
System.out.println("after +1:" + num);
num.decrement();
System.out.println("after -1:" + num);
}
@Test
public void doubleAdder() {
DoubleAdder num = new DoubleAdder();
System.out.println("current value: " + num);
num.add(5);
System.out.println("after +5:" + num);
}
}
3.4 引用类型原子类
概念: 如果想对某个类中的属性进行原子操作,则可以使用 AtomicIntegerFieldUpdater,AtomicLongFieldUpdater 或 AtomicReferenceFieldUpdater。
- 使用引用类型原子类注意事项:
- 成员属性必须
volatile修饰,表示该属性在线程之间立即可见。 - 成员属性不能被
private,static或final修饰。
- 成员属性必须
- 如果很在乎CAS的ABA问题,可以替换如下两种:
AtomicMarkableReference:带版本戳的原子引用类型,版本戳为boolean类型。AtomicStampedReference:带版本戳的原子引用类型,版本戳为int类型。
源码: /javase-advanced/
- src:
c.y.thread.sync.FieldUpdaterTest
/**
* @author yap
*/
public class FieldUpdaterTest {
@Data
@AllArgsConstructor
@NoArgsConstructor
private static class Student implements Serializable {
volatile long id;
volatile String name;
volatile int age;
}
private Student student;
@Before
public void before() {
student = new Student(1L, "zhao-si", 58);
}
@Test
public void atomicLongFieldUpdater() {
AtomicLongFieldUpdater<Student> idUpdater =
AtomicLongFieldUpdater.newUpdater(
Student.class, "id");
System.out.println("++id:" + idUpdater.incrementAndGet(student));
System.out.println("id:" + student.getId());
System.out.println("--id:" + idUpdater.decrementAndGet(student));
System.out.println("id:" + student.getId());
System.out.println("id+=5 then return:"
+ idUpdater.addAndGet(student, 5));
System.out.println("id:" + student.getId());
System.out.println("accumulate then return:" +
idUpdater.accumulateAndGet(student, 5, (left, right) -> {
System.out.print("left:" + left + "\t");
System.out.print("right:" + right + "\n");
return (left - 1) * (right - 9) / 4;
}));
System.out.println("id:" + student.getId());
}
@Test
public void atomicIntegerFieldUpdater() {
AtomicIntegerFieldUpdater<Student> ageUpdater =
AtomicIntegerFieldUpdater.newUpdater(
Student.class, "age");
System.out.println("age++: " + ageUpdater.getAndIncrement(student));
System.out.println("age:" + student.getAge());
System.out.println("age--:" + ageUpdater.getAndDecrement(student));
System.out.println("age:" + student.getAge());
System.out.println("return then age+=5:"
+ ageUpdater.getAndAdd(student, 5));
System.out.println("age:" + student.getAge());
System.out.println("return then accumulate" +
ageUpdater.getAndAccumulate(student, 5, (left, right) -> {
System.out.print("left:" + left + "\t");
System.out.print("right:" + right + "\n");
return (left - 1) * (right - 9) / 4;
}));
System.out.println("age:" + student.getAge());
}
@Test
public void atomicReferenceFieldUpdater() {
AtomicReferenceFieldUpdater<Student, String> nameUpdater =
AtomicReferenceFieldUpdater.newUpdater(
Student.class, String.class, "name");
System.out.println("set name to fei-ji: " +
nameUpdater.getAndSet(student, "fei-ji"));
System.out.println("name:" + student.getName());
System.out.println("cas name to fei-ji: " +
nameUpdater.compareAndSet(student, "fei-ji", "da-pao"));
System.out.println("name:" + student.getName());
}
}
4. ThreadLocal
概念: ThreadLocal被称为本地线程,每个ThreadLocal只能存储一个值。
- 存储:ThreadLocal内部维护了一个ThreadLocalMap,ThreadLocalMap内部维护了一个弱引用Entry:
- key: this,即当前ThreadLocal实例。
- value: 定义ThreadLocal时指定的泛型类的实例。
- 方法:
void set(T value): 以this为key,以value为值,存储到当前线程的ThreadLocalMap中,其他线程获取不到。T get(): 以this为key,取出当前线程的ThreadLocalMap中对应的value值。void remove(): 以this为key,移除当前线程的ThreadLocalMap中对应的value值。T setInitialValue(): ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get(),返回此方法值。
源码: /javase-advanced/
- src:
c.y.thread.sync.ThreadLocalTest
/**
* @author yap
*/
public class ThreadLocalTest {
private static class Person {
}
private ThreadLocal<Person> threadLocal = new ThreadLocal<>();
@Test
public void threadLocal() {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1L);
threadLocal.set(new Person());
System.out.println("set: over");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2L);
// get null
System.out.println("get: " + threadLocal.get());
// prevent memory leaks
threadLocal.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
@SneakyThrows
@After
public void after() {
System.out.println(System.in.read());
}
}