一、线程间的通信之while防止虚假唤醒
在对多个线程操作共享数据的逻辑进行同步时,要避免使用if判断的方式使某线程进入waiting状态,要改为使用while。原因是while可以防止虚假唤醒:
用if使线程进入waiting可能会使判断过的正在等待被唤醒的线程直接进入,不管符不符合if的条件;而while则会重新判断当前正在waiting的线程是否符合while()内的条件,保证多个线程之间不会有虚假唤醒的情况。
举例:四个线程操作一个数字number,number只能为1或者0,两个线程负责将number加1,另外两个线程负责将number减1。四个线程都各自执行十次,最后执行时要保证number的加1和减1循环交替。(注意,这里使用四个线程作为例子是因为:如果只有两个线程,一个线程唤醒的另一个线程是唯一的。但如果设置为四个线程,一个线程唤醒的其他线程不唯一,就会产生随机性),这里使用Condition对象的await()和signalAll()方法来操作线程的等待和唤醒。
public class ProdConsumer {
public static void main(String[] args) {
//获得资源类的对象
AirCondition airCondition = new AirCondition();
//启动四个线程,两个线程调用加一方法,两个线程调用减一方法,分别执行十次
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airCondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airCondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airCondition.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
airCondition.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class AirCondition{
private int number;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//使number加1的方法
public void increment() throws InterruptedException {
lock.lock();
try {
while(number != 0){
//线程进入waiting
condition.await();
}
number ++;
//唤醒其他所有正在waiting的线程
condition.signalAll();
System.out.println(Thread.currentThread().getName()+":"+number);
} finally {
lock.unlock();
}
}
//使number减1的方法
public void decrement() throws InterruptedException {
lock.lock();
try {
while(number == 0){
condition.await();
}
number --;
condition.signalAll();
System.out.println(Thread.currentThread().getName()+":"+number);
} finally {
lock.unlock();
}
}
}
最后的执行结果(应该应该有40行,这里省略):
A:1
B:0
C:1
B:0
C:1
D:0
//......略
但是,如果我将increment和decrement方法中的while全都替换为if
即:
//使number加1的方法
public void increment() throws InterruptedException {
lock.lock();
try {
//while换成if
if(number != 0){
condition.await();
}
number ++;
condition.signalAll();
System.out.println(Thread.currentThread().getName()+":"+number);
} finally {
lock.unlock();
}
}
//使number减1的方法
public void decrement() throws InterruptedException {
lock.lock();
try {
//while换成if
if(number == 0){
condition.await();
}
number --;
condition.signalAll();
System.out.println(Thread.currentThread().getName()+":"+number);
} finally {
lock.unlock();
}
}
这时,运行的结果就可能会产生错误:
A:1
B:0
C:1
A:2
//......略
可以发现,这里出现了2的情况。原因如下:
-
线程A、C是加1的线程,B、D是减1的线程。
-
在第一行,A抢到了锁,操作number加一并释放锁。
-
在第二行的竞争中,C其实最先抢到了锁,但是由于此时number还是1,if条件判断不符合,所以C进入了waiting状态。
-
在第三步C进入waiting状态之后,剩下三个线程又开始竞争锁,此时,A又拿到锁,但是由于if的判断,仍然不符合,所以A也进入等待状态。 此时A,C都进入waiting状态。再拿到锁的只能是B或者D减1的方法。
-
这时B拿到了锁,操作number减1。A,C还在waiting状态。
-
第二行B执行完毕,唤醒其他线程的瞬间:还在waiting状态的A,C瞬间被唤醒。因为if只在线程第一次拿到锁的时候执行单次判断,C,A此时没有被再次进入if判断,先后立即执行了加一的操作,number变为2,出错。
总结:
不能在线程的等待判断逻辑处用if,要用while来保证不会有虚假唤醒,即被唤醒的正在waiting的线程一定会在符合while内条件的情况下才执行。
练习:使用一个lock的多个Condition对象来精准操控线程的执行顺序:
/**线程A打印5次,线程B打印10次,线程C打印15次 --- 循环十遍
*/
public class ConditionDemo {
public static void main(String[] args) {
PrintData printData = new PrintData();
new Thread(()->{
for (int i = 0; i < 10; i++) {
printData.printTimes(1, 2);
}
}, "A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
printData.printTimes(2, 3);
}
}, "B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
printData.printTimes(3, 1);
}
}, "C").start();
}
}
class PrintData{
private int number = 1;//A : 1, B : 2, C : 3
private Lock lock = new ReentrantLock();
//使用同一个lock的多个Condition对象来操控多个线程的工作顺序
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
private Condition[] conditions = {c1, c2, c3};
public void printTimes(int n, int nextN){
lock.lock();
try{
//while防止虚假唤醒
while(number != n){
conditions[n - 1].await();
}
for (int i = 1; i <= number * 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
number = nextN;
conditions[nextN - 1].signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
二、同步方法锁的理解
如下代码,分别改写为8种情况,写出Phone内方法的打印顺序。
class Phone{
public static synchronized void sendEmail() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("email");
}
public synchronized void sendMS() throws Exception{
System.out.println("ms");
}
public void sayHello() throws Exception{
System.out.println("hello");
}
}
public class EightLock {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, "A").start();
//sleep一下保证默认情况下的先后顺序
Thread.sleep(100);
new Thread(() -> {
try {
// phone.sendMS();
// phone.sayHello();
phone2.sendMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "B").start();
}
}
- 1.标准访问,两个方法均为普通同步方法-> email, ms
- 2.在发邮件方法中暂停4秒->仍为email,ms 因为是一把锁
- 3.新增普通非同步sayHello方法在B中执行。->hello , email:
因为普通方法不会被锁 - 4.两部手机phone1在A执行发邮件,phone2在B执行发短信 -> ms, email: 此时先执行没有被暂停四秒的B线程,因为普通同步方法锁的是自身对象,这里有两个对象,就是两把锁,互不干扰。
- 5.两个静态同步方法-> email,ms
- 6.两个静态同步方法,两个手机 ->email,ms:】调用静态的同步方法,锁的就是整个类】锁的是当前类的Class对象,即使是两个不同类的对象,只要是一个类的就会被锁。所以是同一把锁。(注意,前提是二者都是对象)
- 7.一个静态同步方法,一个普通同步方法,同一部手机-> ms email:同理,静态同步方法的锁是Class,普通同步方法的锁是自身对象。二者不一致,所以是两把锁,互不干扰。
- 8.一个静态同步方法,一个普通同步方法,两部手机-> ms email:显然为两把锁。
三、juc中的线程安全的集合类简介
实验:启动三十个不同的线程将三十个随机字符串添加到一个集合类中
这里以ArrayList为例,其实换为set和map也有一样的效果
public void listTest(){
ArrayList<String> list = new ArrayList<>();
//启动三十次三十个不同的线程
for (int i = 0; i < 30; i++) {
new Thread(() -> {
//添加三十个不同的随机字符串
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
运行上面的方法,会报java.util.ConcurrentModificationException
的并发修改异常。原因是:普通ArrayList不是线程安全的。解决方式:
1.使用Vector
2.使用Collections工具类的Collections.synchronizedList(new ArrayList<>())
3.使用 CopyOnWriteArrayList<>()
写时复制的list,使用方法:List<String> list = new CopyOnWriteArrayList<>();
即可。
CopyOnWriteArrayList:
CopyOnWriteArrayList的类声明:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
}
CopyOnWriteArrayList的添加元素的add方法:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
写时复制 copyOnWrite 容器即写时复制的容器 ,往容器添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[]进行 copy 复制出一个新的object[] newElements 然后向新容器object[] newElements 里面添加元素 添加元素后,再将原容器的引用指向新的容器 setArray(newElements);
这样的好处是可以对copyOnWrite容器进行并发的读,而不需要加锁.所以copyOnwrite容器也是一种读写分离的思想,读和写不同的容器.
知识补充:HashSet底层是HashMap,不过这个HashMap的value是一个常量Object。
同理:HashSet,HashMap也是线程不安全的,他们也分别可以通过juc包下的**CopyOnWriteSet**
和 **ConcurrentHashMap**
集合类替代来解决集合的并发问题。