一、基础概念
1.1、CPU核心数和线程数的关系
多核心 :单芯片多处理器( Chip Multiprocessors,简称CMP),其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理,
多线程 :让同一个处理器上的多个线程同步执行并共享处理器的执行资源,可最大限度地实现宽发射、乱序的超标量处理,提高处理器运算部件的利用率,缓和由于数据相关或 Cache未命中带来的访问内存延时。
二者关系 : 目前CPU基本都是多核,很少看到单核CPU。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但Intel引入超线程技术后,使核心数与线程数形成1:2的关系.
1.2、时间片轮转机制 (RR 调度)
定义:系统把所有就绪进程按先入先出的原则排成一个队列。新来的进程加到就绪队列末尾。每当执行进程调度时,进程调度程序总是选出就绪队列的队首进程,让它在CPU上运行一个时间片的时间。时间片是一个小的时间单位,通常为10~100ms数量级。当进程用完分给它的时间片后,系统的计时器发出时钟中断,调度程序便停止该进程的运行,把它放入就绪队列的末尾;然后,把CPU分给就绪队列的队首进程,同样也让它运行一个时间片,如此往复。
根据上面CPU核心数和线程数的关系 1:1的关系,如果我们手机是双核手机,那么我们按道理只能起两个线程,但是在实际的开发过程中并不是这样,我们可能开了十几个线程 "同时" 在执行,这是因为操作系统提供了CPU时间片轮转这个机制,它为每个进程分配一个时间段(即时间片),让他们在一段时间内交替执行。
上下文切换时间:由于时间片轮转进制,会使得进程之间不停的进行切换,进程之间切换涉及到保存和装入到寄存器值及内存映像,更新表格及队列,这个过程是需要消耗时间的。
时间片时间设置: 时间片如果设置太短,会导致过多进程不断切换,由于切换过程会产生上小文切换时间,所以降低CPU效率,设置太长,又会导致相对较短的交互请求响应变差,通常时间片设置在100ms左右比较合理。
1.3、进程和线程
1.3.1、什么是进程?
进程是程序运行资源分配的最小单元
进程是操作系统进行资源分配和调度的独立单元,资源包括CPU,内存空间,磁盘IO等等,同一个进程的所有线程共享该进程的全部资源,进程与进程之间相互独立。
1.3.2、什么是线程?
线程是CPU调度的最小单位,必须依赖进程而存在。
线程是进程的实体,是CPU调度和分派的基本单位,线程基本不拥有系统资源,但是拥有程序计数器、一组寄存器、栈等运行中不可少的资源,同一个进程中的线程共享进程所拥有的全部资源。
1.4、 并发与并行
1.4.1、什么是并发
并发是指一个时间段内,有几个程序都在同一个CPU上运行,但任意一个时刻点上只有一个程序在处理机上运行。
多个线程 一个CPU
跟时间挂钩,单位时间内。
1.4.2、什么是并行
并行是指一个时间段内,有几个程序都在几个CPU上运行,任意一个时刻点上,有多个程序在同时运行,并且多道程序之间互不干扰。
多个线程 多个CPU
1.5、同步与异步
1.5.1、什么是同步
同步:在发出一个同步调用时,在没有得到结果之前,该调用就不返回,直到结果的返回。
好比我给朋友打电话,你要不接电话,我就一直打,这个过程啥也不干,就给你打电话,打到你接电话为止。
1.5.2、什么是异步
异步:在发出一个异步调用后,调用者不会立刻得到结果,该调用就返回了。
同样打电话,我先给你发个消息,告诉我有事找你,然后我就去干我自己的事情去了,等你看到消息给我回电话,当然,你也可以不回我电话。
二、多线程使用
2.1、创建多线程
2.1.1、实现Runnable接口
public static class newRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable");
}
}
调用:
new Thread(new newRunnable()).start();
2.1.2、继承Thread类
public static class newThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("newThread");
}
}
调用:
new newThread().start();
1、Thread 是java里面对线程的抽象概念,我们通过new thread的时候,其实只是创建了一个thread实例,操作系统并没有和该线程挂钩,只有执行了start方法后,才是真正意义上启动了线程。
2、start() 会让一个线程进入就绪队列等待分配CPU,分到CPU后才调用 run() 方法,
3、start() 方法不能重复调用,否则会抛出
IllegalThreadStateException
异常。
2.1.3、实现Callable<V>接口
Callable
接口 是在Java1.5开始提供,可以在任务执行结束后提供返回值。
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
从源码可以看到,Callable
跟 Runnable
对比来看,不同点就在其call
方法提供了返回值和进行异常抛出。
使用:
public static class newCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("newCallable");
Thread.sleep(3000);
return "java1.5后提供,可在任务执行结束返回相应结果";
}
}
对Callable的调用需要 FutureTask
这个类,这个类也是 Java1.5
以后提供
FutureTask<String> futureTask = new FutureTask<>(new newCallable());
futureTask.run();
String result = null;
try {
result = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
执行结果:
image可以看到,我们通过 FutureTask
的 get
方法 ,get
方法会进行阻塞,直到任务结束后,才将返回值进行返回。
2.2、终止线程
2.2.1、 自然终止
线程任务执行完成,则这个线程进行终止。
2.2.2、手动终止
暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的,主要原因是方法的调用不能保证线程资源的正常释放,容易引起其他副作用的产生。
imagesuspend() :在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题
stop() : 终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下
真正安全的终止线程使用 interrupt()
方法
由于线程之间是协作式工作,所以在其他线程使用 interrupt()
终止某个线程时候,这个线程并不会立即终止,只是收到了终止通知,通过检查自身的中断标志位是否为被置为 True
再进行相应的操作,当然,这个线程完全可以不用理会。
中断标志位的判断:
1、isInterrupted()
判断线程是否中断,如果该线程已被中断则返回 true
否则返回 false
/**
* Tests whether this thread has been interrupted. The <i>interrupted
* status</i> of the thread is unaffected by this method.
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if this thread has been interrupted;
* <code>false</code> otherwise.
* @see #interrupted()
* @revised 6.0
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
2、interrupted()
判断线程是否中断,如果该线程已被中断返回 true
,状态返回后该方法会清除中断标志位,重新置为 false
,当第二次再次调用的时候又会返回 false
,(除非重新调用 interrupt()
进行中断 )
/**
* Tests whether the current thread has been interrupted. The
* <i>interrupted status</i> of the thread is cleared by this method. In
* other words, if this method were to be called twice in succession, the
* second call would return false (unless the current thread were
* interrupted again, after the first call had cleared its interrupted
* status and before the second call had examined it).
*
* <p>A thread interruption ignored because a thread was not alive
* at the time of the interrupt will be reflected by this method
* returning false.
*
* @return <code>true</code> if the current thread has been interrupted;
* <code>false</code> otherwise.
* @see #isInterrupted()
* @revised 6.0
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
下面通过一个Demo演示 isInterrupted()
和 interrupted()
的区别
isInterrupted
:
private static class UseThread extends Thread{
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" interrupt start flag ="+isInterrupted());
while(!isInterrupted()){
System.out.println(threadName+" is running");
System.out.println(threadName+" inner interrupt flag ="+isInterrupted());
}
System.out.println(threadName+" interrupt end flag ="+isInterrupted());
}
}
运行上面的程序:
开启线程,休眠一微秒后调用 interrupt()
进行中断
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("test isInterrupted");
endThread.start();
Thread.sleep(1);
endThread.interrupt();
}
结果:
image可以看到 UseThread
的 isInterrupted()
一直为 false
,当主线程执行 endThread.interrupt()
中断方法后,其中断标志被置为 true
,跳出循环,结束 run
方法。我们后续再调用 isInterrupted()
方法打印中断标志的值一直为 true
,并没有更改。
interrupted():
我们简单改了一下代码:
private static class UseThread extends Thread{
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
while(!Thread.interrupted()){
System.out.println(threadName+" is running");
}
System.out.println(threadName+" interrupted end flag ="+Thread.interrupted());
}
}
可以看到,run 方法里面一直循环执行,直到线程被中断,结束后我们再次调用了打印了 Thread.interrupted()
值
调用:
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("test interrupted");
endThread.start();
Thread.sleep(1);
endThread.interrupt();
}
同样休眠一微秒后进行中断操作。
结果:
image我们再分析一下,前面线程结束循环的条件是 Thread.interrupted()
为 true
, 但是当线程结束循环后,我们再次调用 Thread.interrupted()
方法,发现其值为又被置为 false
,说明 Thread.interrupted()
执行后,会清除 中断标志位,并将其重新置为 false
。
注意:处于死锁状态的线程无法被中断
如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait),则在线程在检查中断标示时如果发现中断标示为true
,则会在这些阻塞方法调用处抛出InterruptedException
异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。
我们在前面 isInterrupted
演示的 Demo中 进行修改
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " interrupt start flag =" + isInterrupted());
while (!isInterrupted()) {
try {
// 线程进行休眠3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("sleep error=" + e.getLocalizedMessage());
}
System.out.println(threadName + " is running");
System.out.println(threadName + " inner interrupt flag =" + isInterrupted());
}
System.out.println(threadName + " interrupt end flag =" + isInterrupted());
}
}
我们再执行前面的方法的时候,结果:
image我们可以看到,即使抛了异常,但是线程依旧在执行,这个时候标志位还有没有置为true
,所以我们要注意
抛出InterruptedException
异常的时候 对中断标志位的操作,我们改一下代码,在catch中再次执行 interrupt()
来中断任务
try {
// 线程进行休眠3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
// 在catch方法中,执行interrupt() 方法中断任务。
interrupt();
System.out.println("sleep error=" + e.getLocalizedMessage());
}
结果:
image三、线程之间共享和协作
3.1、 线程之间共享
前面说过,同一个进程的所有线程共享该进程的全部资源,共享资源就会导致一个问题,当多个线程同时访问一个对象或者一个对象的成员变量,可能会导致数据不同步问题,比如 线程A 对数据a进行操作,需要从内存中进行读取然后进行相应的操作,操作完成后再写入内存中,但是如果数据还没有写入内存中的时候,线程B 也来对这个数据进行操作,取到的就是还未写入内存的数据,导致前后数据同步问题。
为了处理这个问题,Java 中引入了关键字 synchronized ( 下一篇文章单独讲)。
3.2、线程之间的协作
线程之间可以相互配合,共同完成一项工作,比如线程A修改了某个值,这个时候需要通知另一个线程再执行后续操作,整个过程开始与一个线程,最终又再另一个线程执行,前者是生产者,后者就是消费者。
3.2.1、 nitify()、notifyAll()、wait() 等待/通知机制
是指一个线程A调用了对象O的 wait()
方法进入等待状态,而另一个线程B调用了对象O的notify()
或者notifyAll()
方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify、notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
我们知道 Object
类是所有类的父类,而 Object
类中就存在相关方法
notify():
通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。
notifyAll():
通知所有等待在该对象上的线程
wait()
调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁
wait(long)
超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
wait (long,int)
对于超时时间更细粒度的控制,可以达到纳秒
下面通过案例说明,双十一的时候,你购买了三件商品,你在家焦急的等待,没事就刷新一下手机看商品快递信息,我们就来模拟一个快递信息的更新,这里以地点变化进行数据更新:
public class NwTest {
// 发货地点
public String location = "重庆";
// 所有货物在不同一趟车上,货物到了下一站,分别更新对应的快递信息
public synchronized void changeLocationNotify(String location) {
this.location = location;
this.notify();
}
// 所有货物在同一趟快递车上,货物到了下一站,全部信息更新。
public synchronized void changeLocationNotifyAll(String location) {
this.location = location;
System.out.println("changeLocationNotifyAll");
this.notifyAll();
}
public static class LocationThread extends Thread {
public final NwTest mNwTest;
public LocationThread(NwTest nwTest) {
this.mNwTest = nwTest;
}
@Override
public void run() {
super.run();
try {
synchronized (mNwTest) {
System.out.println("LocationThread current location : " + mNwTest.location);
// 等待位置更新
mNwTest.wait();
String name = Thread.currentThread().getName();
// 获取当前商品的商家信息
System.out.println("LocationThread——>current thread name : " + name);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取更新后的位置
System.out.println("LocationThread update location : " + mNwTest.location);
}
}
注意:
只能在同步方法或者同步块中使用
wait()
、notifyAll()
、notify()
方法否则会抛
IllegalMonitorStateException
异常
调用:
public static void main(String[] args) throws InterruptedException {
NwTest nwTest = new NwTest();
for (int x = 0; x < 3; x++) {
new LocationThread(nwTest).start();
}
// 模拟三天后
Thread.sleep(3000);
// 通知单个商品信息进行更新
nwTest.changeLocationNotify("合肥");
}
我们启动了三个线程,模拟了你购买的三件货物,如果使用 notify()
,只是使单个商品进行信息更新
结果:
image我们看到三个货物同时发货,其中 Thread_0 最先到达合肥,并进行了数据更新。
如果使用 notifyAll()
,所有商品快递信息都会刷新。
public static void main(String[] args) throws InterruptedException {
NwTest nwTest = new NwTest();
for (int x = 0; x < 3; x++) {
new LocationThread(nwTest).start();
}
Thread.sleep(3000);
// 通知三件商品进行信息更新
nwTest.changeLocationNotifyAll("合肥");
}
结果:
image这就是 notifyAll()
、 notify()
、wait()
基本使用,其中 wait(long)
表示线程会等待 n 毫秒,如果这个时间段内没有收到 notifyAll()
或者 notify()
就自动执行后续方法。
根据上面的Demo,我们可以整理一下 等待和通知的标准范式
wait():
1)获取对象的锁。
2)根据判断条件调用 wait()
方法。
3)条件满足则执行对应的逻辑。
imagenotify()
或者 notifyAll()
1)获得对象的锁。
2)改变条件,发送通知。
3)通知所有等待在对象上的线程。
image以上主要是整理的多线程的一些基本概念,还有 notify()和wait() 的基本使用,关键字 synchronized
准备下一篇单独整理,后续计划整理线程池相关知识以及Android 中 AsyncTask 的源码分析,喜欢的话点个赞呗!
网友评论