8.1 线程概述
人们在日常生活中,很多事情都是可以同时进行的。例如,一个人可以一边听音乐,一边打扫房间,可以一边吃饭,一边看电视。在使用计算机时,很多任务也是可以同时进行的。例如,可以一边浏览网页,一边打印文档,还可以一边聊天,一边复制文件等。
计算机能够同时完成多项任务,例如,让浏览器执行0.001秒,让QQ执行0.001秒,这就是多线程技术。计算机中的CPU即使是单核也可以同时运行多个任务,因为操作系统执行多个任务时就是让CPU对多个任务轮流交替执行。Java是支持多线程的语言之一,它内置了对多线程技术的支持,可以使程序同时执行多个执行片段。
8.1.1 进程
在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。目前大部分计算机上安装的都是多任务操作系统,即能够同时执行多个应用程序,最常见的有Windows、Linux、Unix等。在本教材使用的Windows操作系统下,鼠标右键单击任务栏,选择【启动任务管理器】选项可以打开任务管理器面板,在窗口的【进程】选项卡中可以看到当前正在运行的程序,也就是系统所有的进程,如chrome.exe、QQ.exe等。
任务管理器窗口如下图。
在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天。但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。由于CPU运行速度很快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。
8.1.2 线程
每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一条条线索,被称为线程。操作系统中的每一个进程中都至少存在一个线程。例如,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。
在前面章节所接触过的程序中,代码都是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果,这样的程序称作单线程程序。如果希望程序中实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序。所谓的多线程是指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,它们可以并发执行。
所示的多条线程,看似是同时执行的,其实不然,它们和进程一样,也是由CPU轮流执行的,只不过CPU运行速度很快,故而给人同时执行的感觉。
进程与线程是包含关系,但是多任务即可以由多线程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。具体采用哪种方式,要考虑到进程和线程的特点。
和多线程相比,多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
除此之外,多进程也有一些缺点,具体如下:
● 创建进程比创建线程开销大,尤其是在Windows系统上。
● 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
8.2 线程的创建
Java中提供了两种多线程实现方式,一种是继承java.lang包下的Thread类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码;另一种是实现java.lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码。
8.2.1 继承Thread类创建多线程
在学习多线程之前,先看熟悉的单线程程序。
1 public class Example01 {
2 public static void main(String[] args) {
3 MyThread myThread = new MyThread(); // 创建MyThread实例对象
4 myThread.run(); // 调用MyThread类的run()方法
5 while (true) { // 该循环是一个死循环,打印输出语句
6 System.out.println("Main方法在运行");
7 }
8 }
9 }
10 class MyThread {
11 public void run() {
12 while (true) { // 该循环是一个死循环,打印输出语句
13 System.out.println("MyThread类的run()方法在运行");
14 }
15 }
16 }
从运行结果可以看出,程序一直打印“MyThread类的run()方法在运行”,这是因为该程序是一个单线程程序,第4行代码调用MyThread类的run()方法时,遇到第12~14行代码定义的死循环中,循环会一直进行。因此,MyThread类的打印语句将被无限执行,而main()方法中的打印语句无法得到执行。
如果希望程序中两个while循环中的打印语句能够并发执行,就需要实现多线程。为此Java提供了一个线程类Thread,通过继承Thread类,并重写Thread类中的run()方法便可实现多线程。在Thread类中,提供了一个start()方法用于启动新线程,线程启动后,虚拟机会自动调用run()方法,如果子类重写了该方法便会执行子类中的方法。
接下来,通过修改程序中的案例来演示如何通过继承Thread类的方式来实现多线程。
1 public class Example02 {
2 public static void main(String[] args) {
3 MyThread myThread = new MyThread(); // 创建线程MyThread的线程对象
4 myThread.start(); // 开启线程
5 while (true) { // 通过死循环语句打印输出
6 System.out.println("main()方法在运行");
7 }
8 }
9 }
10 class MyThread extends Thread {
11 public void run() {
12 while (true) { // 通过死循环语句打印输出
13 System.out.println("MyThread类的run()方法在运行");
14 }
15 }
16 }
上述代码中,第5~7行代码定义了一个while循环,并在while循环中死循环打印“main()方法在运行”; 第12~14行代码也定义了一个死循环while,并在循环中打印“MyThread类的run()方法在运行”。利用两个while来模拟多线程环境,从运行结果,可以看到两个循环中的语句都有输出,说明该文件实现了多线程。
为了使读者更好地理解单线程和多线程的执行过程,接下来通过一个图例分析一下单线程和多线程的区别。
从上图可以看出,单线程的程序在运行时,会按照代码的调用顺序执行,而在多线程中,main()方法和MyThread类的run()方法却可以同时运行,互不影响,这正是单线程和多线程的区别。
8.2.2 实现Runnable接口创建多线程
通过继承Thread类可以实现多线程,但是这种方式有一定的局限性。因为Java只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,比如学生类Student继承了Person类,就无法通过继承Thread类创建线程。
为了克服这种弊端,Thread类提供了另外一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target)构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口的类中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。
接下来通过一个案例来演示如何通过实现Runnable接口的方式来创建多线程。
1 public class Example03 {
2 public static void main(String[] args) {
3 MyThread myThread = new MyThread(); // 创建MyThread的实例对象
4 Thread thread = new Thread(myThread); // 创建线程对象
5 thread.start(); // 开启线程,执行线程中的run()方法
6 while (true) {
7 System.out.println("main()方法在运行");
8 }
9 }
10 }
11 class MyThread implements Runnable {
12 public void run() { // 线程的代码段,当调用start()方法时,线程从此处开始执行
13 while (true) {
14 System.out.println("MyThread类的run()方法在运行");
15 }
16 }
17 }
上述代码中,第11~17行代码定义的MyThread类实现了Runnable接口,并在第12~16行代码中重写了Runnable接口中的run()方法,在第4行代码中通过Thread类的构造方法将MyThread类的实例对象作为参数传入,在第5行代码中使用start()方法开启MyThread线程,最后在第6~8行代码中定义了一个while死循环。从运行结果可以看出,main()方法和run()方法中的打印语句都执行了,说明文件8-3实现了多线程。
8.2.3 两种实现多线程方式的对比分析
既然直接继承Thread类和实现Runnable接口都能实现多线程,那么这两种实现多线程的方式在实际应用中又有什么区别呢?接下来通过一种应用场景来分析。
假设售票厅有四个窗口可发售某日某次列车的100张车票,这时,100张车票可以看做共享资源,四个售票窗口需要创建四个线程。为了更直观显示窗口的售票情况,可以通过Thread的currentThread()方法得到当前的线程的实例对象,然后调用getName()方法可以获取到线程的名称。
首先通过继承Thread类的方式创建多线程。
1 public class Example04 {
2 public static void main(String[] args) {
3 new TicketWindow().start(); // 创建第一个线程对象TicketWindow并开启
4 new TicketWindow().start(); // 创建第二个线程对象TicketWindow并开启
5 new TicketWindow().start(); // 创建第三个线程对象TicketWindow并开启
6 new TicketWindow().start(); // 创建第四个线程对象TicketWindow并开启
7 }
8 }
9 class TicketWindow extends Thread {
10 private int tickets = 100;
11 public void run() {
12 while (true) { // 通过死循环语句打印语句
13 if (tickets > 0) {
14 Thread th = Thread.currentThread(); // 获取当前线程
15 String th_name = th.getName(); // 获取当前线程的名字
16 System.out.println(th_name + " 正在发售第 " + tickets-- + " 张票 ");
17 }
18 }
19 }
20 }
从运行结果可以看出,每张票都被打印了四次。出现这样现象的原因是四个线程没有共享100张票,而是各自出售了100张票。在程序中创建了四个TicketWindow对象,就等于创建了四个售票程序,每个程序中都有100张票,每个线程在独立地处理各自的资源。需要注意的是,上述程序中每个线程都有自己的名字,主线程默认的名字是“main”,用户创建的第一个线程的名字默认为“Thread-0”,第二个线程的名字默认为“Thread-1”,以此类推。如果希望指定线程的名称,可以通过调用setName(String name)方法为线程设置名称。
由于现实中铁路系统的票资源是共享的,因此上面的运行结果显然不合理。为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行同一个售票对象的售票方法。简单来说就是四个线程运行同一个售票程序,这时就需要用到多线程的第二种实现方式。
接下来,通过实现Runnable接口的方式来实现多线程的创建。修改上述程序,并使用构造方法Thread(Runnable target, String name)在创建线程对象时指定线程的名称。
1 public class Example05 {
2 public static void main(String[] args) {
3 TicketWindow tw = new TicketWindow(); // 创建TicketWindow实例对象tw
4 new Thread(tw, "窗口1").start(); // 创建线程对象并命名为窗口1,开启线程
5 new Thread(tw, "窗口2").start(); // 创建线程对象并命名为窗口2,开启线程
6 new Thread(tw, "窗口3").start(); // 创建线程对象并命名为窗口3,开启线程
7 new Thread(tw, "窗口4").start(); // 创建线程对象并命名为窗口4,开启线程
8 }
9 }
10 class TicketWindow implements Runnable {
11 private int tickets = 100;
12 public void run() {
13 while (true) {
14 if (tickets > 0) {
15 Thread th = Thread.currentThread(); // 获取当前线程
16 String th_name = th.getName(); // 获取当前线程的名字
17 System.out.println(th_name + " 正在发售第 " + tickets-- + " 张票 ");
18 }
19 }
20 }
21 }
上述程序中,第10~21行代码创建了一个TicketWindow对象并实现了Runnable接口,然后在mian方法中创建了四个线程,在每个线程上都去调用这个TicketWindow对象中的run()方法,这样就可以确保四个线程访问的是同一个tickets变量,共享100张车票。
通过继承Thread类可以实现多线程,通过实现Runnable接口也可以实现多线程,实现Runnable接口相对于继承Thread类来说,具有以下优势:
(1)适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想。
(2)可以避免由于Java的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,因此不能使用继承Thread类的方式,只能采用实现Runnable接口的方式。
小提示:JDK8简化了多线程的创建方法,在创建线程时指定线程要调用的方法,格式如下。
Thread t = new Thread(() -> {//main方法代码}});
下面我们通过一个案例来讲解,如下所示。
1 public class Main {
2 public static void main(String[] args) {
3 Thread t = new Thread(() -> {
4 while (true){
5 System.out.println("start new thread!");
6 }
7 });
8 t.start(); // 启动新线程
9 }
10 }
11 class MyThread extends Thread {
12 public void run() {
13 while (true) { // 通过死循环语句打印输出
14 System.out.println("MyThread类的run()方法在运行");
15 }
16 }
17 }
8.3 线程的生命周期及状态转换
在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当run()方法中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。线程整个生命周期可以分为五个阶段,分别是新建状态(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()方法唤醒该线程。
● 当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。
● 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。
线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。
5.死亡状态(Terminated)
当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。
8.4 线程的调度
在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。所谓分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权。Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。
8.4.1 线程的优先级
在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1~10之间的整数来表示,数字越大优先级越高。
除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级,如下表。
Thread**类的静态常量** | 功能描述 |
---|---|
static int MAX_PRIORITY | 表示线程的最高优先级,值为10 |
static int MIN_PRIORITY | 表示线程的最低优先级,值为1 |
static int NORM_PRIORITY | 表示线程的普通优先级,值为5 |
程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如,main线程具有普通优先级。然而线程优先级不是固定不变的,可以通过Thread类的setPriority(int newPriority)方法进行设置,setPriority()方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。
接下来通过一个案例演示不同优先级的两个线程在程序中的运行情况。
1 // 定义类MaxPriority实现Runnable接口
2 class MaxPriority implements Runnable {
3 public void run() {
4 for (int i = 0; i < 10; i++) {
5 System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
6 }
7 }
8 }
9 // 定义类MinPriority实现Runnable接口
10 class MinPriority implements Runnable {
11 public void run() {
12 for (int i = 0; i < 10; i++) {
13 System.out.println(Thread.currentThread().getName() + "正在输出:" + i);
14 }
15 }
16 }
17 public class Example06 {
18 public static void main(String[] args) {
19 // 创建两个线程
20 Thread minPriority = new Thread(new MinPriority(), "优先级较低的线程");
21 Thread maxPriority = new Thread(new MaxPriority(), "优先级较高的线程");
22 minPriority.setPriority(Thread.MIN_PRIORITY); // 设置线程的优先级为1
23 maxPriority.setPriority(Thread.MAX_PRIORITY); // 设置线程的优先级为10
24 // 开启两个线程
25 maxPriority.start();
26 minPriority.start();
27 }
28 }
上述代码中,第2~8行代码定义了MaxPriority类并实现了Runnable接口,第10~16行代码定义实现了Runnable接口的MinPriority类,并在MaxPriority类与MinPriority类中使用for循环打印正在发售的票数,在第22行代码中使用MIN_PRIORITY方法设置minPriority线程的优先级为1,在第23行代码中使用MAX_PRIORITY方法设置manPriority线程优先级为10。从运行结果可以看出,优先级较高的maxPriority线程先运行了,运行完毕后优先级较低的minPriority线程才开始运行。所以优先级越高的线程获取CPU切换时间片的机率越大。
虽然Java中提供了10个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不会和Java中线程优先级一一对应,因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,而只能把线程优先级作为一种提高程序效率的手段。
8.4.2 线程休眠
在前面已经讲过线程的优先级,优先级高的程序会先执行,而优先级低的程序会后执行。如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用sleep(long millis)方法后,在指定时间(单位毫秒)内该线程是不会执行的,这样其他的线程就可以得到执行的机会了。
sleep(long millis)方法声明会抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。
接下来通过一个案例来演示一下sleep(long millis)方法在程序中的使用。
1 public class Example07 {
2 public static void main(String[] args) throws Exception {
3 // 创建一个线程
4 new Thread(new SleepThread()).start();
5 for (int i = 1; i <= 10; i++) {
6 if (i == 5) {
7 Thread.sleep(2000); // 当前线程休眠2秒
8 }
9 System.out.println("主线程正在输出:" + i);
10 Thread.sleep(500); // 当前线程休眠500毫秒
11 }
12 }
13 }
14 // 定义SleepThread类实现Runnable接口
15 class SleepThread implements Runnable {
16 public void run() {
17 for (int i = 1; i <= 10; i++) {
18 if (i == 3) {
19 try {
20 Thread.sleep(2000); // 当前线程休眠2秒
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 }
24 }
25 System.out.println("SleepThread线程正在输出:" + i);
26 try {
27 Thread.sleep(500); // 当前线程休眠500毫秒
28 } catch (Exception e) {
29 e.printStackTrace();
30 }
31 }
32 }
33 }
上述代码中,第15~31行代码定义了一个SleepThread类并实现了Runnable接口。在SleepThread类中重写了run()方法,run()方法中使用for循环打印线程输出语句;第26~30行代码使用sleep()方法设置线程休眠500毫秒,在第18~24行代码中使用if判断当变量i=3时,线程休眠2000毫秒;第4行中使用new关键词创建了一个SleepThread线程并启动,在第5~12行代码中使用for循环打印主线程的输出语句,并在第10行代码使用sleep()方法设置线程休眠500毫秒,在第6~8行代码中使用if判断当变量i=5时,线程休眠2000毫秒。
在主线程与SleepThread类线程中分别调用了Thread的sleep(500)方法让其线程休眠,目的是让一个线程在打印一次后休眠500毫秒,从而使另一个线程获得执行的机会,这样就可以实现两个线程的交替执行。
从运行结果可以看出,主线程输出2后,SleepThread类线程没有交替输出3,而是主线程接着输出了3和4,这说明了当i等于3时,SleepThread类线程进入了休眠等待状态。对于主线程也一样,当i等于5时,主线程会休眠2000毫秒。
sleep()是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。
【案例8-1】 龟兔赛跑
众所周知的“龟兔赛跑”故事,兔子因为太过自信,比赛中途休息而导致乌龟赢得了比赛.本案例要求编写一个程序模拟龟兔赛跑,乌龟的速度为1米/1500毫秒,兔子的速度为5米/500毫秒,等兔子跑到第700米时选择休息10000毫秒,结果乌龟赢得了比赛。
1 package chapter0401;
2 public class race {
3 private int toristDistance;//乌龟跑过的距离
4 private int rabbitDistance;//兔子跑过的距离
5 /**
6 * 乌龟线程内部类
7 */
8 class Torist extends Thread{
9 @Override
10 public void run() {
11 //分析编程代码
12 for(int i=1;i<=800;i++){
13 //判断兔子是否到达终点
14 if(rabbitDistance==800){
15 //当兔子先800的时候 兔子就已经赢了
16 System.out.println("兔子赢得了比赛,此时乌龟才跑了"+toristDistance+"米");
17 break;
18 }else{
19 //乌龟开始跑
20 toristDistance+=1;
21 //判断距离是否是100的倍数
22 if(toristDistance%100==0){
23 try {
24 if(rabbitDistance==700){
25 System.out.println("乌龟跑了"+toristDistance+"米,此时兔子在睡觉");
26 }else{
27 System.out.println("乌龟跑了"+toristDistance+"米,此时兔子跑过段距离
28 是"+rabbitDistance);
29 }
30 Thread.sleep(1500);
31 } catch (InterruptedException e) {
32 e.printStackTrace();
33 }
34 }
35 }
36 }
37 }
38 }
39 /**
40 * 兔子线程内部类
41 */
42 class Rabbit extends Thread{
43 @Override
44 public void run() {
45 //分析编程代码
46 for(int i=1;i<=800/5;i++){
47 //判断兔子是否到达终点
48 if(toristDistance==800){
49 //当兔子先1000的时候 兔子就已经赢了
50 System.out.println("乌龟赢得了比赛,此时兔子跑了"+rabbitDistance+"米");
51 break;
52 }else{
53 //乌龟开始跑
54 rabbitDistance+=5;
55 //判断距离是否是100的倍数
56 if(rabbitDistance%100==0){
57 try {
58 System.out.println("兔子跑了"+rabbitDistance+"米,乌龟跑过了"
59 +toristDistance);
60 if (rabbitDistance==700) {
61 System.out.println("兔子觉得自己怎么能可以赢得比赛,所以选择睡一会");
62 Thread.sleep(10000);
63 }
64 Thread.sleep(500);
65 } catch (InterruptedException e) {
66 e.printStackTrace();
67 }
68 }
69 }
70 }
71 }
72 }
73 //测试
74 public static void main(String[] args) {
75 //1 外部类实例构建
76 race outer=new race();
77 //2兔子 乌龟线程实例构建
78 Rabbit rabbit=outer.new Rabbit();
79 Torist torist=outer.new Torist();
80 //3 依次启动
81 //在现实中 也不可能两个同时跑 这样也是很公平的
82 rabbit.start();
83 torist.start();
84 }
85 }
8.4.3 线程让步
在篮球比赛中,我们经常会看到两队选手互相抢篮球,当某个选手抢到篮球后就可以拍一会,之后他会把篮球让出来,其他选手重新开始抢篮球,这个过程就相当于Java程序中的线程让步。所谓的线程让步是指正在执行的线程,在某些情况下将CPU资源让给其他线程执行。
线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。
接下来通过一个案例来演示yield()方法的使用。
1 // 定义YieldThread类继承Thread类
2 class YieldThread extends Thread {
3 // 定义一个有参的构造方法
4 public YieldThread(String name) {
5 super(name); // 调用父类的构造方法
6 }
7 public void run() {
8 for (int i = 0; i < 6; i++) {
9 System.out.println(Thread.currentThread().getName() + "---" + i);
10 if (i == 3) {
11 System.out.print("线程让步:");
12 Thread.yield(); // 线程运行到此,作出让步
13 }
14 }
15 }
16 }
17 public class Example08 {
18 public static void main(String[] args) {
19 // 创建两个线程
20 Thread t1 = new YieldThread("线程A");
21 Thread t2 = new YieldThread("线程B");
22 // 开启两个线程
23 t1.start();
24 t2.start();
25 }
26 }
在上述代码中,第20~21行代码中创建了两个线程t1和t2,它们的优先级相同。在8~14行代码的for循环中线程在变量i等于3时,调用Thread的yield()方法,使当前线程暂停,这时另一个线程就会获得执行,从运行结果可以看出,当线程B输出3以后,会做出让步,线程A继续执行,同样,线程A输出3后,也会做出让步,线程B继续执行。
8.4.4 线程插队
现实生活中经常能碰到“插队”的情况,同样,在Thread类中也提供了一个join()方法来实现这个“功能”。当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。
接下来通过案例来演示join()方法的使用。
1 public class Example09{
2 public static void main(String[] args) throws Exception {
3 // 创建线程
4 Thread t = new Thread(new EmergencyThread(),"线程一");
5 t.start(); // 开启线程
6 for (int i = 1; i < 6; i++) {
7 System.out.println(Thread.currentThread().getName()+"输入:"+i);
8 if (i == 2) {
9 t.join(); // 调用join()方法
10 }
11 Thread.sleep(500); // 线程休眠500毫秒
12 }
13 }
14 }
15 class EmergencyThread implements Runnable {
16 public void run() {
17 for (int i = 1; i < 6; i++) {
18 System.out.println(Thread.currentThread().getName()+"输入:"+i);
19 try {
20 Thread.sleep(500); // 线程休眠500毫秒
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 }
24 }
25 }
26 }
在上述代码中,在第4行代码中开启了一个线程t,两个线程的循环体中都调用了Thread的sleep(500)方法,以实现两个线程的交替执行。当main线程中的循环变量为2时,调用t线程的join()方法,这时,t线程就会“插队”优先执行。从运行结果可以看出,当main线程输出2以后,线程一就开始执行,直到线程一执行完毕,main线程才继续执行。
【案例8-2】 Svip优先办理服务
在日常工作生活中,无论哪个行业都会设置一些Svip用户,Svip用户具有超级优先权,在办理业务时,Svip用户具有最大的优先级。
本案例要求编写一个模拟Svip优先办理业务的程序,在正常的业务办理中,插入一个Svip用户,优先为Svip用户办理业务。本案例在实现时,可以通过多线程实现。
1 package chapter0402;
2 public class svip {
3 public static void main(String[]args) throws InterruptedException
4 {
5 new Thread(new normal()).start();
6 }
7 }
8 class special extends Thread{
9 public void run()
10 {
11 System.out.println("svip客户开始办理业务");
12 System.out.println("svip客户办理业务的倒计时");
13 for(int i=10;i>=0;i--)
14 {
15 System.out.println(i+"秒");
16 try {
17 Thread.sleep(1000);
18 } catch (InterruptedException e) {
19 e.printStackTrace();
20 }
21 }
22 System.out.println("svip客户办理完毕");
23 }
24 }
25 class normal extends Thread{
26 public void run(){
27 System.out.println("业务办理窗口在正常排队中");
28 System.out.println("此时来了一位svip客户");
29 Thread t=new Thread(new special());
30 //各走各的逻辑错误,再加入join先执行完special,再执行normal剩下的
31 t.start();
32 try {
33 t.join();
34 } catch (InterruptedException e) {
35 e.printStackTrace();
36 }
37 System.out.println("业务办理窗口又恢复正常排队");
38 }
39 }
8.5 多线程同步
多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
8.5.1 线程安全问题
前面讲解的售票案例,极有可能碰到“意外”情况,如一张票被打印多次,或者打印出的票号为0甚至负数。这些“意外”都是由多线程操作共享资源ticket所导致的线程安全问题。接下来对售票案例进行修改,模拟四个窗口出售10张票,并在售票的代码中使用sleep()方法,令每次售票时线程休眠10毫秒。
1 public class Example10 {
2 public static void main(String[] args) {
3 SaleThread saleThread = new SaleThread(); // 创建SaleThread对象
4 // 创建并开启四个线程
5 new Thread(saleThread, "线程一").start();
6 new Thread(saleThread, "线程二").start();
7 new Thread(saleThread, "线程三").start();
8 new Thread(saleThread, "线程四").start();
9 }
10 }
11 // 定义SaleThread类实现Runnable接口
12 class SaleThread implements Runnable {
13 private int tickets = 10; // 10张票
14 public void run() {
15 while (tickets > 0) {
16 try {
17 Thread.sleep(10); // 经过此处的线程休眠10毫秒
18 } catch (InterruptedException e) {
19 e.printStackTrace();
20 }
21 System.out.println(Thread.currentThread().getName() + "---卖出的票"
22 + tickets--);
23 }
24 }
25 }
在上述代码中,第12~25行代码定义了一个SaleThread类并实现了Runnable接口,第13行代码定义了总票数为10,第14~24行代码重写了run()方法,在run()方法中使用while循环售票,并在第17行代码中添加了sleep()方法休眠线程10毫秒,用于模拟售票过程中线程的延迟,最后在第3~8行代码的中创建并开启四个线程。用于模拟四个售票窗口。
在运行结果中,最后打印售出的票出现了0和负数,这种现象是不应该出现的,因为售票程序中只有当票号大于0时才会进行售票。运行结果中之所以出现了负数的票号是因为多线程在售票时出现了安全问题。
出现这样的安全问题的原因是在售票程序的while循环中添加了sleep()方法,由于线程有延迟,当票号减为1时,假设线程1此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程休眠,这时线程二会进行售票,由于此时票号仍为1,因此线程二也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次,结果中出现了0、-1、-2这样的票号。
8.5.2 同步代码块
线程安全问题其实就是由多个线程同时处理共享资源所导致的,要想解决线程安全问题,必须得保证在任何时刻只能有一个线程访问共享资源。具体示例如下:
while (tickets > 0) {
try {
Thread.sleep(10); // 经过此处的线程休眠10毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---卖出的票"+ tickets--);
}
为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字修饰的代码块中,这个代码块被称作同步代码块。使用synchronized关键字创建同步代码块的语法格式如下:
synchronized(lock){ 操作共享资源代码块 }
上面的格式中,lock是一个锁对象,它是同步代码块的关键。当某一个线程执行同步代码块时,其他线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。
接下来将售票的代码放到synchronized区域中。
1 //定义Ticket1类继承Runnable接口
2 class Ticket1 implements Runnable {
3 private int tickets = 10; // 定义变量tickets,并赋值10
4 Object lock = new Object(); // 定义任意一个对象,用作同步代码块的锁
5 public void run() {
6 while (true) {
7 synchronized (lock) { // 定义同步代码块
8 try {
9 Thread.sleep(10); // 经过的线程休眠10毫秒
10 } catch (InterruptedException e) {
11 e.printStackTrace();
12 }
13 if (tickets > 0) {
14 System.out.println(Thread.currentThread().getName()
15 + "---卖出的票" + tickets--);
16 } else { // 如果 tickets小于0,跳出循环
17 break;
18 }
19 }
20 }
21 }
22 }
23public class Example11 {
24 public static void main(String[] args) {
25 Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
26 // 创建并开启四个线程
27 new Thread(ticket, "线程一").start();
28 new Thread(ticket, "线程二").start();
29 new Thread(ticket, "线程三").start();
30 new Thread(ticket, "线程四").start();
31 }
32 }
上述代码中,将有关tickets变量的操作全部都放到同步代码块中。为了保证线程的持续执行,将同步代码块放在死循环中,直到ticket<0时跳出循环。从运行结果可以看出,售出的票不再出现0和负数的情况,这是因为售票的代码实现了同步,之前出现的线程安全问题得以解决。运行结果中并没有出现线程二和线程三售票的语句,出现这样的现象是很正常的,因为线程在获得锁对象时有一定的随机性,在整个程序的运行期间,线程二和线程三始终未获得锁对象,所以未能显示它们的输出结果。
同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。“任意”说的是共享锁对象的类型。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁,每个锁都有自己的标志位,这样线程之间便不能产生同步的效果。
8.5.3 同步方法
同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:
synchronized 返回值类型 方法名([参数1,...]){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。
接下来通过一个案例演示同步方法的使用。
1 // 定义Ticket1类实现Runnable接口
2 class Ticket1 implements Runnable {
3 private int tickets = 10;
4 public void run() {
5 while (true) {
6 saleTicket(); // 调用售票方法
7 if (tickets <= 0) {
8 break;
9 }
10 }
11 }
12 // 定义一个同步方法saleTicket()
13 private synchronized void saleTicket() {
14 if (tickets > 0) {
15 try {
16 Thread.sleep(10); // 经过的线程休眠10毫秒
17 } catch (InterruptedException e) {
18 e.printStackTrace();
19 }
20 System.out.println(Thread.currentThread().getName() + "---卖出的票"
21 + tickets--);
22 }
23 }
24 }
25 public class Example12 {
26 public static void main(String[] args) {
27 Ticket1 ticket = new Ticket1(); // 创建Ticket1对象
28 // 创建并开启四个线程
29 new Thread(ticket,"线程一").start();
30 new Thread(ticket,"线程二").start();
31 new Thread(ticket,"线程三").start();
32 new Thread(ticket,"线程四").start();
33 }
34 }
上述代码中,第12~23行代码将售票代码抽取为售票方法saleTicket(),并用synchronized关键字把saleTicket()修饰为同步方法,然后在第6行代码中调用saleTicket()。从图8-16所示的运行结果可以看出,同样没有出现0号和负数号的票,说明同步方法实现了和同步代码块一样的效果。
读者可能会有这样的疑问:同步代码块的锁是自己定义的任意类型的对象,那么同步方法是否也存在锁?如果有,它的锁是什么呢?答案是肯定的,同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止。从而达到了线程同步的效果。
有时候需要同步的方法是静态方法,静态方法不需要创建对象就可以直接用“类名.方法名()”的方式调用。这时候读者就会有一个疑问,如果不创建对象,静态同步方法的锁就不会是this,那么静态同步方法的锁是什么?Java中静态方法的锁是该方法所在类的class对象,该对象在装载该类时自动创建,该对象可以直接用类名.class的方式获取。
同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行。但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。
8.5.4 死锁问题
有这样一个场景:一个中国人和一个美国人在一起吃饭,美国人拿了中国人的筷子,中国人拿了美国人的刀叉,两个人开始争执不休:
中国人:“你先给我筷子,我再给你刀叉!”
美国人:“你先给我刀叉,我再给你筷子!”
……
结果可想而知,两个人都吃不到饭。这个例子中的中国人和美国人相当于不同的线程,筷子和刀叉就相当于锁。两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。
接下来通过中国人和美国人吃饭的案例来模拟死锁问题。
1 class DeadLockThread implements Runnable {
2 static Object chopsticks = new Object(); // 定义Object类型的chopsticks锁对象
3 static Object knifeAndFork = new Object(); // 定义Object类型的knifeAndFork锁对象
4 private boolean flag; // 定义boolean类型的变量flag
5 DeadLockThread(boolean flag) { // 定义有参的构造方法
6 this.flag = flag;
7 }
8 public void run() {
9 if (flag) {
10 while (true) {
11 synchronized (chopsticks) { // chopsticks锁对象上的同步代码块
12 System.out.println(Thread.currentThread().getName()
13 + "---if---chopsticks");
14 synchronized (knifeAndFork) { // knifeAndFork锁对象上的同步代码块
15 System.out.println(Thread.currentThread().getName()
16 + "---if---knifeAndFork");
17 }
18 }
19 }
20 } else {
22 while (true) {
23 synchronized (knifeAndFork) { // knifeAndFork锁对象上的同步代码块
24 System.out.println(Thread.currentThread().getName()
25 + "---else---knifeAndFork");
26 synchronized (chopsticks) { // chopsticks锁对象上的同步代码块
27 System.out.println(Thread.currentThread().getName()
28 + "---else---chopsticks");
29 }
30 }
31 }
32 }
33 }
34 }
35}
35 public class Example13 {
36 public static void main(String[] args) {
37 // 创建两个DeadLockThread对象
38 DeadLockThread d1 = new DeadLockThread(true);
39 DeadLockThread d2 = new DeadLockThread(false);
40 // 创建并开启两个线程
41 new Thread(d1, "Chinese").start(); // 创建开启线程Chinese
42 new Thread(d2, "American").start(); // 创建开启线程American
43 }
44 }
在上述代码中,第1~33行代码的DeadLockThread类中创建了Chinese和American两个线程,分别执行run()方法中if和else代码块中的同步代码块。第10~19行代码中设置Chinese线程中拥有chopsticks锁,只有获得knifeAndFork锁才能执行完毕;第21 ~30行代码中设置American线程拥有knifeAndFork锁,只有获得chopsticks锁才能执行完毕。两个线程都需要对方所占用的锁,但是都无法释放自己所拥有的锁,于是这两个线程都处于挂起状态,从而造成了死锁。
【案例8-3】 模拟银行存取钱
在银行办理业务时,通常银行会开多个窗口,客户排队等候,窗口办理完业务,会呼叫下一个用户办理业务。本案例要求编写一个程序模拟银行存取钱业务办理。假如有两个用户在存取钱,两个用户分别操作各自的账户,并在控制台打印存取钱的数量以及账户的余额。
(1) 通过任务描述和运行结果可以看出,该任务需要使用多线程的相关知识来是实现。由于两个用户操作各自的账户,因此我们需要创建两个线程完成每个用户的操作。这里我们使用实现Runnable接口的方法来创建线程。
(2) 既然是储户去银行存款,那么可以得出该任务会涉及到三个类,分别是银行类、储户类和测试类。
(3) 定义一个实体类作为账务的集合,包括用户名、登录名、登录密码、钱包、取钱时间和存钱时间等字段。
(4) 在银行类中需要定义一个账户的实体类、一个存钱的方法、一个取钱的方法、查询余额的方法和获取当前用户的方法。获取等前用户方法需要使用synchronized线程锁判断是是哪一位用户,在存钱和取钱的方法中先调用获取用户方法判断操作者,再进行存取钱操作,需要注意的是在进行取钱操作是,需要判断余额是否大于需要取的钱数。
(5) 在测试类中使用for循环调用线程模拟用户存取钱操作。
(1) 创建用户类
定义一个用户的类,根据用户实现多人同时存取钱功能
1 package chapter0403;
2 import java.util.Date;
3 public class User {
4 private String u_name;//用户名
5 private String u_login_name;//登录名 卡的id
6 private String u_login_pwd;//登录密码
7 private String u_wallet;//钱包
8 private Date draw_money_time;//取钱时间
9 private Date save_money_time;//存钱时间
10 public User(){}
11 public User(String u_name, String u_login_name, String u_login_pwd,
12 String u_wallet) {
13 this.u_name = u_name;
14 this.u_login_name = u_login_name;
15 this.u_login_pwd = u_login_pwd;
16 this.u_wallet = u_wallet;
17 }
18 public User(String u_name, String u_login_name, String u_login_pwd,
19 String u_wallet, Date draw_money_time, Date save_money_time) {
20 this.u_name = u_name;
21 this.u_login_name = u_login_name;
22 this.u_login_pwd = u_login_pwd;
23 this.u_wallet = u_wallet;
24 this.draw_money_time = draw_money_time;
25 this.save_money_time = save_money_time;
26 }
27 public String getU_name() {
28 return u_name;
29 }
30 public void setU_name(String u_name) {
31 this.u_name = u_name;
32 }
33 public String getU_login_name() {
34 return u_login_name;
35 }
36 public void setU_login_name(String u_login_name) {
37 this.u_login_name = u_login_name;
38 }
39 public String getU_login_pwd() {
40 return u_login_pwd;
41 }
42 public void setU_login_pwd(String u_login_pwd) {
43 this.u_login_pwd = u_login_pwd;
44 }
45 public String getU_wallet() {
46 return u_wallet;
47 }
48 public void setU_wallet(String u_wallet) {
49 this.u_wallet = u_wallet;
50 }
51 public Date getDraw_money_time() {
52 return draw_money_time;
53 }
54 public void setDraw_money_time(Date draw_money_time) {
55 this.draw_money_time = draw_money_time;
56 }
57 public Date getSave_money_time() {
58 return save_money_time;
59 }
60 public void setSave_money_time(Date save_money_time) {
61 this.save_money_time = save_money_time;
62 }
63 }
(2) 创建银行业务类
定义一个业务类,实现用户的存取钱功能
1 package chapter0403;
2 import java.math.BigDecimal;
3 import java.text.SimpleDateFormat;
4 import java.util.ArrayList;
5 import java.util.Date;
6 import java.util.List;
7 public class Bank {
8 private List<User> userList=new ArrayList<>();
9 public Bank(List<User> userList) {
10 this.userList = userList;
11 }
12 public List<User> getUserList() {
13 return userList;
14 }
15 public void setUserList(List<User> userList) {
16 this.userList = userList;
17 }
18 //存钱
19 public Boolean saveMoney(String card,String pwd,String moneyNum){
20 User u=getUserByCard(card);
21 synchronized (Bank.class) {
22 if (u.getU_login_name().equals(card) &&
23 u.getU_login_pwd().equals(pwd)) {
24 BigDecimal oldData=new BigDecimal(u.getU_wallet());
25 BigDecimal money=new BigDecimal(moneyNum);
26 u.setU_wallet(oldData.add(money).toString());
27 u.setSave_money_time(new Date());
28 System.out.println(Thread.currentThread().getName()+"存钱
29 ---->"+u.getU_name()+"在"+new SimpleDateFormat("yyyy-MM-dd
30 HH:mm:ss").format(u.getSave_money_time())+"存["+moneyNum+"]钱,余额:
31 "+u.getU_wallet());
32 return true;
33 }
34 }
35 System.out.println(getUserByCard(card).getU_name()+"存钱失败");
36 return false;
37 }
38 //取钱
39 public Boolean getMoney(String card,String pwd,String moneyNum){
40 User u=getUserByCard(card);
41 synchronized (Bank.class) {
42 if (u!=null && u.getU_login_name().equals(card) &&
43 u.getU_login_pwd().equals(pwd)) {
44 BigDecimal oldData=new BigDecimal(u.getU_wallet());
45 BigDecimal money=new BigDecimal(moneyNum);
46 if(oldData.compareTo(money)>=0){
47 u.setU_wallet(oldData.subtract(money).toString());
48 u.setDraw_money_time(new Date());
49 System.out.println(Thread.currentThread().getName()+"取钱
50 ---->"+u.getU_name()+"在"+new SimpleDateFormat("yyyy-MM-dd
51 HH:mm:ss").format(u.getDraw_money_time())+"取["+moneyNum+"]钱
52 ,余额:"+u.getU_wallet());
53 return true;
54 }else {
55 System.out.println(getUserByCard(card).getU_name()+"
56 要取["+moneyNum+"]钱,但余额不足");
57 return false;
58 }
59 }
60 }
61 System.out.println(card+"取钱失败");
62 return false;
63 }
64 //查询余额
65 public String balanceEnquiry(String card,String pwd){
66 for(User u :this.userList){
67 if(u.getU_login_name().equals(card)&&
68 u.getU_login_pwd().equals(pwd)){
69 System.out.println(Thread.currentThread().getName()+":"
70 +u.getU_name()+"余额:"+u.getU_wallet());
71 return u.getU_wallet();
72 }
73 }
74 System.out.println(Thread.currentThread().getName()+":"+card+"
75 操作失败");
76 return null;
77 }
78 //获取当前用户
79 public synchronized User getUserByCard(String card){
80 for(User u :this.userList){
81 if(u.getU_login_name().equals(card)){
82 return u;
83 }
84 }
85 return null;
86 }
87 public void delayTime(Integer nim){
88 try {
89 Thread.sleep(nim);
90 } catch (InterruptedException e) {
91 e.printStackTrace();
92 }
93 }
94 }
(3) 创建测试类
定义测试类,再类中创建客户对象,并创建和开启线程执行存取钱操作
1 package chapter0403;
2 import java.util.ArrayList;
3 import java.util.List;
4 public class BankText {
5 public static void main(String[] args) throws Exception {
6 User u = new User("张三", "132466", "123", "100");
7 User uu = new User("李四", "4600882", "123", "0");
8 List<User> list = new ArrayList<>();
9 list.add(u);
10 list.add(uu);
11 Bank atm = new Bank(list);//初始化数据 模拟
12 Thread t = new Thread() {
13 public void run() {
14
15 for (int i = 0; i < 10; i++) {
16 atm.saveMoney("132466", "123", "12");
17 atm.delayTime(250);
18 atm.getMoney("4600882", "123", "14");
19 atm.delayTime(250);
20 }
21
22 }
23 };
24 Thread tt = new Thread() {
25 public void run() {
26
27 for (int i = 0; i < 10; i++) {
28 atm.getMoney("132466", "123", "2");
29 atm.delayTime(250);
30 atm.saveMoney("4600882", "123", "12");
31 atm.delayTime(250);
32 }
33 }
34 };
35 t.start();
36 tt.start();
37 }
38 }
【案例8-4】 模拟12306售票
互联网为人们带来了巨大的方便,越来越多的事情都可以在互联网办理,很大程度上节约了成本。例如,12306售票系统,乘客可以在该系统抢购自己想要的车票。本案例要求编写一个模拟12306售票的系统,要求如下:假设需要抢票十次才可以抢到一张票,会员需要300毫秒抢一次,普通用户需要800毫秒抢一次。
(1) 查看运行结果分析后,定义3个全局变量作为抢票的次数、还需要抢多少次和已经抢票了多久的值。重写run()方法,在run()方法中使用if判断是否是会员。
(2) 在测试类的main方法中调用已经重写的run()方法。
1 package chapter0404;
2 public class ticket{
3 private int num = 0; // 抢票的次数
4 private int count = 10; // 假定需要抢十次才可以抢到票
5 private int numtime=0; //抢票所用时间
6 boolean flag = false;
7 public void run(String svipname,int time) {
8 while (true) {
9 synchronized (this) {
10 // 抢到票跳出
11 if (count <= 0) {
12 break;
13 }
14 num++;
15 count--;
16 numtime=numtime+time;
17 try {
18 Thread.sleep(time);// 模拟网络延时
19 } catch (InterruptedException e) {
20 // TODO Auto-generated catch block
21 e.printStackTrace();
22 }
23 if(time==300) {
24 if(count>0){
25 System.out.println("抢票信息:尊敬的会员 "+svipname+" 这是
26 第"+num+"次帮你抢票,已用时间"+numtime+"毫秒");
27 }else if(count<=0) {
28 System.out.println("抢票信息:尊敬的会员 "+svipname+"这是
29 第"+num+"次帮你抢票已经抢到你所需要的票,共用时间"+numtime+"毫秒");
30 }
31 }else {
32 if(count>0){
33 System.out.println("抢票信息:尊敬的用户 "+svipname+"这是第
34 "+num+"次帮你抢票,已用时间"+numtime+"毫秒");
35 }else if(count<=0) {
36 System.out.println("抢票信息:尊敬的会员 "+svipname+"这是
37 第"+num+"次帮你抢票已经抢到你所需要的票,共用时间"+numtime+"毫秒");
38 }
39 }
40 }
41 }
42 }
43 }
1 package chapter0404;
2 import java.util.Scanner;
3 public class Test {
4 public static void main(String[] args) {
5 ticket s = new ticket();
6 System.out.println("欢迎来到模拟12306售票");
7 Scanner sc=new Scanner(System.in);
8 System.out.println("请输入你的姓名:");
9 String name=sc.next();
10 System.out.println("请输入你需要开通的会员:1.普通2.会员");
11 int i=sc.nextInt();
12 if(i==1){
13 s.run(name,800);
14 }else if(i==2) {
15 s.run(name,300);
16 }
17 }
18 }
【案例8-5】 小朋友就餐
一圆桌前坐着5位小朋友,两个人中间有一只筷子,桌子中央有面条。小朋友边吃边玩,当饿了的时候拿起左右两只筷子吃饭,必须拿到两只筷子才能吃饭。但是,小朋友在吃饭过程中,可能会发生5个小朋友都拿起自己右手边的筷子,这样每个小朋友都因缺少左手边的筷子而没有办法吃饭。本案例要求编写一个程序解决小朋友就餐问题,使每个小朋友都能成功就餐。
(1) 查看运行结果分析后,每个小朋友相当于一个线程,所以先创建一个Philosopher()方法作为小朋友。
(2) 查看运行结果分析后,创建eating()方法作为小朋友吃饭时的线程,创建thinking()方法作为小朋友玩耍是的线程。
(3) 查看运行结果分析后,需要在获取筷子的方法Fork中先定义一个boolean类型的数组,代表5根筷子的使用情况;再使用synchronized线程锁来控制只有左右手的筷子都未被使用时,才允许获取筷子,且必须同时获取左右手筷子。
(4) 查看运行结果分析后,需要在释放左右手筷子的方法putFork中使用synchronized线程锁来释放筷子。
(5) 最后在Test测试类中调用5次以上方法,代表5位小朋友。
1 package chapter04061;
2 /*每个小朋友相当于一个线程*/
3 public class Philosopher extends Thread{
4 private String name;
5 private Fork fork;
6 public Philosopher(String name,Fork fork){
7 super(name);
8 this.name=name;
9 this.fork=fork;
10 }
11 public void run(){
12 while(true){
13 thinking();
14 fork.takeFork();
15 eating();
16 fork.putFork();
17 }
18 }
19 public void eating(){
20 System.out.println("小朋友"+name+"在吃饭");
21 try {
22 sleep(1000);//模拟吃饭,占用一段时间资源
23 } catch (InterruptedException e) {
24 // TODO Auto-generated catch block
25 e.printStackTrace();
26 }
27 }
28 public void thinking(){
29 System.out.println("小朋友"+name+"在玩游戏");
30 try {
31 sleep(1000);//模拟思考
32 } catch (InterruptedException e) {
33 // TODO Auto-generated catch block
34 e.printStackTrace();
35 }
36 }
37 }
38 class Fork{
39 /*5只筷子,初始为都未被用*/
40 private boolean[] used={false,false,false,false,false,false};
41 /*只有当左右手的筷子都未被使用时,才允许获取筷子,且必须同时获取左右手筷子*/
42 public synchronized void takeFork(){
43 String name = Thread.currentThread().getName();
44 int i = Integer.parseInt(name);
45 while(used[i]||used[(i+1)%5]){
46 try {
47 wait();//如果左右手有一只正被使用,等待
48 } catch (InterruptedException e) {
49 // TODO Auto-generated catch block
50 e.printStackTrace();
51 }
52 }
53 used[i ]= true;
54 used[(i+1)%5]=true;
55 }
56 /*必须同时释放左右手的筷子*/
57 public synchronized void putFork(){
58 String name = Thread.currentThread().getName();
59 int i = Integer.parseInt(name);
60 used[i]= false;
61 used[(i+1)%5]=false;
62 notifyAll();//唤醒其他线程
63 }
64 }
1 package chapter04061;
2 public class Test {
3 public static void main(String []args){
4 Fork fork = new Fork();
5 new Philosopher("0",fork).start();
6 new Philosopher("1",fork).start();
7 new Philosopher("2",fork).start();
8 new Philosopher("3",fork).start();
9 new Philosopher("4",fork).start();
10 }
11 }
8.6 本章小结
本章详细介绍了多线程的基础知识,首先从进程与线程两部分讲解了线程的概述;其次介绍了线程创建的两种方式,又对比了两种创建线程方式的优缺点;接着讲解了线程的生命周期与状态转换;然后从线程的优先级、休眠、让步和插队四方面讲解了线程的调度;最后从多线性的线程安全、同步代码块、同步方法和如何解决死锁问题几方面介绍了多线程的同步。通过本章的学习,读者对Java中多线程已经有了初步的认识,熟练掌握好这些知识,对以后的编程开发大有脾益。