多线程编程(七)——线程间通信基础详解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/swadian2008/article/details/99698940

目录

一、等待通知机制(wait/notify)

1、等待方法/wait()

2、通知方法/notify()

3、通知所有/notifyAll()

4、interrupt()方法遇到wait()方法和sleep()方法

5、wait(long)和sleep(time)

6、notify()通知过早

二、join()方法——线程排队

2.1join()方法的执行效果

2.2join(long)和sleep(long)的区别

2.3join()方法和中断异常

三、ThreadLocal类和InheritableThreadLocal类

1、ThreadLocal类的使用

1.1ThreadLocal的默认值和唯一键值对

2、InheritableThreadLocal类

四、yied()的使用


线程间通信使线程成为一个整体,提高系统之间的交互性,在提高CPU利用率的同时可以对线程任务进行有效的把控与监督

一、等待通知机制(wait/notify)

wait使线程停止运行,notify使停止的线程继续执行。

1、等待方法/wait()

1.1使当前执行代码的线程进行等待

该方法将当前线程置入“预执行队列”中,并在wait()所在的代码行处停止执行,直到接到通知或被中断为止。

1.2wait()方法只能在同步方法或同部块中调用

1.3执行wait()方法后,当前线程立即释放锁

1.4调用wait()方法,线程需要获取该对象的对象级别锁,如果没有持锁,将会抛出IllegalMonitorStateException

2、通知方法/notify()

2.1notify()方法只能在同步方法或同步块中调用

2.2在执行notify()方法后,当前线程不会立即释放锁

一个线程执行notify()方法后,需要等到执行notify()方法的线程将程序执行完,也就是退出synchronized代码块后,当前线程才会释放锁,而成wait状态所在的线程才可以获取该对象的锁。

关于通知/等待机制的完整代码示范:

创建线程——线程A

public class MyThreadA extends Thread {

    private Object lock;

    public MyThreadA(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + ":开始执行wait()方法,执行后释放锁...");
                lock.wait();
                System.out.println(Thread.currentThread().getName() + ":wait()方法被唤醒,得到锁后继续执行...");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyThreadA myThreadA = new MyThreadA(lock);
        myThreadA.start();
        Thread.sleep(1000);
        MyThreadB myThreadB = new MyThreadB(lock);
        myThreadB.start();
    }
}

创建线程——线程B

public class MyThreadB extends Thread {

    private Object lock;

    public MyThreadB(Object lock) {
        super();
        this.lock = lock;
    }

    public void run() {
        try {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + ":得到锁,开始执行notify()方法...");
                lock.notify();
                System.out.println(Thread.currentThread().getName() + ":执行notify()方法结束,没有立即释放锁...");
                for (int i = 0; i < 5; i++) {
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName() + ":还没有释放锁...继续执行锁内循环" + i);
                }
            }
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName() + ":synchronized外方法继续运行,此时锁已经释放...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

代码逻辑解释:

1、创建两个线程:等待线程A(执行wait()方法)和通知线程B(执行notify()方法,用来唤醒等待线程)

2、线程A先获取锁,执行wait()方法,释放锁并进行等待。

3、1秒钟后,线程B启动,并获取锁,执行notify()方法,唤醒线程A。

但此时线程B并没有马上释放锁,因为同步代码块中还有一个循环没有执行完,线程B继续执行循环。

4、线程B循环结束,同步代码块执行完成,释放锁。等待线程A拿到锁,继续执行。

然后,我们看到线程B还有一个同步代码块外的执行逻辑,这个逻辑加在这里的原因是证明,同步代码块外的逻辑并不影响等待/通知机制,notify()只有在该所在同步代码块代码执行完才会释放锁

3、通知所有/notifyAll()

notify()方法可以随机唤醒等待队列中等待同一共享资源的一个(随机的,仅仅一个)线程,并使该线程退出等待队列,进入可运行状态。

notifyAll()方法可以使所有正在等待队列中等待同一资源的全部线程从等待状态进入可运行状态。

4、interrupt()方法遇到wait()方法和sleep()方法

wait()方法执行完会自动释放锁,sleep()方法不会释放锁。

当线程调用wait()方法和sleep()方法时,如果程序在执行过程中再调用interrupt()方法中断线程,程序会抛出中断异常。

测试代码:

创建MyService类

public class MyService {

    public void testMethod(Object lock){
        try {
            synchronized (lock) {
                System.out.println("执行wait()方法开始....");
                lock.wait();
                System.out.println("wait状态被唤醒....");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("程序在执行wait()方法中被中断了...");
        }
    }
}

创建线程

package demo.otherdemo.lock;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Administrator
 * @date 2019/8/17/017 14:26
 * @Version 1.0
 */

public class MyThread extends Thread {

    private Object lock;

    public MyThread(Object lock) {
        super();
        this.lock = lock;
    }

    @Override
    public void run() {
        MyService myService = new MyService();
        myService.testMethod(lock);
    }

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyThread myThread = new MyThread(lock);
        myThread.start();
        Thread.sleep(5000);
        // 调用中断方法,打上中断标记
        myThread.interrupt();
    }
}

执行结果:

上边代码演示,一个线程执行完等待wait()方法后,一直没被唤醒,此时该线程调用interrupt()方法,程序抛出了异常。那么如果是sleep()方法呢?

接下来修改一下程序,把wait()方法修改成sleep()方法

public class MyService {

    public void testMethod(Object lock) {
        try {
            synchronized (lock) {
                System.out.println("执行sleep()方法开始....");
                Thread.sleep(30000);
                System.out.println("执行sleep()方法结束...");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("程序在执行sleep()方法中被中断了...");
        }
    }
}

执行结果如下:

上述演示结果,都证明了结论的正确性。

5、wait(long)和sleep(time)

wait(long)方法,如果线程在等待的一段之间内没有被唤醒,那么超过这个时间,线程将自动唤醒。

sleep(time)方法,线程在睡眠期无法被正常唤醒,只能等到睡眠时间结束,线程才会继续执行。

6、notify()通知过早

如果通知notify()先执行,等待wait()后执行,那么,执行wait()的线程将永远的等待下去,出现类似死锁的情况。

这个问题出现的原因,是等待和通知是分别由两个不同线程去实现的,在并发量大的情况下,两者的执行顺序是得不到保证的,也就是说,等待并不总是出现在通知前。

怎么去解决过早通知问题呢?

解决方案:

在等待和通知方法中添加条件!

比如:

等待线程A需要在条件true下才会执行等待,

通知线程B在执行通知后会把条件更改为false。

这样做的好处是,如果通知线程B先执行,那么等待线程A拿不到等待的条件,就不会进入等待状态,从而避免出现死锁。

通俗说法:

就好比你去火车站乘火车,如果火车已经开走了(通知已经执行),那么车站会便提示你本次列车已经开出(修改条件),等你到车站一看,火车已经开走了(条件发生变化),那你就无需再去等此次列车了(不执行等待)。

测试代码:

public class MyService {

    private Object lock = new Object();

    private boolean condition = true;

    public Runnable runnableA = new Runnable() {
        @Override
        public void run() {
            try {
                synchronized (lock) {
                    // 等待是需要条件的
                    while (condition) {
                        System.out.println(Thread.currentThread().getName()
                                +":开始执行wait()方法...");
                        lock.wait();
                        System.out.println(Thread.currentThread().getName()
                                +":被唤醒...");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };

    public Runnable runnableB = new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName()
                        +":开始执行notify()方法...");
                lock.notify();
                System.out.println(Thread.currentThread().getName()
                        +":notify()方法执行结束...");
                // 已经通知过了,为避免通知先执行,等待线程等不到通知,所以修改条件
                condition = false;
            }
        }
    };

    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        Thread a = new Thread(myService.runnableA);
        a.start();
        Thread.sleep(1000);
        Thread b = new Thread(myService.runnableB);
        b.start();
    }
}

执行结果

从上述代码的逻辑可以看出,等待线程等待是有条件的,通知线程执行完通知都会去修改等待的条件,所以,不管是哪个线程先执行,都不会出现类似死锁的情况。

二、join()方法——线程排队

2.1join()方法的执行效果

join()方法具有使线程排队的作用,有些类似于同步的运行效果。

join()方法和synchronized关键字的区别:

join在内部使用wait()方法进行等待,synchronized关键字使用的是“对象监视器”原理做同步。

join()方法的源码如下:看到wait()了吧?

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

那么执行join()方法的效果到底是什么样子的?我们来一段代码测试以下

测试join()方法的效果:

public class MyThread extends Thread {

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName()
                    + ":方法执行开始...");
            Thread.sleep(3000);
            System.out.println(Thread.currentThread().getName()
                    + ":方法执行结束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在不加join()方法前的执行效果:

public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        Thread.sleep(500);
        System.out.println(Thread.currentThread().getName() + ":执行结束...");
    }

执行结果:

接下来,修改下执行逻辑,加上join()的方法:

public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        // 加上了join的方法
        myThread.join();
        System.out.println(Thread.currentThread().getName() + ":执行结束...");
    }

执行结果:

从上述测试可以看出,添加join()方法后,主线程一直等到线程0结束以后,才开始运行。

2.2join(long)和sleep(long)的区别

join(long)设定线程等待时间,一旦超时,便不再等待。

区别:join(long)方法释放锁,sleep(long)方法不释放锁。

这是因为join(long)方法内部是使用wait(long)方法来实现的,所以该方法具有释放锁的特点。

同样的原因,当join(long)在执行过程中遇到interrupt()方法时,也会抛出中断异常。

2.3join()方法和中断异常

在执行join()方法的过程当中,如果当前线程代用interrupt()方法,当前线程会抛出异常,但是,并不会影响当前线程创建的子线程的运行。

测试代码:

创建线程A

public class ThreadA extends Thread {

    public void run() {
        try {
            while (true) {
                System.out.println(Thread.currentThread().getName() + ":执行方法...");
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建线程B

public class ThreadB extends Thread {
    
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + ":方法执行开始...");
            ThreadA threadA = new ThreadA();
            threadA.setName("B线程中的A线程");
            threadA.start();
            threadA.join();
            System.out.println(Thread.currentThread().getName() + ":方法执行结束...");
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"出现中断异常了...");
            e.printStackTrace();
        }
    }
}

创建线程C

public class ThreadC extends Thread {

    private ThreadB threadB;

    public ThreadC(ThreadB threadB) {
        this.threadB = threadB;
    }

    public void run() {
        threadB.interrupt();
    }
}

创建执行类

public class MyService {

    public static void main(String[] args) throws InterruptedException {
        ThreadB threadB = new ThreadB();
        threadB.setName("线程B");
        threadB.start();
        Thread.sleep(1000);
        ThreadC threadC = new ThreadC(threadB);
        threadC.setName("线程C");
        threadC.start();
    }
}

执行结果:

代码逻辑:

1、创建线程A、线程B、线程C

2、线程A执行死循环(模仿一直执行任务),B线程的任务主要是开启A线程,并代用join()方法,等待A线程运行结束,C线程主要用来中断B线程。

3、当B线程在执行join()的过程当中,C线程开启,B线程中断,抛出异常。但这个时候并没有使程序运行结束,因为B线程创建的子线程A还在继续运行。

三、ThreadLocal类和InheritableThreadLocal类

1、ThreadLocal类的使用

ThreadLocal类可以用来存放线程的数据,每个线程都可以通过ThreadLocal来绑定自己的值,ThreadLocal使变量在线程之间具有隔离性,也就是说ThreadLocal存的变量值是私有的。

验证隔离性:

创建工具类Tools

public class Tools {
    public static final ThreadLocal t1 = new ThreadLocal();
}

创建线程A

public class ThreadA extends Thread {

    public void run() {
        try {
            for (int i = 0; i < 2; i++) {
                Tools.t1.set(Thread.currentThread().getName() + "存入-" + i);
                System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t1.get());
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建线程B

public class ThreadB extends Thread {

    public void run() {
        try {
            for (int i = 0; i < 3; i++) {
                Tools.t1.set(Thread.currentThread().getName() + "存入-" + i);
                System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t1.get());
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建执行类MyService

public class MyService {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();
        threadA.start();
        threadB.start();
        for (int i = 0; i < 3; i++) {
            Tools.t1.set(Thread.currentThread().getName() + "存入-" + i);
            System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t1.get());
            Thread.sleep(200);
        }
    }
}

执行结果:

从上边的结果看到,虽然三个线程同时向t1(t1是唯一的)存储值,但是最后取出来的都是各自线程存放的值,其他线程并不会取到别的线程存放的数据,验证了ThreadLocal类的隔离性。

1.1ThreadLocal的默认值和唯一键值对

ThreadLocal类可以通过重写initialValue()方法来设置默认值,实现初始值不为null的效果。

源码方法的默认值为null,如下:

protected T initialValue() {
        return null;
    }

重写initialValue()方法测试代码:

重新创建一个类继承ThreadLocal类

public class ThreadLocalExt extends ThreadLocal {
    @Override
    protected Object initialValue() {
        return Thread.currentThread().getName()+"-设置的默认值";
    }
}

重新编写工具类

public class Tools {
    public static final ThreadLocalExt t = new ThreadLocalExt();
}

创建线程A,用来展示隔离效果

public class ThreadA extends Thread {

    public void run() {
        try {
            if(Tools.t.get()==null){
                for (int i = 0; i < 2; i++) {
                    Tools.t.set(Thread.currentThread().getName() + "存入-" + i);
                    System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t.get());
                    Thread.sleep(200);
                }
            }
            System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建执行类

public class MyService {

    public static void main(String[] args) throws InterruptedException {
        ThreadA threadA = new ThreadA();
        threadA.start();
        Thread.sleep(200);
        System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t.get());
        Tools.t.set(Thread.currentThread().getName()+"-设置的非默认值1");
        System.out.println(Thread.currentThread().getName() + "取出:" + Tools.t.get());
    }
}

执行结果:

从程序的输出结果,我们可以看到,ThreadLocal类在设置默认值时,默认值会生效,此时默认值仍然具有隔离性。同时我们也看到,当主线程往ThreadLocal类里边设置新值时,默认值会被替换掉。这说明,ThreadLocal只会存储唯一的值,存多个值时,前边的值都会被替换掉。

被替换掉的原因,是ThreadLocal的存储通过Map集合来实现的,而且只存储了唯一的键值对。通过源码,可以看到这个map存值的键是唯一的,这也是为什么不同线程具有不同值的原因,因为不同线程this对象的值是不一样的,而相同线程this值都是相同的

ThreadLocal存储实现的源码:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

2、InheritableThreadLocal类

使用ThreadLocal线程之间不能实现父子线程数据的传递(局限),InheritableThreadLocal类可以使子线程能够获取到父线程设置的值。

如果父线程修改了值,那么子线程将获取到父线程修改后的值。

测试代码:

创建InheritableThreadLocalExt对象

public class InheritableThreadLocalExt extends InheritableThreadLocal {
    @Override
    protected Object initialValue() {
        return Thread.currentThread().getName()+"-InheritableThreadLocal设置的默认值";
    }
}

构建工具类

public class Tools {
    public static InheritableThreadLocalExt t = new InheritableThreadLocalExt();
}

创建A线程

public class ThreadA extends Thread {

    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + ":A线程取出:" + Tools.t.get());
            Thread.sleep(200);
            Tools.t.set(Thread.currentThread().getName()+":A线程修改了值");
            System.out.println(Thread.currentThread().getName() + ":把值修改为——" + Tools.t.get());
            Thread thread = new Thread(new Runnable() {
                // 新建一个线程(子子线程)。看会不会传递值
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":A子线程取出的值:" + Tools.t.get());
                }
            });
            thread.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建执行程序

public class MyService {

    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ":主线程取出的值:" + Tools.t.get());
        // 主线程修改了值
        Tools.t.set(Thread.currentThread().getName()+"-设置的非默认值1");
        System.out.println(Thread.currentThread().getName() + ":把值修改为——" + Tools.t.get());
        Thread.sleep(200);
        ThreadA threadA = new ThreadA();
        threadA.start();
    }
}

执行结果

代码逻辑:

1、创建InheritableThreadLocalExt对象,继承了InheritableThread类,设置默认值,并构造工具。

2、主线程先执行,这个时候默认值打上了主线程的标记(main),然后主线程修改了默认值。

3、线程A在主线程之后执行,这个时候拿到了主线程修改后的值(父子线程数据传递实现了)。

4、线程A中又创建了A子线程,在创建之前,线程A又修改了数据,我们看到A子线程拿到的是线程A修改后的数据(又是一次父子线程的数据传递

其实,这里想说的是,父子线程指的是临近线程,在父线程修改了数据的情况下,A子线程不会去越界取到main线程的值。

另外,只要数据不修改,所有子线程将取到一样的值,这个值在子线程中具有无限传递性,下面是以三个线程执行结果为示例(主线程、线程A都不修改值的情况),但我们知道,就算有更多的线程,其结论也是一样的。

写在最后:如果子线程在取值的同时,父线程修改了InheritableThreadLocal里的值,子线程可能取到修改之前的旧值。

四、yied()的使用

yield()执行线程让步,让步的是CPU的执行权,而不是释放锁。实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

yield()使线程从运行状态转换到可运行状态。

public class MyThread extends Thread {

    public MyThread(String s){
        super(s);
    }

    @Override
    public void run() {
        for(int i =0;i<5;i++){
            System.out.println(getName()+":"+i);
            if("t1".equals(getName())){
                if(i==0){
                    yield();
                }
            }
        }
    }

    public static void main(String[] args) {
        MyThread myThreadA = new MyThread("t1");
        MyThread myThreadB = new MyThread("t2");
        myThreadA.start();
        myThreadB.start();
    }
}

执行结果:

猜你喜欢

转载自blog.csdn.net/swadian2008/article/details/99698940