实验目的
掌握Java程序设计中的线程同步等技术。
实验内容
1
(1). 运行以下三个程序(每个程序运行10次),并对输出结果给出分析。在报告中附上程序截图和详细的文字说明。
程序1:
package problem1_1;
class PrintChar implements Runnable{
private char charToPrint;
private int times;
public PrintChar(char c,int t){
charToPrint=c;
times=t;
}
@Override
public void run(){
for(int i=0;i<times;i++){
System.out.print(charToPrint);
}
}
}
class PrintNum implements Runnable{
private int lastNum;
public PrintNum(int n){
lastNum=n;
}
@Override
public void run(){
for(int i=1;i<=lastNum;i++){
System.out.print(" "+i);
}
}
}
public class TaskThreadDemo {
public static void main(String[] args) {
Runnable printA=new PrintChar('a',100);
Runnable printB=new PrintChar('b',100);
Runnable print100=new PrintNum(100);
Thread thread1=new Thread(printA);
Thread thread2=new Thread(printB);
Thread thread3=new Thread(print100);
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果截图:
运行10次,会发现每一次由于线程同步技术,会导致每一次输出都不一样。但是a、b的输出线程总能在数字线程前完成。当调大ab输出量时,比如次数调整为500时,会出现三个线程同时工作的场景,如下图所示。
程序2:
package problem1_2;
import java.util.concurrent.*;
class PrintChar implements Runnable{
private char charToPrint;
private int times;
public PrintChar(char c,int t){
charToPrint=c;
times=t;
}
@Override
public void run(){
for(int i=0;i<times;i++){
System.out.print(charToPrint);
}
}
}
class PrintNum implements Runnable{
private int lastNum;
public PrintNum(int n){
lastNum=n;
}
@Override
public void run(){
for(int i=1;i<=lastNum;i++){
System.out.print(" "+i);
}
}
}
public class ExecutorDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(new PrintChar('a',500));
executor.execute(new PrintChar('b',500));
executor.execute(new PrintNum(500));
executor.shutdown();
}
}
运行结果截图:
在main()方法中,首先通过Executors.newFixedThreadPool(3)创建一个固定大小为3的线程池,其中线程池中最多同时运行3个线程。然后,通过executor.execute()方法将三个任务提交给线程池执行。分别是两个PrintChar任务,一个打印字符’a’,一个打印字符’b’,每个任务打印100次;还有一个PrintNum任务,打印从1到100的数值。最后,调用executor.shutdown()方法关闭线程池。通过使用线程池和多个任务,可以并发地执行这些任务,提高程序的执行效率。运行效果与程序1类似,当调大ab输出量时,比如将次数调整为500时,会出现三个线程同时工作的场景,如下图所示。
程序3:
package problem1_3;
import java.util.concurrent.*;
public class AccountWithoutSync {
private static Account account = new Account();
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executor.execute(new AddAPennyTask());
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("What is balance?" + account.getBalance());
}
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 {
Thread.sleep(5);
} catch (InterruptedException ex) {
}
balance = newBalance;
}
}
}
运行结果:
(十次运行结果):
首先,在 AccountWithoutSync 类中,有两个静态内部类:AddAPennyTask 类和Account 类。AddAPennyTask 类实现了 Runnable 接口,表示一个任务。在 run 方法中,调用了 account.deposit(1) 方法,向账户存入 1 个单位的金额。Account 类表示一个账户对象,它包含了一个 balance 变量用于保存账户的余额。getBalance 方法用于获取账户的余额,deposit 方法用于存入指定金额到账户。在 deposit 方法中,将原来的余额与存入的金额相加,然后通过 Thread.sleep(5) 方法模拟一段耗时操作,最后更新账户的余额。
在该类中,有一个静态变量 account,表示账户对象。在 main 方法中,创建了一个 ExecutorService 线程池对象 executor,通过 Executors.newCachedThreadPool() 方法创建一个可以根据需要自动调整线程数量的线程池。然后,使用一个循环向线程池提交了 100 个任务,每个任务都是一个 AddAPennyTask 对象,该任务会调用 account.deposit(1) 方法将 1 个单位的金额存入账户。接下来,调用 executor.shutdown() 方法关闭线程池的提交功能。之后,通过一个循环判断线程池是否已经终止执行,等待所有任务执行完成。最后,输出账户的余额,即调用 account.getBalance() 方法获取账户的余额,并打印输出。
结果分析:由于没有使用线程同步机制,所以多个线程可能会同时访问和修改账户的余额,导致并发问题和不确定的结果,所以输出结果是不确定的
2
编写Java应用程序实现如下功能:第一个线程输出数字1,2,…,12,第二个线程输出英文单词数字和月份One January, Two February, …, Twelve December,输出的顺序和格式为1OneJanuary2TwoFebruary…12TwelveDecember,即每1个数字紧跟着2个英文单词的方式。要求线程间实现通信。要求采用实现Runnable接口和Thread类的构造方法的方式创建线程,而不是通过Thread类的子类的方式。在报告中附上程序截图、运行结果截图和详细的文字说明。
package problem2;
public class Main{
private static final Object lock = new Object();
private static int cnt = 1;
private static boolean number_huihe= true;
public static class print_num implements Runnable {
@Override
public void run() {
synchronized (lock) {
try {
while (cnt<= 12) {
if (number_huihe) {
System.out.print(cnt);
number_huihe = false;
lock.notify();
} else {
lock.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static class print_word implements Runnable {
private String[] numbers = {"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve"};
private String[] months = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
@Override
public void run() {
synchronized (lock) {
try {
while (cnt <= 12) {
if (!number_huihe) {
System.out.print(numbers[cnt-1] + months[cnt-1]);
number_huihe = true;
cnt++;
lock.notify();
} else {
lock.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Thread numThread = new Thread(new print_num());
Thread wordThread = new Thread(new print_word());
numThread.start();
wordThread.start();
}
}
运行结果截图:
文字说明:
代码中定义了两个类 print_num 和 print_word,实现了 Runnable 接口。在 print_num 线程的 run 方法中,使用 synchronized (lock) 锁定了共享的 lock 对象。然后,在循环中判断 cnt 的值是否小于等于 12,如果是,则打印当前的数字 cnt,将 number_huihe (该变量用于确定当前是否是打印阿拉伯数字)置为 false,并通过 lock.notify() 唤醒print_word 线程,然后进入等待状态,然后执行print_word 线程。在 print_word 线程的 run 方法中,同样使用 synchronized (lock) 锁定了共享的 lock 对象。然后,在循环中判断 cnt 的值是否小于等于 12,如果是,则打印当前数字对应的英文单词和月份,并将 number_huihe 置为 true,递增 cnt,表示已经进行了一轮。然后通过 lock.notify() 唤醒 print_num 线程,并自己进入等待状态,以此不断循环,直至任务结束。在 main 方法中启动了两个线程,由此完成轮流打印数字和英文数字和英文月份。
3
编写Java应用程序实现如下功能:创建工作线程,模拟银行现金账户取款操作。多个线程同时执行取款操作时,如果不使用同步处理,会造成账户余额混乱,要求使用syncrhonized关键字同步代码块,以保证多个线程同时执行取款操作时,银行现金账户取款的有效和一致。要求采用实现Runnable接口和Thread类的构造方法的方式创建线程,而不是通过Thread类的子类的方式。在报告中附上程序截图、运行结果截图和详细的文字说明。
package problem3;
import java.util.Random;
class Account implements Runnable{
private int balance;
public Account(int b) {
this.balance = b;
}
public int getBalance() {
return balance;
}
public synchronized void withdraw(int amount) {
System.out.println(Thread.currentThread().getName()+"正在取款");
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println("当前余额为"+balance+"元");
System.out.println("取款金额为"+amount+"元");
System.out.println("剩余金额为"+balance+"元");
System.out.println("---------------------");
if(balance<10){
System.out.println("剩余金额为"+balance+"元,"+"余额过少,暂停取款");
System.exit(0);
}
}else{
System.out.println("取款金额为"+amount+"元");
System.out.println("当前余额为"+balance+"元");
System.out.println("取款金额大于当前余额,取款失败");
System.out.println("---------------------");
}
}
@Override
public void run(){
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Random r=new Random();
int w=r.nextInt(66);
withdraw(w);
}
}
}
public class Main {
public static void main(String[] args) {
Account account=new Account(666);
Thread thread1=new Thread(account);
Thread thread2=new Thread(account);
Thread thread3=new Thread(account);
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果截图:
文字说明:
代码定义了一个Account类,实现了Runnable接口,getBalance函数用于返回余额,withdraw函数用于取款并打印取款时的信息,用synchronized关键字确保了同一时刻只有一个线程可以进行取款操作,run() 方法是接口 Runnable 的实现方法,在其中使用一个无限循环模拟不断进行取款操作的场景。在每次循环中,线程会休眠 1 秒钟,然后生成一个随机的取款金额,并调用 withdraw() 方法进行取款操作。在main方法里头,创建了一个Account对象,并启动三个线程,模拟多个线程并发地对同一个账户进行取款的场景,当余额低于10元的时候,终止程序。
4
有一座东西向的桥,只能容纳一个人,桥的东边有20个人(记为E1,E2,…,E20)和桥的西边有20个人(记为W1,W2,…,W20),编写Java应用程序让这些人到达对岸,每个人用一个线程表示,桥为共享资源,在过桥的过程中输出谁正在过桥(不同人之间用逗号隔开)。运行10次,分别统计东边和西边的20人先到达对岸的次数。要求采用实现Runnable接口和Thread类的构造方法的方式创建线程,而不是通过Thread类的子类的方式。在报告中附上程序截图、运行结果截图和详细的文字说明。
package problem4;
class Person implements Runnable {
private String name;
private int num;
public Person(String name, int num) {
this.name = name;
this.num = num;
}
@Override
public void run() {
System.out.println(name + "正在过桥......");
if(name.startsWith("E")){
Main.east_cnt++;
}
else{
Main.west_cnt++;
}
if (Main.east_cnt == 20) {
Main.east_first_cnt++;
Main.east_cnt = 0;
Main.west_cnt = -1;
} else if (Main.west_cnt == 20) {
Main.west_first_cnt++;
Main.west_cnt = 0;
Main.east_cnt = -1;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static int east_cnt = 0;
public static int west_cnt = 0;
public static int east_first_cnt = 0;
public static int west_first_cnt = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("第" + (i + 1) + "次过桥:");
Person[] east = new Person[20];
Person[] west = new Person[20];
Thread[] threadWest = new Thread[20];
Thread[] threadEast = new Thread[20];
for (int j = 0; j < 20; j++) {
int index = j + 1;
east[j] = new Person("E" + index, j + index);
west[j] = new Person("W" + index, j + index);
threadEast[j] = new Thread(east[j]);
threadWest[j] = new Thread(west[j]);
}
for (int j = 0; j < 20; j++) {
threadEast[j].start();
threadWest[j].start();
}
for (int j = 0; j < 20; j++) {
try {
threadEast[j].join();
threadWest[j].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("----------------------");
}
System.out.println("东边先到达对岸的次数:" + east_first_cnt);
System.out.println("西边先到达对岸的次数:" + west_first_cnt);
}
}
运行结果截图:
文字说明:
该代码首先定义了Person类,实现了Runnable接口,具有名字,编号这两个属性,然后
实现了run()方法,用于模拟人过桥的行为。在run()方法中,首先打印出人的名字表示正在过桥,然后根据人的名字判断是来自东边还是西边,根据条件对Main类中的静态计数变量进行更新,通过Thread.sleep(100)模拟人过桥的时间。在Main类里面,具有四个静态变量,分别是east_cnt,west_cnt,east_first_cnt,west_first_cnt,人在过桥时,east_cnt,west_cnt用于记录当前过桥的人是东边还是西边的,当east_cnt或west_cnt先达到20时,更新east_first_cnt或west_first_cnt,这两个变量用来统计东边和西边的20人先到达对岸的次数。main方法中,模拟了十次过桥,在每次循环中,创建两个长度为 20 的 Person 数组,分别表示东边和西边的人,然后创建两个长度为 20 的 Thread 数组,分别用于管理东边和西边人的线程,然后初始化人数组和线程数组,并启动所有线程。接着使用循环等待所有线程执行完毕,打印分隔符,表示一次过桥结束。循环结束后,打印东边和西边先到达对岸的次数。