Java 多线程

线程的概念

对于一般程序而言,其结构大都可以分为一个入口、一个出口、一个顺次执行的语句序列。这样的语句结构称为进程,它是程序的一次动态执行,对应了代码加载、执行至完毕的全过程。

进程即是程序在处理机中的一次运行。在这样一个结构中不仅包含程序代码,也包括了系统资源的概念。

在单 CPU 计算机内部,微观上讲,同一时间只能有一个线程运行。实现多线程即从宏观上使多个作业同时执行。

程序:为完成特定任务,用某种语言编写的一组指令的集合。

进程:运行中的程序。当你运行一个程序,系统就会为该进程分配空间。进程是程序的一次执行过程。是一个动态过程:有其自身产生、存在、消亡的过程。

线程:由进程创建的,进程的一个实体。一个进程可以有多个线程。

单线程:同一时刻,只允许执行一个线程。

多线程:同一时刻,可以执行多个线程。

并发:同一时刻,多个任务交替执行,造成一种貌似并行的状态。单核 CPU 实现的多任务就是并发。

并行:同一时刻,多个任务同时进行。多核 CPU 可以实现并行。

线程的结构

在 Java 中,线程由以下 3 部分组成:

  • 虚拟 CPU:封装在 java.lang.Thread 类中,控制着整个线程的运行
  • 执行的代码:传递给 Thread 类,由其控制按序执行
  • 处理的数据:传递给 Thread 类,是在代码执行过程中需要处理的数据

线程的状态

Java 的线程是通过包 java.lang 中定义的类 Thread 来实现的。当生成了一个 Thread 类后就产生了一个线程。通过该对象实例,可以启动线程、终止线程,或暂时挂起线程

线程共有 4 种状态:新建(New)、可运行(Runnable)、死亡(Dead)、阻塞(Blocked)

  • 新建(New):

    线程对象刚刚创建,还未启动(New)。此时还处于不可运行状态,但已有了相应内存空间及其他资源

  • 可运行(Runnable):

    此时线程已经启动,处于线程的 run() 方法中。这种情况下线程可能正在运行;也可能没有运行,但只要 CPU 空闲就会立刻运行。

    可以运行但没在运行的线程都排在一个队列中,这个队列称为就绪队列。

    可运行状态下,运行中的线程处于运行状态(Running),未运行线程处于就绪状态(Ready)。

    调用 start() 方法可以让线程进入可运行状态。

  • 死亡(Dead):

    线程死亡(Terminated)的原因有两个:一是 run() 方法最后一个语句执行完毕,二是线程遇到异常退出

  • 阻塞(Blocked):

    一个正常运行的线程因为特殊原因被暂停执行,就进入阻塞状态(Blocked)。

    阻塞时线程不能进入就绪对流排队,必须等到引起阻塞的原因消除,才能重新进入队列排队。

    引起阻塞的方法很多,sleep() 和 wait() 是两个常用的阻塞方法

  • 中断线程:

    • void interrupt():向一个线程发送一个中断请求,并把该线程的 interruptd 状态变为 true。

      中断阻塞线程的场合,会抛出 InterruptException 异常

    • static boolean interrupted():检测当前线程是否被中断,并重置状态 interrupted 的值。

      连续调用该方法的场合,第二次调用会返回 false

    • boolean isInterrupted():检测当前线程是否中断。不改变 interrupted 的值

线程的使用

在 Java 中线程使用有两种方法:

  1. 继承 Thread 类,重写 run 方法

    1
    public class Thread implements Runnable		//可见 Thread 也是实现了 Runable 接口
  2. 实现 Runable 接口,重写 run 方法

继承 Thread 类

Thread 类是 Java 用于表示线程的类。那么,一个类被定义为其子类,则该类也能用来表示线程

1
2
3
4
5
6
public static void main(String[] args) {
Type type = new Type();
type.start(); //开始线程
//如果用 run 方法,则还是停留在主线程
// 那样,相当于 串行。执行完毕才继续
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Type extends Thread {						//先继承 Thread 类
int i = 0;
@Override
public void run() {
while (true) {
System.out.println(i);
try {
Thread.sleep(100); //休眠 100 毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i++ == 10) { //i = 10 时停止循环
break;
}
}
}
}

关于 start() 方法

1
2
3
4
5
6
public synchronized void start() {
...
start0();
}

private native void start0(); //start0 是 native。即,底层方法
  1. start() 方法调用了一个 start0() 底层方法
  2. start0() 是本地方法,由 JVM 调用,底层是 c/c++ 实现
  3. 真正的多线程效果,是 start0(),而不是 run()
  4. start() 方法调用 start0() 方法后,该线程不一定会立刻执行,只是将线程变成了可运行状态。具体何时运行,由 CPU 统一调度

实现 Runable 接口

Runnable 是 Java 用以实现线程的接口。从根本上将,任何实现线程的类都必须实现该接口。

1
2
3
4
5
public static void main(String[] args) {
Runnable type = new Type(); //Runable 没有 start()方法
Thread thread = new Thread(type); //所以,这里使用了 静态代理
thread.start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Type implements Runnable {				//这部分和 Thread 相似
@Override
public void run() {
int i = 0;
while (true){
System.out.println(i << i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (++i > 15){
break;
}
}
}
}

关于 静态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Thread implements Runable {}
...
private Runnable target;
...
public Thread(Runnable target) { //构造器
init(null, target, "Thread-" + nextThreadNum(), 0);
//这句话可以先理解为 this.target = target;
}
...
public void run() {
if (target != null) {
target.run();
}
}
...
}

相当于,先创建了一个新线程,然后在新线程中调用 run 方法

继承 Thread 和 实现 Runable 的区别

  1. 从 Java 设计来看,两者本质上没有区别。Thread 类本身就实现了 Runable 接口
  2. 实现 Runable 接口的方式更加适合多个线程共享一个资源的情况,且避免了单继承的限制。建议使用。

线程中止

  1. 当线程结束后,会自动退出

  2. 还可以通过使用变量来控制 run 方法退出的方式来停止线程,即 通知方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public void run() {
    while (active) { //这个场合,只要外部控制 active 即可
    try {
    Thread.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    move();
    }
    }

线程常用方法

  • setName(name):设置线程名称,使之与参数 name 相同

  • getName():返回线程名称

  • start():线程开始执行。JVM 调用 start0 方法

    该方法会创建新的线程,新线程调用 run

  • run():到下面玩跑步

    就是简单的方法调用,不会产生新线程。

  • setPriority(int priority):更改线程优先级

    getPriority():获取线程优先级

    priority 范围:

    • MAX_PRIORITY:最高优先级(10)
    • MIN_PRIORITY:最低优先级(1)
    • NORM_PRIORITY:不高不低,真是好极了的优先级(5)

    每个线程都有一个优先级。Java 线程调度采用如下优先级策略:

    • 优先级高的先执行,优先级低的后执行
    • 每个线程创建时会被自动分配一个优先级。默认的场合,继承父类优先级
    • 任务紧急的线程,优先级较高
    • 同优先级线程按 “先进先出” 原则调度
  • sleep(int millsecond):让线程休眠指定的时间

    该方法是 Thread 类的静态方法,可以直接调用

  • interrupt():中断线程(不是 中止)

  • yield():线程的礼让。让出 CPU 让其他线程执行。因为礼让的时间不确定,所以不一定礼让成功。

    本质是 RUNNING 切换为 READY,即让当前线程放弃执行权

  • wait():导致当前线程等待

    直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法才能唤醒此线程

    notify()notifyAll():唤醒因 wait() 阻塞的线程。

    这些方法(wait()notify()notifyAll())只能在 synchrnized 方法或代码块中调用

  • join():线程的插队。插队的线程一旦插入成功,则必定先执行完插队线程的所有任务

    将导致其他线程的等待,直到 join() 方法的线程结束

    join(long timeout):join,但是时间到后也能结束其他线程的等待

  • isAlive():测试当前线程是否在活动

  • Thread.currentThread():引用当前运行中的线程

用户线程和守护线程

  • 用户线程:也叫工作线程。当线程任务执行完毕或通知方式结束

  • 守护线程:一般是为工作线程服务的。当所有线程结束,守护线程自动结束

    常见的守护线程:垃圾回收机制

    1
    2
    3
    Thread thraed = new Thread(bullet);
    thread.setDeamon(true); //这样,子线程被设置为主线程的守护线程
    thread.start();

线程的生命周期

线程的状态有

  • NEW:尚未启动

  • RUNNABLE:在 JVM 中执行的线程

    可细分为 READY 和 RUNNING

  • BLOCKED:被阻塞等待监视器锁定的线程

  • WAITING:正等待另一个线程执行特定动作的线程

  • TIMED_WAITING:正等待另一个线程执行特定动作达到等待时间的线程

  • TERMINATED:已退出的线程

线程的互斥

在多线程编程,一些敏感数据不允许被多个线程同时访问。此时就用同步访问技术,保证数据在任意时刻,最多有一个线程同时访问,以保证数据的完整性。

也可以这样理解:线程同步,即当有一个线程对内存进行操作时,其他线程都不能对这个内存地址进行操作(被阻塞),直到该线程完成操作,再让下一线程进行操作。

互斥锁

在 Java 语言中,引入了 “对象互斥锁” 的概念,也称为监视器,来保证共享数据操作的完整性

每个对象都对应一个可称为 “互斥锁” 的标记,这个标记用来保证在任一时刻都只能有一个线程访问对象。

Java 语言中,有 2 种方式实现互斥锁:

  • 用关键字 volatile 声明一个共享数据(变量)。一般很少使用该关键字
  • 用关键字 synchronized 声明共享数据的一个方法或一个代码

同步的局限性:导致程序的执行效率要降低。

非静态的对象,同步方法的锁可以是 this,也可以是其他对象(要求是同一对象)

静态对象,同步方法的锁为当前类本身

  1. 同步代码块

    1
    2
    3
    synchronized (对象) {		//得到对象的锁,才能操作同步代码
    需要被同步代码;
    }

    在第一个线程持有锁定标记时,如果另一个线程企图执行该代码块语句,将从对象中索取锁定标记。

    因为此时该标记不可得,古该线程不能继续执行,而是加入等待队列。

    程序运行完 synchronized 代码块后,锁定标记会被自动返还。即使该同步代码块执行过程中抛出异常也是如此。一个线程多次调用该同步代码块的场合,也会在最外层执行完毕后正确返还。

  2. 放在方法声明中,表示整个方法为同步方法

    因为 synchronized 语句的参数必须是 this,因此允许下面这种简洁的写法:

    1
    2
    3
    public synchronized void method(){
    代码;
    }

线程死锁

多个线程都占用了对方的资源,不肯相让,就导致了死锁。编程时要避免死锁的产生。

  • 以下操作会释放锁

    1. 当前线程的同步方法、同步代码块执行结束。
    2. 当前线程在同步方法、同步代码块中遇到 breakreturn
    3. 当前线程在同步方法、同步代码块中出现了未处理的 Error
    4. 当前线程在同步方法、同步代码块中执行了 wait() 方法,当前线程暂停,并释放锁
  • 以下操作不会释放锁

    1. 执行同步方法、同步代码块时,程序调用 Thread.sleep() 或 Thread.yield() 方法暂停当前线程的执行,不会释放锁

    2. 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁

      所以,应尽量避免使用 suspend() 和 resume() 来控制线程

线程的同步

Java 中,可以使用 wait()notify()notifyAll() 来协调线程间的运行速度关系。这些方法都被定义在 java.lang.Object 中

Java 中的每个对象实例都有两个线程队列和它相连。一个用以实现等待锁定标志的线程,另一个用来实现 wait() 和 notify() 的交互机制

  • wait():让当前线程释放所有其持有的 “对象互斥锁”,进入等待队列

  • notify()notifyAll():唤醒一个或所有在等待队列中等待的线程,并将他们移入同一个等待 “对象互斥锁” 的队列。

    执行这些方法时如果没有等待中的线程,则其不会生效,也不会被保留到以后再生效

1
2
3
4
synchronized (key) {
if (key.value == 0) key.wait();
key.value--;
}
1
2
3
4
synchronized (key) {
key.value++;
key.nitifyAll();
}

因为调用这些方法时必须持有对象的 “对象互斥锁”,所以上述方法只能在 synhronized 方法或代码块中执行。