线程是一个单独程序流程。多线程是指一个程序可以同时运行多个任务,每个任务由一 个单独的线程来完成。也就是说,多个线程可以同时在一个程序中运行,并且每一个线程完 成不同的任务。程序可以通过控制线程来控制程序的运行,例如线程的等待、休眠、唤起线 程等。
线程是程序运行的基本单位,一个程序中可以同时运行多个线程。如果程序被设置为多 线程,可以提高程序运行的效率和处理速度。Java 中线程的实现通常有两种方法:派生 Thread 类和实现 Runnable 接口。
什么是线程
传统的程序设计语言同一时刻只能执行单任务操作,效率非常低,如果网络程序在接收 数据时发生阻塞,只能等到程序接收数据之后才能继续运行。随着 Internet 的飞速发展,这 种单任务运行的状况越来越不被接受。如果网络接收数据阻塞,后台服务程序就会一直处于 等待状态而不能继续任何操作。这种阻塞情况经常发生,这时的 CPU 资源完全处于闲置状态。 多线程实现后台服务程序可以同时处理多个任务,并不发生阻塞现象。
Java 中有两种方法创建线程: 一种是对 Thread 类进行派生并覆盖 run 方法;另一种是通过实现 runnable 接口创建。
线程的创建
class ThreadDemo1 extends Thread{
//声明 ThreadDemo1 构造方法
ThreadDemo1(){} //声明 ThreadDemo1 带参数的构造方法
ThreadDemo1(String szName) {
super(szName); //调用父类的构造方法
}
//重载 run 函数
public void run() {
for (int count = 1,row = 1; row < 10; row++,count++) //循环计算输出的*数目 {
for (int i = 0; i < count; i++) //循环输出指定的 count 数目的* {
System.out.print('*'); //输出*
}
System.out.println(); //输出换行符 }
}
public static void main(String argv[ ]){
//创建,并初始化 ThreadDemo1 类型对象
ThreadDemo1 td = new ThreadDemo1();
td td.start(); //调用 start()方法执行一个新的线程
}
Thread 创建线程步骤
通常创建一个线程的步骤如下。
(1)创建一个新的线程类,继承 Thread 类并覆盖 Thread 类的 run()方法。
class ThreadType extends Thread{
public void run(){
……
}
}
(2)创建一个线程类的对象,创建方法与一般对象的创建相同,使用关键字 new 完成。
ThreadType tt = new ThreadType();
(3)启动新线程对象,调用 start()方法。
tt.start();
(4)线程自己调用 run()方法。
Runnable 接口创建线程
通常实现 Runnable 线程的步骤如下。
(1)创建一个实现 Runnable 接口的类,并且在这个类中重写 run 方法。
class ThreadType implements Runnable{
public void run(){
……
}
}
(2)使用关键字 new 新建一个 ThreadType 的实例。
Runnable rb = new ThreadType ();
(3)通过 Runnable 的实例创建一个线程对象,在创建线程对象时,调用的构造函数是 new Thread(ThreadType),它用 ThreadType 中实现的 run()方法作为新线程对象的 run()方法。
Thread td = new Thread(rb);
(4)通过调用 ThreadType 对象的 start()方法启动线程运行。
td.start();
线程周期
线程的整个周期由线程创建、可运行状态、不可运 行状态和退出等部分组成,这些状态之间的转化是通过线程提供的一些方法完成的。
一个线程有 4 种状态,任何一个线程都处于这 4 种状态中的一种状态。
创建(new)状态:调用 new 方法产生一个线程对象后、调用 start 方法前所处的状 态。线程对象虽然已经创建,但还没有调用 start 方法启动,因此无法执行。当线程 处于创建状态时,线程对象可以调用 start 方法进入启动状态,也可以调用 stop 方法 进入停止状态。
可运行(runnable)状态:当线程对象执行 start()方法后,线程就转到可运行状态。 进入此状态只是说明线程对象具有了可以运行的条件,但线程并不一定处于运行状 态。因为在单处理器系统中运行多线程程序时,一个时间点只有一个线程运行,系 统通过调度机制实现宏观意义上的运行线程共享处理器。因此一个线程是否在运行, 除了线程必须处于 Runnable 状态之外,还取决于优先级和调度。
不可运行(non Runnable)状态:线程处于不可运行状态是由于线程被挂起或者发 生阻塞,例如对一个线程调用 wait()函数后,它就可能进入阻塞状态;调用线程的 notify 或 notifyAll 方法后它才能再次回到可执行状态。
退出(done)状态:一个线程可以从任何一个状态中调用 stop 方法进入退出状态。 线程一旦进入退出状态就不存在了,不能再返回到其他的状态。除此之外,如果线 程执行完 run 方法,也会自动进入退出状态。
注意:stop()、suspend()和 resume()方法现在已经不提倡使用,这些方法在虚拟机中可能 引起“死锁”现象。suspend()和 resume()方法的替代方法是 wait()和 sleep()。线程的退出通常 采用自然终止的方法,建议不要人工调用 stop()方法。
线程调度
多线程应用程序的每一个线程的重要性和优先级可能不同,例如有多个线程都在等待获 得 CPU 的时间片,那么优先级高的线程就能抢占 CPU 并得以执行;当多个线程交替抢占 CPU 时,优先级高的线程占用的时间应该多。因此,高优先级的线程执行的效率会高些,执行速 度也会快些。 在 Java 中,CPU 的使用通常是抢占式调度模式不需要时间片分配进程。抢占式调度模式 是指许多线程同时处于可运行状态,但只有一个线程正在运行。当线程一直运行直到结束, 或者进入不可运行状态,或者具有更高优先级的线程变为可运行状态,它将会让出 CPU。
public final void setPriority(int newPriority)
设置线程的优先级为 newPriority 。 newPriority 的值必须在 MIN_PRIORITY 到 MAX_PRIORITY 范围内,通常它们的值分别是 1 和 10。目前 Windows 系统只支持 3 个级别的优 先级,它们分别是Thread.MAX_PRIORITY、Thread.MIN_PRIORITY和Thread.NORM_PRIORITY。
public final int getPriority()
获得当前线程的优先级。
线程异步同步
当把一语句块声明为 synchornized,在同一时间,它的访问线程之一才能执行该语句块。 用关键字 synchonized 可将方法声明为同步,格式如下。
class 类名{
public synchonized 类型名称 方法名称(){
......
}
}
对于同步块,synchornized 获取的是参数中的对象锁。
synchornized(obj) { //…………………. }
当线程执行到这里的同步块时,它必须获取 obj 这个对象的锁才能执行同步块;否则线 程只能等待获得锁。必须注意的是 obj 对象的作用范围不同,控制情况不尽相同。示例如下。
public void method() {
Object obj= new Object(); //创建局部 Object 类型对象 obj
synchornized(obj) //同步块 { //…………….. }
}
上面的代码创建了一个局部对象 obj。由于每一个线程执行到 Object obj = new Object() 时都会产生一个 obj 对象,每一个线程都可以获得创建的新的 obj 对象的锁,不会相互影响, 因此这段程序不会起到同步作用。如果同步的是类的属性,情况就不同了。
线程通信
多线程之间可以通过消息通信,以达到相互协作的目的。Java 中线程之间的通信是通过 Object 类中的 wait()、notify()、notifyAll()等几种方法实现的。Java 中每个对象内部不仅有一 个对象锁之外,还有一个线程等待队列,这个队列用于存放所有等待对象锁的线程。
生产者与消费者是一个很好的线程通信的例子。生产者在一个循环中不断生产共享数 据,而消费者则不断地消费生产者生产的共享数据。二者之间的关系可以很清楚地说明,必 须先有生产者生产共享数据,才能有消费者消费共享数据。因此程序必须保证在消费者消费 之前,必须有共享数据,如果没有,消费者必须等待产生新的共享数据。生产者和消费者之 间的数据关系如下。 生产者生产前,如果共享数据没有被消费,则生产者等待;生产者生产后,通知消 费者消费。 消费者消费前,如果共享数据已经被消费完,则消费者等待;消费者消费后,通知 生产者生产。 为了解决生产者和消费者的矛盾,引入了等待/通知(wait/notify)机制。等待通知使用 wait 方法,通知消费生产使用 notifyAll()或者 notify()方法,程序 10.12 将举一个多线程通信即消 费者和生产者的例子。
//文件:程序 10.12 Producer.java 描述: 生产者消费者线程
class Producer extends Thread //实现生产者线程 {
Queue q; //声明队列 q
Producer(Queue q) //生产者构造方法 {
this.q = q; //队列 q 初始化
}
public void run() {
for(int i=1;i<5;i++) //循环添加元素 {
q.put(i); //给队列中添加新的元素
}
}
}
class Consumer extends Thread {
Queue q; //声明队列 q
Consumer(Queue q) //消费者构造方法 {
this.q = q; //队列 q 初始化
}
public void run() {
while(true) //循环消费元素 {
q.get(); //获取队列中的元素
}
}
}
Producer 是一个生产者类,该生产者类提供一个以共享队列作为参数的构造方法,它的 run 方法循环产生新的元素,并将元素添加于共享队列;Consumer 是一个消费者类,该消费 者类提供一个以共享队列作为参数的构造方法,它的 run 方法循环消费元素,并将元素从共 享队列删除。
共享队列类是用于保存生产者生产、消费者消费的共享数据。共享队列有两个域:value (元素的数目)、isEmpty(队列的状态)。共享队列提供了 put 和 get 两个方法。
class Queue {
int value = 0; // 声明,并初始化整数类型数据域 value
boolean isEmpty = true; // 声明,并初始化布尔类型数据域 isEmpty,用于判断队列的状态
// 生产者生产方法
public synchronized void put(int v) {
// 如果共享数据没有被消费,则生产者等待
if (!isEmpty) {
try {
System.out.println("生产者等待");
wait(); // 进入等待状态
} catch (Exception e) // 捕获异常
{
e.printStackTrace(); // 异常信息输出
}
}
value += v; // value 值加 v
isEmpty = false; // isEmpty 赋值为 false
System.out.println("生产者共生产数量:" + v); // 输出字符串信息
notify(); // 生产之后通知消费者消费
}
// 消费者消费的方法
public synchronized int get() {
// 消费者消费前,如果共享数据已经被消费完,则消费者等待
if (isEmpty) {
try {
System.out.println("消费者等待"); // 输出字符串信息
wait(); // 进入等待状态
} catch (Exception e) // 捕获异常
{
e.printStackTrace(); // 异常信息输出
}
}
value--; // value 值-1
if (value < 1) {
isEmpty = true; // isEmpty 赋值 true
}
System.out.println("消费者消费一个,剩余:" + value); // 输出信息
notify(); // 消费者消费后,通知生产者生产
return value; // 返回 value
}
}
线程的常见面试题
1.什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。
2.线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。
3. 如何在Java中实现线程?
在语言层面有两种方式。java.lang.Thread 类的实例就是一个线程但是它需要调用java.lang.Runnable接口来执行,由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程。
4. 用Runnable还是Thread?
这个问题是上题的后续,大家都知道我们可以通过继承Thread类或者调用Runnable接口来实现线程,所以如果你要继承其他类,当然是调用Runnable接口好了。
5 Thread 类中的start() 和 run() 方法有什么区别?
这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
6.什么是线程安全?Vector是一个线程安全类吗?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的
7. 如何在两个线程间共享数据?
你可以通过共享对象来实现这个目的,或者是使用像阻塞队列这样并发的数据结构。或者使用线程通信
8. 什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程
9.现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用join方法实现。
10.在java中wait和sleep方法的不同?
通常会在电话面试中经常被问到的Java线程面试问题。最大的不同是在等待时wait会释放锁,而sleep一直持有锁。Wait通常被用于线程间交互,sleep通常被用于暂停执行。
3.用Java写代码来解决生产者——消费者问题。
参考上面线程通信的线程语句
11.多线程的问题,怎么样创新线程?线程安全怎么理解?怎么避免死锁?
如何创建新线程:
继承Thread类创建线程类
通过Runnable接口创建线程类
通过Callable和Future创建线程
线程安全:
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
常用的避免死锁的方法:
1、有序资源分配法
2、银行家算法