JUC学习笔记二:线程间的通信;同步方法锁的理解;juc集合类简介

一、线程间的通信之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的情况。原因如下:

  1. 线程A、C是加1的线程,B、D是减1的线程。

  2. 在第一行,A抢到了锁,操作number加一并释放锁。

  3. 在第二行的竞争中,C其实最先抢到了锁,但是由于此时number还是1,if条件判断不符合,所以C进入了waiting状态。

  4. 在第三步C进入waiting状态之后,剩下三个线程又开始竞争锁,此时,A又拿到锁,但是由于if的判断,仍然不符合,所以A也进入等待状态。 此时A,C都进入waiting状态。再拿到锁的只能是B或者D减1的方法。

  5. 这时B拿到了锁,操作number减1。A,C还在waiting状态。

  6. 第二行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**集合类替代来解决集合的并发问题。

发布了16 篇原创文章 · 获赞 2 · 访问量 451

猜你喜欢

转载自blog.csdn.net/qq_31314141/article/details/104192847