9. Java多线程2 (第十二天)

一   线程间通信1 - 示例代码

线程间的通信:其实就是多个线程操作同一个资源,但操作的动作不同。

之前写的程序,每个线程运行的代码都是一致的。

而现在需要线程之间通信,它们代码要不一致。

A线程往资源写数据,B线程往资源读数据,同时运行。

这样就有两个run方法同时运行,也就是有两个类。

Input类对象存入数据 → 对象(String name,String Sex) → Output类对象读取数据

首先,资源要描述一下,Input和Output要描述一下。

 
package Test3;

class Res {
	public String name;
	public String sex;
}

class Input implements Runnable {
	Res res;
	boolean flag = true;

	public Input(Res res) {
		this.res = res;
	}

	public void run() {
		while (true) {
			if (flag == true) {
				res.name = "麦克";
				res.sex = "男";
				flag = false;
			} else {
				res.name = "May";
				res.sex = "girl";
				flag = true;
			}
		}
	}
}

class Output implements Runnable {
	Res res;

	public Output(Res res) {
		this.res = res;
	}

	public void run() {
		while (true) {
			System.out.println(res.name + " --- " + res.sex);
		}
	}

}

public class ThreadTongXin {

	public static void main(String[] args) {

		Res res = new Res();

		new Thread(new Input(res)).start();
		new Thread(new Output(res)).start();
	}
}

程序输出:

麦克 --- girl
麦克 --- 男
May --- girl
麦克 --- 男
麦克 --- girl
May --- 男
 

为什么会出现输出的数据混乱,而且一下子输出一大片同一个人数据的情况呢?

原因:一下子出一大片 的原因是Outpu输出完,不一定是Input获取到CPU执行权,

可以继续是Output获取到,连续都获取到了,就会出现一大片一个人的数据了。

另外,Input往Res类的对象res里刚存完name,Output就获取到CPU执行权,

然后就输出了,性别的数据也就混乱了。

二   线程间通信2 - 解决安全问题

当然是“同步”。

两个类的while(true)中加上同步代码块,发现还是不行。

此时应该想到,同步需要满足两个前提:

1.这个程序只同步了一个线程,另外一个没有同步,而它们处理的是同一资源;

2.不是同一个锁,改为内存中唯一的对象,例如:Input.class,Res.class,res都行。

class Input implements Runnable {
	Res res;
	boolean flag = true;

	public Input(Res res) {
		this.res = res;
	}

	public void run() {
		while (true) {
			synchronized(Res.class){   //注意这里
			if (flag == true) {
				res.name = "麦克";
				res.sex = "男";
				flag = false;
			} else {
				res.name = "May";
				res.sex = "girl";
				flag = true;
			}
		}
		}
	}
}

class Output implements Runnable {
	Res res;

	public Output(Res res) {
		this.res = res;
	}

	public void run() {
		while (true) {
			synchronized(Res.class){  //注意这里
			System.out.println(res.name + " --- " + res.sex);
		}
		}
	}

}

为节省篇幅,仅截取Input和Ouput两个被修改类的代码。

三   线程间通信3 - 等待唤醒机制

现在需要将程序修改为:一个写入后,等另一个输出才继续写入。

为了满足需求,可以再Res类中加入标记,用于标记Input类的对象是否已写入新值。

模拟执行过程:

Input对象判断标记为false,无新值,写入并将标记改为true,Input对象再输入判断到为true,就等着。

Output对象判断,标记为true有内容,输出然后修改标记为false,唤醒Input的对象,它自己等着。

等待的线程在哪里?

线程运行时,内存中会建立线程池,等待的线程就存放在线程池中,

notify唤醒线程池中的线程,通常唤醒第一个wait的。

notify的原理,可以用“抓人游戏”做类比。

如果想唤醒线程池中全部正在wait的线程,可以用notifyAll()方法。

注意:wait()、notify()、notifyAll()是定义在Object类中的方法,

观察一下这三个方法在API文档中的描述。






注意文档中“必须拥有对象监视器”和“在此对象监视器上”的描述,

这表明,这三个方法,必须用在 synchronized“同步”当中,不然会抛异常,

这里十分重要,因为这个原因,上机浪费了一个多半小时!!!

参考: IllegalMonitorStateException 异常例子的解决方法

而且,必须标示出wait()方法所操作的线程所属的锁。

为什么?因为同步有可能会出现嵌套。


Q:为什么定义在Object类?

A:因为“锁”可以是任意对象,任意对象能调用的方法,当然是要在Object类中。

示例代码:

/*
线程间通讯:
其实就是多个线程在操作同一个资源,
但是操作的动作不同。
 */
import java.lang.*;

class Res {
	String name;
	String sex;
	boolean flag = false;
}

class Input implements Runnable {
	private Res r;

	Input(Res r) {
		this.r = r;
	}

	public void run() {
		int x = 0;
		while (true) {
			synchronized (r) {
				if (r.flag) {
					try {
						r.wait();
					} catch (Exception e) {
					}
				}
				if (x == 0) {
					r.name = "mike";
					r.sex = "man";
				} else {
					r.name = "丽丽";
					r.sex = "女女女女女";
				}
				x = (x + 1) % 2;
				r.flag = true;
				r.notify();
			}
		}
	}
}

class Output implements Runnable {
	private Res r;

	Output(Res r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			synchronized (r) {
				if (!r.flag) {
					try {
						r.wait();
					} catch (Exception e) {
					}
				}
				System.out.println(r.name + "    " + r.sex);
				r.flag = false;
				r.notify();
			}
		}
	}
}

public class Demo {
	public static void main(String args[]) {
		Res r = new Res();
		Input in = new Input(r);
		Output out = new Output(r);
		Thread t1 = new Thread(in);
		Thread t2 = new Thread(out);
		t1.start();
		t2.start();
	}
}

 

四   线程间通信4 - 代码优化

class Res {
	private String name;
	private String sex;
	private boolean flag = false;

	public synchronized void set(String name, String sex) {
		if (this.flag) {
			try {
				this.wait();
			} catch (Exception e) {
			}
		}
		this.name = name;
		this.sex = sex;
		this.flag = true;
		this.notify();
	}

	public synchronized void out() {
		if (!this.flag) {
			try {
				this.wait();
			} catch (Exception e) {
			}
		}
		System.out.println(this.name + "    " + this.sex);
		this.flag = false;
		this.notify();
	}
}

class Input implements Runnable {
	private Res r;

	Input(Res r) {
		this.r = r;
	}

	public void run() {
		int x = 0;
		while (true) {
			if (x == 0) {
				r.set("mike", "man");
			} else {
				r.set("丽丽", "女女女女女");
			}
			x = (x + 1) % 2;
		}
	}
}

class Output implements Runnable {
	private Res r;

	Output(Res r) {
		this.r = r;
	}

	public void run() {
		while (true) {
			r.out();
		}
	}
}

public class Demo {
	public static void main(String args[]) {
		Res r = new Res();
		new Thread(new Input(r)).start();
		new Thread(new Output(r)).start();
	}
}

加上private和getter、setter方法赋值时需要同步,因为可能出现安全问题。

五   线程间通信5 - 生产者消费者问题

上面的示例代码,输出的是姓名,不好分辨。

现在通过生产者消费者问题(“生产一个、消费一个……”)来进行说明。

通过带编号产品(新产品编号在setter方法中+1)来进行区分。

Q:单个生产单个消费没问题,那如果换为多个生产多个消费呢?

  可能会出现生产一个,消费两个的情况,或者生产两个消费一个,为什么呢?

A:因为没判断标记flag,只要将标记判断的if改为while就可以了,

  但会全部等待了。还要notifyAll()。

 
package Test3;

class Resource {

	private String name;
	private int conut = 1;
	private boolean ResControlFlag = false;

	public synchronized void Set(String name) {

		while (true) {
			while (ResControlFlag) {
				try {
					this.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}

			this.name = name + this.conut++;
			System.out.println(Thread.currentThread().getName() + " --------> "
					+ this.name);
			ResControlFlag = true;
			this.notifyAll();
		}
	}

	public synchronized void Out() {

		while (true) {

			while (!ResControlFlag) {
				try {
					this.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}

			System.out.println(Thread.currentThread().getName() + " <--- "
					+ this.name);
			ResControlFlag = false;
			this.notifyAll();

		}

	}

}

class Producer implements Runnable {

	private Resource res;

	public Producer(Resource res) {
		this.res = res;
	}

	public void run() {
		res.Set("电脑");
	}
}

class Consumer implements Runnable {

	private Resource res;

	public Consumer(Resource res) {
		this.res = res;
	}

	public void run() {
		res.Out();
	}
}

public class ProducerConsumerDemo {

	public static void main(String[] args) {

		Resource res = new Resource();
		new Thread(new Producer(res), "生产者").start();
		new Thread(new Consumer(res), "消费者").start();
		new Thread(new Producer(res), "生产者").start();
		new Thread(new Consumer(res), "消费者").start();
		new Thread(new Producer(res), "生产者").start();
		new Thread(new Consumer(res), "消费者").start();

	}
}

Q:对于多个生产者和消费者,为什么要定义while判断标记?

A:为了让被唤醒的线程再一次判断标记。

Q:为什么要使用notifyAll?

A:因为需要唤醒对方线程,因为只用notify,

  容易出现只唤醒本方线程的情况,导致程序中的所有线程都等待。

六   线程间通信6 - 生产者消费者问题(升级版)

Q:这时候,问题就来了,notifyAll会唤醒在此对象监视器上等待的所有线程,

  怎样才能“不唤醒自己,而只唤醒其他线程呢?”

A:JDK1.5中提供了现成的升级解决方案:将“同步synchronized”替换成“显式锁Lock”操作,

   将 object 中的 wait ,notify,notifyAll,替换成了condition 对象。

public interface Lock

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。

Lock这个接口,把隐式的加锁,变成显式的加锁了。


public interface Condition :

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,

以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。

其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

synchronized  →  Lock

Condition   →   wait、notify 和 notifyAll

示例代码:

import java.util.concurrent.locks.*;

class Demo {
	public static void main(String args[]) {
		Resource r = new Resource();
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);
		Thread t1 = new Thread(pro);
		Thread t3 = new Thread(pro);
		Thread t2 = new Thread(con);
		Thread t4 = new Thread(con);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

class Resource {
	private String name;
	private int count = 1;
	private boolean flag = false;
	private Lock lock = new ReentrantLock();
	private Condition condition_pro = lock.newCondition();
	private Condition condition_con = lock.newCondition();

	public void set(String name) throws InterruptedException {
		lock.lock();
		try {
			while (flag) {
				condition_pro.await();
			}
			this.name = name + "--" + count++;
			System.out.println(Thread.currentThread().getName() + "...生产者..."
					+ this.name);
			flag = true;
			condition_con.signal();
		} finally {
			lock.unlock();
		}
	}

	public void out() throws InterruptedException {
		lock.lock();
		try {
			while (!flag) {
				condition_con.await();
			}
			System.out.println(Thread.currentThread().getName()
					+ "...消费者..........." + this.name);
			flag = false;
			condition_pro.signal();
		} finally {
			lock.unlock();
		}
	}
}

class Producer implements Runnable {
	private Resource res;

	Producer(Resource res) {
		this.res = res;
	}

	public void run() {
		while (true) {
			try {
				res.set("+商品+");
			} catch (InterruptedException e) {

			}
		}
	}
}

class Consumer implements Runnable {
	private Resource res;

	Consumer(Resource res) {
		this.res = res;
	}

	public void run() {
		while (true) {
			try {
				res.out();
			} catch (InterruptedException e) {

			}
		}
	}
}

更多用法和说明直接看API文档就行了,说得很清楚。
 

七   线程停止

查阅API文档,Thread类的stop()方法已经过时。

那我们应该如何让线程停止呢?    

停止线程只有一种方法,就是让run()方法结束。

让run()方法结束的两种情况:

1.通常情况:多线程运行,运行代码通常是循环结构。只要控制循环,就可以让run方法结束,也就是线程结束。

2.特殊情况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。  

            当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结状态进行清除。

            强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。

            使用interrupt()方法,将处于冻结状态的线程强制的恢复到运行状态中来

class Demo {
	public static void main(String args[]) {
		StopThread st = new StopThread();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		t1.start();
		t2.start();
		int num = 0;
		while (true) {
			if (num++ == 60) {
				st.changeFlag();
				t1.interrupt();
				t2.interrupt();
				break;
			}
			System.out.println(Thread.currentThread().getName() + num);
		}
	}
}

class StopThread implements Runnable {
	private boolean flag = true;

	public void run() {
		while (flag) {
			System.out.println(Thread.currentThread().getName() + "....run");
		}
	}

	public void changeFlag() {
		flag = false;
	}
}

八   守护线程

守护线程:可以理解为“后台线程”,把线程标记为“后台线程”后,

         它开启、运行等等状态跟“前台线程”都没有区别,就结束有区别,

          当所有”前台线程”都结束后,”后台线程”会自动结束。

void  setDaemon(boolean on) :将该线程标记为守护线程或用户线程。

知识点:

1.setDeamon(true)设为守护线程

2.全剩下守护线程,程序结束

九   Join方法

join()方法:

把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。

比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

class Demo {
	public static void main(String args[]) throws Exception {
		StopThread st = new StopThread();
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		t1.start();
		t1.join();
		t2.start();
		t2.join();
		for (int x = 0; x < 70; x++) {
			System.out.println(Thread.currentThread().getName() + "    " + x);
		}
		System.out.println("over");
	}
}

class StopThread implements Runnable {
	public void run() {
		for (int x = 0; x < 70; x++) {
			System.out.println(Thread.currentThread().getName() + "    " + x);
		}
	}
}

join()方法,抢先获得CPU的执行权,先于主线程执行,

例如,t1.join();。此时,主线程处于冻结状态,只有t1可以运行;

只有t1运行结束,主线程才可以运行,等待让主线程让出执行权的线程死亡。一般用于临时加入线程。

join方法的位置会有影响,如果放在多个线程开启的下面,例如,t1.start();t2.start();t1.join();,

t1和t2会交替执行,因为t1抢的是主线程的运行权,和t2无关。主线程依然在t1结束后运行,和t2的执行无关。

当A线程执行到了B线程的join方法时,A线程就会等待,等B线程都执行完,A线程才会执行。可以用来临时加入线程执行。可以嵌套使用。

如果t1等待,会造成主线程无法运行,所以使用interrupt方法来结束冻结状态,继续运行。

因为是强制恢复运行,所以可能会出现异常,所以join方法会抛出InterruptedException异常。

开启线程的线程组,称为主线程组,里面的成员构成一个线程组。也可以通过ThreadGroup创建新的对象,封装进其他的组。很少用。

十   优先级和yield方法

1、关于线程的优先级

线程的执行有优先级,可通过setPriority(int newPriority)方法更改线程的优先级。

优先级:代表抢资源的频率,设置越高,执行越频繁,优先执行。

所有线程的优先级,包括主线程,优先级默认设置为5,共有10级,数字越大,越优先执行。

如果数字相差不大,优先程度几乎没有差别,只有1、5、10之间最明显,

分别对应Thread类的三个字段:MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY

虽然可以设为最高级,也只是相对高而已,不可能只执行一个线程。

2、yield方法

Thread.yield();暂停当前正在执行的线程对象,并执行其他线程。

多个线程运行时,通过临时强制释放线程的执行权,使得其他线程可以执行。

这样可以减缓线程的运行,使得线程都有机会运行。

3、开发时如何编写线程

Q:什么时候用多线程?

A:当某些代码需要同时运行时,可以用单独的线程进行封装。

      也可以封装成类,或者对象。

示例代码:

 new Thread()//通过Thread类建立匿名类
{
       public void run()
       {
              for (int x=0;x<30;x++ )
              {
                     System.out.println(Thread.currentThread().getName()+"...Jobs..."+x);
              }
       }
}.start();
Runnable r = new Runnable()//通过Runnable接口建立匿名类对象
{
       public void run()
       {
              for (int x=0;x<30;x++ )
              {
                     System.out.println(Thread.currentThread().getName()+"...Intel..."+x);
              }
       }
};
new Thread(r).start();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值