线程
Java 多线程和线程同步
进程和线程
进程和线程
- 操作系统中运⾏多个软件
- ⼀个运⾏中的软件可能包含多个进程
- ⼀个运⾏中的进程可能包含多个线程
CPU 线程和操作系统线程
- CPU 线程
- 多核 CPU 的每个核各⾃独⽴运⾏,因此每个核⼀个线程
- 「四核⼋线程」:CPU 硬件⽅在硬件级别对 CPU 进⾏了⼀核多线程的⽀持(本质上依然是每个核⼀个线程)
- 操作系统线程:操作系统利⽤时间分⽚的⽅式,把 CPU 的运⾏拆分给多条运⾏逻辑,即为操作系统的线程
- 单核 CPU 也可以运⾏多线程操作系统
- CPU 线程
线程是什么:按代码顺序执⾏下来,执⾏完毕就结束的⼀条线
- UI 线程为什么不会结束?因为它在初始化完毕后会执⾏死循环,循环的内容是刷新界⾯
多线程的使用
Thread 和 Runnable
1 | Thread thread = new Thread() { |
1 | Runnable runnable = new Runnable() { |
ThreadFactory
1 | ThreadFactory factory = new ThreadFactory() { |
Executor 和线程池
常⽤: newCachedThreadPool()
1 | Runnable runnable = new Runnable() { |
短时批量处理: newFixedThreadPool()
1 | ExecutorService executor = Executors.newFixedThreadPool(20); |
Callable 和 Future
1 | Callable<String> callable = new Callable<String>() { |
线程同步与线程安全
synchronized
synchronized ⽅法
1 | private synchronized void count(int newValue) { |
synchronized 代码块
1 | private void count(int newValue) { |
1 | synchronized (monitor1) { |
synchronized 的本质
保证方法内部或代码块内部资源(数据)的互斥访问。即同一时间、由同一个Monitor监视的代码,最多只能有一个线程在访问
保证线程之间对监视资源的数据同步。即,任何线程在获取到Monitor后的第一时间,会先将共享内存中的数据复制到自己的缓存中;任何线程在释放Monitor的第一时间,会先将缓存中的数据复制到共享内存中。
volatile
- 保证加了 volatile 关键字的字段的操作具有原⼦性和同步性,其中原⼦性相当于实现了针对单⼀字段的线程间互斥访问。因此 volatile 可以看做是简化版的 synchronized。
- volatile 只对基本类型 (byte、char、short、int、long、float、double、boolean) 的赋值操作和对象的引用赋值操作有效。
java.util.concurrent.atomic
包:
下面有
AtomicInteger``AtomicBoolean
等类,作用和volatile基本一致,可以看做是通用版的volatile。1
2
3AtomicInteger atomicInteger = new AtomicInteger(0);
...
atomicInteger.getAndIncrement();
Lock/ReentrantReadWriteLock
同样是「加锁」机制。但使⽤⽅式更灵活,同时也更麻烦⼀些。
1
2
3
4
5
6
7
8
9Lock lock = new ReentrantLock();
...
lock.lock();
try {
x++;
} finally {
lock.unlock();
}finally 的作⽤:保证在⽅法提前结束或出现 Exception 的时候,依然能正常释放锁。
⼀般并不会只是使⽤
Lock
,⽽是会使⽤更复杂的锁,例如ReadWriteLock
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
private int x = 0;
private void count() {
writeLock.lock();
try {
x++;
} finally {
writeLock.unlock();
}
}
private void print(int time) {
readLock.lock();
try {
for (int i = 0; i < time; i++) {
System.out.print(x + " ");
}
System.out.println();
} finally {
readLock.unlock();
}
}
线程安全问题的本质:
在多个线程访问共同的资源时,在某⼀个线程对资源进⾏写操作的中途(写⼊已经开始,但还没结束),其他线程对这个写了⼀半的资源进⾏了读操作,或者基于这个写了⼀半的资源进⾏了写操作,导致出现数据错误。
锁机制的本质:
通过对共享资源进⾏访问限制,让同⼀时间只有⼀个线程可以访问资源,保证了数据的准确性。
不论是线程安全问题,还是针对线程安全问题所衍⽣出的锁机制,它们的核⼼都在于共享的资源,⽽不是某个⽅法或者某⼏⾏代码。
线程异步
应用启动时,系统会为应用创建一个名为“主线程”的执行线程( UI
线程)。 此线程非常重要,因为它负责将事件分派给相应的用户界面小部件,其中包括绘图事件。 此外,它也是应用与 Android UI
工具包组件(来自 android.widget
和 android.view
软件包的组件)进行交互的线程。
系统不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI
线程中实例化,并且对每个组件的系统调用均由该线程进行分派。 因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown()
或生命周期回调方法)始终在进程的 UI
线程中运行。
Android 的单线程模式必须遵守两条规则:
- 不要阻塞
UI
线程 - 不要在
UI
线程之外访问Android UI
工具包
为解决此问题,Android 提供了几种途径来从其他线程访问 UI
线程:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
AsyncTask
AsyncTask
封装了Thread
和Handler
,并不适合特别耗时的后台任务,对于特别耗时的任务来说,建议使用线程池。
基本使用
方法 | 说明 |
---|---|
onPreExecute() |
异步任务执行前调用,用于做一些准备工作 |
doInBackground(Params...params) |
用于执行异步任务,此方法中可以通过publishProgress 方法来更新任务的进度,publishProgress 会调用onProgressUpdate 方法 |
onProgressUpdate |
在主线程中执行,后台任务的执行进度发生改变时调用 |
onPostExecute |
在主线程中执行,在异步任务执行之后 |
1 | import android.os.AsyncTask; |
- 异步任务的实例必须在
UI
线程中创建,即AsyncTask
对象必须在UI
线程中创建。 execute(Params... params)
方法必须在UI
线程中调用。- 不要手动调用
onPreExecute()
,doInBackground()
,onProgressUpdate()
,onPostExecute()
这几个方法。 - 不能在
doInBackground()
中更改UI
组件的信息。 - 一个任务实例只能执行一次,如果执行第二次将会抛出异常。
execute()
方法会让同一个进程中的AsyncTask
串行执行,如果需要并行,可以调用executeOnExcutor
方法。
工作原理
AsyncTask.java
1 |
|
sDefaultExecutor
是一个串行的线程池,一个进程中的所有的 AsyncTask
全部在该线程池中执行。AysncTask
中有两个线程池(SerialExecutor
和 THREAD_POOL_EXECUTOR
)和一个 Handler
(InternalHandler
),其中线程池 SerialExecutor
用于任务的排队,THREAD_POOL_EXECUTOR
用于真正地执行任务,InternalHandler
用于将执行环境从线程池切换到主线程。
AsyncTask.java
1 | private static Handler getMainHandler() { |
HandlerThread
HandlerThread
集成了 Thread
,却和普通的 Thread
有显著的不同。普通的 Thread
主要用于在 run
方法中执行一个耗时任务,而 HandlerThread
在内部创建了消息队列,外界需要通过 Handler
的消息方式通知 HanderThread
执行一个具体的任务。
HandlerThread.java
1 |
|
IntentService
IntentService
可用于执行后台耗时的任务,当任务执行后会自动停止,由于其是 Service
的原因,它的优先级比单纯的线程要高,所以 IntentService
适合执行一些高优先级的后台任务。在实现上,IntentService
封装了 HandlerThread
和 Handler
。
IntentService.java
1 |
|
IntentService
第一次启动时,会在 onCreatea
方法中创建一个 HandlerThread
,然后使用的 Looper
来构造一个 Handler
对象 mServiceHandler
,这样通过 mServiceHandler
发送的消息最终都会在 HandlerThread
中执行。每次启动 IntentService
,它的 onStartCommand
方法就会调用一次,onStartCommand
中处理每个后台任务的 Intent
,onStartCommand
调用了 onStart
方法:
IntentService.java
1 | private final class ServiceHandler extends Handler { |
可以看出,IntentService 仅仅是通过 mServiceHandler 发送了一个消息,这个消息会在 HandlerThread 中被处理。mServiceHandler 收到消息后,会将 Intent 对象传递给 onHandlerIntent 方法中处理,执行结束后,通过 stopSelf(int startId) 来尝试停止服务。(stopSelf() 会立即停止服务,而 stopSelf(int startId) 则会等待所有的消息都处理完毕后才终止服务)。
线程池
线程池的优点有以下:
- 重用线程池中的线程,避免因为线程的创建和销毁带来性能开销。
- 能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
- 能够对线程进行管理,并提供定时执行以及定间隔循环执行等功能。
java 中,ThreadPoolExecutor 是线程池的真正实现:
ThreadPoolExecutor.java
1 | /** |
类型 | 创建方法 | 说明 |
---|---|---|
FixedThreadPool | Executors.newFixedThreadPool(int nThreads) | 一种线程数量固定的线程池,只有核心线程并且不会被回收,没有超时机制 |
CachedThreadPool | Executors.newCachedThreadPool() | 一种线程数量不定的线程池,只有非核心线程,当线程都处于活动状态时,会创建新线程来处理新任务,否则会利用空闲的线程,超时时长为60s |
ScheduledThreadPool | Executors.newScheduledThreadPool(int corePoolSize) | 核心线程数是固定的,非核心线程数没有限制,非核心线程闲置时立刻回收,主要用于执行定时任务和固定周期的重复任务 |
SingleThreadExecutor | Executors.newSingleThreadExecutor() | 只有一个核心线程,确保所有任务在同一线程中按顺序执行 |