本文主要介绍线程的定义,创建,使用,停止,状态图和常用方法。
主要用于概念扫盲和梳理。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。
先说并发
1. 为什么要用到并发
一直以来,硬件的发展极其迅速,也有一个很著名的"摩尔定律",可能会奇怪明明讨论的是并发编程为什么会扯到了硬件的发展,这其中的关系应该是多核CPU的发展为并发编程提供的硬件基础。摩尔定律并不是一种自然法则或者是物理定律,它只是基于认为观测数据后,对未来的一种预测。
按照所预测的速度,我们的计算能力会按照指数级别的速度增长,不久以后会拥有超强的计算能力,正是在畅想未来的时候,2004年,Intel宣布4GHz芯片的计划推迟到2005年,然后在2004年秋季,Intel宣布彻底取消4GHz的计划,也就是说摩尔定律的有效性超过了半个世纪戛然而止。
但是,聪明的硬件工程师并没有停止研发的脚步,他们为了进一步提升计算速度,而不是再追求单独的计算单元,而是将多个计算单元整合到了一起,也就是形成了多核CPU。短短十几年的时间,家用型CPU,比如Intel i7就可以达到4核心甚至8核心。而专业服务器则通常可以达到几个独立的CPU,每一个CPU甚至拥有多达8个以上的内核。因此,摩尔定律似乎在CPU核心扩展上继续得到体验。因此,多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。
顶级计算机科学家Donald Ervin Knuth如此评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者无计可施了导致的,他们将摩尔定律的责任推给了软件开发者。
另外,在特殊的业务场景下先天的就适合于并发编程。比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将所有的像素遍历一边都需要很长的时间,面对如此复杂的计算量就需要充分利用多核的计算的能力。又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
2. 并发编程缺点
- 频繁的上下文切换
时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片
一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。
通常减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。
- 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
- CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
- 线程安全
3. 一些概念
3.1 同步与异步
同步和异步通常用来形容一次方法调用
。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。
比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。
3.2 并发与并行
并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。
真正的并行也只能出现在拥有多个CPU的系统中。
3.3 阻塞和非阻塞
阻塞和非阻塞通常用来形容多线程间的相互影响
,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。
3.4 临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。
进程与线程
进程--资源分配的最小单位
- 每个进程都有独立的代码和数据空间(进程上下文)
- 一个进程包含多个线程
- 进程间的切换会有较大的开销
线程--cpu调度的最小单位
- 同一类线程共享代码和数据空间,
- 每个线程有独立的运行栈和程序计数器(PC)
- 线程切换开销小
其他
- 多进程是指操作系统能同时运行多个任务(程序)。
- 多线程是指在同一程序中有多个顺序流在执行。
- 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
线程创建、使用、停止
创建线程
- 在Java中要想实现多线程,有三种手段,一种是继承Thread类,另外一种是实现Runnable接口,三是实现Callable 接口。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
1. 继承Thread 类,覆盖run方法(推荐使用Runable)
- run()方法是多线程程序的一个约定,所有的多线程代码都在run方法里面。
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
- 我们复写的是run()方法,但是执行的却是start()方法。
- start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
PrimeThread p = new PrimeThread(143);
p.start();
2. 实现Runnable接口,覆盖run方法
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target)
构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
或
new Thread(new PrimeRun(143)).start();
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
3. 实现 Callable 接口
与 Runnable 相比,Callable 可以有返回值,返回值通过FutureTask
进行封装。
public class MyCallable implements Callable<Integer> {
public Integer call() {
// ...
}
public static void main(String[] args) {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}
Thread和Runnable的区别
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的
单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
-
线程池
只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
使用线程
方法
类别 | 方法签名 | 简介 |
---|---|---|
构造方法 | thread() | - |
- | thread(String name) | - |
- | thread(Runnable target) | - |
- | thread(Runnable target,String name) | - |
常用方法 | void start() | 启动线程 |
- | static void sleep(long millis) | 休眠 |
- | static void sleep(long millis,int nanos) | 休眠 |
- | void join() | 使其他线程等待当前线程终止 |
- | void join(long millis) | 使其他线程等待当前线程终止 |
- | void join(long millis,int nanos) | 使其他线程等待当前线程终止 |
- | static void yield( ) | 当前运行线程释放处理器资源 |
获取线程引用 | static Thread currendThread() | 返回当前运行的线程引用 |
注意事项
- main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。
- 在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。
停止线程
错误方法
- stop()方法。
它会使线程戛然而止,我们不知道它完成了什么工作,没有完成什么工作,因此是错误的。 - interrupt()方法。
不要使用interrupt()方法,因为在线程run()中调用sleep(),join(),yeild()方法时,中断状态会被清除,isinterrupe=false
如何正确的停止线程
使用退出标志。
volatile boolean keepRunning = true;
代码示例
public class ActorThread extends Thread {//继承Thread创建线程
// 覆盖run------------------------------------------------------
public void run() {
System.out.println(getName() + "是一个演员!");
int count = 0;
boolean keepRunning = true;
while (keepRunning) {
System.out.println(getName() + "登台演出:" + (++count));
//标注法停止线程-----------------------------------------------
if (count == 15) {
keepRunning = false;
}
//测试sleep()-----------------------
if (count % 5 == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(getName() + "的演出结束了!");
}
public static void main(String[] args) {
Thread actor = new ActorThread();
actor.setName("Mr. Thread");
actor.start();
Thread actressThread = new Thread(new Actress(), "Ms. Runnable");
actressThread.start();
}
}
class Actress implements Runnable {//实现Runnable接口创建线程
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "是一个演员!");
int count = 0;
boolean keepRunning = true;
while (keepRunning) {
System.out.println(Thread.currentThread().getName() + "登台演出:" + (++count));
if (count == 15) {
keepRunning = false;
}
if (count % 5 == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName() + "的演出结束了!");
}
}
输出
线程状态转换
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(sleep是不会释放持有的锁)
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
常用函数说明
sleep(long millis):线程暂停执行
在指定的毫秒数内让当前正在执行的线程休眠。
sleep()
可能会抛出 InterruptedException。因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
join(long millis):指等待t线程一段时间(或终止)
- join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程(终止)”。
- 目的
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
yield():暂停当前正在执行的线程对象,并执行其他线程。
- yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。
- 目的
使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
wait()
本小节转载自使用wait/notify/notifyAll实现线程间通信的几点重要说明
用于线程间通信
在Java中,可以通过配合调用Object对象的wait()
方法和notify()
方法或notifyAll()
方法来实现线程间的通信。
语义
在线程中调用wait()
方法,将阻塞等待其他线程的通知(其他线程调用notify()
方法或notifyAll()
方法),在线程中调用notify()
方法或notifyAll()
方法,将通知其他线程从wait()
方法处返回。
Object是所有类的超类,它有5个方法组成了等待/通知机制的核心:notify()、notifyAll()、wait()、wait(long)和wait(long,int)。在Java中,所有的类都从Object继承而来,因此,所有的类都拥有这些共有方法可供使用。而且,由于他们都被声明为final,因此在子类中不能覆写任何一个方法。
使用中需要注意
-
wait()
--- "我去等待了"
public final void wait() throws InterruptedException,IllegalMonitorStateException
- 该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。
- 进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出
IllegalMonitorStateException
,它是RuntimeException的一个子类,因此,不需要try-catch结构。
-
notify()
---"大家来争我"
public final native void notify() throws IllegalMonitorStateException
- 该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用notify()时没有持有适当的锁,也会抛出
IllegalMonitorStateException
- 该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify后,当前线程不会马上释放该对象锁,wait所在的线程并不能马上获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。这里需要注意:它们等待的是被notify或notifyAll,而不是锁。这与下面的notifyAll()方法执行后的情况不同。
notifyAll()
public final native void notifyAll() throws IllegalMonitorStateException
该方法与notify()方法的工作方式相同,重要的一点差异是:
notifyAll使所有原来在该对象上wait的线程统统退出wait的状态(即全部被唤醒,不再等待notify或notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll线程退出调用了notifyAll的synchronized代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
- wait(long)和wait(long,int)
显然,这两个方法是设置等待超时时间的,后者在超值时间上加上ns,精度也难以达到,因此,该方法很少使用。对于前者,如果在等待线程接到通知或被中断之前,已经超过了指定的毫秒数,则它通过竞争重新获得锁,并从wait(long)返回。另外,需要知道,如果设置了超时时间,当wait()返回时,我们不能确定它是因为接到了通知还是因为超时而返回的,因为wait()方法不会返回任何相关的信息。但一般可以通过设置标志位来判断,在notify之前改变标志位的值,在wait()方法后读取该标志位的值来判断,当然为了保证notify不被遗漏,我们还需要另外一个标志位来循环判断是否调用wait()方法。 - 深入理解:
如果线程调用了对象的wait()方法,那么线程便会处于该对象的
等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
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()和yield()的区别
- sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
- 另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
三线程打印ABC
要求
java实现三个线程A B C:A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。
方法一.使用synchronized() + wait(),nitify()
- 题意
问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC→ThreadA……循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。 - 思路
主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就是前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印字母,之后首先调用self.notifyAll()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。
程序运行的主要过程就是
A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B;
线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C;
线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A……
- 注意
为了避免JVM启动ThreadA、ThreadB、ThreadC三个线程顺序的不确定性。需要让A,B,C三个线程以确定的顺序启动,中间加一段sleep()
确保前一个线程已启动。
代码实现
public class ABC {
public static class ThreadPrinter implements Runnable {
private String name;
private Object prev;//前一个对象的锁
private Object self;//自己的锁
private ThreadPrinter(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {// 多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
synchronized (prev) { // 先获取 prev 锁
synchronized (self) {// 再获取 self 锁
System.out.print(name);
count--;
self.notifyAll();// 先释放 self,唤醒其他线程竞争self锁
}
try {
prev.wait(); // 再释放 prev,休眠等待唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws Exception {
Object a = new Object();
Object b = new Object();
Object c = new Object();
ThreadPrinter pa = new ThreadPrinter("A", c, a);
ThreadPrinter pb = new ThreadPrinter("B", a, b);
ThreadPrinter pc = new ThreadPrinter("C", b, c);
new Thread(pa).start();
Thread.sleep(10);
new Thread(pb).start();
Thread.sleep(10);
new Thread(pc).start();
Thread.sleep(10);
}
}
输出
方法二. 使用Lock
代码实现
import java.util.concurrent.locks.*;
public class ABC_Lock {
private static Lock lock = new ReentrantLock();//通过Lock锁来保证线程的访问的互斥
private static int state = 0;//控制ABC的打印语句是否执行,获取Lock之后的判断
static class ThreadA extends Thread {
@Override
public void run() {
int count = 10;
while(count > 0) {
try {
lock.lock();
while (state % 3 == 0) {//多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
System.out.print("A");
state++;
count--;
}
} finally {
lock.unlock();// lock()和unlock()操作结合try/catch使用
}
}
}
}
static class ThreadB extends Thread {
@Override
public void run() {
int count = 10;
while(count > 0) {
try {
lock.lock();
while (state % 3 == 1) {//多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
System.out.print("B");
state++;
count--;
}
} finally {
lock.unlock();// lock()和unlock()操作结合try/catch使用
}
}
}
}
static class ThreadC extends Thread {
@Override
public void run() {
int count = 10;
while(count > 0){
try {
lock.lock();
while (state % 3 == 2) {//多线程并发,不能用if,必须用循环测试等待条件,避免虚假唤醒
System.out.print("C");
state++;
count--;
}
} finally {
lock.unlock();// lock()和unlock()操作结合try/catch使用
}
}
}
}
public static void main(String[] args) {
new ThreadA().start();
new ThreadB().start();
new ThreadC().start();
}
}