Java程序设计——多线程

目录

一、多线程的概念

1.进程

2.线程

二、线程创建的两种方式

1.继承Thread类创建多线程

2.实现Runnable接口创建多线程

3.两种实现多线程方式的对比分析

实现Runnable接口相对于继承Thread类来说,有如下好处:

4.后台线程

三、线程的生命周期及状态转换

四、线程的调度

1、线程的优先级

2、线程的休眠

​编辑

3、线程的让步

4、线程的插队 

五、线程的安全和同步

1、线程安全

2、同步代码块

3、同步方法

4、死锁问题

六、多线程通信

1、问题引出

2、问题如何解决


一、多线程的概念

1.进程

——在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。

——目前大部分计算机上安装的都是多任务操作系统,即能够同时执行多个应用程序

——在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程。

2.线程

——每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看做程序执行的一条条线索,被称为线程。

——操作系统中的每一个进程中都至少存在一个线程。当一个Java程序启动时,就会产生了一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。

——前面章节所接触过的程序中,代码都是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果,这样的程序称作单线程程序。

——如果希望程序中实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序。多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行。

二、线程创建的两种方式

1.继承Thread类创建多线程

public class Test_Thread {
	public static void main(String[] args) {
		MyThread1 mt1 = new MyThread1();
		MyThread2 mt2 = new MyThread2();
		mt1.start();
		mt2.start();

	}
}

class MyThread1 extends Thread{
	public int i = 1;
	
	public void run() {
		while(i<=100) {
			System.out.println("i = "+i);
			i+=2;
		}
	}
}

class MyThread2 extends Thread{
	public int j = 2;
	
	public void run() {
		while(j<=100) {
			System.out.println("j = "+j);
			j+=2;
		}
	}
}

2.实现Runnable接口创建多线程

通过继承Thread类实现了多线程,但是这种方式有一定的局限性。因为Java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread

Thread类提供了另外一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target))构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口中的run()方法作为运行代码,而不需要调用Thread类中的run()方法

public class Test_Thread {

	public static void main(String[] args) {
		
	//	继承Thread类创建多线程
		MyThread1 mt1 = new MyThread1();
		mt1.start();
		
	//	实现Runnable接口创建多线程	
		MyThread2 mt2 = new MyThread2();
		Thread th = new Thread(mt2);
		th.start();

	}
}

class MyThread1 extends Thread{
	public int i = 1;
	
	public void run() {
		while(i<=100) {
			System.out.println("i = "+i);
			i+=2;
		}
	}
}

class MyThread2 implements Runnable{
	public int j = 2;
	
	public void run() {
		while(j<=100) {
			System.out.println("j = "+j);
			j+=2;
		}
	}
}

 MyThread类实现了Runnable接口,并重写了Runnable接口中的run()方法,通过Thread类的构造方法将MyThread类的实例对象作为参数传入。从运行结果可以看出,例程实现了多线程。

3.两种实现多线程方式的对比分析

假设售票厅有四个窗口可发售某日某次列车的100张车票,这时,100张车票可以看做共享资源,四个售票窗口需要创建四个线程,为了更直观显示窗口的售票情况,可以通过ThreadcurrentThread()方法得到当前的线程的实例对象,然后调用getName()可以获取到线程的名称。

①、通过继承Thread类创建多线程

public class Sale_Ticket {

	public static void main(String[] args) {
		
//	创建一个线程对象并调用start()方法	
		TicketWindow tw = new TicketWindow();
		tw.start();
		
//	创建一个线程对象并调用start()方法	
		new TicketWindow().start();
		new TicketWindow().start();
		new TicketWindow().start();
		
	}

}

class TicketWindow extends Thread{
	public int ticket = 100;
	
	public void run() {
		while(true) {
			if(ticket > 0) {
				Thread th = Thread.currentThread(); // 获取当前线程对象
				String th_name = th.getName();		// 获取当前线程名称
				System.out.println(th_name+"正在售卖第"+ticket--+"张票");
			}
			else
				break;
		}
	}
}

可以看出,每张票都被打印了四次。出现这样现象的原因是四个线程没有共享100张票,而是各自出售了100张票。在程序中创建了四个TicketWindow对象,就等于创建了四个售票程序,每个程序中都有100张票,每个线程在独立地处理各自的资源。

②、通过实现Runnable接口创建多线程

为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行这同一个售票对象的售票方法,简单来说就是四个线程运行同一个售票程序,这时就需要用到多线程的第二种实现方式。

将例程进行修改,并使用构造方法Thread(Runnable target, String name)在创建线程对象的同时指定线程的名称。

public class Sale_Ticket {

	public static void main(String[] args) {
		
		TicketWindow tw = new TicketWindow();
	
		new Thread(tw, "窗口1").start();
		new Thread(tw, "窗口2").start();
		new Thread(tw, "窗口3").start();
		new Thread(tw, "窗口4").start();
		
	}

}

class TicketWindow implements Runnable{
	public int ticket = 100;
	
	public void run() {
		while(true) {
			if(ticket > 0) {
				Thread th = Thread.currentThread(); // 获取当前线程对象
				String th_name = th.getName();		// 获取当前线程名称
				System.out.println(th_name+"正在售卖第"+ticket--+"张票");
			}
			else
				break;
		}
	}
}

只创建了一个TicketWindow对象,然后创建了四个线程,在每个线程上都去调用这个TicketWindow对象中的run()方法,这样就可以确保四个线程访问的是同一个tickets变量,共享100张车票。


实现Runnable接口相对于继承Thread类来说,有如下好处:

1 、适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想。
2 、可以避免由于 Java 的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,所以不能用继承 Thread 类的方式,那么就只能采用实现 Runnable 接口的方式。

4.后台线程

对Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程运行,这个进程就会结束。这里提到的前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程,如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。

public class Behind_Thread {

	public static void main(String[] args) {
	// 	判断当前线程是否是后台线程
		System.out.println("main是后台线程吗?"+Thread.currentThread().isDaemon());
		
	//	判断新创建th线程是否是后台线程
		DaemonThread dt = new DaemonThread();
		Thread th = new Thread(dt,"后台线程");
		System.out.println("th是后台线程吗?"+th.isDaemon());
		th.setDaemon(true); //	设置线程th为后台线程
		th.start();
		for(int i=1; i<=5; i++) {
			System.out.println(i);
		}
	}

}

class DaemonThread implements Runnable{ // 后台线程与普通线程一样,概念相对不同
	public void run() {
		while(true) {
			System.out.println(Thread.currentThread().getName()+"——正在运行");
		}
	}
}

 

当开启线程 th 后,会执行死循环中的打印语句,但我们将线程 th 设置为后台线程后,当前台线程死亡后, JVM 会通知后台线程。由于后台线程从接收指令,到作出响应,需要一定的时间,因此,打印了几次“后台线程——正在运行 ”语句后,后台线程也结束了。由此说明进程中只有后台线程运行时,进程就会结束。

三、线程的生命周期及状态转换

线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。

 线程各种状态的转换关系,箭头表示可转换的方向,其中,单箭头表示状态只能单向的转换,比如线程只能从新建状态转换到就绪状态,反之则不能,双箭头表示两种状态可以互相转换,比如就绪状态和运行状态可以互相转换。

1、新建状态(New)

创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其它Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。

2、就绪状态(Runnable)

当线程对象调用了start()方法后,该线程就进入就绪状态(也称可运行状态)。处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件,能否获得CPU的使用权开始运行,还需要等待系统的调度。

3、运行状态(Running)

如果处于就绪状态的线程获得了CPU的使用权,开始执行run()方法中的线程执行体,则该线程处于运行状态。当一个线程启动后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就结束了),当使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其它线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。 

4、阻塞状态(Blocked) 

 一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃CPU的使用权,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

 当线程试图获取某个对象的同步锁时,如果该锁被其它线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其它线程所持有的锁。

当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回。

 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程。

 当线程调用了Threadsleep(long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。

当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。

5、死亡状态(Terminated)

线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其它状态

四、线程的调度

1、线程的优先级

优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。

线程的优先级用1~10之间的整数来表示,数字越大优先级越高。

除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级

Tread类的静态变量功能描述

static int MAX_PRIORITY

表示线程的最高优先级,相当于值10

static int MIN_PRIORITY

表示线程的最低优先级,相当于值1

static int NORM_PRIORIY

表示线程的普通优先级,相当于值5

 程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。然而线程优先级不是固定不变的,可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。

public class Priority_Thread {

	public static void main(String[] args) {
		//	创建线程
		Thread maxPriority = new Thread(new MaxPriorityThread(), "优先级较高的线程");
		Thread minPriority = new Thread(new MinPriorityThread(), "优先级较低的线程");
		
		//	设置线程优先级
		maxPriority.setPriority(10);
		minPriority.setPriority(Thread.MIN_PRIORITY);
		
		//	启动线程
		maxPriority.start();
		minPriority.start();

	}

}

class MaxPriorityThread implements Runnable{
	public void run() {
		for(int i = 0; i<5; i++) {
			System.out.println(Thread.currentThread().getName()+"————is running");
		}
	}
}

class MinPriorityThread implements Runnable{
	public void run() {
		for(int j = 0; j<5; j++) {
			System.out.println(Thread.currentThread().getName()+"————is running");
		}
	}
}

2、线程的休眠

如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。

当前线程调用sleep(long millis)方法后,在指定时间(参数millis)内该线程是不会执行的,这样其它的线程就可以得到执行的机会了。

public class Sleep_Thread {

	public static void main(String[] args) throws InterruptedException {
		
		new Thread(new SleepThread()).start(); // 创建一个线程并启动
		for(int j = 0; j<3; j++) {
			System.out.println("主线程---is running");
			Thread.sleep(5000);	//	当前线程休眠5秒
		}		
	}
}

class SleepThread implements Runnable{

	public void run() {
		for(int i = 0; i<3; ++i) {
			System.out.println("子线程---is running");
			try {
				Thread.sleep(1000);	//	当前线程休眠1秒
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

开启了两个线程,在这两个线程中分别调用了Threadsleep()方法,目的是让一个线程在打印一次后休眠5000毫秒,从而使另一个线程获得执行的机会,这样就可以实现两个线程的交替执行。当一个主线程休眠结束时,子线程已运行并打印完毕。

3、线程的让步

线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

public class Yield_Thread {

	public static void main(String[] args) {
		// 创建两个线程并开启
		new YieldThread("线程A").start();
		new YieldThread("线程B").start();
	}
}

class YieldThread extends Thread{
	
	public YieldThread(String name) {
		super(name);   // 调用父类的构造方法,给当前线程赋予一个名字
	}
	
	public void run() {
		for(int i = 1; i<=5; ++i) {
			System.out.println(Thread.currentThread().getName()+"---is running");
			if(i==3) {
				System.out.println(Thread.currentThread().getName()+"---线程让步");
				Thread.yield();
			}
		}
	}
}

创建了两个线程,它们的优先级相同。两个线程在循环变量i等于3时,都会调用Threadyield()方法,使当前线程暂停,这时另一个线程就会获得执行,从运行结果可以看出,当线程B输出3次以后,会做出让步,线程A继续执行,同样,线程A输出3次后,也会做出让步,线程B继续执行。

4、线程的插队 

当在某个线程中调用其它线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。

public class Join_Thread {

	public static void main(String[] args) throws InterruptedException {
		// 创建一个线程并启动
		Thread th = new Thread(new JoinThread(),"线程A");
		th.start();
	
		for(int i=1; i<=5; i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
			if(i==3) {
				System.out.println("其它线程插队");	
				th.join();	// 调用其它线程的join方法,该线程会被堵塞
			}
		}
	}
}

class JoinThread implements Runnable{
	public void run() {
		for(int i=1; i<=5; i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
}

 

main线程中开启了一个线程th,main线程中的循环变量为3时,调用th线程的join()方法,这时,th线程就会“插队”优先执行。从运行结果可以看出,当main线程输出3以后,线程A就开始执行,直到线程A执行完毕,main线程才继续执行。

五、线程的安全和同步

1、线程安全

在售票实例中,极有可能碰到“意外”情况,如一张票被打印多次,或者打印出的票号为0甚至负数。这些“意外”都是由多线程操作共享资源ticket所导致的线程安全问题

public class Ticket_Thread {

	public static void main(String[] args) {
	
		TicketThread th = new TicketThread();// 创建一个TicketThread对象

	//	创建并开启四个线程
		new Thread(th, "窗口一").start();;
		new Thread(th, "窗口二").start();;
		new Thread(th, "窗口三").start();;
		new Thread(th, "窗口四").start();;
		
	}

}

class TicketThread implements Runnable{
	private int ticket = 5;
	
	public void run() {
		while(ticket > 0) {
			try {
				Thread.sleep(1000);	// 当前线程休眠1秒
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}	
			System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
		}
	}
}

 

在售票程序的while循环中添加了sleep()方法,这样就模拟了售票过程中线程的延迟。由于线程有延迟,当票号减为1时,假设窗口一此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程休眠,这时窗口二会进行售票,由于此时票号仍为1,因此窗口二也会进入循环,同理,四个窗口都会进入while循环,休眠结束后,四个窗口都会进行售票,这样就相当于将票号减了四次,结果中出现了0-1-2这样的票号。

 最后几行打印售出的票为0和负数,这种现象是不应该出现的,因为在售票程序中做了判断只有当票号大于0时才会进行售票。运行结果中之所以出现了负数的票号是因为多线程在售票时出现了安全问题。

2、同步代码块

要想解决售票实例中的线程安全问题,必须得保证下面用于处理共享资源的代码在任何时刻只能有一个线程访问。

为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块,其语法格式如下:

synchronized(lock){

      操作共享资源块
}                                                                

lock是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。

public class Synchronized_Thread {

	public static void main(String[] args) {
		
	//	创建一个SynchronizedThread对象
		SynchronizedThread st = new SynchronizedThread();

	//	创建并开启线程
		new Thread(st, "线程一").start();
		new Thread(st, "线程二").start();
		new Thread(st, "线程三").start();
		new Thread(st, "线程四").start();
	}

}

class SynchronizedThread implements Runnable{
	
	private int tickets = 6;
	Object lock = new Object();	// 定义任意一个对象,用作同步代码块的锁
	
	public void run() {
		while(true) {
			try {
				Thread.sleep(500);	// 当前线程休眠500毫秒
			} catch (InterruptedException e) {
				e.printStackTrace();
			}	
			synchronized(lock) {
				if(ticket > 0)
					System.out.println(Thread.currentThread().getName()+"正在售第"+tickets--+"张票");
				else
					break;
			}
		}
	}
}

将有关tickets变量的操作全部都放到同步代码块中。为了保证线程的持续执行,将同步代码块放在死循环中,直到ticket<0时跳出循环。因此,从运行结果可以看出,售出的票不再出现0和负数的情况,这是因为售票的代码实现了同步,之前出现的线程安全问题得以解决。

3、同步方法

同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。

在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:

synchronized 返回值类型 方法名(参数){

}

被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其它线程都会发生阻塞,直到当前线程访问完毕后,其它线程才有机会执行方法。

public class Synchronized_Thread {

	public static void main(String[] args) {
		
	//	创建一个SynchronizedThread对象
		SynchronizedThread st = new SynchronizedThread();

	//	创建并开启线程
		new Thread(st, "线程一").start();
		new Thread(st, "线程二").start();
		new Thread(st, "线程三").start();
		new Thread(st, "线程四").start();
	}

}

class SynchronizedThread implements Runnable{
	
	private int ticket = 6;
	
	public void run() {
		while(true) {
			try {
				Thread.sleep(500);	// 当前线程休眠500毫秒
			} catch (InterruptedException e) {
				e.printStackTrace();
			}	

			if(ticket < 0)
				break;
			else
				sale_Ticket();
			
		}
	}
	
	public synchronized void sale_Ticket() {	//	同步方法sale_Ticket
		if(ticket > 0)
			System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票");
	}
}

 

将售票代码放到售票方法sale_Ticket()中,并用synchronized关键字把sale_Ticket()修饰为同步方法,然后在run()方法中调用该方法。从运行结果可以看出,同样没有出现0号和负数号的票,说明同步方法实现了和同步代码块一样的效果。 

4、死锁问题

 结果可想而知,两个人都吃不到饭。这个例子中的中国人和美国人相当于不同的线程,筷子和刀叉就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。

public class DeadLock_Thread {

	public static void main(String[] args) {
	//	创建两个DeadLockThread对象
		DeadLockThread d1 = new DeadLockThread(true);
		DeadLockThread d2 = new DeadLockThread(false);
		
	//	创建并开启两个线程
		new Thread(d1, "Chinese").start();;
		new Thread(d2,"American").start();;
	}

}

class DeadLockThread implements Runnable{
	
//	定义Object类型的chopsticks、knifeAndFork锁对象
	static Object chopsticks = new Object();
	static Object knifeAndFork = new Object();
	
	private boolean flag;	//	定义boolean类型的变量flag
	public DeadLockThread(boolean flag) {
		this.flag = flag;
	}
	
	public void run() {
		if(flag) {
			while(true) {
				synchronized(chopsticks) { 	// chopsticks锁对象的同步代码块
					System.out.println(Thread.currentThread().getName()+"————if————chopsticks");
				
				synchronized(knifeAndFork) { 	// knifeAndFork锁对象的同步代码块
					System.out.println(Thread.currentThread().getName()+"————if————knifeAndFork");		
				}
			}
		}
	}
		else {
			while(true) {
				synchronized(knifeAndFork) { 	// knifeAndFork锁对象的同步代码块
					System.out.println(Thread.currentThread().getName()+"————else————knifeAndFork");
				
				synchronized(chopsticks) { 		// chopsticks锁对象的同步代码块
					System.out.println(Thread.currentThread().getName()+"————else————chopsticks");
						
				}
			}
		}
		}
}
}

创建了ChineseAmerican两个线程,分别执行run()方法中ifelse代码块中的同步代码块。Chinese线程中拥有chopsticks锁,只有获得knifeAndFork锁才能执行完毕,而American线程拥有knifeAndFork锁,只有获得chopsticks锁才能执行完毕,两个线程都需要对方所占用的锁,但是都无法释放自己所拥有的锁,于是这两个线程都处于挂起状态,从而造成了死锁。

六、多线程通信

1、问题引出

①、假设有两个线程同时去操作同一个存储空间,其中一个线程负责向存储空间中存入数据,另一个线程负责则取出数据。

通过一个案例来实现上述情况,首先定义一个类,在类中使用一个数组来表示存储空间,并提供数据的存取方法,具体实现如例程所示

class Storage{
	//	定义一个数组存储数据
	private int[] array = new int[10];
	
	//	inPos为存入时数组下标,outPos为取出时数组下标
	private int inPos,outPos;
	
	//	定义一个put()方法向数组中存入数据
	public void put(int num) {
		array[inPos] = num;
		System.out.println("在array【" + inPos + "】中放入数据——"+array[inPos]);
		inPos++;	//	存完让下标往后移动
		
		if(inPos == array.length)
			inPos = 0;
		}	
		
	//	定义一个get()方法从数组中取数据
		public void get() {
			int data = array[outPos];
			System.out.println("在array【" + outPos + "】中取出数据——"+array[outPos]);
			outPos++;	//	取完元素后,位置加1
			
			if(outPos == array.length)
				outPos = 0;
	}
}

定义的数组array用来存储数据,put()方法用于向数组存入数据,get()方法用于获取数据。针对数组元素的存取操作都是从第一个元素开始依次进行的,每当操作完数组的最后一个元素时,索引都会被置为0,也就是重新从数组的第一个位置开始存取操作。

 ②、接下来实现两个线程同时访问上例中的共享数据,这两个线程都需要实现Runnable接口,具体如例程所示。

class Input implements Runnable{ // 输入线程类
	private Storage st;    //    用一个类的对象作为另一个类的属性
	private int num;
	
	public Input(Storage st) {
		this.st = st;
	}
	
	public void run() {
		while(true) {
			st.put(num++); // 将num存入数组,每次存完num自增
		}
	}
}

class Output implements Runnable{ // 输出线程类
	private Storage st;
	private int num;
	
	public Output(Storage st) { // 接收一个Storage对象
		this.st = st;
	}
	
	public void run() {
		while(true) {
			st.get();	//	循环取出元素
		}
	}
}

定义了两个类InputOutput,它们都实现了Runnable接口,并且构造方法中都接收一个Storage类型的对象。在Input类的run()方法中使用while循环不停地向存储空间中存入数据num,并在每次存入数据后将num进行自增,从而实现存入自然数1234……的效果。在Output类的run()方法中使用while循环不停地从存储空间中取出数据。

③、最后需要写一个测试程序,开启两个线程分别运行InputOutput类中的代码,如例程所示。

public class Communication_Thread {

	public static void main(String[] args) {
		Storage st = new Storage();	//	创建数据存储类对象
		
	//	创建Input、Output的对象
		Input ip = new Input(st);
		Output op = new Output(st);
		
	// 创建两个线程并开启
		new Thread(ip).start();
		new Thread(op).start();

	}

}

 

可以看到,Input线程依次向数组中存入递增的自然数1234…..,而Output线程依次取出数组中的数据。其中特殊标记的两行运行结果表示在取出数字12后,紧接着取出的是23,这样的现象明显是不对的。我们希望出现的运行结果是依次取出递增的自然数。之所以出现这种现象是因为在Input线程存入数字13时,Output线程并没有及时取出数据,Input线程一直在持续地存入数据,直到将数组放满,又从数组的第一位置开始存入212223…,当Output线程再次取数据时,取出的不再是13而是23

2、问题如何解决

如果想解决上述问题,就需要控制多个线程按照一定的顺序轮流执行,此时需要让线程间进行通信。在Object类中提供了wait()notify()notifyAll()方法用于解决线程间的通信问题,由于Java中所有类都是Object类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。

方法声明

功能描述

void wait()

使当前线程放弃同步锁并进入等待,直到其它线程进入此同步锁,并调用notify()方法,或notifyAll()方法唤醒该线程为止

void notify()

唤醒此同步锁上等待的第一个调用wait()方法的线程
void notifyAll()

唤醒此同步锁上调用wait()方法的所有线程 

class Storage{
	//	定义一个数组存储数据
	private int[] array = new int[10];
	
	//	inPos为存入时数组下标,outPos为取出时数组下标
	private int inPos,outPos;
	private int count;

	//	定义一个put()方法向数组中存入数据
	public synchronized void put(int num) {
	//	如果放入数据等于array的长度,此线程等待	
			try {
				while(count == arrayv.length)
					this.wait();
				
				array[inPos] = num;
				System.out.println("在array【" + inPos + "】中放入数据——"+array[inPos]);
				inPos++;	//	存完让下标往后移动
				
				if(inPos == array.length) // 放完数据后再从array【0】开始
					inPos = 0;
				count++;	//	每放一个数据,count++
				this.notify();
				
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
		
	}	
		
	//	定义一个get()方法从数组中取数据
		public synchronized void get() {
            // 如果count等于0,则此线程等待

			try {
				while(count == 0)
					this.wait();
				
				int data = array[outPos];
				System.out.println("在array【" + outPos + "】中放入数据——"+array[outPos]);
				array[outPos] = 0; // 取出后,当前位置的元素置为0
				outPos++;	//	取完元素后,位置加1
				
				if(outPos == array.length)
					outPos = 0;
				
				count--;    //   每取出一个元素,count-1
				this.notify();
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
	}
}

Storage类是对例程的改写,首先通过使用synchronized关键字将put()方法和get()方法修饰为同步方法,之后每操作一次数据,便调用一次notify()方法唤醒对应同步锁上等待的线程。当存入数据时,如果count的值与array数组的长度相同,说明数组已经添满,此时就需要调用同步锁的wait()方法使存入数据的线程进入等待状态。同理,当取出数据时如果count的值为0,说明数组已被取空,此时就需要调用同步锁的wait()方法,使取出数据的线程进入等待状态。从运行结果可以看出,存入的数据和取出的数据都是依次递增的自然数。

完整代码:

public class Communication_Thread {

	public static void main(String[] args) {
		Storage st = new Storage();	//	创建存储数据类对象
		
	//	创建Input、Output的对象
		Input ip = new Input(st);
		Output op = new Output(st);
		
	// 创建两个线程并开启
		new Thread(ip).start();
		new Thread(op).start();
	}
}

class Input implements Runnable{ // 输入线程类
	private Storage st;
	private int num;
	
	public Input(Storage st) {
		this.st = st;
	}
	
	public void run() {
		while(true) {
			st.put(num++); // 将num存入数组,每次存完num自增
		}
	}
}

class Output implements Runnable{ // 输出线程类
	private Storage st;
	private int num;
	
	public Output(Storage st) { // 接收一个Storage对象
		this.st = st;
	}
	
	public void run() {
		while(true) {
			st.get();	//	循环取出元素
		}
	}
}

class Storage{
	//	定义一个数组存储数据
	private int[] cell = new int[10];
	
	//	inPos为存入时数组下标,outPos为取出时数组下标
	private int inPos,outPos;
	private int count;
	//	定义一个put()方法向数组中存入数据
	public synchronized void put(int num) {
	//	如果放入放入数据等于cell的长度,此线程等待	
			try {
				while(count == cell.length)
					this.wait();
				
				cell[inPos] = num;
				System.out.println("在cell【" + inPos + "】中放入数据——"+cell[inPos]);
				inPos++;	//	存完让下标往后移动
				
				if(inPos == cell.length)
					inPos = 0;
				count++;	//	每放一个数据,count++
				this.notify();
				
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
		
	}	
		
	//	定义一个get()方法从数组中取数据
		public synchronized void get() {
			try {
				while(count == 0)
					this.wait();
				
				int data = cell[outPos];
				System.out.println("在cell【" + outPos + "】中放入数据——"+cell[outPos]);
				cell[outPos] = 0; // 取出后,当前位置的元素置为0
				outPos++;	//	取完元素后,位置加1
				
				if(outPos == cell.length)
					outPos = 0;
				
				count--;
				this.notify();
			} catch (InterruptedException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
	}
}
  • 9
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
第二次作业: 1. 编写点类(Point类),属性成员有x,y,都是double数据类型。需要为Point类编写构造函数。 编写直线类(Line类),需要提供两点确定一条直线的函数功能。 如果两点重合,可以返回异常或者返回null引用来解决这个问题。 直线类的数据成员和函数成员请自行设计。 2. 给定文本文件,文件名称为a.txt,文件内容为一个8行8列的字符矩阵,内容为1和0字符, 请编程计算出该矩阵中水平方向或者垂直方向或者斜线方 向连续1最多的个数。 例如: 11001101 10110101 01010101 11001000 01010101 11001101 00011000 11110000 3. 编写程序求出1万以内的所有素数,并将这些素数输出到一个文本文件中,每行文本只包含一个素数数据。 该文本文件内容要求可以用记事本程序来查看。 4. 编写程序求出1万以内的所有素数,然后再判断这些素数中哪些是由素数拼接而成的。 例如素数23就符合条件,23本身是素数,其由素数2,和素数3拼接(连接)组成。 素数29就不满足条件,2是素数,而9不是素数。素数307不满足条件,不能忽略0. 7907这个素数符合条件,7是素数,907是素数。 需要把符合条件的拼接素数全部输出,并统计个数。 5. 要求从控制台输入英语单词及单词解释两项数据, 把录入的数据追加到文件中。要求提供单词查询功能。 用户输入单词后,从单词库文件中查找,如果存在则输出 该单词的解释。注意,单词不能有重复,如果重复则覆盖替换 以前的解释数据。 6. 通过命令行参数输入一个文件夹的路径名称,然后编写程序找出该文件夹下文件名称重复并且文件大小也一样的文件, 如果没有“重复文件”,则输出“没有重复文件”的提示,如果有,需要输出文件名称,和文件所在的文件夹路径(绝对路径)。 提示,需要遍历该文件夹下所有子文件夹,设计一个文件类,属性包括文件名称,文件路径,文件大小,然后进行“重复” 判断,如果文件重复,则需要记录并输出,有可能有文件名重复,但是文件大小不一样,重复的文件可能不止2个,可能 在不同的子文件夹下有多个文件重复。 7. 霍夫曼编码实现压缩文本文件,见文件huffman.rar. 对文件数据读写等功能已经实现,程序在Q2Resources.zip中。 Q2Resources.zip中的文件禁止修改。请将TextZip.java文件所有未实现的函数按照要求给以实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

来得晚一些也行

观众老爷,请赏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值