好友
阅读权限10
听众
最后登录1970-1-1
|
Java提供了3种多线程的创建方式: (1)继承javal.ang包中的Thread类,重写 Thread类的run()方法,在run()方法中实现多线程代码。 (2)实现javal.ang.Runnable接口,在run()方法中实现多线程代码。 (3)实现java.util.concurrent.Callable接口,重写call()方法,并使用 Future接口获取call()方法返回的结果。 案例1:运行下面的程序,分析输出结果package cn.edu.ahut.p13;
public class Example01 {
​
public static void main(String[] args) {
​
MyThread01 myThread = new MyThread01(); // 创建MyThread01实例对象
​
myThread.run(); // 调用MyThread01类的run()方法
​
while (true) { // 该循环是一个死循环,打印输出语句
​
System.out.println("Main方法在运行");
​
}
​
}
​
}
​
class MyThread01 {
​
public void run() {
​
while (true) { // 该循环是一个死循环,打印输出语句
​
System.out.println("MyThread类的run()方法在运行");
​
}
​
}
​
}
根据代码分析,结果会是无限循环地打印两行信息:"MyThread类的run()方法在运行""Main方法在运行"这是因为在main方法中,首先创建了一个MyThread01的实例对象myThread,然后调用了myThread.run()方法。但是需要注意的是,myThread.run()并不会创建一个新的线程来执行MyThread01类中的代码,而是直接在当前的主线程上执行。因此,MyThread01类的run()方法会一直循环打印信息"MyThread类的run()方法在运行"。同时,在main方法中,存在一个无限循环的while循环,不会停止执行。在每次循环中,会打印信息"Main方法在运行"。因此,程序会无限循环地交替打印这两行信息,直到程序被手动终止。 Thread类 为了实现多线程,Java提供了一个线程类Thread,通过继承Thread类,并重写Thread类中的run()方法便可实现多线程。在Thread类中提供了一个start()方法用于启动新线程,新线程启动后,JVM会自动调用run()方法,如果子类重写了run()方法便会执行子类中的run()方法。 案例2:Thread类实现多线程package cn.edu.ahut.p13;
​
public class Example02 {
public static void main(String[] args) {
MyThread02 myThread = new MyThread02(); // 创建MyThread02的线程对象
myThread.start(); // 开启线程
while (true) { // 通过死循环语句打印输出
System.out.println("main()方法在运行");
}
}
}
​
class MyThread02 extends Thread {
public void run() {
while (true) { // 通过死循环语句打印输出
System.out.println("MyThread类的run()方法在运行");
​
}
}
}运行截图;file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509181508403.png?lastModify=1688552545 代码中有两个线程:主线程(main)和自定义线程(MyThread02)。主线程和自定义线程都通过死循环语句打印输出。当程序运行时,主线程首先创建并启动了自定义线程对象myThread。然后,主线程进入一个无限循环,在循环内部打印输出"main()方法在运行"。同时,自定义线程对象myThread也进入一个无限循环,在循环内部打印输出"MyThread类的run()方法在运行"。因为两个线程都是无限循环的,所以会一直执行打印输出语句。输出结果将交替出现,没有固定的顺序。可能的输出结果是:main()方法在运行
MyThread类的run()方法在运行
main()方法在运行
MyThread类的run()方法在运行
...由于两个线程都在不断地执行循环体,没有终止条件,所以程序将一直运行下去。这是一个典型的多线程并发执行的例子。Runnable接口 继承Thread类实现多线程的弊端 因为Java只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,比如学生类Student继承了Person类,那么Student类就无法再通过继承Thread类创建线程 Thread类提供了另外一个构造方法Thread(Runnable target),其中参数类型Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target)构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的对象,这样创建的线程将实现了Runnable接口中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。 案例3:通过实现Runnable接口的方式来创建多线程package cn.edu.ahut.p13;
​
class MyThread03 implements Runnable {
public void run() {// 线程的代码段,当调用start()方法时,线程从此处开始执行
while (true) {
System.out.println("MyThread类的run()方法在运行");
}
}
}
​
public class Example03 {
public static void main(String[] args) {
MyThread03 myThread = new MyThread03(); // 创建MyThread03的实例对象
Thread thread = new Thread(myThread); // 创建线程对象
thread.start(); // 开启线程,执行线程中的run()方法
while (true) {
System.out.println("main()方法在运行");
}
}
}该代码创建了一个实现了Runnable接口的线程类MyThread03,并在其中实现了run()方法。在run()方法中,使用一个无限循环来输出"MyThread类的run()方法在运行"。在Example03的main()方法中,首先创建了MyThread03的实例对象myThread,然后通过该实例对象创建了一个Thread对象thread。接下来调用thread的start()方法来启动线程,从而执行线程中的run()方法。在启动线程后,主线程继续执行while循环,输出"main()方法在运行"。由于主线程和MyThread03线程是并行执行的,因此两个循环的输出会交替进行。然而,由于MyThread03的run()方法中使用了一个无限循环,线程不会自动结束。因此,无论是主线程还是MyThread03线程,都会一直输出相应的内容,程序不会停止。因此,运行该代码会不断交替输出"main()方法在运行"和"MyThread类的run()方法在运行",并且程序不会停止,需要手动终止程序。运行结果:file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509181942794.png?lastModify=1688552545 实现Callable接口 通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于run()方法没有返回值,因此无法从新线程中获取返回结果。为了解决这个问题,Java提供了一个Callable接口,来满足这种既能创建新线程又可以有返回值的需求。 Callable接口的方式创建线程步骤 (1)创建一个Callable接口的实现类,同时重写Callable接口的call()方法。 (2)创建Callable接口的实现类对象。 (3)通过FutureTask线程结果处理类的有参构造方法封装Callable接口实现类对象。 (4)调用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例。 (5)调用线程实例的start()方法启动线程。 示例:通过实现Callable接口的方式来创建多线程package cn.edu.ahut.p13;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
​
class MyThread04 implements Callable<Object> {
// 重写Callable接口的call()方法
public Object call() throws Exception {
int i = 0;
while (i++ < 5) {
System.out.println(Thread.currentThread().getName()
+ "的call()方法在运行");
}
return i;
}
}
public class Example04{
public static void main(String[] args) throws InterruptedException, ExecutionException {
​
MyThread04 myThread = new MyThread04(); // 创建Callable接口的实例对象
//使用FutureTask封装MyThread04类
FutureTask<Object> ft1 = new FutureTask<>(myThread);
//使用Thread(Runnable target ,String name)构造方法创建线程对象
Thread thread1 = new Thread(ft1, "thread");
//调用线程对象的start()方法启动线程
thread1.start();
//通过FutureTask对象的方法管理返回值
System.out.println(Thread.currentThread().getName()+ "的返回结果:"+ ft1.get());
int a=0;
while (a++<5) {
System.out.println("main()方法在运行");
}
}
}结果:file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509182619734.png?lastModify=1688552545分析:给定的代码会先执行MyThread04的call()方法,然后才会执行main()方法。call()方法会打印五次线程名,而main()方法会打印五次"main()方法在运行"。由于使用了FutureTask,main()方法会等待call()方法执行完毕并获取其返回结果后才会继续执行四、线程操作的相关方法案例5:演示不同优先级的两个线程的运行情况 步骤一:定义MaxPriority类并实现Runnable接口。在MaxPriority中,使用for循环打印正在发售的票数。代码如下所示:步骤一:定义MaxPriority类并实现Runnable接口。在MaxPriority中,使用for循环打印正在发售的票数。代码如下所示: class MaxPriority implements Runnable {
​
public void run() {
​
for (int i = 0; i < 5; i++) {
​
System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
​
}
​
}
​
} 步骤二:定义MinPriority类并实现Runnable接口。在MinPriority中,使用for循环打印正在发售的票数。代码如下所示: class MinPriority implements Runnable {
​
public void run() {
​
for (int i = 0; i < 5; i++) {
​
System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
​
}
​
}
​
} 步骤三:定义main()方法,创建两个线程,分别设置线程的优先级,然后开启线程。代码如下所示:public static void main(String[] args) {
// 创建两个线程
Thread minPriority = new Thread(new MinPriority(), "优先级较低的线程");
Thread maxPriority = new Thread(new MaxPriority(), "优先级较高的线程");
minPriority.setPriority(Thread.MIN_PRIORITY); // 设置线程的优先级为1
maxPriority.setPriority(Thread.MAX_PRIORITY); // 设置线程的优先级为10
// 开启两个线程
maxPriority.start();
​
minPriority.start();
​
}
​
​file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509183538279.png?lastModify=1688552545运行结果分析:优先级较高的maxPriority线程先运行了,运行完毕后优先级较低的minPriority线程才开始运行。所以优先级越高的线程获取CPU切换时间片的机率越大。案例6:演示sleep()方法在程序中的使用。具体步骤如下 步骤一:定义SleepThread类并实现Runnable接口。重写run()方法,在run()方法中使用for循环打印线程输出语句;使用if判断当变量i=3时,调用sleep()方法线程休眠2000毫秒。代码如下所示: class SleepThread implements Runnable {
public void run() {
for (int i = 1; i <= 8; i++) {
if (i == 3) {
try {
Thread.sleep(2000); // 当前线程休眠2秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("SleepThread线程正在输出:" + i);
try {
Thread.sleep(500); // 当前线程休眠500毫秒
catch (Exception e) {
e.printStackTrace();
}
}
}
} 步骤二:定义main()方法,使用new关键词创建SleepThread线程并启动,使用for循环打印主线程的输出语句,使用if判断当变量i=5时,线程休眠2000毫秒。代码如下所示: public static void main(String[] args) throws Exception {
// 创建一个线程
new Thread(new SleepThread()).start();
for (int i = 1; i <= 8; i++) {
if (i == 5) {
Thread.sleep(2000); // 当前线程休眠2000毫秒
}
System.out.println("主线程正在输出:" + i);
Thread.sleep(500); // 当前线程休眠500毫秒
}
}
​file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509184924501.png?lastModify=1688552545- 主线程和SleepThread线程同时开始执行。
- 当主线程输出到第5次时,它会休眠2秒。 这是因为在主线程的for循环中,当i等于5时,通过调用Thread.sleep(2000)来休眠当前线程(即主线程)2秒。
- 当SleepThread线程输出到第3次时,它会休眠2秒。 这是因为在SleepThread线程的for循环中,当i等于3时,通过调用Thread.sleep(2000)来休眠当前线程(即SleepThread线程)2秒。
- 休眠结束后,主线程和SleepThread线程继续执行直到完成各自的for循环。
- 输出结果中交替显示了主线程和SleepThread线程的输出内容。
案例7:演示join()方法在程序中的使用。具体代码如下所示。 package cn.edu.ahut.p13;
​
public class Anli_7 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new JoinRunnable(), "thread");// 创建线程
thread.start(); // 开启thread线程
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出:" + i);
if (i == 2) {
thread.join(); // 调用join()方法
}
}
}
​
static class JoinRunnable implements Runnable {
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + "输出:" + i);
}
}
}
}file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509185646272.png?lastModify=1688552545- 线程的执行是并发的,thread线程和主线程可以同时执行。
- 当主线程执行到thread.join()方法时,主线程会等待thread线程执行完毕才继续执行。
- 在thread线程执行期间,主线程会阻塞,直到thread线程执行完毕。
- 在本例中,thread线程执行了输出1、2、3的操作,然后主线程继续执行,输出了3、4、5。
通过使用join()方法,主线程可以等待其他线程执行完毕后再继续执行。这对于需要等待其他线程的结果或操作完成后再进行后续处理的情况非常有用。 Thread类除了提供一个无参数的线程插队join()方法外,还提供了带有时间参数的线程插队方法join(long millis)。当执行带有时间参数的join(long millis)进行线程插队时,必须等待插入的线程指定时间过后才会继续执行其他线程。即join()表示在被调用线程执行完成之后才能执行其他线程。join(long millis)则表示被调用线程执行millis毫秒之后,无论是否执行完毕,其他线程都可以和它来争夺CPU资源。 修改上面的main函数,join()方法改为join(long millis)方法,第7行代码修改如下所示: thread.join(3000); // 调用join()方法并将参数设置为3000 修改JoinRunnable类中的第14行替换为如下所示的代码: try {
Thread.currentThread().sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"输出:"+i);
​
​file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509190029003.png?lastModify=1688552545则执行结果可能会有所变化。修改后的代码在每次输出之前使当前线程休眠1500毫秒(1.5秒),然后再输出信息。分析结果及原因:- 主线程输出了1。
- 主线程输出了2,此时调用了thread.join()方法。
- thread线程开始执行,输出了1,然后休眠1.5秒。
- thread线程恢复执行,输出了2,然后休眠1.5秒。
- thread线程恢复执行,输出了3,然后休眠1.5秒。
- thread线程执行完毕,主线程继续执行,输出了3。
- 主线程输出了4。
- 主线程输出了5。
原因分析:- 修改后的代码在每次输出之前使当前线程休眠1500毫秒(1.5秒)。
- thread线程执行期间,会先输出1,然后休眠1.5秒,再输出2,再休眠1.5秒,最后输出3。
- thread线程执行完毕后,主线程继续执行,按照原来的逻辑输出了3、4、5。
注意事项: 在实际编程中,使用Thread.sleep()方法使线程休眠时要注意异常处理,如上述代码中使用了try-catch块来捕获InterruptedException异常并打印异常信息。五、线程同步步骤一:定义SaleThread类并实现Runnable接口;定义私有int类型变量tickets,表示总票数,初始值为10;重写run()方法,在run()方法中使用while循环售票;调用sleep()方法使线程休眠300毫秒,用于模拟售票过程中线程的延迟;具体代码如下所示:步骤二:定义main()方法,创建并开启四个线程,用于模拟四个售票窗口。具体代码如下所示:package cn.edu.ahut.p13;
​
class SaleThread implements Runnable {
private int tickets = 10; // tickets表示总票数:10张票
public void run() {
while (tickets > 0) {
try {
Thread.sleep(300); //线程休眠300毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---卖出的票"+ tickets--);
​
}
​
}
​
}
public class Anli_8 {
public static void main(String[] args) {
​
SaleThread saleThread = new SaleThread(); // 创建SaleThread对象
​
// 创建并开启四个线程
​
new Thread(saleThread, "线程一").start();
​
new Thread(saleThread, "线程二").start();
​
new Thread(saleThread, "线程三").start();
​
new Thread(saleThread, "线程四").start();
​
}
}运行结果分析: 在CPU主频不是特别快的情况下,可能打印售出的票出现了0和负数,这种现象是不应该出现的,原因是在售票程序的while循环中调用了sleep()方法,出现了线程延迟。假设当票号减为1时,线程1获取了CPU执行权,出售1号票,对票号进行判断后,进入while循环,在售票之前调用sleep()方法进入休眠;线程1休眠之后,线程2获取了CPU执行权,会进行售票,由于此时票号仍为1,所以线程2也会进入循环。同理,线程3和线程4也会进入while循环。休眠结束后,四个线程都会继续售票,这样就相当于将票号减了四次,因此结果会出现0和负数这样的票号。file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509190650726.png?lastModify=1688552545案例: 对上面的售票案例进行修改,将用于售票的代码放在synchronized同步代码块中。使用同步代码块解决线程安全的问题。具体如下。 步骤一:修改上节中的案例,将有关tickets变量的操作全部都放到同步代码块中。为了保证线程持续执行,将同步代码块放在死循环中,直到ticket<0时跳出循环。代码如下:package cn.edu.ahut.p13;
class Ticket1 implements Runnable {
private int tickets = 10; // 定义变量tickets,并赋值10
Object lock = new Object(); // 定义任意一个对象,用作同步代码块的锁
public void run() {
while (true) {
synchronized (lock) { // 定义同步代码块
try {
Thread.sleep(300); // 经过的线程休眠300毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
​
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()+ "---卖出的票" + tickets--);
} else { // 如果 tickets小于0,跳出循环
break;
}
}
}
}
} 步骤二:定义main()方法,创建并开启四个线程,用于模拟四个售票窗口。代码如下所示:public class Anli_9 {
public static void main(String[] args) {
​
Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
// 创建并开启四个线程
new Thread(ticket, "线程一").start();
new Thread(ticket, "线程二").start();
new Thread(ticket, "线程三").start();
new Thread(ticket, "线程四").start();
}
​
}
​
​运行结果:file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509190956005.png?lastModify=1688552545 案例2:修改售票案例,在Ticket1类中定义一个同步方法saleTicket(),用于实现售票功能。使用同步方法解决线程安全的问题。具体如下。 步骤一:将售票代码抽取为售票方法saleTicket(),并用synchronized关键字修饰saleTicket()方法。代码如下所示:class Ticket1 implements Runnable {
private synchronized void saleTicket() {
if (tickets > 0) {
try {
Thread.sleep(300); // 经过的线程休眠300毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---卖出的票"+ tickets--);
}
}
​ 步骤二:定义Ticket1类实现Runnable接口,在重写的run()方法中的while循环中调用抽取的售票方法saleTicket()。省略定义main()方法的步骤,参考上例即可。具体代码如下所示:​
// 定义Ticket1类实现Runnable接口
private int tickets = 10;
public void run() {
while (true) {
saleTicket(); // 调用售票方法
if (tickets <= 0) {
break;
}
}
}
} 运行程序,分析输出结果file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509191658306.png?lastModify=1688552545 死锁案例1:分析下面可能出现死锁的案例程序package cn.edu.ahut.p13;
class A{
synchronized void first(B b){
String name = Thread.currentThread().getName();
System.out.println(name+" entered A.first() ");
try{
Thread.sleep(1000);
}catch(Exception e){
System.out.println(e.getMessage());
}
System.out.println(name+" trying to call B.last()");
b.last();
}
synchronized void last(){
System.out.println("inside A.last");
}
}
class B{
synchronized void first(A a){
String name = Thread.currentThread().getName();
System.out.println(name+" entered B.first()");
​
try{
Thread.sleep(1000);
}
​
catch(Exception e){
System.out.println(e.getMessage());
}
​
System.out.println(name+" trying to call A.last()");
​
a.last();
}
synchronized void last(){
System.out.println("inside B.last");
}
}
class Example implements Runnable{
A a = new A();
B b = new B();
Example(){
Thread.currentThread().setName("main_thread");
new Thread(this).start();
a.first(b);
System.out.println("back in main_thread");
}
public void run(){
Thread.currentThread().setName("user_thread");
b.first(a);
System.out.println("back in user_threaad");
}
}
public class Anli_10 {
public static void main(String[] args) {
new Example();
}
}
​结果file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509192137016.png?lastModify=1688552545它创建了两个类 A 和 B,每个类中都有两个同步方法 first() 和 last()。在 Example 类中创建了两个对象 a 和 b,并启动了一个新线程。主线程通过调用 a.first(b) 方法开始执行,然后进入了 A 类的同步代码块。在同步代码块中,主线程休眠1秒后尝试调用 b.last() 方法,但需要获取 B 对象的锁。此时,新线程已经获取了 B 对象的锁,并进入了 B 类的同步代码块。新线程也休眠1秒后尝试调用 a.last() 方法,但需要获取 A 对象的锁。由于主线程已经释放了 A 对象的锁,新线程获取到锁并执行了 a.last() 方法,然后返回到主线程继续执行。最终,输出结果将显示两个线程交替执行 first() 和 last() 方法的过程。死锁案例2:模拟中国人、美国人吃饭的程序 步骤一:定义DeadLockThread类实现Runnable接口,创建Chinese和American两个线程,分别执行run()方法中if和else代码块中的同步代码块。if中设置Chinese线程中拥有chopsticks锁,只有当Chinese线程获得knifeAndFork锁后才能执行完毕;else中设置American线程拥有knifeAndFork锁,只有获得American线程获得chopsticks锁后才能执行完毕。代码如下所示:package cn.edu.ahut.p13;
class DeadLockThread implements Runnable {
static Object chopsticks = new Object(); // 定义Object类型的chopsticks锁对象
static Object knifeAndFork = new Object(); // 定义Object类型的knifeAndFork锁对象
private boolean flag; // 定义boolean类型的变量flag
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");
}
}
}
}
}
}步骤二:定义main()方法,创建两个DeadLockThread对象,然后创建并开启两个线程。代码如下所示:public class Anli_11 {
public static void main(String[] args) {
// 创建两个DeadLockThread对象
DeadLockThread d1 = new DeadLockThread(true);
DeadLockThread d2 = new DeadLockThread(false);
// 创建并开启两个线程
new Thread(d1, "Chinese").start(); // 创建开启线程Chinese
new Thread(d2, "American").start(); // 创建开启线程American
}
}运行结果:file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509192946852.png?lastModify=1688552545 重入锁案例1:修改卖票程序的线程,使用重入锁模拟多个窗口售票。具体如下。 步骤一:定义一个同步方法saleTicket(),在saleTicket()方法中,调用lock()方法为票数加锁;调用lock()方法为票数释放锁。代码如下所示: private void saleTicket() {
//调用lock()方法为票数加锁
reentrantLock.lock();
if (tickets > 0) {
​
try {
Thread.sleep(300); // 经过的线程休眠300毫秒
​
} catch (InterruptedException e) {
e.printStackTrace();
}
​
System.out.println(Thread.currentThread().getName() + "---卖出的票"+ tickets--);
​
}
​
//调用lock()方法为票数释放锁
reentrantLock.unlock();
}
}
​ 步骤二:定义ReentrantLockTest类实现Runnable接口,创建ReentrantLock类的对象reentrantLock;调用售票方法saleTicket()。代码如下所示:package cn.edu.ahut.p13;
import java.util.concurrent.locks.ReentrantLock;
class ReentrantLockTest implements Runnable {
private int tickets = 10;
private ReentrantLock reentrantLock = new ReentrantLock();
public void run() {
while (true) {
saleTicket(); // 调用售票方法
if (tickets <= 0) {
break;
}
}
}
​步骤三:定义main()方法,创建ReentrantLockTest对象,然后创建并开启四个线程,模拟多个窗口售票。代码如下所示:package cn.edu.ahut.p13;
​
​
public class Anli_12 {
​
public static void main(String[] args) {
// 创建ReentrantLockTest对象
ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
// 创建并开启四个线程
new Thread(reentrantLockTest, "线程一").start();
new Thread(reentrantLockTest, "线程二").start();
new Thread(reentrantLockTest, "线程三").start();
new Thread(reentrantLockTest, "线程四").start();
​
}
}结果分析:运行结果与使用synchronized的结果是一致的。如果需要在此基础上添加多把锁,只需要调用lock()方法即可。注意:使用重入锁是加了几把锁就必须释放几把锁,如果不释放会导致线程处于阻塞状态。 比较:同步锁会导致线程对象阻塞,重入锁可通过tryLock()方法尝试加锁,不会导致线程阻塞。运行结果file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509193720532.png?lastModify=1688552545 六、线程交互案例1:多线程协作完成计算任务package cn.edu.ahut.p13;
class ThreadB extends Thread{
​
int total=0;
​
public void run(){
synchronized (this) {
for (int i = 0; i < 101; i++)
total += i;
//计算完成了,唤醒在此对象监视器上等待的单个线程。
notify();
}
}
}
​
public class Anli_13 {
public static void main(String[] args){
ThreadB b = new ThreadB();
//启动计算线程
b.start();
//线程main拥有b对象上的锁。
synchronized(b){
try {
String name=Thread.currentThread().getName();
System.out.println(name+"等待对象b完成计算……");
//当前线程main等待
b.wait();
System.out.println("对象b计算完成,结果="+b.total);
System.out.println(name+"继续运行!");
​
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
​ 代码分析: 定义方法getElement(),设定类型通配符的下限为Number,设定后调用该方法时传入的类型实参必须是Number类型或者是Number的父类; 创建3个Collection对象,分别传入了Number类型、Object类型和Integer类型的类型实参; 分别将创建的3个Collection对象作为参数调用getElement()方法,由于list3创建时传入的Integer类型不是Number的父类,出现编译异常。运行结果:file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509193927990.png?lastModify=1688552545 案例2:生产者-消费者问题 生产者-消费者问题是最著名的进程同步问题。描述了一组生产者向一组消费者提供产品,它们共享一个有界缓冲区,生产者向其中投放产品,消费者从中取得产品。生产者-消费者问题是许多相互合作进程的一种抽象。只要缓冲区未满,生产者就可以把产品送入缓冲区,类似地,只要缓冲区未空,消费者便可以从缓冲区中取走物品并消耗它。禁止生产者向满的缓冲区输送产品,也禁止消费者从空的缓冲区中提取物品。 要点:不同类型的多线程对象交互协作package cn.edu.ahut.p13;
//食品类定义
class Food {
private String producer;
public Food(String producer) {
super();
this.producer = producer;
}
@Override
public String toString() {
return producer;
}
}
//缓冲区类用于存放生产的食品,由于生产者与消费者都要使用缓冲区资源,因此将缓冲区读写方法定义成同步方法
class BufferArea {
private int index = 0;
private Food[] foodArray = new Food[10];
public synchronized void produce(int id, Food food) {
System.out.println("缓冲区产品个数:" + index + " 个");
while (index == foodArray.length) {
System.out.println("缓冲区满,开始等等。。。。。。");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
foodArray[index] = food;
index++;
System.out.println(food);
}
public synchronized Food take(int id) {
System.out.println("缓冲区产品个数:" + index + " 个");
while (index == 0) {
System.out.println("缓冲区空,开始等等。。。。。。");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
index--;
System.out.println("第" + id + "个消费者消费了 -->" + foodArray[index]);
return foodArray[index];
}
}
//生产者线程,工作时生产食品,如果缓冲区已满需要等待消费者的通知才能继续生产
class Producer implements Runnable {
private BufferArea bufferArea;
private int id;
public Producer(int id,BufferArea bufferArea) {
super();
this.id = id;
this.bufferArea = bufferArea;
}
@Override
public void run() {
boolean isdone=false;
int count = 0;
while(!isdone){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Food food = new Food("第" + id + "个生产者生产的食品,FOODID:"+count);
count ++;
bufferArea.produce(id,food);
if(count == 100) isdone=true;
}
}
}
//消费者线程,如果缓冲区是空的,需要等待生产者线程通知
class Customer implements Runnable {
private BufferArea bufferArea;
private int id;
public Customer(int id,BufferArea bufferArea) {
super();
this.id = id;
this.bufferArea = bufferArea;
}
@Override
public void run() {
boolean isdone=false;
int count = 0;
while(!isdone){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
bufferArea.take(id);
count ++;
if(count == 100) isdone=true;
}
}
}
//main函数,运行结果请大家分析
public class Anli_14 {
public static void main(String[] args) {
BufferArea bufferArea = new BufferArea();
// 两个厨师两个客户
Producer p1 = new Producer(1, bufferArea);
Producer p2 = new Producer(2, bufferArea);
Customer c1 = new Customer(1, bufferArea);
Customer c2 = new Customer(2, bufferArea);
new Thread(p1).start();
new Thread(p2).start();
new Thread(c1).start();
new Thread(c2).start();
}
}file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509194710681.png?lastModify=1688552545 练习:线程交互示例--团结就是力量 应用场景:多名学生值日,教室里共有500套桌椅需要擦净,值日生没有进行明确分工,能者多劳,团结一致,最终将教室里的桌椅擦得干干净净。 程序要求:编写多线程程序,为每名学生创建单独的线程,以桌椅为操作对象,要尽量做到分工合理,并记录每人负责的桌椅数,擦完500套桌椅后即退出应用程序。 要点:多线程定义、线程同步、同类型的多线程对象协作 注意:本例共享桌椅数据的多个线程类型是相同的,应该使用同步块还是同步方法实现交互?代码如下:package cn.edu.ahut.p13;
​
class DeskChairCleaner implements Runnable {
private static int totalDesksChairs = 500; // 教室里的桌椅总数
private static int cleanedDesksChairs = 0; // 已清洁的桌椅数
​
private int cleanedByMe = 0; // 当前线程已清洁的桌椅数
​
@Override
public void run() {
while (true) {
synchronized (DeskChairCleaner.class) {
if (totalDesksChairs == cleanedDesksChairs) {
break; // 所有桌椅都已清洁完毕,退出循环
}
​
if (cleanedByMe < totalDesksChairs) {
cleanedByMe++; // 当前线程清洁一套桌椅
cleanedDesksChairs++; // 已清洁的桌椅数增加
System.out.println(Thread.currentThread().getName() + " 已清洁 " + cleanedByMe + " 套桌椅");
}
}
}
}
}
public class Exercise {
public static void main(String[] args) {
final int numStudents = 5; // 学生数
​
Thread[] threads = new Thread[numStudents];
​
for (int i = 0; i < numStudents; i++) {
threads = new Thread(new DeskChairCleaner(), "学生" + (i + 1));
}
​
for (Thread thread : threads) {
thread.start();
}
}
​
}
​通过创建DeskChairCleaner类实现Runnable接口,并在run()方法中使用同步块来控制对共享数据的访问。每个线程通过获取DeskChairCleaner.class对象的锁来确保同一时间只有一个线程在执行同步块内的代码。在同步块中,首先检查是否所有桌椅都已清洁完毕,如果是,则退出循环。然后,每个线程尝试清洁一套桌椅,将已清洁的桌椅数量增加。在每次清洁完一套桌椅后,会输出当前线程的名称和已清洁的桌椅数量。主程序中创建了numStudents个线程,并启动它们。每个线程都是以学生的名称命名,通过new Thread(new DeskChairCleaner(), "学生" + (i + 1))创建。每个线程在运行时,都会执行DeskChairCleaner类中的run()方法。在run()方法中,通过同步块的方式对共享数据进行访问和更新。每个线程尝试清洁一套桌椅,并将已清洁的桌椅数量进行增加。当所有桌椅都被清洁完毕后,即totalDesksChairs等于cleanedDesksChairs,线程会退出循环,任务完成。这样,通过使用同步块的方式,多个线程可以安全地共享数据并协同工作,每个线程可以独立地记录自己负责的桌椅数量,并在任务完成后退出。file://C:/Users/jay-qiao/AppData/Roaming/Typora/typora-user-images/image-20230509195105336.png?lastModify=1688552545
|
免费评分
-
查看全部评分
|
发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。 |
|
|
|
|