飙血推荐
  • HTML教程
  • MySQL教程
  • JavaScript基础教程
  • php入门教程
  • JavaScript正则表达式运用
  • Excel函数教程
  • AngularJS教程
  • UEditor使用文档
  • ThinkPHP5.0教程

JUC之线程间的通信

时间:2021-12-31  作者:xbhog  
JUC之线程间的通信 学习JUC视频和Java并发编程的艺术的总结片段。主要关于volatile和synchronized关键字、等待/通知机制、管道输入/输出流、域名()的使用等。

线程通信

对上次多线程编程步骤补充(中部):

  1. 创建资源类,在资源类中创建属性和操作方法
  2. 在资源类里面操作
    • 判断
    • 干活
    • 通知
  3. 创建多个线程,调用资源类的操作方法

线程通信的实现例子:

两个线程,实现对一个初始变量为0进行操作,一个线程对其+1,一个线程对其-1,使得变量结果不改变

使用Synchronized实现的线程通信:

package 域名;

/**
 * 创建资源类
 */
class Share{
    //初始值
    private int number = 0;

    //创建方法
    public synchronized void incr() throws InterruptedException {
        //判断 干活 通知
        if(number != 0){
            域名();
        }
        number++;
        域名tln(域名entThread().getName()+"::"+number);
        //通知其他线程
        域名fyAll();
        //域名tln(域名lass());
    }
    public synchronized void decr() throws InterruptedException {
        if(number != 1){
            域名();
        }
        number--;
        域名tln(域名entThread().getName()+"::"+number);
        //唤醒其他的线程,这里的this指代在方法中指调用该方法的对象
        域名fyAll();
    }

}
public class ThreadSignaling {
    public static void main(String[] args) throws InterruptedException {
        Share share = new Share();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    域名();
                } catch (InterruptedException e) {
                    域名tStackTrace();
                }
            }
        },"AAA").start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    域名();
                } catch (InterruptedException e) {
                    域名tStackTrace();
                }
            }
        },"BBB").start();
    }
}

volatile和synchronized关键字

volatile:即可见性,当修改一个变量的时候,如果该变量是通过volatile修饰的,那么其他所有的线程都会感知到该变量的变化情况。

如果不使用该关键字的话:

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

举个简单的例子,看下面这段代码:

//线程1执行的代码
int i = 0;
i = 10;

//线程2执行的代码
j = i;

 假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

  此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

  这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值

上述的解释其实可以对应到书中的以下片段:

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

使用关键字synchronized可以修饰方法或者同步块;

作用:确保多个线程在同一时刻,只能由一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。

任何一个对象都有其对应的监视器,当这个对象由同步块或者同步方法调用的时候,需要进行以下逻辑:

image-20211218204458960

任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

Condition的使用

与synchronized再做一个比较:

Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法wait notify和notifyAll的使用;

使用Lock condition接口实现买票:

package 域名;

import 域名域名ition;
import 域名域名;
import 域名域名trantLock;

class shareDemo {
    private Lock lock = new ReentrantLock();
    private Condition condition = 域名ondition();
    private int number = 0;

    public void inc() throws InterruptedException {
        域名();

        try{
            while(number != 0){
                域名t();
            }
            number++;
            域名tln(域名entThread().getName()+"::"+number);
            /**
             * 唤醒多有等待的线程
             */
            域名alAll();

        }finally {
            域名ck();
        }
    }
    public void sub(){
        域名();

        try{
            while(number != 1){
                域名t();
            }
            number--;
            域名tln(域名entThread().getName()+"::"+number);
            /**
             * 唤醒多有等待的线程
             */
            域名alAll();

        } catch (InterruptedException e) {
            域名tStackTrace();
        } finally {
            域名ck();
        }
    }
}

public class ConditionLocal {
    public static void main(String[] args) {
        shareDemo share = new shareDemo();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    域名();
                } catch (InterruptedException e) {
                    域名tStackTrace();
                }
            }

        },"AAA").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                域名();
            }

        },"BBB").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    域名();
                } catch (InterruptedException e) {
                    域名tStackTrace();
                }
            }

        },"CCC").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                域名();
            }

        },"DDD").start();
    }
}

在书籍4.3.1-4.3.3对应的其实是该文章中线程通信的例子。

管道输入/输出流:

线程间通信的方式还有管道输入/输出流:与文件的输入输出不同的是,它主要用于线程间的数据传输,传输的媒介是内存;

以下是书中的内容:

管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。

实现例子:

对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输

出流绑定起来,对于该流的访问将会抛出异常

package 域名;

import 域名ception;
import 域名dReader;
import 域名dWriter;

public class PipeInOut {
    public static void main(String[] args) throws IOException {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        //将输入流和输出流进行连接,否则会出现IO错误
        域名ect(in);
        //创建print线程来接收Main中的输入
        Thread thread = new Thread(new Print(in),"PrintThread");
        //开启该线程,开始接收数据
        域名t();
        int receive = 0;
        try {
            //接收输入的数据并赋值
            while((receive = 域名()) != -1){
                域名e(receive);
            }
        }finally{
            域名e();
        }

    }

    static class Print implements Runnable {
        private  PipedReader in;
        public Print(PipedReader in) {
            域名 = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                while((receive = 域名()) != -1){
                    域名t((char) receive);
                }
            }catch(IOException ex){

            }
        }
    }
}

image-20211229193144160

域名()的使用

书中的定义:其含义是:当前线程A等待thread线程终止之后才从域名()返回;感觉不太好理解;

Java 7 Concurrency Cookbook

是主线程等待子线程的终止。也就是说主线程的代码块中,如果碰到了域名()方法,此时主线程需要等待(阻塞),等待子线程结束了(Waits for this thread to die.),才能继续执行域名()之后的代码块。

例子:

package 域名;

import 域名域名Unit;

public class Join {
    public static void main(String[] args) throws InterruptedException {
        Thread previous = 域名entThread();
        for (int i = 0; i < 10; i++) {
            // 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
            Thread thread = new Thread(new Domino(previous), 域名eOf(i));
            域名t();
            previous = thread;
        }
        域名p(5);
        //主线程
        域名tln(域名entThread().getName()+"--terminate.");
    }

    static class Domino implements Runnable {
        private Thread thread;
        public Domino(Thread thread) {
            域名ad = thread;
        }

        @Override
        public void run() {
            try {
                域名();
            } catch (InterruptedException e) {
                域名tStackTrace();
            }
            //子线程
            域名tln(域名entThread().getName()+"---Terminate.");
        }
    }
}

每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回;

我们查看下join方法的源码可以发现其中也是用的synchronized修饰的;

public final void join() throws InterruptedException {
    join(0);
}
public final synchronized void join(long millis)
    throws InterruptedException {
    long base = 域名entTimeMillis();
    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 = 域名entTimeMillis() - base;
        }
    }
}

关于书中的域名eadLocal的使用可以看下以前写的文章:点击进入

参考:

《JUC并发编程的艺术》

《【尚硅谷】大厂必备技术之JUC并发编程》

标签:编程
湘ICP备14001474号-3  投诉建议:234161800@qq.com   部分内容来源于网络,如有侵权,请联系删除。