上文中——同步代码块和同步函数主要解决:多线程做相同的事(调用同一个run函数)
本文通过生产者和消费者问题(1对1,多对对)探讨多线程同步问题。
1.线程间通信
多线程在处理同一资源,但任务却不同
2.obj.wait(),obj.notify(),obj.notifyAll()
1)必须存在synchronized(Obj)的同步代码块或同步函数中。(因为必须标示wait,notify等方法所属的锁)
2)锁可以是任何对象,这些方法定在在Object中。(方法必须标示锁,锁可以是任意对象,任意对象可以调用的方法必然是Object类中的方法)
3)wait:线程在获取对象锁后,通过wait方法主动释放对象锁,同时本线程阻塞。直到有其它线程调用该对象锁的notify()唤醒该线程,才能继续获取对象锁,并继续执行。(线程存入线程池)。
4)notify:相当于对象锁的唤醒操作。调用后,并不是马上就释放对象锁,而是在相应的synchronized(){}语句块执行结束后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程(★★★唤醒的线程继续执行,而不是重新执行)。
2.wait()与sleep()区别
1)前者释放cpu执行权并释放锁,后者释放cpu执行权,不释放锁
2)wait可以指定时间也可以不指定,sleep必须指定
3.例子
两个线程同时操作对象,先输入然后输出,循环显示
Person.java
- package mypackage;
- public class Person {
- String name;
- String sex;
- boolean flag;
- }
- package mypackage;
- public class Input implements Runnable {
- Person p;
- public Input(Person p) {
- this.p = p;
- }
- public void run() {
- int x = 0;
- while (true) {
- synchronized (p) {
- if(p.flag){
- try {
- p.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- if (x % 2 == 0) {
- p.name = "Tom";
- p.sex = "male";
- } else {
- p.name = "Lily";
- p.sex = "female";
- }
- p.flag=true;
- p.notify();
- }
- x++;
- }
- }
- }
- package mypackage;
- public class Out implements Runnable {
- Person p;
- public Out(Person p) {
- this.p = p;
- }
- public void run() {
- while (true) {
- synchronized (p) {
- if (!p.flag) {
- try {
- p.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- System.out.println(p.name + "..." + p.sex);
- p.flag = false;
- p.notify();
- }
- }
- }
- }
- import mypackage.*;
- public class Demo {
- public static void main(String[] args)
- {
- Person p=new Person();
- Input in=new Input(p);
- Out out=new Out(p);
- Thread t1=new Thread(in);
- Thread t2=new Thread(out);
- t1.start();
- t2.start();
- }
- }
- public class Person {
- private String name;
- private String sex;
- private boolean flag;
- public synchronized void input(String name,String sex){
- if(flag)
- try {
- this.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- this.name=name;
- this.sex=sex;
- flag=true;
- notify();
- }
- public synchronized void out(){
- if(!flag)
- try {
- this.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("name:"+name+",sex:"+sex);
- flag=false;
- notify();
- }
- }
单个生产者-单个消费者
5.多个线程同时输入,多个线程同时输出情况下,引发异常(★★★)
但是如果变为多个生产者-多个消费者时就会出现下面问题
因为线程被唤醒后从挂起地方开始接着执行。一个生产者-一个消费者时,生产者被唤醒是不用再判断盘子是否还有食物,因为是消费者唤醒它的。
但是在多生产者-多消费者时,this锁中有生产者又有消费者,消费者使用notify有可能唤醒的仍然是消费者,造成死锁。所以我们就唤醒this锁中的全部线程。但是唤醒全部线程时如果不使用while再次判断,而是if已经判断过接着执行,那么有可能别的生产者又开始生产,导致接连生产(2-n)个(n是生产者线程个数)但是没有消费。所以:
总结:在多生产者多消费者是必须使用while()判断条件。(notifyAll影响程序执行效率)
分析:t0执行后阻塞-->t1阻塞-->t2执行后阻塞,唤醒t0-->t3阻塞(仅t0运行)--> t0输入数据后,并没有输出(异常!!!),唤醒t1,再次输入。
原因:if判断只执行一次,在t1阻塞后重新得到执行权,不会再次判断是否要运行,导致不该运行的线程运行,导致数据异常。
解决:将if换成while,解决判断是否运行,但会导致死锁,可将notify换成notifyAll解决。
6.Lock接口 用于替代synchronized,旧锁换新锁
1)提供了比使用synchronized方法和语句更广泛的锁定操作,允许更灵活的结构,支持多个Condition对象(支持多个监视器对象),Condition代替Object中的wait,notify,notifyAll。
2)将synchronized隐式实现转变为Lock对象的显示实现
3)使用
Lock l = 实现;
l.lock();//获得锁(上锁)
try
{
}
finally {
l.unlock();//释放锁(解锁)
}
- import java.util.concurrent.locks.*;
- public class Product {
- String name;
- int Id;
- boolean flag;
- int no = 1;
- // 创建锁对象
- Lock l = new ReentrantLock();
- // 通过已有锁对象创建该锁上的监视器对象,一个监视生产者,一个监视销售者
- Condition in = l.newCondition();
- Condition out = l.newCondition();
- void input(String name) {
- l.lock();
- try {
- while (flag)
- try {
- in.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- this.name = name;
- this.Id = no++;
- System.out.println("生产:" + name + "," + Id);
- flag = true;
- out.signal();
- } finally {
- l.unlock();
- }
- }
- void out() {
- l.lock();
- try {
- while (!flag)
- try {
- out.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("销售:" + name + ",id:" + Id + ",继续加油");
- flag = false;
- in.signal();
- } finally {
- l.unlock();
- }
- }
- }
以前每个锁对象一出现都有一组监视器wait() ,notify(),notifyAll()
现在锁对象与监视器分离开,监视器都放在condition(await(),signal().signalAll())接口中,用的时候将锁对象和监视器联系起来就行(每个锁对象可以有多个监视器)
因为唤醒对方进程,以前不能实现,我们通过唤醒所有进程的办法实现,这就造成了程序效率很低,(唤醒的对方进程中只有一个能够执行,但是唤醒所有其他进程不满足的又会重新挂起,影响效率)
但是使用了condition接口后,我们的一个锁对象却可以定义两个监视器。这样就可以只唤醒对方线程中的一个。
7.停止线程
1)stop方法,涉及安全问题,已过时
2)run方法结束,通常任务中会有循环结构,控制循环标记,但如果线程处于堵塞状态,无法读取标记,则无法结束
3)interrupt方法,将线程从阻塞状态强制恢复到运行状态来,让线程具备cpu执行资格,但会发生InterruptException,可以在catch中处理循环标记
8.守护线程
setDaemon方法必须在启动线程前调用,当正在运行的线程都是守护线程时(),Java 虚拟机退出。
非守护线程可以理解为前台线程,守护线程守护前台线程,前台线程都结束,守护线程也结束。
9.join
当前线程等待该线程终止,后运行
10.经典面试题
3个线程顺序打印10次ABC
11.获取线程状态方法getState()
对应枚举 Thread.State(6种状态)
12.获取当前程序所有线程
- ThreadGroup topGroup = null;
- ThreadGroup group = Thread.currentThread().getThreadGroup();
- // 遍历线程组树,获取根线程组
- while (group != null) {
- topGroup = group;
- group = group.getParent();
- }
- // 此线程组中活动线程的估计数
- int estimatedSize = topGroup.activeCount();
- System.out.println("active:" + estimatedSize);
- Thread[] slackList = new Thread[estimatedSize];
- // 获取根线程组的所有线程
- int actualSize = topGroup.enumerate(slackList);
- //遍历
- for (Thread t : slackList) {
- System.out.println("ID:" + t.getId() + ",Name:" + t.getName()
- + ",isAlive:" + t.isAlive());