一、 实验目的
1. 掌握Java程序设计中的线程同步等技术。
二、实验内容与要求
(1). 运行以下三个程序(每个程序运行10次),并对输出结果给出分析。
(2). 编写Java应用程序实现如下功能:第一个线程生成一个随机数,第二个线程每隔一段时间读取第一个线程生成的随机数,并判断它是否是奇数。要求采用实现Runnable接口和Thread类的构造方法的方式创建线程,而不是通过Thread类的子类的方式。在报告中附上程序截图、完整的运行结果截图和简要文字说明。(20分)
(3). 编写Java应用程序实现如下功能:第一个线程输出数字1-26,第二个线程输出字母A-Z,输出的顺序为1A2B3C...26Z,即每1个数字紧跟着1个字母的方式。要求线程间实现通信。要求采用实现Runnable接口和Thread类的构造方法的方式创建线程,而不是通过Thread类的子类的方式。在报告中附上程序截图、运行结果截图和详细的文字说明。(20分)
(4). 编写Java应用程序实现如下功能:创建工作线程,模拟银行现金账户存款操作。多个线程同时执行存款操作时,如果不使用同步处理,会造成账户余额混乱,要求使用syncrhonized关键字同步代码块,以保证多个线程同时执行存款操作时,银行现金账户存款的有效和一致。要求采用实现Runnable接口和Thread类的构造方法的方式创建线程,而不是通过Thread类的子类的方式。在报告中附上程序截图、运行结果截图和详细的文字说明。(25分)
三、实验过程及结果
1.运行以下三个程序(要求每个程序运行10次),并对输出结果给出分析。
程序1:
package example1;
//PrintChar任务类,实现打印指定次数的某个字符的任务
class PrintChar implements Runnable{ //由Runnable接口实现PrintChar类
private char charToPrint;//待打印的字符charToPrint
private int times; //打印次数
public PrintChar(char c, int t) { //带参构造方法,用于设置成员变量
charToPrint = c;
times = t;
}
//重写Runnable接口中的run()方法,定义新的规则,即多次打印字符
@Override
public void run() {
for(int i = 0; i < times; i++) {
System.out.print(charToPrint);
}
}
}
//PrintNum任务类,实现逐个打印从1到lastNum的任务
class PrintNum implements Runnable{//继承Runnable接口
private int lastNum;
public PrintNum(int n) {
lastNum = n;
}
//重写Runnable接口中的run()方法,定义新的规则,即打印1到lastNum
@Override
public void run() {
for(int i = 1; i <= lastNum; i++) {
System.out.print(" " + i);
}
}
}
public class TaskThreadDemo {
static public void main(String[] args) {
//创建任务,创建可运行对象printA,printB,print100
Runnable printA = new PrintChar('a', 100);
Runnable printB = new PrintChar('b', 100);
Runnable print100 = new PrintNum(100);
//利用以上可运行对象及Thread类创建线程对象thread1,thread2,thread3,将Runnale接口的子类对象作为实际的参数传递给Thread 类的构造函数
Thread thread1 = new Thread(printA);
Thread thread2 = new Thread(printB);
Thread thread3 = new Thread(print100);
//用以上三个线程对象启动线程
thread1.start();
thread2.start();
thread3.start();
}
}
程序运行10次后,每次运行的结果都不一样,thread1,thread2,thread3三个线程在执行各自的任务时不是按程序顺序有规律地运行,而是无规律地交替执行。原因是当有多个线程的时候,线程之间属于竞争状态,交替占用CPU资源,因此运行结果是带有随机性的。
使用多线程的优缺点:
优点: 1、适当的提高程序的执行效率(多个线程同时执行)。
2、适当的提高了资源利用率(CPU、内存等)。
缺点: 1、占用一定的内存空间。
2、线程越多CPU的调度开销越大。
3、程序的复杂度会上升。
程序2:
package example1;
import java.util.concurrent.*;
class PrintChar1 implements Runnable{
private char charToPrint;
private int times;
public PrintChar1(char c, int t) {
charToPrint = c;
times = t;
}
@Override
public void run() {
for(int i = 0; i < times; i++) {
System.out.print(charToPrint);
}
}
}
class PrintNum1 implements Runnable{
private int lastNum;
public PrintNum1(int n) {
lastNum = n;
}
@Override
public void run() {
for(int i = 1; i <= lastNum; i++) {
System.out.print(" " + i);
}
}
}
public class ExecutorDemo {
static public void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);//使用Executor类获取一个ThreadPoolExecutor线程池,创建容器大小为n的线程池,表示正在执行中的线程只有n个,若有超过n个线程,则需排队
//将线程放进池子里执行任务
executor.execute(new PrintChar1('a', 100));
executor.execute(new PrintChar1('b', 100));
executor.execute(new PrintNum1(100));
executor.shutdown();//池子中没有任务时关闭线程,所有任务都执行完
}
}
运行程序10次后,发现每次运行的结果也不尽相同,并不是先把a、b两个字母一次性多次打印完后再输出数字,而是交替随机输出,但交替输出的同时,总是有优先输出a、b,再按顺序输出数字的趋势。
线程池有如下的优势:
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
程序3:
package example1;
import java.util.concurrent.*;
public class AccountWithoutSync {
private static Account account = new Account();//new一个私有的Account类静态对象account
static public void main(String[] args) {
//创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
ExecutorService executor = Executors.newCachedThreadPool();
for(int i = 0; i < 100; i++) {
executor.execute(new AddAPennyTask());//execute执行Runnable类型的AddAPennyTask任务,所属顶层接口为Executor
}
executor.shutdown();//池子中没有任务时关闭线程,所有任务都执行完
while(!executor.isTerminated()) {//isTerminated(),当调用shutdown()方法后,并且所有提交的任务完成后返回为true
}
System.out.println("What is balance? " + account.getBalance());
}
//AddAPennyTask 任务类继承Runnable接口
private static class AddAPennyTask implements Runnable{
public void run() {
account.deposit(1);
}
}
//线程同步,同步方法
private static class Account{
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
int newBalance = balance + amount;
//try-catch代码块抛出异常
try {
Thread.sleep(5);//线程休眠5ms
}
catch(InterruptedException ex) {
}
balance = newBalance;
}
}
}
程序运行10次后,第一次运行结果为“What is balance? 1”,随后多次的运行的结果均为“What is balance? 2”,而后我又增加了创建和启动线程的数目,balance值也相应增加。
2. 编写Java应用程序实现如下功能:第一个线程生成一个随机数,第二个线程每隔一段时间读取第一个线程生成的随机数,并判断它是否是奇数。要求采用实现Runnable接口和Thread类的构造方法的方式创建线程,而不是通过Thread类的子类的方式。
package example2;
class Random{ //定义Random类产生随机数
private int num;
//synchronized修饰getRandomNumber()方法,线程Thread1进到同步代码块后,会将同步代码块中的代码全部锁住,当线程Thread2也进到此处,线程Thread2只能处于等待状态,需要等到线程Thread1执行完同步代码后才能够进入。
public synchronized void getRandomNum(){
for(int i = 0; i < 10; i++){//循环10次,读取10个随机数
num = (int)(Math.random() * 1000);
//sleep()和wait()方法需要抛出中断异常,判断当前进程是否处于interrupted状态
try{
Thread.sleep(105);//线程挂起(休眠)105ms
}
catch(InterruptedException e){}
notify(); //唤醒在此对象监视器等待的单个线程
try{
wait(); //当某个线程获取到锁后,发现当前还不满足执行的条件,就可以调用对象锁的wait方法,进入等待状态。
}
catch(InterruptedException e){}
}
}
//synchronized关键字实现每隔一段时间读取随机数
public synchronized void isOddNumber(){
for(int i = 0; i < 10; i++){
System.out.println("每隔500ms后产生的随机数为" + num);
if(num % 2 != 0){//判断是否为奇数,不能被2整除的为奇数
System.out.println("该数字为奇数");
}
else{
System.out.println("该数字为偶数");
}
以下try-catch代码块为受Thread.sleep() 和 Object.wait() 支持的中断机制,它允许一个线程请求另一个线程停止它正在做的事情。当抛出InterruptedException 异常时,会停止当前进程而提前返回。
try{
Thread.sleep(500);//休眠500ms,即每隔500ms输出信息
}
catch(InterruptedException e){}
notify();
try{
wait();
}
catch(InterruptedException e){}
}
}
}
class Task1 implements Runnable{ //Task1继承Runnable接口
private Random random; //声明Random类对象random作为Task1的数据成员
public Task1(Random r){ //构造方法
random = r;
}
//重写Runnable接口的run方法,方法内调用Random类的getRandomNum()方法,完成获取随机数的任务
public void run(){
random.getRandomNum();
}
}
class Task2 implements Runnable{
private Random random;
public Task2(Random r){
random = r;
}
//重写Runnable接口的run方法,方法内调用Random类的isOddNumber()方法,完成判断是否是奇数的任务
public void run(){
random.isOddNumber();
}
}
public class Example2_1 {
public static void main(String args[]){
Random r = new Random(); //声明Random类对象r
Thread t1,t2;
t1 = new Thread(new Task1(r));
t2 = new Thread(new Task2(r));//创建两个线程t1,t2
t1.start();
t2.start();//调用start()方法执行线程,同时run()方法会被调用
}
}
通过查资料,我还了解到java有这样一种中断策略:
1.提供一种标记方式,用来标记是否需要中断。
2.提供一种检测标记状态方式,检测该标记。
3.对于简单的阻塞状态(可响应中断),通过抛出InterruptedException异常的方式。
4.对于复杂的阻塞状态(不可响应中断),通过上层主动在代码中判断该标记的状态,去决定各种自定义的处理方式。
3. 编写Java应用程序实现如下功能:第一个线程输出数字1-26,第二个线程输出字母A-Z,输出的顺序为1A2B3C...26Z,即每1个数字紧跟着1个字母的方式。要求线程间实现通信。要求采用实现Runnable接口和Thread类的构造方法的方式创建线程,而不是通过Thread类的子类的方式。
package example3;
class Print
{
//使用关键字synchronized实现线程的同步,在执行一个线程时,其他线程会排队 等候,该线程结束。
public synchronized void printNumber(){
for(int i = 1; i <= 26; i++){
System.out.print(i);//输出数字1至26
//try-catch代码块抛出异常
try{
Thread.sleep(50);//线程休眠50ms
}
catch(InterruptedException e){}
notify();
try{
wait();
}
catch(InterruptedException e){}
}
}
public synchronized void printChar(){
for(int i = 0; i < 26; i++){
System.out.print((char)('A'+ i));//输出A至Z 26个字母
//try-catch代码块抛出异常
try{
Thread.sleep(50);//线程休眠50ms
}
catch(InterruptedException e){}
notify(); //唤醒在此对象监视器等待的单个线程
try{
wait();//当某个线程获取到锁后,发现当前还不满足执行的条件,就可以调用对象锁的wait方法,进入等待状态。
}
catch(InterruptedException e){}
}
}
};
class Number implements Runnable
{
private Print p;
public Number(Print pp){
p = pp;
}
public void run(){
p.printNumber();
}
};
class Char implements Runnable
{
private Print p;
public Char(Print pp){
p = pp;
}
public void run(){
p.printChar();
}
};
public class Example3_1 {
public static void main(String args[]){
Print p = new Print();
Thread t1,t2;
t1 = new Thread(new Number(p));
t2 = new Thread(new Char(p));
t1.start();
t2.start();
}
}
4. 编写Java应用程序实现如下功能:创建工作线程,模拟银行现金账户存款操作。多个线程同时执行存款操作时,如果不使用同步处理,会造成账户余额混乱,要求使用syncrhonized关键字同步代码块,以保证多个线程同时执行存款操作时,银行现金账户存款的有效和一致。要求采用实现Runnable接口和Thread类的构造方法的方式创建线程,而不是通过Thread类的子类的方式。
package example4;
import java.util.concurrent.*;
//Account类实现存款并计算余额,输出信息的功能
class Account{
private int balance = 1000;//设置账户初始余额为1000
public synchronized void deposit(String threadname, int b){
balance += b;//计算存款后余额
System.out.println(threadname + " 存款金额为: " + b + " 账户余额为: " + balance);//输出当前线程名称、存款金额及账户余额
notifyAll();//notifyAll()使所有原来在该对象上等待被notify的所有线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
//try-catch代码块抛出异常
try{
Thread.sleep(100);//休眠(挂起)100ms
}
catch(InterruptedException e){}
try{
wait();//wait()暂时停止目前进程的执行,直到有信号来到或子进程结束。
}
catch(InterruptedException e){}
}
};
//Money类继承Runnable接口,完成存款任务
class Money implements Runnable{
private Account acc;//声明Account类对象acc,作为实例变量
private int money;
public Money(Account acc, int money){
this.acc = acc;
this.money = money;
}
public void run(){
Thread t = Thread.currentThread();//Thread.currentThread()可以获取当前线程的引用,此处用于在没有线程对象又需要获得线程信息时通过Thread.currentThread()获取当前代码段所在线程的引用。
acc.deposit(t.getName(), money);//Thread.currentThread().getName()获得当前线程名称
}
}
public class Example4_1 {
public static void main(String args[]){
Account task = new Account();//声明task对象
Thread t1, t2, t3;
t1 = new Thread(new Money(task, 7));
t2 = new Thread(new Money(task, 9));
t3 = new Thread(new Money(task, 11));//用Thread类的构造方法创建线程t1,t2,t3
t1.start();
t2.start();
t3.start();//启动线程
}
}