20.从0开始深入理解并发、线程与等待通知机制(下

91 阅读4分钟

深入学习 Java 的线程

线程间的通信和协调、协作

synchronized内置锁

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,用来处理不可预料的结果。

效果:并发的执行 → 串行执行

  • 对象锁

    • 作用于对象实例
    • 作用于对象实例方法
  • 类锁

    • 作用于类的静态方法
    • 作用于类的class对象

    通过以下实验分析得出结论:如果是相同的锁,它的累加次数应该是遍历次数的倍数,反之不是。

public class SynTest {

    public static void main(String[] args) {

        var increment = new Increment();

        var c1 = new Count(increment);
        var c2 = new Count(increment);
        c1.start();
        c2.start();
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(increment.getNum());
    }

    private static class Count extends Thread {
        private Increment increment;

        public Count(Increment increment) {
            this.increment = increment;
        }

        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
//                increment.run(); //无锁
//                increment.run1(); //实例锁
//                increment.run2(); //成员方法锁
//                increment.run3(); //Integer 成员变量
                increment.run4(); //类锁
            }
        }
    }

    static class Increment {
        private Integer num = 0;

        public Increment() {
        }

        public Integer getNum() {
            return num;
        }

        public void setNum(Integer num) {
            this.num = num;
        }

        public void run() {
            num++;
        }

        public void run1() {
            synchronized (this) {
                num++;
            }
        }

        public synchronized void run2() {
            num++;
        }

        public void run3() {
            //Synchronization on a non-final field 'num'
            synchronized (num) {
                num++;
            }
        }

        public void run4() {
            //Synchronization on a non-final field 'num'
            synchronized (Increment.class) {
                num++;
            }
        }
    }
}


volatile,最轻量的通信/同步机制

public class VolatileTest {

    private volatile static boolean ready;
    private static int number;

    public static void main(String[] args) {
        System.out.println("main start, number = " + number + " ready = " + ready);
        PrintThread printThread = new PrintThread();
        printThread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ready = true;
        number += 100;
        System.out.println("main modify value");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main end, number = " + number + " ready = " + ready);
    }

    private static class PrintThread extends Thread{
        @Override
        public void run() {
            System.out.println("PrintThread is running.......");
            while(!ready){
                //System.out.println("lll");
            };//无限循环
            number ++;
            System.out.println("PrintThread number = "+ number);
        }
    }
}

不加volatile时,子线程无法感知主线程修改了ready的值,从而不会退出循环,而加了volatile后,子线程可以感知主线程修改了ready的值,迅速退出循环。

volatile最适用的场景:一个线程写,多个线程读。

等待/通知机制

对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

  • notify()
    • 通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。
  • notifyAll()
    • 通知所有等待在该对象上的线程
  • wait()
    • 调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁
  • wait(long)
    • 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
  • wait (long,int)
    • 对于超时时间更细粒度的控制,可以达到纳秒

等待和通知的范式

等待方:

synchronized(对象){
    while(条件不满足){
        对象.wait();
    }
    对应的处理逻辑
}

通知放:

synchronized(对象){
    改变条件
    对象.notifyAll();    
}

面试题

调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?

yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。

调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。

调用notify()系列方法后,对锁无影响,线程只有在syn同步代码执行完后才会自然而然的释放锁,所以notify()系列方法一般都是syn同步代码的最后一行。

为什么wait和notify方法要在同步块中调用?

主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。

如果消费者和生产者不竞争一把锁,可能出现lost wake up问题。

为什么你应该在循环中检查等待条件?

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方法效果更好的原因。

CompletableFuture

JDK1.8才新加入的一个实现类CompletableFuture,实现了Future, CompletionStage两个接口。实现了Future接口,意味着可以像以前一样通过阻塞或者轮询的方式获得结果。



ExecutorService executor = Executors.newCachedThreadPool();

// 创建异步任务
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {

    }
    return 50;
}, executor);

// 在任务完成时执行操作
future.thenAccept(result -> {
    System.out.println("Result: " + result);
});

future.thenRun(() -> {
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    System.out.println("run ....");
});

// 主线程执行其他操作
System.out.println("Main end");