并行与并发
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生(注意的是时间间隔,间隔是有大小的)。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:在一台处理器上同时处理多个任务,在多台处理器上同时处理多个任务。如 hadoop 分布式集群
这里另外再用一张经常用的图来解释并发与并行之间的关系
并发就像只有一台咖啡机能制造咖啡(单核 CPU)的店铺,然而有序列 A 和序列 B 两队人在排队,在一个时间段内(咖啡机可能造出多杯咖啡),序列 A 和序列 B
的人可能都会领取到咖啡,在这个时间段内多个人领取到咖啡这个过程称为 并发,这个时间段类似于多个 CPU 时间片,一个 CPU 时间片执行一个任务。并行就像有两台咖啡机的店铺,然后在同一个时刻,都会有两个人领取到咖啡,这样的过程称为 并行。
Java 多线程实现方式
平时所说的多线程,其实就是并发知识的底层实现,Java 现在有多种多线程的实现方式,最基础的两种方式是:
- 方式一:继承 Thread 类(一般不用,继承会把类的特性限制太死)
1 | public class MyThread extends Thread { |
- 方式二:实现 Runnable 接口
1 | public class ThreadB implements Runnable { |
还有其它实现方式,如实现 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.
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 区别
共同点:
- 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
- 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 。
不同点:
- Thread类的方法:sleep(),yield()等 。Object的方法:wait()和notify()等
- 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。 - wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用
- sleep 必须捕获异常,而 wait,notify和notifyAll 不需要捕获异常
所以sleep() 和 wait() 方法的最大区别是:sleep() 睡眠时,保持对象锁,仍然占有该锁;而 wait() 睡眠时,释放对象锁。但是 wait() 和 sleep() 都可以通过 interrupt() 方法打断线程的暂停状态,从而使线程立刻抛出 InterruptedException(但不建议使用该方法)。