JavaSE(5):java多线程技术


五、 Java线程技术

 

1、基础概念

程序:是一段静态的代码,是人们解决问题的思维方式在计算机汇中的描述,是应用软件执行的蓝本,是一个静态的概念。

进程:是程序的一个运行例程,是程序的一次动态执行过程,进程是计算机进行资源分配的独立单位。

线程:是进程中相对独立的一个执行单元,是操作系统调度的基本单位,一个进程可以包含多个线程。进程并不执行代码,它只是代码存放的地址空间,进程地址空间中所存放的代码由线程来执行,线程的执行由操作系统负责调度。

线程的运行机制:在java中每一个线程都有一个独立的程序计数器和方法调用栈。程序计数器用来记录线程当前执行程序代码的位置,指向下一条要执行的指令。方法调用栈是用来描述线程在执行时一系列的方法调用过程,栈中每一个元素称为一个栈帧,每一个栈帧对应一个方法调用,帧中保存方法调用的参数、局部变量及执行过程中的临时数据。

 

 

2、线程的创建和启动

 

线程的创建有两种方法:继承Thread类、实现Runnable接口

 

继承Thread类:Thread类的每个实例或其子类的实例都是一个线程,故可以通过Thread类或它的派生类创建线程实例并启动一个新的线程。

自定义Thread类的派生类创建线程,主要是覆盖方法run(),在这个方法中加入线程执行代码即可。

实现Runnable接口:推荐使用这种方法,因为java类是单继承的,继承了Thread类则无法继承其他类。Runnable接口只有一个方法run()。过程如下:1、定义Runnable接口的实现类,重写该接口的run方法。2、创建Runnable实现类的实例,以该实例作为Thread类的参数创建一个线程对象,这个Thread对象就是需要的线程对象。

可见,当使用Runnable接口时,不能直接创建该接口实现类的对象作为线程,必须从Thread类的一个实例内部运行它。

CreateThread.java(两种创建线程的方法实现、及其对比)

package blog5;

/**
 * 演示两种实现线程的方法
 */
public class CreateThread {

	public static void main(String[] args) {
		MyThread exThread = new MyThread();
		//Runnable实现类也必须寄托于Thread类才能创建线程
		Thread runThread = new Thread(new MyRunnableThread());
		exThread.start();
		runThread.start();
	}
}

class MyRunnableThread implements Runnable{
	@Override
	public void run() {
		for(int j=0;j<100;j++){
			System.out.println("Runnable Thread print:" + j);
		}
		System.out.println(Thread.currentThread().getName() + " is Over...");
	}
}

class MyThread extends Thread{
	
	public MyThread() {
		super();
	}
	
	@Override
	public void run() {
		for(int j=0;j<100;j++){
			System.out.println("Extends Thread print:" + j);
		}
		System.out.println(Thread.currentThread().getName() + " is Over...");
	}
}


TwoThreadPrintTogather.java(两个线程共同实现打印工作)

package blog5;

/**
 * 两个线程共同打印0—99,但会有线程安全问题
 *
 */
public class TwoThreadPrintTogather implements Runnable{
	static int i=0;
	
	@Override
	public void run() {
		for(;i<100;i++){
			System.out.println(Thread.currentThread().getName()+ ":" + i);
		}
	}
	
	public static void main(String[] args) {
		Thread thread1 = new Thread(new TwoThreadPrintTogather());
		Thread thread2 = new Thread(new TwoThreadPrintTogather());
		thread1.start();
		thread2.start();
	}
}

 


3、线程的启动

创建一个线程对象之后,仅仅是在内存中出现了一个线程类的实例对象,线程并不会自动开始运行,必须调用线程对象的start方法来启动线程。它实现了为线程分配必要的资源,使线程处于可以运行状态,同时调用了线程的run方法来运行线程。

注:run方法可以通过线程的start自动在新线程中执行,也可以当做一般的函数被类的对象调用,但是不会在新的线程中执行

 

4、线程状态与线程控制

线程状态:线程对象被创建时,线程的生命周期开始,直到该对象被撤消为止。在这个周期中,线程并不是一创建就进入运行状态,线程启动之后,也不是一直处于运行状态,线程在生命周期内主要有以下几个状态,并可以相互装化。

创建状态(NEW)、可运行状态(Runnbale)、运行状态(Running)、阻塞状态(Blocked)、等待状态(Waiting)、计时等待(Timed Waiting)、死亡状态(Dead)。

创建状态(NEW):创建了一个线程对象而没有启动它,则会处于该状态。

可运行状态(Runnable):创建状态的线程对象调用了start方法之后,进入了可运行状态。此时JVM为其创建了方法调用栈和程序计数器。注:线程处于可运行状态只是说明其具有运行条件,不一定是正在运行中,还得取决于系统的调度,分配cpu时间片

运行状态(Running):处在“可执行状态”的线程对象一旦获得了 CPU 控制权,就会转换到“运行状态”,在“运行状态”下,线程状态占用 CPU 时间片段,执行run 方法中的代码,处在“执行状态”下的线程可以调用 yield 方法,该方法用于主动出让 CPU 控制权。线程对象出让控制权后回到“可执行状态”,重新等待调度。

阻塞和等待状态:由线程的运行状态转变而来,使线程放弃当前处理机会(占用cpu机会),直到线程调试器重新激活它(系统调度重新让它分配到cpu上)。进入该状态有以下几种原因:

①当线程试图获取一个内部的对象锁,但是该锁被其他线程占有时,该线程进入阻塞状态。

②线程调用wait方法等待另一个线程通知,或调用join方法等待另一个线程结束,线程会进入等待状态。

③线程调用sleepwaitjoin等方法时,传入了一个超时参数,线程在这些时间内进入计时等待状态。

解除阻塞的方法:睡眠状态超时、调用 join 后等待其他线程执行完毕、I/O 操作执行完毕、调用阻塞线程的 interrupt 方法(线程睡眠时,调用该线程的interrupt方法会抛出InterruptedException)。

死亡状态(Dead):run方法执行完成、线程抛出未捕获的ExceptionError、调用该线程的stop方法,这三种情况会使线程死亡,一旦死亡,不能重新工作。可以使用 Thread 类的 isAlive 方法判断线程是否活着。


ThreadStatus.java(线程状态控制,主要是sleepjoin方法、interrupt方法的使用)

package blog5;

/**
 * 线程状态的转化练习,实现线程的控制
 * 主要为sleep、yield、join、interrupt的使用
 * 注意:yield和sleep的区别
 */
public class ThreadStatus {
	public static void main(String[] args) {
		Thread threadA = new Thread(new MyThreadA());
		threadA.setName("线程A");
		
		Thread threadB = new Thread(new MyThreadB());
		threadB.setName("线程B");
		
		Thread threadC = new Thread(new MyThreadA());
		threadC.setName("线程C");
		
		threadA.start();
		threadB.start();
		threadC.start();
		
		for(int i=0;i<30;i++){
			if(i==5){
				System.out.println("主线程打印到5啦,把线程B的睡眠中断吧,让它也有机会打印...");
				threadB.interrupt();
			}
			if(i==0){
				System.out.println("先让主线程睡眠,让A、C线程打印吧(B也在睡眠中)...");
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
				}
			}
			if(i==10){
				System.out.println("主线程打印到10了,别打印了,先让B打印完吧");
				try {
					threadB.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println("here is main thread..." + i);
			Thread.yield();//主线程每打印完一次,让所有活线程重新抢cpu
		}
	}
}

class MyThreadA implements Runnable{
	
	int i = 1;
	
	public void run() {
		for(;i<=20;i++){
			System.out.println(Thread.currentThread().getName() + ":" + i);
			Thread.yield();//打印完一次,让所有活线程重新抢cpu
		}
	}
}

class MyThreadB extends Thread{
	
	public void run() {
		try {
			sleep(100000);
		} catch (InterruptedException e) {
			System.out.println("睡眠被中断了");
		}
		for(int i=0;i<20;i++){
			System.out.println("中断睡醒打印:"+i);			
		}
	}
}

 


线程控制

Join方法:调用join方法的线程对象强制运行,该线程强制运行期间,其他线程无法运行,必须等待该线程结束后其他线程才能运行。也称联合线程。

使用流程:A线程执行过程中,调用B线程join方法,则A进入等待,B执行完之后,A恢复执行。

Sleep方法:线程休眠。A正在执行,调用Asleep方法,A进入计时等待状态。等待计时结束后,并不会返回运行状态,而是可运行状态,所以不能保证sleep结束后会立即执行,需要依靠系统的调度。Sleep只能控制当前正在运行的线程。

Yield方法:与sleep类似,但是A调用yield方法不会使A进入阻塞状态,而是让A由运行状态返回到可运行状态,同时让系统再重新调度一下(重新分抢cpu使用权),所以,若A的优先级很高,这个方法不会让A让出执行权的。

线程优先级:(较少使用)决定线程的cpu调度优先程度。通过setPrioritygetPriority设置和获取优先级。Main方法默认为NORM_PRIORITY(值为5),最大为MAX_PRIORITY,值为10,最小为MIN_PRIORITY1)。

Daemon后台线程:处于后台运行,为其他线程提供服务,也成为守护线程,JVM的垃圾回收就是典型的后台线程。

特点:所有的前台线程都死亡,则后台线程自动死亡。

设置后台线程:setDaemon(true)方法,必须在start之前调用,否则抛异常。前台线程创建的线程默认是前台线程。判断用isDaemon方法。如果一个线程时Daemon线程,那么它创建的任何线程也会自动具备Daemon属性。

ThreadContral.java(三种控制方法的使用),见前文ThreadStatus.java

DeamonTest.java(Daemon线程的实例)

package blog5;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 守护线程实例
 * 流程:当用户输入yes,主线程创建一个守护线程,主线程开始睡眠10ms,此时间内,守护线程输出字符,
 * 但是,当主线程睡眠结束,不管守护线程run方法完毕否(输出还没结束),程序都会终止。
 * 当用户输入非yes,主线程创建一个用户线程,然后进入睡眠10ms,不管主线程睡眠完毕否,用户线程会一直执行
 * 其run方法,知道用户线程也结束了,程序才结束。
 * 所以,有些IO操作不要放在Daemon线程中,会造成资源未输出就已经关闭了程序。
 */
public class DaemonTest {
	public static void main(String[] args) throws IOException, InterruptedException {
		System.out.println("Thread's daemon status,yes or no:");
		// 建立缓冲字符输入流
		BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
		String str;
		// 从键盘读取一个字符串
		str = stdin.readLine(); //异常抛出不处理
		if(str.equals("yes")){
			//创建守护线程,并启动
			ThreadTest t = new ThreadTest();
			t.setDaemon(true);
			t.start();
		}else{
			new ThreadTest().start(); //创建用户线程并启动
		}
		Thread.sleep(10);
	}

}

class ThreadTest extends Thread {
	@Override
	public void run() {
		// 输出当前线程是否为后台线程
		for (int i = 0; i < 100; i++) {
			System.out.println("NO. " + i + " Daemon is " + isDaemon());
			try {
				sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}


 

5、线程同步安全问题

多线程在访问同一个共享数据时,一个线程还没执行完,领一个线程参与进来执行,会导致共享数据的错误,所以应该避免这种情况,对共享数据操作的语句,只能让一个线程都执行完毕,其他线程不能参与执行。

三种方法解决这个问题。

①同步代码块:synchronized(obj){ }obj是一个同步对象

②同步方法:synchronized returnType methodNameparamList{ }通常不直接在run方法上添加。同步方法的同步监听器其实是this。静态方法的默认同步锁是当前方法所在类的.calss对象。

③同步锁:与synchronized类似,但是功能更强大,通常使用ReentrantLock(可重入锁)

注:使用synchronized关键字时,避免在其方法或代码块中使用sleepyield方法,因为该程序块占用着对象锁,其他线程只能等该线程醒来或执行完毕才能执行,严重影响效率,且逻辑不符。当然与与同步程序块无关的线程可以获得更多的运行时间。

ThreadSafty.java(同步锁的实现实例、线程同步的两个案例:账户操作例程、分苹果例程)

 

package blog5;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 线程同步安全问题,本章重点,本例代码实际是融合了3个习题
 * 通过实例阐述三种实现线程同步的方法:
 * 1 信用卡账户问题(同步代码块):建立一个信用卡账户,起始信用额100w,然后模拟透支、存款等
 * 	 						多个操作,显然银行账户User对象是个竞争资源,应该把修改余额
 * 							的语句放在同步代码块中,将余额设为私有变量防止直接访问。
 * 2 小朋友分苹果问题(同步方法):小朋友小明和小强分别取拿苹果,则苹果数的修改应放在同步方法中。
 * 							实现模拟两个小朋友拿苹果的过程。
 * 3 同步锁Lock的实现:模拟多个线程卖票过程
 */
public class ThreadSafty {
	
	/* 练习1
	// 测试信用卡庄户问题:同步代码块
	public static void main(String[] args) {
		//建立一个用户,和六个操作该用户的线程。
		User u = new User("张三",100);
		//注:此处User u 以参数形式传递给六个线程,那么6个线程中的属性User是同一个u,还是分别根据参数
		//在内存中新建6个相同的u,我觉得是引用数据类型传递的是栈的地址,也就是说所有线程面对的是同一个u
		BankCountThread bkt1 = new BankCountThread("线程1", u, 30);
		BankCountThread bkt2 = new BankCountThread("线程2", u, -30);
		BankCountThread bkt3 = new BankCountThread("线程3", u, -60);
		BankCountThread bkt4 = new BankCountThread("线程4", u, 20);
		BankCountThread bkt5 = new BankCountThread("线程5", u, 100);
		BankCountThread bkt6 = new BankCountThread("线程6", u, -90);
		bkt1.start();
		bkt2.start();
		bkt3.start();
		bkt4.start();
		bkt5.start();
		bkt6.start();
	}
	*/
	
	/* 练习2
	//测试分苹果问题:同步方法
	public static void main(String[] args) {
		AppleThread apple = new AppleThread();
		Thread thread1 = new Thread(apple);
		Thread thread2 = new Thread(apple);
		thread1.setName("明明");
		thread2.setName("强强");
		thread1.start();
		thread2.start();
	}
	*/
	
	// 练习3
	// 同步锁lock实现:买票过程
	public static void main(String[] args) {
		SaleTicketsThread saleThread = new SaleTicketsThread();
		Thread t1 = new Thread(saleThread);
		Thread t2 = new Thread(saleThread);
		Thread t3 = new Thread(saleThread);
		t1.setName("线程A");
		t2.setName("线程B");
		t3.setName("线程C");
		t1.start();
		t2.start();
		t3.start();
	}
 
}

//练习1,银行账户线程,采用继承Thread方法
class BankCountThread extends Thread{
	private User u;
	private int y=0;
	
	public BankCountThread(String name,User u,int y) {
		super(name);
		this.u = u;
		this.y = y;
	}
	
	@Override
	public void run() {
		u.oper(y);
	}
}

//练习1辅助类:银行账户类
class User{
	@SuppressWarnings("unused")
	private String code; //id code number
	private int cash;    //balance in the count
	
	public User(String code,int cash) {
		this.cash = cash;
		this.code = code;
	}
	
	//存取操作
	public void oper(int x){
		try{
			Thread.sleep(10);
			synchronized(this){
				this.cash += x;
				System.out.println(Thread.currentThread().getName()
						+"操作,增加了"+ x +",当前余额为:"+this.cash);
			}
			Thread.sleep(10);
		}catch ( InterruptedException e){
			e.printStackTrace();
		}
	}
}

//练习2:分苹果线程,采用实现Runnable接口方法 
class AppleThread implements Runnable{
	private int appleCount = 5;
	//拿苹果方法,synchronized方法实际是以此对象实例this为锁的,保证一次只有一个线程进入修改appleCount
	public synchronized boolean getApple(){
		if(appleCount>0){
			//the apple count more than one,take an apple
			appleCount--;
			try {
				//拿苹果模拟需要1分钟,也充分可以测试到线程安全问题
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"拿走了一个苹果,还剩下个" + appleCount + "苹果");
			return true;
		}else{
			return false;			
		}
	}
	
	@SuppressWarnings("unused")
	public void run() {
		boolean flag;
		while( (flag = getApple()) ){
			//循环执行getApple,直到getApple返回false,也就是苹果被拿完
		}
		System.out.println(Thread.currentThread().getName()+"线程结束...");
	}
	
}


// 练习3:同步锁lock实现线程安全编程
class SaleTicketsThread implements Runnable {
	private int tickets = 50;				  //票总数
	private Lock myLock = new ReentrantLock();//同步锁对象
	
	@SuppressWarnings("unused")
	@Override
	public void run() {
		boolean flag;
		while( (flag = sale()) ){
			
		}
		System.out.println(Thread.currentThread().getName() + "结束操作...");
	}
	
	// 卖票操作
	public boolean sale(){
		myLock.lock(); //上锁
		if(tickets>0){
			try {
				tickets--;
				System.out.println(Thread.currentThread().getName() 
						+ "卖出1张票,还剩 " + tickets + " 张票...");
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally{
				myLock.unlock(); //解锁
			}
			return true;
		}else{			
			myLock.unlock(); //解锁,不要忘了这一句,否则会有线程永远无法结束,永远处于等待锁的状态
			return false;
		}
	}
}

//用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而用Lock需要我们手动释放锁,
//所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内。
//如果说这就是Lock,那么它不能成为同步问题更完美的处理方式,通常还有一种更好用的读写锁(ReadWriteLock),
//我们会有一种需求,在对数据进行读写的时候,为了保证数据的一致性和完整性,需要读和写是互斥的,写和写是互斥的,
//但是读和读是不需要互斥的,这样读和读不互斥性能更高些。


6、线程通信

典型的生产者与消费者问题:生产者和消费者共享存放产品的仓库,若仓库为空时,消费者无法消费,仓满时,生产者无法继续生产。Java通过waitnotifynotifyAll三个方法提供了线程的通信。这三个方法都只能在synchronized关键字范围内使用。

wait()方法:中断方法的执行,使本线程等待,暂时让出 cpu 的使用权,并允许其他线程使用这个同步方法,即同时让出对象锁,直到其他使用同一个对象锁线程调用notify方法或notifyAll方法为止。

notify()方法:唤醒由于使用这个同步方法(同步对象锁)而处于等待线程的 某一个结束等待。

notifyall()方法:唤醒所有由于使用这个同步方法(同步对象锁)而处于等待的线程结束等待。

以上三个方法是Object的方法而不是Thread的成员方法,这三个方法必须用同步对象来调用。若是synchronized同步方法,则因为该类默认的实例this就是同步对象,可以在同步方法中调用这三个方法;若是synchronized修饰的同步代码块,同步锁是括号中的对象,所以必须使用该对象调用对象的这三个方法。

若用Lock代替同步方法或同步代码块,Condition代替同步对象锁,Condition对象通过Lock对象的newCondition方法创建,await等价waitsignal等价notifysignalAll等价notifyAll

 

Waitsleep的区别:1、这两个方法来自不同的类分别是ThreadObject;2、最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法;3waitnotifynotifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围);4sleep必须捕获异常,而waitnotifynotifyAll不需要捕获异常

 

ThreadCommunite.java(两个实例介绍线程通信:买票实例、生产消费者实例,一个lock线程通信实例)

 

package blog5;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 线程通信的实例,本章重点,同样本文件融合三个练习。
 * 通过3个实例介绍本知识点
 * 1 生产者与消费者实例
 * 2 买票实例:例题:A,B,C,买票,5元一张,售票员目前只有一张5元零钱
			 A持20元,B、C持5元去排队买票,若A排在前面,只能先等待,B、C买完之后再买(此时才能找零)
 * 3 lock实现的线程通信:此处摘抄一段他人总结的代码作为学习
 */
public class ThreadCommnunite {
	
	/* 习题1
	// 生产者与消费者:生产者不断的生成A—Z的数据,而消费者不断取出数据,但是必须生成之后再取
	public static void main(String[] args) {
		//创建数据对象和消费者、生产者线程
		ShareDate date = new ShareDate();
		new Thread(new Productor(date)).start();
		new Thread(new Consumer(date)).start();
	}
	*/
	
	// 习题2
	// 买票例程:A,B,C,买票,5元一张,售票员目前只有一张5元零钱
	// A持20元,B、C持5元去排队买票,若A排在前面,只能先等待,B、C买完之后再买(此时才能找零)
	public static void main(String[] args)  {
		Saler saleThread = new Saler();
		Thread t1 = new Thread(saleThread);
		Thread t2 = new Thread(saleThread);
		Thread t3 = new Thread(saleThread);
		t1.setName("A");
		t2.setName("B");
		t3.setName("C");
		t1.start();
		t2.start();
		t3.start();
	}
	
}

//习题1:生产者与消费者共享数据
class ShareDate{
	private char date;
	private boolean writeable = true; //可以写入吗?初始为可写入,程序开始先写再取
	
	//写数据
	public synchronized void setDate(char d){
		if(!writeable){
			try {
				//此时不能写入,writeable == false,即写了还没取出,未消费,此时等待
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//此时可以写入,writeable == true,则生产一个数据
		this.date = d;
		writeable = false; //修改状态标志,变成不能写,需要消费。
		notify(); //唤醒等待中的线程
	}
	
	//此时两个方法 getDate和setDate都是synchronized是否使用同一个锁对象this?我想是的.........
	public synchronized char getDate(){
		if(writeable){
			try {
				//此时标识告诉我们应该写入,所有等待写如数据,等待生产
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//此时writeable==false,即生产已经结束,可以消费
		writeable = true;
		notify();
		return date;
	}
}

//练习1:生产者类
class Productor implements Runnable{
	private ShareDate shareDate;
	
	public Productor(ShareDate shareDate) {
		this.shareDate = shareDate;
	}
	
	@Override
	public void run() {
		for(char c = 'A';c<='Z';c++){
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			shareDate.setDate(c);
			System.out.println("生产者生产一个数据:" + c );
		}
		System.out.println("生产任务完成,生产者线程结束...");
	}
}

//练习1:消费者类
class Consumer implements Runnable{
	private ShareDate shareDate;
	
	public Consumer(ShareDate shareDate) {
		this.shareDate = shareDate;
	}
	
	@Override
	public void run() {
		char c;
		do{
			c = shareDate.getDate();
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("..消费者取出一个数据:" + c );
		}while(c!='Z');		
		
		System.out.println("消费任务完成,消费者线程结束...");
	}
}

// 习题2:售票员类,是一个共享数据,三个买票者线程都是通过该类获得票
class Saler implements Runnable{
	private int fiveCount = 1;
	
	@Override
	public void run() {
		sale();
	}
	
	//同步方法,一次只能一个人来卖票
	public synchronized void sale(){
		if("A".equals(Thread.currentThread().getName())){
			//当A来买票时,看钱能找开不
			if(fiveCount<3){
				try {
					//找不开钱则等待
					System.out.println("找零不够,A等待!");
					wait();
					//当被唤醒等待的时候,就可以卖给A票了
					fiveCount -= 3;
					System.out.println("卖给A一张票,找零15");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}else{
				//能找开钱,直接卖给A,这种情况其实不会发生
				fiveCount -= 3;
				System.out.println("卖给A一张票,找零15");
			}
		}else { //当是B或者C来买票的时候,B或者C进入该方法
			fiveCount++;
			System.out.println("卖给"+ Thread.currentThread().getName()+"一张票");
			if(fiveCount>=3){
				notify(); //当钱够了叫醒A线程来买票
			}
		}
	}
}

// 习题3:lock实现的线程通信
class BoundedBuffer {  
	   final Lock lock = new ReentrantLock();//锁对象  
	   final Condition notFull  = lock.newCondition();//写线程条件   
	   final Condition notEmpty = lock.newCondition();//读线程条件   
	  
	   final Object[] items = new Object[100];//缓存队列  
	   int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;  
	  
	   public void put(Object x) throws InterruptedException {  
	     lock.lock();  
	     try {  
	       while (count == items.length)//如果队列满了   
	         notFull.await();//阻塞写线程  
	       items[putptr] = x;//赋值   
	       if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0  
	       ++count;//个数++  
	       notEmpty.signal();//唤醒读线程  
	     } finally {  
	       lock.unlock();  
	     }  
	   }  
	  
	   public Object take() throws InterruptedException {  
	     lock.lock();  
	     try {  
	       while (count == 0)//如果队列为空  
	         notEmpty.await();//阻塞读线程  
	       Object x = items[takeptr];//取值   
	       if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0  
	       --count;//个数--  
	       notFull.signal();//唤醒写线程  
	       return x;  
	     } finally {  
	       lock.unlock();  
	     }  
	  }  
	   
//	      这是一个处于多线程工作环境下的缓存区,缓存区提供了两个方法,put和take
//	   put是存数据,take是取数据,内部有个缓存队列,具体变量和方法说明见代码
//     这个缓存区类实现的功能:有多个线程往里面存数据和从里面取数据
//	     其缓存队列(先进先出后进后出)能缓存的最大数值是100,多个线程间是互斥的
//	     当缓存队列中存储的值达到100时,将写线程阻塞,并唤醒读线程
//	     当缓存队列中存储的值为0时,将读线程阻塞,并唤醒写线程
	   
	   //优点是能明确哪种情况下唤醒那种线程。
}

 

 


附重要知识:重点,线程同步安全和线程通信。

package exercise;
//两个线程共同打印0—99,解决线程安全问题。线程同步时的安全问题


/**
 * 线程知识;
 * 1、sleep、wait、join、yield的作用、特点、用法(通常sleep、yield少在synchronized中使用,而wait、notify在同步块中使用)
 * 2、解决 线程同步 问题的方法:synchronized
 * 3、解决 线程通信 问题的方法:wait 、 notify方法。生产者与消费者实例
 * 
 * 生产者与消费者实例三要素:共享的数据(生成和消费的对象,提供消费方法和生产方法,且这两种方法互相通过标识wait、notify)、消费者、生产者
 */
public class Test07_Thread {
	public static void main(String[] args) {
		MyThread a = new MyThread();
		Thread t1 = new Thread(a);
		Thread t2 = new Thread(a);
		t1.setName("T1");
		t2.setName("T2");
		t1.start();
		t2.start();
	}
}

class MyThread implements Runnable{
	private int i=0;

	@Override
	public void run() {
		try {
			while(!print()){Thread.yield();}
			System.out.println("over");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	public synchronized boolean  print() throws InterruptedException{
		if(i<=100){
			System.out.println(Thread.currentThread().getName() + " " + i);
			i++;
		}
		Thread.sleep(10);
		if(i<=100){
			return false;
		}else{
			return true;
		}
	}
}

package exercise;
//两个线程共同打印0—99,解决线程安全问题。采用线程通信的方法,产生交替打印的效果
public class Test08_Thread2 {
	public static void main(String[] args) {
		Data d = new Data();
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		t1.setName("A");
		t2.setName("B");
		t1.start();
		t2.start();
	}
}

class Data implements Runnable{
	private int i=0;

	@Override
	public void run() {
		print();
	}
	
	public synchronized void print(){
		for(;i<=100;){
			if("A".equals(Thread.currentThread().getName())){
				if(i%2==0){
					System.out.println(Thread.currentThread().getName() + " " + i);
					i++;
					notify();
				}else{
					try {
						wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}else{
				if(i%2==0){
					try {
						wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}else{
					System.out.println(Thread.currentThread().getName() + " " + i);
					i++;
					notify();
				}
			}
		}
	}
}


效果:前者交替随机打印。后者AB严格依次打印

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值