什么是线程安全
在高并发场景下, 线程的执行结果是确定的, 符合期望的;反之, 线程不安全可能造成违背直接的结果.
什么是线程安全的类
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
如何实现线程安全的类
- 线程封闭: 把对象封装到线程里面, 只有唯一一个线程可以看到这个对象.就算这个对象不是线程安全的也能实现线程安全, 因为在单线程环境下, 不存在并发竞争问题.
- 栈封闭: 在线程里面创建的局部变量都是线程安全的
- ThreadLocal: ThreadLocal是用一个Map实现的, key对应线程, value对应缓存的对象, 每个线程都有独立的value对象, 实现了类似线程封闭的效果.注意:ThreadLocal本身是线程安全的, 但是外部逻辑可能对缓存对象做线程不安全操作.
- 无状态的类: 没有属性的类叫做无状态类, 这个类只可能有成员方法, 实现了类似栈封闭的效果
- 不可变的类: 所有成员变量加上final关键字, 不可修改也就没有并发对写问题. 注意: 如果成员变量是对象, final只能保证对象的引用不可更改, 而对象成员属性依然可能存在并发问题.
- 加锁或CAS: 加内置锁或者显式锁或者CAS, 保证代码块或者方法原子性
死锁
死锁: 两个以上线程互相持有对方的锁, 并永不释放, 这种现象可叫做死锁.
学术定义:
- 互斥条件
- 请求和保持条件
- 不剥夺条件
- 环路等待条件
手写死锁代码:
/**
* @Author kk
* @Date 2023-10-14
* 手写死锁示例
*/
public class DeadLockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void playA() {
synchronized (lock1) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
SleepTools.second(1);
synchronized (lock2) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
}
}
}
public static void playB() {
synchronized (lock2) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
SleepTools.second(1);
synchronized (lock1) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
}
}
}
public static void main(String[] args) {
new Thread(DeadLockDemo::playA).start();
playB();
}
}
监控死锁
- jdk命令: jstack pid
- arthas命令: thread -b
解决死锁:
解决死锁的方案也就是推翻前面说的死锁原则
- 获取多个锁的顺序保证一致
- 获取锁失败后释放锁
有序占锁示例:
/**
* @Author kk
* @Date 2023-10-14
* 打破死锁示例-有序占锁
*/
public class DeadLockBreakDemo1 {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
private void playA() {
synchronized (lock1) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
SleepTools.second(1);
synchronized (lock2) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
}
}
}
private void playB() {
synchronized (lock1) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
SleepTools.second(1);
synchronized (lock2) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
}
}
}
public static void main(String[] args) {
DeadLockBreakDemo1 demo = new DeadLockBreakDemo1();
new Thread(demo::playA).start();
demo.playB();
}
}
抢锁失败释放占有锁示例:
/**
* @Author kk
* @Date 2023-10-14
* 打破死锁示例-抢锁失败释放锁
*/
public class DeadLockBreakDemo2 {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
private static final Random R = new Random();
public void playA() throws InterruptedException {
while (true) {
if (lock1.tryLock()) {
try {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
if (lock2.tryLock()) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
lock2.unlock();
System.out.println(ThreadTools.getThreadName() + "释放了lock2");
break;
}
} finally {
lock1.unlock();
System.out.println(ThreadTools.getThreadName() + "释放了lock1");
}
}
Thread.sleep(R.nextInt(10));
}
}
public void playB() throws InterruptedException {
while (true) {
if (lock2.tryLock()) {
try {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
if (lock1.tryLock()) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
lock1.unlock();
System.out.println(ThreadTools.getThreadName() + "释放了lock1");
break;
}
} finally {
lock2.unlock();
System.out.println(ThreadTools.getThreadName() + "释放了lock2");
}
}
Thread.sleep(R.nextInt(50));
}
}
public static void main(String[] args) throws Exception {
DeadLockBreakDemo2 demo = new DeadLockBreakDemo2();
new Thread(() -> {
try {
demo.playA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
demo.playB();
}
}
打破死锁可能存在的问题
- 动态顺序占锁: 加锁代码是有序, 但是加锁对象可动态传参改变, 见下方示例
- 活锁: 多线程同时获取第一把锁成功, 同时获取第二把锁失败, 重复循环, 线程是RUNNABLE状态.
动态顺序占锁示例:
/**
* @Author kk
* @Date 2023-10-14 3:53
* 动态顺序死锁示例
*/
public class DeadLockTrendsDemo {
private void playA(Object lock1, Object lock2) {
synchronized (lock1) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
SleepTools.second(1);
synchronized (lock2) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
}
}
}
private void playB(Object lock1, Object lock2) {
synchronized (lock1) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
SleepTools.second(1);
synchronized (lock2) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
}
}
}
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
DeadLockTrendsDemo demo = new DeadLockTrendsDemo();
new Thread(() -> demo.playA(lock1, lock2)).start();
demo.playB(lock2, lock1);
}
}
解决动态顺序占锁: 给加锁对象hash排序, 按顺序获取锁
/**
* @Author kk
* @Date 2023-10-14 3:53
* 动态顺序死锁 - 对象手动排序后加锁解决
*/
public class DeadLockTrendsFixDemo {
private static final Object lock3 = new Object();
private void playA(Object lock1, Object lock2) {
List<Object> lockList = sortObj(lock1, lock2);
if (lockList != null) {
synchronized (lockList.get(0)) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
SleepTools.second(1);
synchronized (lockList.get(1)) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
}
}
} else {
// hash 相等, 引用第三方锁
synchronized (lock3) {
synchronized (lock1) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock1");
SleepTools.second(1);
synchronized (lock2) {
System.out.println(ThreadTools.getThreadName() + "拿到了lock2");
}
}
}
}
}
// 按hash排序, 升序存到列表; 若hash相等, 返回null
private List<Object> sortObj(Object lock1, Object lock2) {
int hash1 = System.identityHashCode(lock1);
int hash2 = System.identityHashCode(lock2);
if (hash1 == hash2) {
return null;
} else if (hash1 > hash2) {
return Arrays.asList(lock2, lock1);
} else {
return Arrays.asList(lock1, lock2);
}
}
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
DeadLockTrendsFixDemo demo = new DeadLockTrendsFixDemo();
new Thread(() -> demo.playA(lock1, lock2)).start();
demo.playA(lock2, lock1);
}
}
解决活锁问题: 获取锁失败后, 休眠随机数, 避免同一时刻竞争一把锁,代码参考上面DeadLockBreakDemo2
线程饥饿
线程优先级低导致长时间分不到CPU时间片