Java教程

java基础-线程通信

本文主要是介绍java基础-线程通信,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

1、通信的方式:

1.1、文件共享

1.2、变量共享

1.3、线程协作-JDK API

1.3.1suspend/remuse

1.3.2 wait/notify机制

 1.3.3 park/unpark机制

2、总结

3、伪唤醒

 4、结语


1、通信的方式:

要想实现多个线程之间的协同,如:线程的执行顺序、获取某个线程的执行结果等。

涉及到线程之间的通信,分为下面四类:

①文件共享

②网络共享

③共享变量

④JDK提供的线程协调API:suspend/resume、wait/notify、park/unpark

1.1、文件共享

线程1写数据到文件里,线程2读取文件中的数据内容,实现数据的交换 

 代码示例:

import java.nio.file.Files;
import java.nio.file.Paths;

public class Text {

       //共享文件
    public static String filePath= "text.txt";

    public static void main(String[] args) throws Exception {

        //线程1写入数据
        new Thread(()->{
           try{

               while(true){
                   Files.write(Paths.get(filePath),("当前时间" + String.valueOf(System.currentTimeMillis())).getBytes());
                   Thread.sleep(1000L);
               }

           }catch (Exception e){
               e.printStackTrace();
           }
        }).start();

        //线程2,读数据
        new Thread(() ->{

            try{
                while(true){
                    Thread.sleep(1000L);
                    byte[] allBytes = Files.readAllBytes(Paths.get(filePath));
                    System.out.println(new String(allBytes));
                }
            }catch (Exception e){
                e.printStackTrace();
            }

        }).start();
    }
}

1.2、变量共享

 线程1写数据到内存里(某个变量),线程2读取内存中(某个变量)的数据内容,实现数据的交换 

代码示例:

public class Text {

    //共享变量
    public static String content = "";

    public static void main(String[] args) throws Exception {

        //线程1写入数据
        new Thread(()->{
           try{

               while(true){
                   content = "当前时间" + String.valueOf(System.currentTimeMillis());
                   Thread.sleep(1000L);
               }

           }catch (Exception e){
               e.printStackTrace();
           }
        }).start();

        //线程2,读数据
        new Thread(() ->{

            try{
                while(true){
                    Thread.sleep(1000L);
                    System.out.println(new String(content));
                }
            }catch (Exception e){
                e.printStackTrace();
            }

        }).start();
    }
}

1.3、线程协作-JDK API

JDK中对于需要多线程协作完成某一任务场景,提供了对于API支持。

多线程协作的典型场景:生产者-消费者模型。(线程阻塞,线程唤醒)

示例1:线程1去买包子,没有包子,则不再执行。线程2生产包子,通知线程1继续执行。

场景:线程1买包子,包子店没有包子则等待。线程2生产包子,并通知线程1可以买包子了。 

1.3.1suspend/remuse

API-被弃用的suspend挂起目标线程,通过remuse可以恢复线程执行。

太容易产生死锁,所以被弃用

正常用法:

public class Text {

    //共享变量
    public static Object baozidian = null;

    public static void main(String[] args) throws Exception {

        Thread consumerThread = new Thread(()->{

            try{

                if (baozidian == null){

                    System.out.println("暂时没有包子,进入等待...");
                    Thread.currentThread().suspend();//消费者卡在这,等待通知
                }
                System.out.println("买到包子 回家!");

            }catch (Exception e){
                e.printStackTrace();
            }

        });

        consumerThread.start();
        //主线程等待3秒,再生产包子。 让thread线程先执行
        Thread.sleep(3000L);
        baozidian = new Object();
        consumerThread.resume();
        System.out.println("生产了包子,通知消费者可以购买!");
    }
}

结果:

suspend/remuse死锁写法: 

第一种:

死锁的suspend/resume。 suspend并不会像wait一样释放锁,故此容易写出死锁代码

public class Text {

    //共享变量
    public static Object baozidian = null;

    public static void main(String[] args) throws Exception {

        Thread consumerThread = new Thread(()->{

            try{

                if (baozidian == null){
                    System.out.println("暂时没有包子,进入等待...");
                    synchronized(Text.class){ //拿到锁,
                        Thread.currentThread().suspend();//挂起阻塞,并没有释放锁
                    }


                }
                System.out.println("买到包子 回家!");

            }catch (Exception e){
                e.printStackTrace();
            }

        });

        consumerThread.start();
        //主线程等待3秒,再生产包子。 让consumerThread线程先执行
        Thread.sleep(3000L);
        baozidian = new Object();
        synchronized(Text.class){ //consumerThread没有释放锁,拿不到锁。
            consumerThread.resume();//拿不到锁  唤醒不了consumerThread。产生死锁
        }

        System.out.println("生产了包子,通知消费者可以购买!");
    }
}

结果:

consumerThread 线程拿到锁,没有释放就挂起。主线程拿不到锁,不能唤醒consumerThread线程,从而 产生死锁

suspend/remuse死锁写法: 

第二种:

remuse先通知唤醒线程consumerThread。suspend后面又使线程consumerThread挂起。得不到通知,导致永久挂起阻塞不能执行。

public class Text {

    //共享变量
    public static Object baozidian = null;

    public static void main(String[] args) throws Exception {

        Thread consumerThread = new Thread(()->{

                if (baozidian == null){
                    System.out.println("暂时没有包子,进入等待...");

                    try{
                        Thread.sleep(5000L); //模拟处理时间,等待5秒
                    }catch (Exception e){
                        e.printStackTrace();
                    }

                    Thread.currentThread().suspend();//挂起阻塞
                }
                System.out.println("买到包子 回家!");
        });

        consumerThread.start();
        //主线程等待3秒,再生产包子。 让consumerThread线程先执行
        Thread.sleep(3000L);
        baozidian = new Object();
        consumerThread.resume();//通知 consumerThread 执行

        System.out.println("生产了包子,通知消费者可以购买!");
    }
}

1.3.2 wait/notify机制

这个方法只能由同一对象锁的持有者线程调用,也就是写在同步块里,否则会抛出illegalmonitorStateException异常。

wait 导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。

notify/notifyAll 方法唤醒一个或所有正在等待这个对象锁的线程。

推荐理由:wait可以自动解锁,但是对顺序执行有要求,需要先wait后notify

注意:

虽然会wait自动解锁,但是对顺序有要求,如果在notify被调用之后,才开始wait方法的调用,线程会永远处于WAITING状态。

public class Text {
    
    public static Object baozidian = null;

    /** 正常的wait/notify */
    public void waitNotifyTest() throws Exception {
        // 启动线程
        new Thread(() -> {
            if (baozidian == null) { // 如果没包子,则进入等待
                synchronized (this) {
                    try {
                        System.out.println("1、进入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2、买到包子,回家");
        }).start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消费者");
        }
    }


    public static void main(String[] args) throws Exception {
        Text text = new Text();

        text.waitNotifyTest();;
    }
}

上述代码:先执行wait方法使线程等待,后执行notifyAll方法唤醒所有等待的线程。

 导致程序永久等待的wait/notify

notify先通知唤醒,后面执行wait使线程等待。notify已经执行,不再唤醒,永远等待

但是wait释放锁,所以比较好

public class Text {

    public static Object baozidian = null;


    /** 会导致程序永久等待的wait/notify */
    public void waitNotifyDeadLockTest() throws Exception {
        // 启动线程
        new Thread(() -> {
            if (baozidian == null) { // 如果没包子,则进入等待
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                synchronized (this) {
                    try {
                        System.out.println("1、进入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2、买到包子,回家");
        }).start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消费者");
        }
    }


    public static void main(String[] args) throws Exception {
        Text text = new Text();


        text.waitNotifyDeadLockTest();
    }
}

 1.3.3 park/unpark机制

 线程调用park则等待“”许可”,unpark方法为指定线程提供“许可(permit)”。

不要求park/unpark的调用顺序。

多次调用unpark之后,再调用park,线程会直接运行。

多次调用unpark,只能得到一次“许可”,不会叠加许可。再调用park获得“许可”,直接运行,消耗一次许可。再调用park,线程则进入等待状态。当unpark又提供一次许可。则线程继续执行一次。

正常的park/unpark 

LockSupport.park()将当前线程,也就是消费者线程挂起,3秒之后,主线程 LockSupport.unpark(consumerThread);指定消费者线程继续执行。

执行的先后顺序,不影响。

    /** 正常的park/unpark */
    public void parkUnparkTest() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) { // 如果没包子,则进入等待
                System.out.println("1、进入等待");
                LockSupport.park();
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        LockSupport.unpark(consumerThread);
        System.out.println("3、通知消费者");
    }

死锁的park/unpark

park并不是基于监视器锁的方式实现的,jvm底层提供的另外一种线程挂起方式。

park拿到锁,使当前消费者线程挂起。 unpark拿不到锁,无法是消费者线程继续执行

/** 死锁的park/unpark */
    public void parkUnparkDeadLockTest() throws Exception {
        // 启动线程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) { // 如果没包子,则进入等待
                System.out.println("1、进入等待");
                // 当前线程拿到锁,然后挂起
                synchronized (this) {
                    LockSupport.park();
                }
            }
            System.out.println("2、买到包子,回家");
        });
        consumerThread.start();
        // 3秒之后,生产一个包子
        Thread.sleep(3000L);
        baozidian = new Object();
        // 争取到锁以后,再恢复consumerThread
        synchronized (this) {
            LockSupport.unpark(consumerThread);
        }
        System.out.println("3、通知消费者");
    }

2、总结

在同步代码块中:

suspend/resume用法:(被弃用)

对同步锁的使用有要求,不会释放锁,容易死锁。对顺序有要求,先调用suspend后resume。容易导致永久挂起。

wait/notify用法:

对同步锁没有要求,wait方法会自动释放锁。对顺序有要求,先调用wait后notify。容易导致永久挂起。

park/unpark用法:

对同步锁的使用有要求,不会释放锁,容易死锁。对顺序没有要求,park挂起消费者线程,unpak给消费者线程执行“许可”后,消费者线程继续执行。

虽然都有缺陷,但是都比已经弃用的suspend/resume要好。

3、伪唤醒

警告!之前代码用if语句来判断,是否进入等待状态,是错误的。

官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

伪唤醒是指:线程并非是notify、notifyAll、unpark等api调用而唤醒的,是更底层的原因导致的。

 在上面所有例子中,将if改为while,防止伪唤醒。

 4、结语

本章内容,设计很多JDK多线程开发工具类,它底层实现的原理。

这篇关于java基础-线程通信的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!