实验简介
多线程技术是任何一门编程语言所必备的基本特性,同时也是目前的应用程序普遍使用的一种提升执行效率的方法。本实验主要为大家讲解在Java中,如何应用多线程技术,进行并发用户的处理,以及在多线程应用过程中的一些特殊注意事项。
实验目的
1.理解Java的原生多线程技术的操作方法。
2.理解Java中多线程的生命周期。
3.理解多线程的同步,中断,优先级,等待,唤醒,合并,死锁等情况。
4.对Java当中的一些线程不安全的情况有所认知。
实验流程
1.进程与线程。
目前主流的操作系统基本上是多任务操作系统,允许计算机在同一时刻同时运行多个程序。操作系统中独立执行的程序被称为进程。例如在Windows操作系统中可以同时运行Microsoft Word和Microsoft PowerPoint两个程序,这两个程序就是两个不同的进程。进程占有系统资源,拥有资源使用权。我们可以在Windows的任务管理器中看到一个一个的进程运行情况,如下图所示:

从上述任务管理器中我们可以看到,一个应用程序对应一个进程名称和进程ID号,也可以观察到这个应用程序对应的CPU,内在,磁盘和网络消耗情况。对于许多操作系统来说,一个进程中可包括一个或多个线程,线程是进程的实体,一个线程就是进程的一条执行路径,拥有多个线程的进程可以同时完成多种功能。例如在Microsoft Word中,我们可以边编辑文档边执行打印功能。多线程的并发执行通常是指在逻辑上的同时,并非物理上的同时。多线程程序设计中的各个线程彼此独立,这样各个线程可以乱序或交叉执行。
线程又被称为轻量级进程。较之于进程,线程相互之间的通信代价小,这使得多任务的线程比多任务的进程需要的开销要小。同一个进程中的多个线程共享内存等资源,这些线程可以访问相同的对象和变量,在使用进程时要注意对其它进程的影响。我们可以在任务管理器中打开资源监控器,看到每一个进程对应的线程数量及资源消耗情况,如下图所示:

2.线程的生命周期。
一个线程从诞生到死亡整个生存期内存在9个基本状态:
(1)新建(Born) :新建的线程处于新建状态,new的时候即为新建。
(2)就绪(Ready) :在创建线程后,它将处于就绪状态,等待start()方法被调用。
(3)运行(Running):线程在开始执行时进入运行状态,当run()方法执行时
(4)睡眠(Sleeping):线程的执行可通过使用sleep()方法来暂时中止。睡眠后,线程进入就绪状态。
(5)等待(Waiting):如果调用了wait()方法,线程将处于等待状态。
(6)挂起(Suspended):在临时停止或中断线程的执行时,线程就处于挂起状态。
(7)恢复(Resume):在挂起的线程被恢复执行时,可以说它已被恢复
(8)阻塞(Blocked):在线程等待一个事件时(例如输入/输出操作),就称其处于阻塞状态。
(9)死亡(Dead):在run()方法已完成执行或其stop()方法被调用之后,线程就处于死亡状态。

3.利用Thread类实现线程。
Thread类位于java.lang包中,实现java.lang.Runnable接口,继承java.lang.Object类。Thread是Java的进程管理的关键类,其中定义了大量的关于进程管理的操作方法,其常用方法如下:
方法定义 | 方法说明 |
public void run() | 线程体所在位置,由Java虚拟机调用,通常被覆写的方法 |
public void start() | 使该线程进入就绪态:Java虚拟机调用该线程的run()方法 |
public void interrupt() | 中断进程,但事实是线程会继续执行 |
public static void yield() | 暂停当前正在执行的线程对象,并执行其它线程 |
public static void sleep(long millis) | 在millis毫秒数内让 当前正在执行的线程休眠(暂停执行) |
public static void sleep(long millis,int nanos) | 在millis毫秒数加nanos纳秒数内让当前正在执行的线程休眠(暂停执行): |
public final void wait() | 导致当前线程等待,直到其它线 程调用此对象的notify()方法或notifyAll() 方法 |
public final void wait(long timeout) | 导致当前线程等待, 直到其它线程调用此对象的 notify() 方法或notifyAll() 方法,或者其它某个线程中断当前线程,或者超过timeout毫秒数 |
public final void wait(long timeout, int nanos) | 致当前线 程等待,直到其它线程调用此对象的notify() 方法或 notifyAll() 方法,或者其它某个线程 中断当前线程,或者超过timeout毫秒数加nanos纳秒数 |
public final void notify() | 唤醒在此对象监视器上等待的所有线程 |
public final void join() | 等待该线程终止 |
public final void join(long millis) | 等待该线程终止的时 间最长为 millis毫秒 |
public final void join(long millis, int nanos) | 等待该线程 终止的时间最长为 millis 毫秒加 nanos纳秒。 |
在 Java 中实现多线程的方法之一是创建一个类并让该类继承 Thread 类并覆盖类中的run()方法(该方法没有任何参数),具体代码如下:
package com.woniuxy.thread;
public class MyThread extends Thread { public static void main(String[] args) { // 实例化当前类,并调用start()方法 MyThread mt = new MyThread(); mt.start(); } // 重写父类的run方法,线程执行时会自动调用该方法 public void run() { System.out.println("当前线程名称为:" + this.getName()); } } |
当我们运行上述方法后,会在控制台打印一次当前线程的名称。这相当于新建了一个线程,那么如何新建多个线程呢?
其实方法也非常简单,那就是直接利用循环即可完成多线程的创建,请看如下代码:
package com.woniuxy.thread;
public class MyThread extends Thread { public static void main(String[] args) { // 实例化当前类,并调用start()方法 for (int i=0; i<10; i++) { MyThread mt = new MyThread(); mt.start(); } } // 重写父类的run方法,线程执行时会自动调用该方法 public void run() { System.out.println("当前线程名称为:" + this.getName()); } } |
上述代码中我们为MyThread类创建了10个线程来运行,运行结果的输出内容如下:
当前线程名称为:Thread-0 当前线程名称为:Thread-2 当前线程名称为:Thread-1 当前线程名称为:Thread-3 当前线程名称为:Thread-5 当前线程名称为:Thread-4 当前线程名称为:Thread-6 当前线程名称为:Thread-8 当前线程名称为:Thread-7 当前线程名称为:Thread-9 |
我们可以看到,线程的运行并非按顺序执行的,而是具备随机性。上述的线程状态经历了新建,就绪,运行和死亡四种状态,这也是一个线程常见的几种最基本的状态。此处我们需要注意的是,循环创建的MyThread实例是10个,对应于新建了10个线程,相应的每一个线程只运行1次,就叫多线程单循环,与我们平时的一个实例循环运行10次是完全不一样的效果。比如如果我们将上述代码的循环放在run()方法中,代码如下:
public class MyThread extends Thread { public static void main(String[] args) { MyThread mt = new MyThread(); mt.start(); }
public void run() { for (int i=0; i<10; i++) { System.out.println("当前线程名称为:" + this.getName()); } } } |
则最终的运行效果会变为一个线程的运行情况,线程名称只有一个:
当前线程名称为:Thread-0 当前线程名称为:Thread-0 当前线程名称为:Thread-0 当前线程名称为:Thread-0 当前线程名称为:Thread-0 当前线程名称为:Thread-0 当前线程名称为:Thread-0 当前线程名称为:Thread-0 当前线程名称为:Thread-0 当前线程名称为:Thread-0 |
4.利用Runnable接口实现线程。
除了上述通过继承Thread类构造线程对象外,Java还提供了通过Runnable接口获得线程对象的方法。Runnable接口位于java.lang包中,只有一个run()方法。实现Runnable 接口的类通过实例化一个对象并将这个对象作为运行目标,就可以运行线程而无需创建Thread的子类。Runnable的接口与Thread超类的使用大致相同,以MyThread为例,其代码可以这样修改:
package com.woniuxy.thread;
public class MyRunnable implements Runnable { public static void main(String[] args) { MyRunnable mr = new MyRunnable(); for (int i=0; i<10; i++) { Thread t = new Thread(mr); t.start(); } } @Override public void run() { // 由于MyRunnable没有父类,我们不能直接使用this.getName方法 // 来获取当前线程的名称,而应该使用更加通用的方式获取 System.out.println("当前线程名称为:" + Thread.currentThread().getName()); } } |
上述代码的输入与MyThread类的输出是一模一样的。当然,通常情况下,我们并不建议在自己的类当中并发自己的类实例作为线程,这样比较容易搞混淆,技术上虽然是没有任何问题的。本书对于一些简单的知识点,将采取内部类的方式来执行,这样也会比较方便大家区分。
5.主线程与子线程。
通常情况下,任何一段Java代码都是通过main()方法来开始运行的,这个时候JVM或开启一个主线程用于执行main()方法及后续被调用的程序。
所以,我们将运行Java代码的这一个线程称之为主线程,而由主线程在执行代码过程中开启的线程称之为子线程。
但是这里需要特别注意的是,主线程一旦产生了子线程,那么子线程的命运便交由CPU来处理了,主线程与子线程再无关系,各自运行各自的任务,运行完成该结束就结束。但是对于进程而言,必须要等到所有的线程都运行结束后,进程才能正常结束。我们来看看下面一段代码:
package com.woniuxy.thread;
public class MainThread { public static void main(String[] args) { for (int i=1; i<=10; i++) { // 通过SubThread构造参数指定线程名称 SubThread sub = new SubThread("子线程-" + i); Thread t = new Thread(sub); t.start(); } // 主线程main方法正常运行,但是并不一定在最后运行 System.out.println("这是主线程在运行:" + Thread.currentThread().getName()); } }
${comment['nickname']} ${comment['createtime']}
${comment.content}
${reply.nickname} 回复 ${comment.nickname}
${reply.createtime}
回复内容:${reply.content}
|