乐趣区

关于java:线程间通信

我是阿福,公众号 JavaClub 作者,一个在后端技术路上摸盘滚打的程序员,在进阶的路上,共勉!
文章已收录在 JavaSharing 中,蕴含 Java 技术文章,面试指南,资源分享。

把握的技术点如下:

  • 应用 wait/notify 实现线程间的通信
  • 线程的生命周期
  • 生产者 / 消费者模式的实现
  • 办法 join 的应用
  • ThreadLocal 类的应用

线程间通信

3.1 应用 wait/notify 实现线程间的通信

3.1.1 期待 / 告诉机制的实现

什么是期待 / 告诉机制

期待 / 告诉机制在咱们生存中亘古未有,比方在就餐时就会呈现,如下图所示:

  • 厨师做完一道菜的工夫不确定,所以厨师将菜品放到“菜品传递台”上的工夫也不确定。
  • 服务员取到菜的工夫取决于厨师,所以服务员就有“期待”(wait)的状态。
  • 厨师将菜放到“菜品传递台”上,其实就相当于一种告诉(notify), 这是服务员能力拿到菜交给就餐者。

这个过程就呈现了“期待 / 告诉”机制。

应用专业术语讲

期待 / 告诉机制,是指线程 A 调用了对象 O 的 wait()办法进入期待状态,而线程 B 调用了对象 O 的 notify()/notifyAll()办法,线程 A 收到告诉后退出期待队列,进入可运行状态,进而执行后续操作。上述两个线程通过对象 O 来实现交互,而对象上的 wait()办法和 notify()/notifyAll()办法的关系就如同开关信号一样,用来实现期待方和告诉方之间的交互工作。

期待 / 告诉机制的实现

wait()办法的作用

是使以后线程进入阻塞状态,同时在调用 wait()办法之前线程必须取得该对象的对象级别锁,即 只能在同步办法或同步代码块中调用 wait()办法 。在执行 wait() 办法之后,以后线程 开释锁

notify() notifyAll()办法的作用:

就是用来告诉那些期待该对象的对象锁的其余线程,如果有多个线程期待,则由线程布局器随机筛选其中一个呈 wait()状态的线程,对其发动告诉 notify, 并使它获取该对象的对象锁。

须要阐明的是 :在执行 notify() 办法之后,以后线程不会马上开释该对象锁,呈 wait()状态的线程也不能马上获取该对象锁,要等到执行 notify()办法的线程将程序执行完,也就是退出 synchronized
代码块,以后线程才会开释锁,而呈 wait()状态所在的线程才能够获取对象锁。

强调 notify(),notifyAll()也是在同步办法或者是同步代码块中调用,即在调用之前必须取得该对象的对象级别锁

用一句话总结一下 wait 和 notify: wait 使线程进行运行,而 notify 使进行的线程持续运行

上面代码实现一个示例:

创立MyList.java, 代码如下:

public class MyList {private static List list=new ArrayList();
    public static void add(){list.add("anyString");
    }
    public static int size(){return list.size();
    }
}

自定义线程类 MyThread1.java, MyThread2.javaMyThread3.java代码如下:

public class MyThread1 extends Thread {
    private Object lock;

    public MyThread1(Object lock) {this.lock = lock;}

    @Override
    public void run() {
        try {synchronized (lock) {if (MyList.size() != 5) {System.out.println("开始 wait time=" + System.currentTimeMillis());
                    lock.wait();
                    System.out.println("完结 wait time=" + System.currentTimeMillis());
                }
            }
        } catch (InterruptedException e) {e.printStackTrace();
        }

    }
}

public class MyThread2 extends Thread {

    private Object lock;

    public MyThread2(Object lock) {this.lock = lock;}

    @Override
    public void run() {synchronized (lock) {for (int i = 0; i < 10; i++) {MyList.add();
                if (MyList.size() == 5) {lock.notify();
                    System.out.println("已发出通知");
                }
                System.out.println("增加了" + (i + 1) + "个元素!!");
            }
        }
    }
}

创立测试类 Test.java

public class Test {public static void main(String[] args) {Object lock=new Object();
       MyThread1 myThread1=new MyThread1(lock);
       myThread1.start();
       MyThread2 myThread2=new MyThread2(lock);
       myThread2.start();}
}

程序代码运行后果如下:

开始 wait time=1618832467129
增加了 1 个元素!!
增加了 2 个元素!!
增加了 3 个元素!!
增加了 4 个元素!!
已发出通知
增加了 5 个元素!!
增加了 6 个元素!!
增加了 7 个元素!!
增加了 8 个元素!!
增加了 9 个元素!!
增加了 10 个元素!!
完结 wait time=1618832467130

从运行的后果来看,这也阐明 notify()办法执行后不是立刻开释锁。


3.2 线程的生命周期

线程生命周期转换图

线程的状态

线程从创立,运行到完结总是处于五种状态之一:新建状态,就绪状态,运行状态,阻塞状态,死亡状态。

  • 新建状态:线程对象被创立后就进入了新建状态,Thread thread = new Thread();
  • 就绪状态 (Runnable):也被称之为“可执行状态”,当线程被 new 进去后,其余的线程调用了该对象的 start() 办法,即 thread.start(),此时线程位于“可运行线程池”中,只期待获取 CPU 的使用权,随时能够被 CPU 调用。进入就绪状态的过程除 CPU 之外,其余运行所需的资源都曾经全副取得。
  • 运行状态(Running):线程获取 CPU 权限开始执行。留神:线程只能从就绪状态进入到运行状态。
  • 阻塞状态(Bloacked):阻塞状态是线程因为某种原因放弃 CPU 的使用权,临时进行运行,晓得线程进入就绪状态后能力有机会转到运行状态。

阻塞的状况分三种:

(1)、期待阻塞 :运行的线程执行 wait() 办法,该线程会开释占用的所有资源,JVM 会把该线程放入“期待池中”。进入这个状态后是不能主动唤醒的,必须依附其余线程调用 notify()或者 notifyAll()办法能力被唤醒。
(2)、同步阻塞:运行的线程在获取对象的(synchronized)同步锁时,若该同步锁被其余线程占用,则 JVM 会吧该线程放入“锁池”中。

(3)、其余阻塞 :通过调用线程的 sleep() 或者 join()或收回了 I / O 申请时,线程会进入到阻塞状态。当 sleep()状态超时、join()期待线程终止或者超时、或者 I / O 处理完毕时,线程从新回到就绪状态。

  • 死亡状态(Dead):线程执行实现或者因异样退出 run 办法,该线程完结生命周期。

阻塞线程办法的阐明:

  • wait(), notify(),notifyAll()这三个办法是联合应用的,都属于 Object 中的办法,wait 的作用是使以后线程开释它所持有的锁进入期待状态(开释对象锁),而 notify 和 notifyAll 则是唤醒以后对象上的期待线程。
  • sleep() 和 yield()办法是属于 Thread 类中的 sleep()的作用是让以后线程休眠(正在执行的线程被动让出 CPU,而后 CPU 就能够去执行其余工作),即以后线程会从“运行状态”进入到阻塞状态”,但依然放弃对象锁。当延时工夫过后该线程从新阻塞状态变成就绪状态,从而期待 CPU 的调度执行。
  • yield()的作用是退让,它可能让以后线程从运行状态进入到就绪状态”,从而让其余期待线程获取执行权,然而不能保障在以后线程调用 yield()之后,其余线程就肯定能取得执行权,也有可能是以后线程又回到“运行状态”持续运行。

wait (),sleep()的区别:

1、sleep()睡眠时,放弃对象锁,依然占有该锁, 而 wait()开释对象锁.
2、wait 只能在同步办法和同步代码块外面应用,而 sleep 能够在任何中央应用。
3、sleep 必须捕捉异样,而 wait 不须要捕捉异样


3.3 生产者 / 消费者模式的实现

生产者消费者问题(Producer-consumer problem),也称无限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,而后反复此过程;与此同时,消费者也在缓冲区耗费这些数据。生产者和消费者之间必须放弃同步,要保障生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时耗费数据。不够欠缺的解决办法容易呈现死锁的状况,此时过程都在期待唤醒。

解决生产者 / 消费者问题的办法可分为两类

(1)采纳某种机制爱护生产者和消费者之间的同步;
(2)在生产者和消费者之间建设一个管道。第一种形式有较高的效率,并且易于实现,代码的可控制性较好,属于罕用的模式。第二种管道缓冲区不易控制,被传输数据对象不易于封装等,实用性不强。因而本文只介绍同步机制实现的生产者 / 消费者问题。

同步问题外围在于

如何保障同一资源被多个线程并发拜访时的完整性。罕用的同步办法是采纳信号或加锁机制,保障资源在任意时刻至少被一个线程拜访。Java 语言在多线程编程上实现了齐全对象化,提供了对同步机制的良好反对。在 Java 中一共有四种办法反对同步,其中前三个是同步办法,一个是管道办法。

(1)wait() / notify()办法

(2)await() / signal()办法

(3)BlockingQueue 阻塞队列办法

(4)PipedInputStream / PipedOutputStream

上面咱们通过 wait() / notify()办法实现生产者和消费者模式:

代码场景:

当缓冲区已满时,生产者线程进行执行,放弃锁,使本人处于等状态,让其余线程执行;
当缓冲区已空时,消费者线程进行执行,放弃锁,使本人处于等状态,让其余线程执行。

当生产者向缓冲区放入一个产品时,向其余期待的线程收回可执行的告诉,同时放弃锁,使本人处于期待状态;
当消费者从缓冲区取出一个产品时,向其余期待的线程收回可执行的告诉,同时放弃锁,使本人处于期待状态。

代码实现:

创立仓库Storage.java 代码:

public class Storage {
    // 仓库容量
    private final int MAX_SIZE = 10;
    // 仓库存储的载体
    private LinkedList<Object> list = new LinkedList<>();

    public void produce() {synchronized (list) {while (list.size() + 1 > MAX_SIZE) {System.out.println("【生产者" + Thread.currentThread().getName()
                        + "】仓库已满");
                try {list.wait();
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
            list.add(new Object());
            System.out.println("【生产者" + Thread.currentThread().getName()
                    + "】生产一个产品,现库存" + list.size());
            list.notifyAll();}
    }

    public void consume() {synchronized (list) {while (list.size() == 0) {System.out.println("【消费者" + Thread.currentThread().getName()
                        + "】仓库为空");
                try {list.wait();
                } catch (InterruptedException e) {e.printStackTrace();
                }
            }
            list.remove();
            System.out.println("【消费者" + Thread.currentThread().getName()
                    + "】生产一个产品,现库存" + list.size());
            list.notifyAll();}
    }

}

创立生产者线程Producer.java,消费者线程Consumer.java, 代码如下:

public class Producer implements Runnable {

    private Storage storage;

    public Producer(){}

    public Producer(Storage storage){this.storage = storage;}

    @Override
    public void run(){while(true){storage.produce();
        }
    }

}
public class Consumer implements Runnable{
    private Storage storage;

    public Consumer(){}

    public Consumer(Storage storage){this.storage = storage;}

    @Override
    public void run(){while(true){storage.consume();
        }
    }

}

创立测试类TestPc.java

public class TestPc {public static void main(String[] args) {Storage storage = new Storage();
        Thread p1 = new Thread(new Producer(storage));
        p1.setName("张三");
        p1.start();
        Thread c1=new Thread(new Consumer(storage));
        c1.start();
        c1.setName("李四");

    }


}

程序运行的局部后果:

【消费者李四】生产一个产品,现库存 8
【消费者李四】生产一个产品,现库存 7
【消费者李四】生产一个产品,现库存 6
【消费者李四】生产一个产品,现库存 5
【消费者李四】生产一个产品,现库存 4
【生产者张三】生产一个产品,现库存 5
【生产者张三】生产一个产品,现库存 6
【生产者张三】生产一个产品,现库存 7
【生产者张三】生产一个产品,现库存 8
【生产者张三】生产一个产品,现库存 9
【生产者张三】生产一个产品,现库存 10
【生产者张三】仓库已满
【消费者李四】生产一个产品,现库存 9
【消费者李四】生产一个产品,现库存 8
【消费者李四】生产一个产品,现库存 7


3.4 办法 join 的应用

在很多状况下,主线程创立并启动子线程,如果子线程中进行大量的运算,主线程往往早于子线程完结。这时主线程要期待子线程实现之后再完结。比方子线程解决一个数据,主线程要获得这个数据中的值,就要用到 join()办法。

join()办法就是期待线程对象销毁。

创立测试 MyJoinThread.java 代码:


public class MyJoinThread extends Thread{

    @Override
    public void run() {int secondValue= (int) (Math.random() * 10000);
        System.out.println(secondValue);
        try {Thread.sleep(secondValue);
        } catch (InterruptedException e) {e.printStackTrace();
        }
    }
}

创立测试类 TestJoin.java 代码:

public class TestJoin {public static void main(String[] args) throws InterruptedException {MyJoinThread myJoinThread=new MyJoinThread();
        myJoinThread.start();
        //myJoinThread.join();
        System.out.println("我想当 myJoinThread 对象执行结束我再执行,答案是不确定的");
    }
}

代码的运行后果:

我想当 myJoinThread 对象执行结束我再执行,答案是不确定的
9618

把 myJoinThread.join()代码正文去掉运行代码执行后果如下:

82
我想当 myJoinThread 对象执行结束我再执行,答案是不确定的

所以得出结论是:join()办法使所属线程对象 myJoinThread 失常执行 run()办法中的工作,而使以后线程 main 进行有限的阻塞,期待 myJoinThread 销毁完再继续执行 main 线程前面的代码。

办法 join()具备使线程排队运行的作用,有点相似同步运行的成果。

join()和 synchronized 的区别是:join()在外部应用 wait()办法进行期待,而 synchronized 关键字应用的是“对象监听器”的原理做的同步。

办法 join()与 sleep(long)的区别

办法 join(long)的性能在外部应用的是 wait(long)办法实现的,所用 join(long)办法具备开释锁的特点。

办法 join(long)源代码如下:

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;}
        }
    }

从源代码中能够理解到,当执行 wait(long)办法后,以后线程的锁被开释,那么其余线程能够调用此线程中的同步办法了。

而 Thread.sleep(long)办法不开释锁。


3.5 ThreadLocal 类的应用

咱们晓得变量值的共享能够应用 public static 变量的模式,如果想实现每一个线程都有本人的共享变量该如何解决呢?JDK 中提供 ThreadLocal 正是解决这样的问题。

类 ThreadLocal 次要解决的就是为每个线程绑定本人的值,能够将 ThreadLocal 类比喻成全局存放数据的盒子,盒子中能够存储每一个线程的公有数据。

创立 run.java 类,代码如下:

public class run {private static  ThreadLocal threadLocal=new ThreadLocal();

    public static void main(String[] args) {if (threadLocal.get()==null){System.out.println("从未放过值");
           threadLocal.set("我的值");
       }
        System.out.println(Thread.currentThread().getName()+"线程:"+threadLocal.get());
    }
    

}

代码的运行后果:

从未放过值
main 线程: 我的值

从图中运行后果来看,第一次调用 threadLocal 对象的 get 办法返回为 null, 通过调用 set()赋值后值打印在管制台上,类 ThreadLocal 解决的是变量在不同线程间的隔离性,也就是不同的线程领有本人的值,不同线程的值能够寄存在 ThreadLocal 类中进行保留的。

验证线程变量的隔离性

创立 ThreadLocalTest 我的项目,类 Tools.java代码如下:

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

创立线程类 MyThread1.java ,MyThread2.java代码如下:

public class MyThread1 extends Thread {

    @Override
    public void run() {for (int j = 0; j < 5; j++) {Tools.local.set(j+1);
            System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get());
            try {Thread.sleep(200);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }
    }
}

public class MyThread2 extends Thread {


    @Override
    public void run() {for (int i = 0; i < 5; i++) {Tools.local.set(i+1);
            System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get());
            try {Thread.sleep(200);
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }
    }
}

创立 run.java 测试类

public class run {public static void main(String[] args) {MyThread1 myThread1=new MyThread1();
       myThread1.setName("myThread1 线程");
       myThread1.start();
       MyThread2 myThread2=new MyThread2();
       myThread2.setName("myThread2 线程");
       myThread2.start();}
}

程序运行后果:

myThread1 线程 get value:1
myThread2 线程 get value:1
myThread2 线程 get value:2
myThread1 线程 get value:2
myThread1 线程 get value:3
myThread2 线程 get value:3
myThread2 线程 get value:4
myThread1 线程 get value:4
myThread1 线程 get value:5
myThread2 线程 get value:5

尽管 2 个线程都向 local 中 set()数据值,但每个线程还是能取到本人的数据。


文章参考:

《Java 多线程编程核心技术》
https://blog.csdn.net/ldx1998…
https://blog.csdn.net/MONKEY_…

看到这里明天的分享就完结了,如果感觉这篇文章还不错,来个 分享、点赞、在看 三连吧,让更多的人也看到~

欢送关注集体公众号 「JavaClub」,定期为你分享一些技术干货。

退出移动版