并发和多线程
术语和概念
竞态条件
两条指令的执行具有逻辑关系或者依赖性,多线程执行时会产生故障,这是操作系统层面的定义,一般说的是线程间通信的所产生的故障
现在有少数人可能有偏解,会把业务的非预期执行行为和结果也归纳为这个,其实应该不包含业务上的执行情况
为什么会产生竞态条件的问题
因为无论操作系统层面,还是java虚拟机层面,锁实现的基本原理是通过锁定某个对象或者说内存来实现的,并不是通过操作系统精确定位某个到线程然后暂停它或者恢复它
我们最常见的错误:java.lang.IllegalMonitorStateException: current thread is not owner,这使java为了防止此种竞态条件发生的强制检测
先写几个工具类备用
private static void wait(Object lock) {
try {
synchronized (lock) {
lock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void notify(Object lock) {
synchronized (lock) {
lock.notify();
}
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void join(Thread ... ts) {
for(Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static void start(Thread ... ts) {
for(Thread t : ts) {
t.start();
}
}
设定一个常见面试题:三个线程abc,同时开始,但是a先结束,b再结束,c最后。
预想如下的执行过程
第一版代码,尝试人为控制不发生竞态条件,但是java会检测出来
/**
* 不加以控制
*/
public static void testNoControl() {
Object bLock = new Object();
Object cLock = new Object();
Thread a = new Thread(() -> {
System.out.println("a开始");
System.out.println("a结束");
synchronized (bLock) {
bLock.notify();
}
});
Thread b = new Thread(() -> {
System.out.println("b开始");
try {
bLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b结束");
synchronized (cLock) {
cLock.notify();
}
});
Thread c = new Thread(() -> {
System.out.println("c开始");
try {
cLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("c结束");
});
start(a, b, c);
join(a, b, c);
}
/**
* 尝试使线程休眠,等待后续线程先获取到锁对象上的等待机会
*/
public static void testControlBySleep() {
Object bLock = new Object();
Object cLock = new Object();
Thread a = new Thread(() -> {
System.out.println("a开始");
sleep(2000);
System.out.println("a结束");
bLock.notify();
});
Thread b = new Thread(() -> {
System.out.println("b开始");
wait(bLock);
System.out.println("b结束");
sleep(1000);
cLock.notify();
});
Thread c = new Thread(() -> {
System.out.println("c开始");
wait(cLock);
System.out.println("c结束");
});
start(a, b, c);
join(a, b, c);
}
结果肯定不行,java.lang.IllegalMonitorStateException: current thread is not owner
第二版代码,在同步代码块中wait和notify
/**
* 尝试在同步代码块中让线程等待和唤醒
*/
public static void testControlBySynchronized() {
Object bLock = new Object();
Object cLock = new Object();
Thread a = new Thread(() -> {
System.out.println("a开始");
sleep(2000);
System.out.println("a结束");
notify(bLock);
});
Thread b = new Thread(() -> {
System.out.println("b开始");
wait(bLock);
System.out.println("b结束");
sleep(1000);
notify(cLock);
});
Thread c = new Thread(() -> {
System.out.println("c开始");
wait(cLock);
System.out.println("c结束");
});
start(a, b, c);
join(a, b, c);
}
这一段就可以正确执行
那么即使使用了同步代码块,如果不控制唤醒和等待时机,是否会产生死等
会的,因为notify先调用就会导致wait永远没有机会唤醒,把上述sleep注释掉,很大几率就会死等
那么最常用的正确姿势是什么样的
简单方式:外部标志或者共享变量;
/**
* 使用共享变量控制
*/
public static void testByShareVariable() {
StringBuffer executed = new StringBuffer();
Thread a = new Thread(() -> {
System.out.println("a开始");
while(!executed.isEmpty()) {
}
executed.append("a");
System.out.println("a结束");
notify(executed);
});
Thread b = new Thread(() -> {
System.out.println("b开始");
while(executed.indexOf("a") < 0) {
wait(executed);
}
executed.deleteCharAt(0);
executed.append("b");
System.out.println("b结束");
notify(executed);
});
Thread c = new Thread(() -> {
System.out.println("c开始");
while(executed.indexOf("b") < 0) {
wait(executed);
}
executed.deleteCharAt(0);
System.out.println("c结束");
});
start(a, b, c);
join(a, b, c);
}
因为abc需要顺序结束,其实我们只使用一个共享变量就可以完成这个操作,但是写起来实际上还是费点脑筋的,为了降低出错的几率,java也给我们提供了一些列工具类,只需要理解api的意思就可以完成上述操作,比如countdowanlatch
/**
* 利用CountdownLatch 计数器,这中方式和自定义外部共享变量的方式是一样的意思
*/
public static void testCountdownLatch() {
CountDownLatch a = new CountDownLatch(1);
CountDownLatch b = new CountDownLatch(1);
new Thread(() -> {
System.out.println("我是a");
a.countDown();
}).start();
new Thread(() -> {
try {
a.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是b");
b.countDown();
}).start();
new Thread(() -> {
try {
b.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是c");
}).start();
}
临界区
应当需要我们去关注的多线程并发问题的那部分代码,范围越小越好,实际表现就是同步代码块的区域
临界区
应当需要我们去关注的多线程并发问题的那部分代码,范围越小越好,实际表现就是同步代码块的区域
阻塞队列
当需要考虑并发问题时,对集合或者共享变量的一切操作任务,都考优先虑使用阻塞队列,极简单任务考虑使用标志或者状态