深入了解Android多线程(一)Java线程基础
前言
【深入了解Android多线程】当前分为三个部分,这三个部分一起阅读,能更好的帮助你理解,Android在多线程方面设计与优化。
正文
CPU线程与操作系统线程
我们知道CPU的核心数量决定了CPU能同时处理多少个任务的能力,比如E3-1231V3型CPU,4核8线程,这里线程就是指CPU线程,不过intel使用了超线程技术,使得4核心的CPU具备了同时处理8个任务的能力。
而我们在程序开发中操作的线程则是操作系统线程,操作系统线程并不具备并发处理任务的能力,但是通过各种算法(例如:时间片轮转算法)可以让操作系统线程间断占据CPU线程的算力,给人一种操作系统线程可以并发执行任务的错觉。这里间断占据CPU线程的时间就是“时间片”。本文所说线程都是操作系统线程,简称线程。
进程与线程
在说线程之前,我们需要明白它和进程的区别
进程是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位
线程是单个进程中执行中每个任务。线程是进程中执行运算的最小单位。
从上面的定义我们可以看出,操作系统进行资源分配的基本单位是进程,但是CPU不同,CPU作为运算单元,是以线程为基本单位进行资源分配的。一个进程可以包含多个线程,但是一个线程只能属于一个进程。
借用一张图来解释进程和线程的关系
从图中可以看出,线程1-线程N共享进程的堆内存(heap)和方法区资源(Method Area),但是每个线程有自己的程序计数器(PC)和栈区(Stack)
程序计数器(PC)
其中 PC 计数器本质上是一块内存区域,用来记录线程当前要执行的指令地址,CPU 一般是使用时间片轮转方式让线程轮询占用的,因此当前线程 CPU 时间片用完后,要让出 CPU,这时 PC 计数器就会记录下当前线程下次要执行的命令的地址,等下次轮到该线程占有 CPU 执行时,就从 PC 计数器获取自己将要执行的命令的地址继续执行。
栈(Stack)
每个线程有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其它线程是访问不了的。
堆(heap)
堆是一个进程中最大的一块内存,是进程创建时候创建的,堆是被进程中的所有线程共享的。堆里面主要存放使用new 创建的对象实例。例如:
Object obj=new Object();
上述语句就是在堆上创建一个Object对象的实例,需要注意的是obj作为一个引用变量,在内存中是存在stack上的,只是obj指向了Object在堆上的地址。
方法区(Method Area)
方法区用来存放 JVM 加载的类信息、常量、静态变量等信息,也是线程共享的。
并行与并发
并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。
并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。
--百度百科
在Android开发中,为了防止UI线程阻塞,对于耗时、网络请求等操作,必须放置在非UI线程中执行,所以不可避免的会遇到线程与线程的交互。
Java中的线程
在java开发中开启一个新线程的方式。
1.继承Thread类
public static void main(String[] args) {
Thread mThread=new MyThread();
mThread.start();
}
public static class MyThread extends java.lang.Thread{
@Override
public void run() {
System.out.println("");
super.run();
}
}
2.实现Runable接口
public static void main(String[] args) {
Thread mThread=new Thread(new MyRunnable());
mThread.start();
}
public static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("");
}
}
3.直接使用函数体
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("");
}
});
4.使用ThreadFractory
ThreadFactory factory=new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r,"自定义线程-1");
}
};
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println("doing something");
}
};
Thread thread=factory.newThread(runnable);
thread.start();
5.实现Callable接口
FutureTask<String> task=new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName());
return "done";
}
});
new Thread(task).start();
6.使用线程池
Executors.newCachedThreadPool().submit(new Runnable() {
@Override
public void run() {
System.out.println("doing something");
}
});
上大致讲述6种开启多线程的方式,部分开启方式只是其他方式的简写或是特例。
在Android种最常用的当属第6种,使用线程池创建线程。你可能会说还有AsyncTask、HandlerThread等等,这种后续文章会说明具体的使用场景。
了解了java程序如何开启一个新的线程后,我们就会面对一个新的问题,当两个线程同时操作一个共享变量时,变量在内存中究竟会发生怎样的变化呢,为了弄清楚这个问题,我们需要先研究一下java的内存模型。
Java的内存模型(JMM)
在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的。
image.png
在Java中所有的变量都存储在主内存(Main Memory)中。每个线程有自己的工作内存(Working Memory,工作内存往往是CPU内的高速缓存),线程的工作内存中保存了该线程使用到的变量在的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间数据的传递都需要通过主内存来完成。
总结一下就是,一个线程对一个共享变量的操作大致分为三步
- 1.当前线程首先从主内存拷贝共享变量到自己的工作内存
- 2然后对工作内存里的变量进行处理
- 3.处理完后更新变量值到主内存
上述三步操作是不具原子性的,即:当线程A在修改了共享变量C的值后,如何还未将共享变量C的值从工作内存刷新到主内存,此时线程B开始读取共享变量C,线程B读取到的值就是共享变量C未被线程A修改之前的值。
如果线程B的设计目的是,在线程A操作变量C的基础上在做进一步的操作,实际上到了这一步程序已经出现了bug,因为线程B读取到的值还是共享变量C未被线程A修改之前的值,那么如何保证操作的原子性呢?答案就是锁
Java线程安全与锁
在java中可以通过Synchronized关键字来给方法或代码块加锁,使其保护资源的互斥访问。
synchronized 关键字代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。
Synchronized在内存中的表现
1.被修饰代码块或方法会把在 Synchronized 块内使用到的变量从线程的工作内存中清除,在 Synchronized 块内使用该变量时就不会从线程的工作内存中获取了,而是直接从主内存中获取,退出 Synchronized 块,则会把 Synchronized 块内对共享变量的修改刷新到主内存。
注意:被Synchronized修饰的方法,如果不指定监视器,则默认监视器就是这个类。线程在调用方法时,会首先获取该方法的监视器,如果该监视器已经被其他线程获取,那么此线程将会阻塞。
可以通过如下代码指定代码块的监视器,其中Object就是指定的监视器,它需要是一个Object的类型对象
synchronized (Objet) {}
看一个例子,在程序中让两个线程分别累加两个不同的变量,每隔一秒钟,输出结果。
private void run() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) { countA(); }
}
}, "线程A").start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) { countB(); }
}
}, "线程B").start();
}
private int countA = 0;
private int countB = 0;
//累加方法A加锁,防止其他线程篡改结果
private synchronized void countA() {
try {
countA++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--" + countA);
}
//累加方法B加锁,防止其他线程篡改结果
private synchronized void countB() {
try {
countB++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--" + countB);
}
这段代码实际的执行结果和预想的结果,大不相同。只有线程A可以正常输出结果,线程B无法执行。
屏幕快照 2019-06-07 17.50.12.png
为什么会出现这样的结果?
原因在于,synchronized修饰的方法,默认监视器是当前的类,代码中countA()与countB()的监视器都是同一个类。线程B在执行countB()时,监视器始终被线程A持有,线程B一直处于等待状态。
知道了原因,我们就可以为countA()和countB()指定不同监视器,即可正常执行。
private final Object monitorA=new Object();
private final Object monitorB=new Object();
//累加方法A加锁,并指定监视器
private void countA() {
synchronized (monitorA) {
try {
countA++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--" + countA);
}
}
//累加方法B加锁,并指定监视器
private void countB() {
synchronized (monitorB) {
try {
countB++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--" + countB);
}
}
其他Java线程同步 相关API的解释
1.interrupt()
中断线程,仅仅设置线程的中断标志为 true 并立即返回,线程的运行状态并不会发生变化。
2.isInterrupted()
检测当前线程是否被中断,如果是返回 true,否则返回 false。
3.interrupted()
检测当前线程是否被中断,如果是返回 true,否则返回 false。与isInterrupted 不同的是,该方法发现当前线程被中断后会清除中断标志。
注意:当一个线程处于休眠(sleep)、等待(wait、join)的时候,如果其他线程中断了它,则处于休眠的线程会立即抛出 java.lang.InterruptedException 异常,并重置中断状态。Java 中的线程中断只是简单设置中断标志,至于剩下的事情就需要程序员自己来做,比如根据中断标志来判断是否退出执行。
在Android中如果只希望线程休眠而不希望被打断,可以使用SystemClock.sleep()来替代Thread.sleep()
4.wait()
wait()是Object的方法,也就是说几乎所有的对象都有该方法,当一个线程调用了一个对象的wait方法,该线程就会被挂起,直到出现下列情况:
1.其他的线程调用该对象的notify()或notifyAll()方法。
2.其他线程调用了该线程的interrupt()方法,如果该线程没有指明如何处理中断事件,则会抛出InterruptedException
需要注意的是在调用对象的wait()时,如果没有获得事先获得该对象的监视锁,则会抛出IllegalMonitorStateException。
5.wait(long timeout)与wait(long timeout,int naos)
该方法相比 wait() 方法多一个超时参数,不同在于如果一个线程调用了共享对象的该方法挂起后没有在指定的 timeout ms 时间内被其它线程调用该共享变量的 notify() 或者 notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。需要注意的是如果在调用该函数时候 timeout 传递了负数会抛出 IllegalArgumentException 异常。
wait(long timeout,int naos)内部是调用 wait(long timeout),只是当 nanos > 0 时让参数一递增 1,用处不多,不再过多介绍。
6.notify()
一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程,一个共享变量上可能会有多个线程在等待,具体唤醒哪一个等待的线程是随机的。 类似 wait 系列方法,只有当前线程已经获取到了该共享变量的监视器锁后,才可以调用该共享变量的 notify() 方法,否者会抛出 IllegalMonitorStateException 异常。
7.notify()
不同于 nofity() 方法在共享变量上调用一次就会唤醒在该共享变量上调用 wait 系列方法被挂起的一个线程,notifyAll() 则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。
8.Thread.join()
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
9.Thread.yield()
当一个线程执行了yield()方法后,就会进入Runnable(就绪状态),【不同于sleep()和join()方法,因为这两个方法是使线程进入阻塞状态】。除此之外,yield()方法还与线程优先级有关,当某个线程调用yield()方法时,就会从运行状态转换到就绪状态后,CPU从就绪状态线程队列中只会选择与该线程优先级相同或者更高优先级的线程去执行。
下面使用一个经典的案例来加深对线程各种API的理解和使用。
生产、消费者模型
假设这样一种场景,一个工厂有一条流水线和三个车间,其中两个生产车间一个包装车间。流水线上最多只能有5件商品,超过5件商品,生产车间需要停产,但是如果生产车间多次停产,则认为产能过剩,需要停止一条生产车间。
这样的场景我们大致可以简化为下面代码:
//产品流水线
private final LinkedBlockingDeque<String> line = new LinkedBlockingDeque<>();
//流水线上最多可容纳5个产品,超过上限,则需要停产
private final static int MAX_SIZE = 5;
//累计休息次数,超过5次,则停产
private int count = 0;
private Thread addThread, removeThread, addThread2;
//产品生产车间,用于生产产品打上标签,并放入流水线中
private void addProduct() {
addThread = new Thread(new Runnable() {
@Override
public void run() {
//让程序一直执行
while (true) {
synchronized (line) {
//流水线上产品已满,停止生产,进入等待状态
while (line.size() == MAX_SIZE) {
try {
System.out.println(Thread.currentThread().getName() + "休息");
count++;
if (count >= 5) {
//5次停产后,打断生产车间2的工作
addThread2.interrupt();
}
//当前线程进入等待状态
line.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int sign = new Random().nextInt(10);
line.add("产品" + sign);
System.out.println(Thread.currentThread().getName() + "生产了:产品" + sign);
//唤醒包装车间
line.notifyAll();
}
try {
//生产完一个产品,休息一秒
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "生产车间1:");
addThread.start();
}
//产品包装车间,从流水线中取出产品,并将其包装好
private void removeProduct() {
removeThread = new Thread(new Runnable() {
@Override
public void run() {
//让程序一直执行
while (true) {
synchronized (line) {
//流水线上没有产品了,停止包装,进入等待状态
while (line.size() == 0) {
try {
System.out.println("停止包装");
//当前线程进入等待状态
line.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
System.out.println(Thread.currentThread().getName() + "包装了" + line.take());
//唤醒生产车间
line.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//包装完一个产品,休息两秒
try {
Thread.sleep(1300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "包装车间:");
removeThread.start();
}
//生产车间2,当生产车间5次停止生产后,会关闭此车间。
private void addProduct2() {
addThread2 = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
synchronized (line) {
try {
while (line.size() == MAX_SIZE) {
//当前线程进入等待状态
System.out.println(Thread.currentThread().getName() + "休息");
count++;
if (count>=5){
Thread.currentThread().interrupt();
}
line.wait();
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "停止生产");
return;
}
int sign = new Random().nextInt(10);
line.add("产品" + sign);
System.out.println(Thread.currentThread().getName() + "生产了:产品" + sign);
//唤醒包装车间
line.notifyAll();
}
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "停止生产");
//如果被中断的线程正在睡眠,则通过return终止线程
return;
}
}
}
}, "生产车间2:");
addThread2.start();
}