Java基础(九) 多线程

来源:互联网 发布:免费交换机网管软件 编辑:程序博客网 时间:2024/06/16 03:28

Java基础(九)多线程

  • 线程综述
  • 线程的创建和启动
  • 线程状态
  • 线程调度
  • 线程同步
  • 线程通信
  • 线程死锁
  • 线程让步
  • 调度线程的优先级
  • interrupt、isInterrupted、interrupted三个方法

1. 线程综述

1.1 什么是线程

进程是指运行中的程序,每一个进程都有自己独立的内存空间。一个应用程序可以启动多个进程。比如,对于IE浏览器来说,每打开一个IE窗口,就启动了一个进程。同样来说,每执行一次java.exe程序,就是启动了一个独立的Java虚拟机进程。该进程的任务是解析并且执行Java程序。

线程是进程的一个执行流程。一个进程可以有多个线程组成。即在一个进程内执行不同的线程,他们分别执行不同的任务,当一个进程内有多个线程同时执行的时候称为并行计算。

1.2 Java内的线程

在Java虚拟进程中,执行代码的任务是由线程来完成的。每当启动一个Java虚拟机进程的时候,Java虚拟机都会创建一个主线程。该线程会从程序的入口main()方法入口进入。

计算机中机器指令的真正的执行者是CPU,线程必须获得CPU的使用权,才能执行一条指令。Java内大致可以把线程分成2种:前台线程(执行线程)和后台线程(守护线程)。

1.3 进程和线程

线程是进程的子集单位,一个进程可以包括多个线程。

线程和进程的最大的区别是:
* 每个进程都需要操作系统为其分配独立的内存地址空间。
* 同一个进程内的线程在同一个内存地址空间内工作,这些线程可能会共享同一块的空间和系统资源。就好比,大家可以共享,免费查看百度文库上的文件一样。

2. 线程的创建和启动

前面提及的Java虚拟机进程的主线程,它从main()方法的主线程开始运行。此外,用户还可以创建自己的线程。它和主线程并发执行。创建线程主要有2种方式:

  • 扩展java.lang.Thread类
  • 实现Runnable接口

2.1. 扩展java.lang.Thread类

Thread类就是线程类,它最主要的方法是:

  • run() 包含线程启动的时候所需要执行的代码;
  • start() 用于启动线程

用户的线程类只需要继承Thread类即可,需要覆盖Thread类内的run()方法。在Thread类内的run()方法定义如下所示:public void run()//不抛异常 子类亦不能抛出异常。异常使用try--catch进行处理

  • 主线程和用户线程并发运行

    • Thread类的run()方法是专门被自身的线程执行的,主线程不能调用Thread类的run()方法,否则违背了Thread类提供run()方法的初衷;

    • 每个线程都有默认名字,主线程默认的名字为main, 用户创建的第一个线程的默认名字为”Thread-0”, 第二个线程的默认名字为”Thread-1”, 依引类推。Thread类的setName()方法可以显示地设置线程的名字;

    Thread thread = Thread.currentThread(); //返回当前正在执行这行代码的 线程引用;String name = thread.getName();        //获得线程名字;
  • 多个线程可以共享一个类的对象实例(和线程的本质有关)

  • 不要覆盖Thread类的start()方法

创建了一个线程对象,线程并不自动开始运行,必须调用它自己的start()方法。对于以下代码:

//Machine为Thread类的子类 Machine machine = new Machine(); machine.start(); //当用new语句创建Machine对象时,仅仅在堆区内出现一个Machine对象,此时Machine线程并没有被启动。当主线程执行Machine对象的start()方法时,该方法会启动Machine线程。也就是new的时候,只是分配了一段空间,但是程序并没有启动。需要启动资源,就要调用start()方法。
  • 一个线程只能被执行一次
//Machine为Thread类的子类Machine machine = new Machine();machine.start();machine.start();         //抛出IllegalThreadStateException异常

2.2. 实现Runnable接口

Java允许多继承,一旦继承了Thread类,就不能再继承其他的类。为了解决这样的一个问题,Java提供了java.lang.Runnable接口。它的run()方法和Thread类的方法一样,示例如下public void run()

具体操作的示例如下:// A 实现了Runnable接口,但是想启动它必须依托于Thread类A a = new A();Thread thread = new Thread(a); //详细可以查看Thread类的构造函数thread.start();

3. 线程状态

线程在生命周期中有着不同的状态,这个和操作系统,也就是CS内线程和进程的设计模式有着很多相似的状况。(操作系统的状态模式,之后再补。)

在Java内,线程大概包括如下的5个状态:

  • 新建状态(New)

New语句创建出来后,线程对象处于新建状态。和Java类中的对象一样,仅仅在堆中被分配了内存空间。

  • 就绪状态(Runnable)

当一个Thread类对象被创建出来之后,再调用其start()方法,该线程处于就绪状态。

处于此状态的线程处于可运行池中,等待CPU的使用权。

  • 运行状态(Running)

处于就绪状态的线程,获取到CPU的使用权后,就会进入运行状态。在单CPU的情况下,应当同时只有一个线程进行运行状态;在多核CPU的情况下,会出现多个线程并发执行的情况。

  • 阻塞状态(Blocked)

线程因为某些原因放弃对于CPU的操作权,暂停运行状态。当线程处于阻塞状态,Java虚拟机不会给线程分配CPU,直到线程进入就需状态,它才有机会到达运行状态。

常见的阻塞状态主要有如下三类:    1. 位于对象等待池的线程状态(Block in object's wait pool): 线程执行了wait()方法后的状态。    2. 位于对象锁池中的线程状态(Block in object's lock pool): 当线程处于某种运行状态,试图获得某个对象的同步锁的时候,该对象的同步锁被其他线程暂用,Java虚拟机会讲这个线程放到这个对象的对象锁池中。    3. 其他阻塞状态(Otherwise Blocked): 当线程执行了sleep()方法,或者调用了其他线程的join()方法,或者进行了IO操作后请求后,线程会进入这个状态。    (PS: 当一个线程执行System.in.read()方法时,就会发出一个I/O请求,该线程放弃cpu, 进入阻塞状态,直到I/O处理完毕,该线程才会恢复运行。)
  • 死亡状态(Dead)

当线程退出run()方法之后,线程进入死亡状态,该线程的生命周期结束。线程有可能是正常执行完run()方法退出,也有可能是异常导致的不正常的退出。但是,不论何种方式的退出,都不会对其他的线程造成影响。

4. 线程调度

对于单核CPU的计算机而言,在任意时刻只能执行一条机器指令。每个线程只有获取到CPU的指令,才能够执行。所谓多线程并发执行,从宏观的角度来看,就是多个线程依次获取CPU的运行资源,执行各自的任务。

但是,在线程池中,存在多个等待CPU资源的指令。所以,Java虚拟机一项任务就是调度线程,即为线程分配CPU的使用权。主要有2种模型:

  • 分时调度模型: 所有的线程轮流获取CPU的使用权,并且平均分配CPU的使用时间。
  • 抢占式调度模型: 优先让线程池中优先级较高的线程获取CPU资源(优先级较高)。如果一个线程池内的所有线程优先级都是一样的话,那么随机选择一个线程,使其占用CPU。处于可运行状态的线程会一直占用CPU,直到线程结束,释放CPU资源。Java便采用的是这种方式。

一个线程会因为以下的原因放弃CPU:

  • Java线程让当前线程放弃CPU,转到就绪状态;
  • 当前线程因为某些原因进入阻塞状态;
  • 线程运行结束。

线程的调度不是跨平台的,他有时与操作系统息息相关。在某些系统中,只要线程没有阻塞,那么其会一直执行下去,不会放弃CPU资源;而在某些系统中,即使运行中的线程没有遇到阻塞,也会在一段时间后进入阻塞状态,放弃CPU资源。

其中主要的方法有如下几类:

  • stop()

Thread类的stop()方法可以终止一个线程,但是在JDK1.2中,废弃了stop()方法。

?在实际的开发过程中,一般是在受控制的线程内编写一个状态码,其他的线程通过改变线程内的状态码,来控制线程的动态终止、暂停和恢复运行。

  • isAlive()

用来判断线程是否死亡。函数为final boolean isAlive()。该线程如果处于可运行状态、运行状态、对象的等待队列和对象的锁池中,则会返回true的值。

  • Thread.sleep(5000)

当先线程,放弃CPU资源,进入阻塞状态。当睡眠结束后,进入就绪状态,但不一定会立即获取CPU的资源。当在睡眠状态时,线程被中断,会爆出InterrupedException异常,由异常处理块处理异常。

  • void sleepingThread.interrupt()

中断某个线程。

  • otherThread.isInterrupt()

测试某个线程是否被中断,与static boolean interrupt()不同的是:对他的调用不会改变该线程的’中断’状态。(即对于线程A 进行 isInterrupt()方法,无论线程A是‘阻塞’还是‘中断’状态,调用这个方法逗不回进行改变。)

  • public void join() / public void join(long TimeOut)

挂起当前线程(一般是主线程),直到它调用的主线程结束后才会运行。线程A中调用线程B.join(),线程A阻塞。因为是线程A调用了A.join()方法,谁调用谁阻塞。

5. 线程同步

线程的职责就是执行一些操作,而多数的操作都会涉及处理数据。比如如下的例子:

a+=i;a-=i;System.out.println(a);

多个线程在同时共享一个实例变量时,都可能引起一些共享资源的争夺。为了能够使程序正常工作,确保资源能够被正确的修改。Java引入了同步机制,具体的做法是在需要同步控制的代码块内加上synchronized标记,这样的代码被称为同步代码。(与之相反的是异步代码)

对于Java内的对象,每一个对象有且仅有一个同步锁(synchronized)。在任何时刻,最多只允许一个线程拥有这把锁。当一个线程试图执行带有synchronized标记的代码块时,该线程必须首先获取this关键字引用的对象的锁。

  • 如果这个锁已经被其他线程使用,Java虚拟机会把这个线程放到this指定对象的锁池中,线程进入阻塞状态。在对象的锁池中可能会有许多等待锁的线程。等到其他的线程释放了锁,Java虚拟机会从锁池中随机取出一个线程,使这个线程拥有锁,并且转到就绪状态。
  • 加入这个锁没有被其他线程占用,线程就会获得这把锁,开始执行同步的代码块。一般情况下,线程知道执行完同步代码块,才会释放锁,使得其他线程能够获得锁。

如果一个方法中的所有代码都属于同步代码,则可以直接在方法前使用synchronized修饰。

public synichronized String pop(){...}等同于public String pop(){    synchronized(this){...}}

线程同步的特性:

  • 如果一个同步代码块和非同步代码块同时操作共享资源,仍然会造成对共享资源的竞争。
    因为当一个线程执行一个对象的同步代码块时,其他线程仍然可以执行对象的非同步代码块。
  • 每个对象都有一把同步锁。
  • 在静态方法前面也可以使用synchronized修饰符。此时该同步锁的对象为类对象(类的class对象)
  • 当一个线程开始执行同步代码块时,并不意味着必须以不中断的方式运行。进入同步代码块的线程也可以执行Thread.sleep()或者执行Thread.yield()方法,此时它没有释放锁,只是把运行的机会(即CPU)让给了其他线程。
  • synchronized声明不会被继承。

    同步是解决共享资源竞争的有效手段。当一个线程已经在操纵共享资源时,其他共享线程只能等待。为了提升并发性能,使同步代码块包涵尽可能少的操作,使得一个线程能够尽快释放锁,减少其他线程等待锁的世界

6. 线程通信

  • 锁对象.wait(): 执行该方法的线程,释放对象的锁,Java虚拟机把该线程放到该对象的等待池中。该线程等待其他线程将它唤醒;
  • 锁对象.notify():执行该方法的线程唤醒在对象的等待池中等待的一个线程。Java虚拟机从对象的等待池中随机选择一个线程,把它转到对象的锁池中。如果对象的等待池没有任何线程,那么notify()方法什么也不做。
  • 锁对象.notifyAll():会把对象的等待池中的所有线程都转到对象的锁池中。
    注意:notify notifyAll只会唤醒等待池中等待同一个锁对象的线程,因为同一个时刻在等到池中可能会有多个线程,而这多个线程可能是在等待不同的锁对象。

    假如t1线程和t2线程共同操纵一个s对象,这两个线程可以通过s对象的wait()和notify()方法来进行通信。通信流程如下:

  • 当t1线程执行对象s的一个同步代码块时,t1线程持有对象s的锁,t2线程在对象s的锁池中等待;

  • t1线程在同步代码块中执行s.wait()方法, t1释放对象s的锁,进入对象s的等待池;在对象s的锁池中等待锁的t2线程获得了对象s的锁,执行对象s的另一个同步代码块;
  • t2线程在同步代码块中执行s.notify()方法,Java虚拟机把t1线程从对象s的等待池移到对象s的锁池中,在那里等待获得锁。t2线程执行完同步代码块,释放锁。t1线程获得锁,继续执行同步代码块。(对象池和锁池?)

7. 线程死锁

A->B B->A.即A线程必须等待B线程结束才能运行,B线程也必须等待线程A线程运行结束才能运行。造成互相等待,但是互相又逗无法解决的尴尬局面。(死锁的解决办法会令写一篇博客提及)

8. 线程让步

Thread.yield()静态方法,如果此时有相同优先级的其他线程处于就绪状态。那么yield()方法会把当先线程放入阻塞池中,将同优先级的线程进入执行状态。但是如果没有相同级别的线程,那么yield()方法什么也不会做。

sleep()方法和yield()方法的区别:

相同点
* 都会使当前的线程挂起,使其他线程获取CPU资源,进入运行状态。

不同点:

  • sleep()方法不考虑其他线程的优先级,yield()会给相同优先级或者高一级别的优先级的线程机会。
  • sleep()会进入阻塞状态,yield()会进入就绪状态;
  • sleep()会跑出InterruptedException异常,但是yield()不会抛出任何异常。
  • sleep()会比yield()拥有更高的可移植性。

对于大多数程序员来说,yield()方法的唯一用途是在测试期间人为地提高程序的并发性能,以帮助发现一些隐藏的错误,所以yield()并不常用。

9. 线程调度优先级

注意:优先级高的线程只能获得较多运行的概率,但是实际中不一定真的有效。

线程优先级的使用原则和操作系统有着密切的联系。因此,在Java内的线程的调度时完全受其所运行平台的操作吸引的线程调度程序控制的。所以,虽然我们可以设置线程的优先级,但是在很多的时候不一定体现出来。

例如:比如在一个操作系统,采用轮询制恶使用CPU原则,各个进程平均分配CPU时间,那么,线程的优先级就是毫无用处的。

所有的处于就绪状态的线程根据优先级放在可运行池中,优先级低的线程获得较少的运行机会,优先级高的线程获得较多的运行机会。Thread类的setPropertity(int)和getPropertity()方法分别用来设置优先级和读取优先级。优先级用整数来表示,取值范围为1-10,Thread类以下有3个静态常量。

  • MAX_PROPERTITY: 10 最高
  • MIN_PROPERTITY: 1 最低
  • NORM_PROPERTY: 5 默认优先级

其他:

  • stop() 终止线程运行 (过时)
  • resume() 使暂停的线程恢复运行(过时)
  • suspend() 暂停线程,不释放锁(过时)

释放对象的锁:

  • 执行完同步代码块;
  • 执行同步代码块的过程中,遇到异常而导致线程终止,释放锁;
  • 执行同步代码块的过程中,执行了锁所属对象的wait()方法,释放锁进入对象的等待池;

线程不释放锁:

  • Thread.sleep()方法 放弃CPU,进入阻塞状态;
  • Thread.yield()方法,放弃CPU,进入就绪状态;
  • suspend()方法,暂停当前线程,已经过时。

10. interrupt、isInterrupted、interrupted三个方法

1 0