线程通信方式

文章目录

  • 1.概念
  • 2.java中有几个关于线程间通信的关键字
    • 2.1 volatile
    • 2.2 synchronized
  • 3.通信
    • 3.1 等待--通知 wait/notify
      • 3.1.1 相关方法
      • 3.1.2 注意事项
      • 3.1.3 等待—通知机制的经典范式
      • 3.1.4 Thread.join()
    • 3.2 共享内存
      • 3.2.1 同步—synchronized
      • 3.2.2 信号量—volatile
      • 3.2.3 synchronized VS volatile
      • 3.2.4 循环队列:生产者消费者模型
      • 3.2.5 面试题:sleep()和wait()的区别
    • 3.3 管道流
    • 3.4 ReentrantLock/Condition消息队列方式

1.概念

线程通信:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺

通信的方式:

  • 等待–通知
  • 共享内存
  • 管道流
  • ReentrantLock/Condition消息队列方式

线程同步:当有一个线程在对内存进行操作时,其它线程都不可以对这个内存地址进行操作,知道该线程完成操作,其它线程才能对该内存地址进行操作【同步,仅仅传递的是控制信息,就是我什么时候运行结束,你什么时候可以来】

注:对于线程间通信来说,线程间同步可以归纳为线程间通信的一个子集,对于线程通信指的是两个线程之间可以交换一些实时的数据信息,而线程同步只交换一些控制信息。

2.java中有几个关于线程间通信的关键字

2.1 volatile

volatile:可以用来修饰字段(成员变量)
作用:告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

2.2 synchronized

synchronized:可以修饰方法或者以同步块的形式来进行使用
作用:主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性

3.通信

3.1 等待–通知 wait/notify

一个线程修改了一个对象的值,二另一个线程感知到了变化,然后进行相应的操作,整个开始是一个线程,而最终执行又是另一个线程

等待—通知机制使用的是使用同一个对象锁,如果两个线程使用的是不同的对象锁,那它们之间是不能用等待—通知机制的通信的

3.1.1 相关方法

方法名称 含义
notify() 通知一个对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程取得了对象的锁
notifyAll() 通知所有等待在该对象上的线程
wait() 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被终端才会返回,调用wait()方法后,会释放对象的锁
wait(long timeout) 超时等待一段时间,没有通知就超时返回,参数时间是毫秒
wait(long timeout, int nanos) 对于超时时间更细粒度的控制,可达到纳秒

3.1.2 注意事项

  • 使用wait()、notify()和notifyAll()时需要先对调用对象加锁
  • 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
  • notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
  • notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED。
  • 从wait()方法返回的前提是获得了调用对象的锁

等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。在synchronized修饰的同步方法或者修饰的同步代码块中使用Object类提供的wait(),notify()和notifyAll()3个方法进行线程通信。

3.1.3 等待—通知机制的经典范式

等待方遵循如下原则。
1)获取对象的锁。
2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。
3)条件满足则执行对应的逻辑。

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

通知方遵循如下原则。
1)获得对象的锁。
2)改变条件。
3)通知所有等待在对象上的线程。

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

线程通信方式_第1张图片

完整代码:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class Tread {

    static boolean flag = true;
    static Object lock = new Object();

    public static void main(String[] args) {
        Thread waitThread = new Thread(new Wait(),"waitThread");
        waitThread.start();
        try {
            TimeUnit.SECONDS.sleep(1);//TimeUnit.SECONDS.sleep()这个方法可以精确到任意时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread notifyThread = new Thread(new Notify(),"notifyThread");
        notifyThread.start();
    }

    static class Wait implements Runnable{

        @Override
        public void run() {
            synchronized (lock){
                while (flag){
                    try {
                        System.out.println(Thread.currentThread() + "flag is true.waitting@" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                        //getId(); 获取该线程的标识符
                        //getName(); 获取该线程名称
                        //getState(); 获取该线程状态

                        //wait vs  sleep
                        //wait:wait之后会放弃锁并进入对象的等待队列中,进入等待状态
                        //当被唤醒后会自动重新获得锁
                        //sleep: 直接去睡觉不会释放锁
                        //线程状态由RUNNING 变成 WAITING 并将当前线程放置到对象的等待队列中

                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread() + "flag is false.running@" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            }
        }
    }

    static class Notify implements Runnable{

        @Override
        public void run() {
            synchronized (lock){
                //获取lock的锁,然后进行通知,通知时不会释放lock的锁
                //直到当前线程释放了lock后,waitThread才能从wait方法中返回
                System.out.println(Thread.currentThread() + "hold lock.notify@" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                //notify() 方法将等待队列中的一个等待线程从等待队列中移到同步队列中,
                //而 notifyAll() 方法则是将等待队列中所有线程全部移动到同步对象。
                //被移动的线程状态由 WAITING 变为 BLOCKED
                flag = false;
                lock.notifyAll();
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            synchronized (lock){
                System.out.println(Thread.currentThread() + "hold lock.notify@" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                try {
                    //而sleep就是直接去睡觉不会释放锁
                    //所以 lock.wait() 想要 re-obtain ownership of the monitor and resumes execution【重新获得监视器的所有权并恢复执行】
                    // 必须等待 睡眠结束
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            synchronized (lock){
                System.out.println(Thread.currentThread() + "hold lock.notify@" + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                try {
                    //而sleep就是直接去睡觉不会释放锁
                    //所以 lock.wait() 想要 re-obtain ownership of the monitor and resumes execution【重新获得监视器的所有权并恢复执行】
                    // 必须等待 睡眠结束
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

线程通信方式_第2张图片

3.1.4 Thread.join()

如果一个线程A执行了thread.join()语句,其含义是: 当前线程A等待thread线程终止之后才从thread.join()返回。 线程Thread除了提供join()方法之外,还提供了join(long millis)和join(longmillis, int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回。

需求:创建了10个线程,编号0~9,每个线程调用前一个线程的join()方法 ,也就是线程0结束了,线程1才能从join()方法中返回,而线程0需要等待main线程结束。

import java.util.concurrent.TimeUnit;

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {

        Thread previous = Thread.currentThread();

        //第一次previous时main线程
        for (int i = 0; i < 10; i++) {
            //每个线程拥有当前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
            Thread thread = new Thread(new Domino(previous),String.valueOf(i));
            thread.start();
            //将当前创建的线程Thread赋值给previous
            previous = thread;
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName() + "terminate.");

    }

    static class Domino implements Runnable{

        private Thread thread;

        public Domino(Thread thread){
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                thread.join();//当前等待上一个线程执行结束
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "terminate.");
        }

    }
}

线程通信方式_第3张图片

从上述输出可以看到,每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知)。

3.2 共享内存

3.2.1 同步—synchronized

关键字synchronized可以修饰方法或者同步块,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。【这种方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会消耗服务器资源】

线程A执行完,再让线程B执行,使用对象锁实现:

package priv.wdragon.communications;

public class Objectlock {
    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    System.out.println("ThreadA" + i);
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    System.out.println("ThreadB" + i);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 线程A和线程B需要访问同一个对象lock,谁获得锁,谁就先执行,
        // 这里控制的是让线程A先执行(Thread.sleep(10)为的就是A先获得锁)。
        // 线程B要等线程A执行完再执行,所以是同步的,这就实现了线程间的通信
        new Thread(new ThreadA()).start();
        Thread.sleep(10);// 让线程先睡眠10ms,可以确保A先获得锁
        System.out.println("-----------------");
        new Thread(new ThreadB()).start();
    }
}

线程通信方式_第4张图片

3.2.2 信号量—volatile

java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝,所以程序在执行过程中,一个线程看到的变量并不一定是最新的

关键字volatile可以用来修饰字段(成员变量),就是告知程序对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

volatile能够保证内存的可见性,如果在一个线程里面改变了这个变量的值(该变量是由volatile关键字修饰的),那么其它线程对这种改变是立马可见的。

线程A输出0,然后线程B输出1,再然后线程A输出2…

package priv.wdragon.communications;

public class Signal {
    private static volatile int signal = 0;

    static class ThreadA implements Runnable {
        @Override
        public void run(){
            while (signal < 5) {
                if (signal % 2 == 0) {
                    System.out.println("ThreadA: " + signal);
                    synchronized (this) {
                        signal++;
                    }
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 1){
                    System.out.println("ThreadB: " + signal);
                    synchronized (this){
                        signal++;
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操
        // 作,所以我们需要使⽤ synchronized 给它“上锁”
        new Thread(new ThreadA()).start();
        Thread.sleep(10);
        new Thread(new ThreadB()).start();
    }
}

线程通信方式_第5张图片

3.2.3 synchronized VS volatile

synchronized主要做的是多线程顺序执行,也就是同一个时间只有一个线程在执行,线程A执行完了再让线程B执行
volatile主要做的是让多线程间共享的变量保证一致,也就是线程A对变量操作了,线程B对变量操作时是知道线程A对变量的操作的,是在线程A操作后的变量上进行操作。

3.2.4 循环队列:生产者消费者模型

生产者消费者模式,是通过一块缓冲区作为容器,来解决生产者和消费者之间的强耦合关系。通俗来讲就是,在该模型之前,只有当前顾客来了,店家才会生产商品,这样的话,来多个顾客就会将大量时间浪费在排队上,白白浪费时间。而有了生产者消费者模式之后,店家在没有顾客的时候,也生产商品,并将商品放在一个容器中,顾客来了直接拿即可,只有当容器满了便停止生产。而生活中大多都采用这个模型。

使用synchronized实现多生产者消费者模型:

package priv.wdragon.communications;

/**
 * 生产者生产物品超过20个就会停一下,消费者消费
 */

public class ProducerAndConsumer {
    public static void main(String[] args) {
        Client client = new Client();

        Producer producer = new Producer(client);
        Consumer consumer = new Consumer(client);

        producer.setName("peoducer1");
        consumer.setName("consumer1");

        producer.start();
        consumer.start();


    }
}


class Client {
    private int num = 0;

    public synchronized void product() {
        if (num < 20) {
            num++;
            System.out.println(Thread.currentThread().getName() + " 生产者生产了第 " + num + " 个产品");
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void consume() {
        if (num > 0) {
            System.out.println(Thread.currentThread().getName() + " 消费者消费了第" + num + " 个产品");
            num--;
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer extends Thread {
    private Client client;

    public Producer(Client client) {
        this.client = client;
    }

    @Override
    public void run() {

        System.out.println(getName() + " 生产者开始生产.......");
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            client.product();
        }
    }
}


class Consumer extends Thread {
    private Client client;

    public Consumer(Client client) {
        this.client = client;
    }

    @Override
    public void run() {
        System.out.println(getName() + " 消费者开始消费.......");
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            client.consume();
        }
    }
}

注意:

  • 生产者生产的时候,消费者不能消费;
  • 消费者消费的时候,生产者不能生产;
  • 容器空了,消费者不能消费,需要通知生产者该干活了;
  • 容器满了,生产者不能生产;
  • 生产者和消费者共享一个容器,共享数据作为锁。

生产者消费者模型的优点:

解耦,通过缓冲区,将生产者与消费者之间进行解耦,谁也不关心谁,只关心容器中的商品数量;
提高速率,通过平衡生产者和消费者的处理的处理能力,提高整体的运作能力。

3.2.5 面试题:sleep()和wait()的区别

  • sleep()是Thread类的方法,wait()是Object类中的方法,因此任意对象都可以调用wait()方法,但仅有Thread类对象才能让调用sleep方法;
  • sleep()和wait()的功能都是让线程休眠,但sleep()不需要唤醒,wait()需要唤醒;
  • 线程在调用sleep()后,不会释放对象锁,因此其他进程仍然进不了同步块,但wait()调用后会立即释放对象锁;
  • wait()方法必须在同步块或者同步方法中使用,但sleep()没有该要求。

3.3 管道流

管道是基于“管道流”的通信方式,管道输入/输出流要用于线程之间的数据传输,而传输的媒介为内存。

管道输入/输出流的体现:

  • 基于字符的:PipedWriter、PipedReader
  • 基于字节流的:PipedOutputStream、PipedInputStream

使用管道多半与I/O流相关。当我们一个线程需要先向另一个线程发送一个信息(比如字符串)或者文件等等时,就需要使用管道通信了。像消息传递机制,通过管道,将一个线程中的消息发送给另一个。

实例代码:

基于PipedWriter和PipedReader的实现ReaderThread和WriterThread的通信
WriterThread写了内容,ReaderThread读到并打印

package priv.wdragon.communications;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

public class Piped {

    public static void main(String[] args) throws IOException, InterruptedException {
        PipedWriter pipedWriter = new PipedWriter();
        PipedReader pipedReader = new PipedReader();
        pipedWriter.connect(pipedReader);
        // 注意:
        // 对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,
        // 如果没有将输入/输出流绑定起来,对于该流的访问将会抛出异常。
        new Thread(new ReaderThread(pipedReader)).start();
        Thread.sleep(10);
        new Thread(new WriterThread(pipedWriter)).start();
    }

    static class ReaderThread implements Runnable {
        private PipedReader in;
        public ReaderThread(PipedReader in){
            this.in = in;
        }
        @Override
        public void run() {
            System.out.println("This is a Reader");
            int receice = 0;
            try {
                while ((receice=in.read()) != -1){
                    System.out.println("read " + (char)receice);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

    static class WriterThread implements Runnable {
        private PipedWriter out;

        public WriterThread(PipedWriter out) {
            this.out = out;
        }

        @Override
        public void run() {
            System.out.println("This is a Writer");
            try {
                out.write("write A");
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

线程通信方式_第6张图片

3.4 ReentrantLock/Condition消息队列方式

ReentrantLock锁的实现原理虽然和synchronized不同,但是它和synchronized一样都是通过保证线程间的互斥访问临界区,来保证线程安全,实现线程间的通信。
相比于synchronized使用Object类的三个方法来实现线程的阻塞和运行两个状态的切换,ReentrantLock使用Condition阻塞队列的await()、signal()、signalAll()三个方法来实现线程阻塞和运行两个状态的切换,进而实现线程间的通信。

package priv.wdragon.communications;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLocConditon {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        List<String> list = new ArrayList<>();
        // 实现线程A
        Thread threadA = new Thread(() -> {
            lock.lock();
            for (int i = 1; i <= 10; i++) {
                list.add("abc");
                System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (list.size() == 5)
                    condition.signal();

            }
            lock.unlock();
        });
        // 实现线程B
        Thread threadB = new Thread(() -> {
            lock.lock();
            if (list.size() != 5) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程B收到通知,开始执行自己的业务...");
            lock.unlock();
        });
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }
}

线程通信方式_第7张图片

你可能感兴趣的:(线程,java,jvm,算法)