Java 多线程学习——基础概念

并行与并发

  • 解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生(注意的是时间间隔,间隔是有大小的)。

  • 解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。

  • 解释三:在一台处理器上同时处理多个任务,在多台处理器上同时处理多个任务。如 hadoop 分布式集群

这里另外再用一张经常用的图来解释并发与并行之间的关系

  • 并发就像只有一台咖啡机能制造咖啡(单核 CPU)的店铺,然而有序列 A 和序列 B 两队人在排队,在一个时间段内(咖啡机可能造出多杯咖啡),序列 A 和序列 B
    的人可能都会领取到咖啡,在这个时间段内多个人领取到咖啡这个过程称为 并发,这个时间段类似于多个 CPU 时间片,一个 CPU 时间片执行一个任务。

  • 并行就像有两台咖啡机的店铺,然后在同一个时刻,都会有两个人领取到咖啡,这样的过程称为 并行。

upload successful

Java 多线程实现方式

平时所说的多线程,其实就是并发知识的底层实现,Java 现在有多种多线程的实现方式,最基础的两种方式是:

  • 方式一:继承 Thread 类(一般不用,继承会把类的特性限制太死)
1
2
3
4
5
6
7
8
9
10
public class MyThread extends Thread {  
  public void run() {
   System.out.println("MyThread.run()");
  }
}

MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
  • 方式二:实现 Runnable 接口
1
2
3
4
5
6
7
8
9
10
public class ThreadB implements Runnable {

@Override
public void run() {
System.out.println("ThreadB");
}
}

Thread thread = new Thread(new ThreadB());
thread.start();
  • 还有其它实现方式,如实现 Callable 接口、Future 等,参考 JAVA多线程实现的四种方式

  • 这里 run() 方法内的逻辑表示线程执行的业务逻辑,而让线程启动的方法是 start()

  • start() 方法,即启动线程的方法,通过了解 start() 源码,执行 start 时,会有两个线程并发执行,当前线程去调用 start() 方法,另外一个线程会调 run() 方法开始具体的业务逻辑。

  • 需要注意的是,一个线程不能被启动多次,只有当它业务逻辑执行完成之时,才会启动下一次,否则会抛出 IllegalThreadStateException 异常。

线程的六种状态

根据 java.lang.Thread.State 枚举类源码可知,有 6 种不同的线程状态。These states are virtual machine states which do not reflect any operating system thread states.

upload successful

  • NEW(新建):仅定义了一个线程对象,还未调用它的 start() 方法。

    Thread state for a thread which has not yet started.

  • RUNNABLE(可运行):调用了线程的 start 方法,已经被 JVM 执行,但还在等待系统其它资源,如 CPU 时间片、I/O 等,根据操作系统知识可进一步将可运行状态细分为就绪、运行中两种状态(Java 并没有进一步细分)。

    Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.

    • 就绪:调用了 start 方法,但没有得到 CPU 时间片的状态
    • 运行中:得到了 CPU 时间片的状态,正在执行
  • BLOCKED(阻塞):线程等待 monitor lock 的状态,只有得到了这个对象锁之后,状态才会由阻塞变为就绪

    等待 monitor lock 进入被锁住的代码块,或者在调用 wait() 方法之后又进入被锁住的代码块(A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling {@link Object#wait() Object.wait}.)

// TODO 下面内容还待考证

- 等待阻塞:运行的线程执行 o.wait() 方法,JVM 会把该线程放入等待队列(waitting queue)中

- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中

- 其他阻塞:运行的线程执行 Thread.sleep(long ms) 或t.join() 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态,当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行(runnable)状态
  • WAITING(等待)

    A thread in the waiting state is waiting for another thread to perform a particular action.

    • 等待另一个线程的特定操作的状态,等待被其他线程唤醒

    • 等待的过程是主动等待的,而阻塞是得不到对象锁而被动阻塞住

    • 当线程拿到锁之后,调用相应锁对象的 wait()、join()、继承 LockSupport 类的 park() ,调用其中的方法线程就会处于这个状态。

  • TIMED_WAITING(有限期等待或超时等待)

    Thread state for a waiting thread with a specified waiting time.

    • 等待另一个线程执行指定等待时间的操作的线程处于此状态
    • 等待另一个线程特定时间,时间过后会自动唤醒
    • 线程执行 sleep()、wait(long)、join(long)、LockSupport.parkNanos 、LockSupport.parkUntil 方法时会处于这种状态
  • TERMINATED(终止):线程业务逻辑执行完成退出的状态
    Thread state for a terminated thread.The thread has completed execution.

线程具有的方法

主要讲解它的方法以及状态的切换、锁持有状态

  • Thread.sleep(long millis),一定是当前线程调用此方法,线程进入 TIME_WAITING 状态,但不释放对象锁,millis 后线程自动苏醒进入就绪状态。

    作用:给其它线程执行机会的最佳方式。

  • Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的 cpu 时间片,由运行状态变会就绪状态,让 OS 再次选择线程。

    作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证 yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield() 不会导致阻塞。

  • t.join()/t.join(long millis),当前线程里调用其它线程 t 的 join 方法,当前线程进入 TIME_WAITING 状态,当前线程不释放已经持有的对象锁。线程 t 执行完毕或者 millis 时间到,当前线程进入就绪状态。

  • obj.wait(),当前线程调用对象的 wait() 方法,当前线程释放对象锁,进入等待队列。依靠 notify()/notifyAll() 唤醒或者 wait(long timeout) timeout 时间到自动唤醒。

  • obj.notify() 唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll() 唤醒在此对象监视器上等待的所有线程。

终止线程的 4 种方式

  • 正常结束

  • 退出标识:有些线程需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程

  • Interrupt() 方法

    • 线程处于阻塞状态:调用 interrupt() 方法会抛出异常,此时要退出线程必须 catch 异常,并使用 break 方法退出。

    • 线程不处于阻塞状态:使用 isInterrupted() 判断线程的中断标志来退出循环,当使用 interrupt() 方法时,中断标志就会置 true

  • stop 方法:thread.stop() 调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用 thread.stop() 后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性

wait 和 sleep 区别

共同点:

  1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
  2. wait() 和 sleep() 都可以通过 interrupt() 方法 打断线程的暂停状态 ,从而使线程立刻抛出 InterruptedException。
    如果线程 A 希望立即结束线程 B,则可以对线程 B 对应的 Thread 实例调用 interrupt 方法。如果此刻线程 B 正在 wait/sleep /join,则线程 B 会立刻抛出 InterruptedException,在 catch() {} 中直接 return 即可安全地结束线程。
    需要注意的是,InterruptedException 是线程自己从内部抛出的,并不是 interrupt() 方法抛出的。对某一线程调用 interrupt() 时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出 InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出 InterruptedException 。

不同点:

  1. Thread类的方法:sleep(),yield()等 。Object的方法:wait()和notify()等
  2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
    sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  3. wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用
  4. sleep 必须捕获异常,而 wait,notify和notifyAll 不需要捕获异常

所以sleep() 和 wait() 方法的最大区别是:sleep() 睡眠时,保持对象锁,仍然占有该锁;而 wait() 睡眠时,释放对象锁。但是 wait() 和 sleep() 都可以通过 interrupt() 方法打断线程的暂停状态,从而使线程立刻抛出 InterruptedException(但不建议使用该方法)。

博客参考