3、Android中的线程
线程分为两种:
- UI/Main Thread (主线程)
- Worker Thread(工作线程)
一个线程总是由另一个线程启动,所以总有一个特殊的线程,叫做主线程。它是应用启动并执行的第一个线程。每次启动一个新工作线程,都会从主线程分出一条独立的线。
3.1 UI/Main Thread (主线程)
启动应用程序时,系统会为应用程序创建一个执行线程,称为 "main"。该线程非常重要,因为它负责将事件发送到适当的用户界面小部件,包括绘图事件。与Android UI toolkit (来自Android.widget和Android.view包的组件)交互的线程。
因此,主线程有时也称为UI线程。但是,在特殊情况下,应用程序的主线程可能不是它的UI线程。
注意:构建工具将@MainThread和 @UiThread注释视为可互换的,因此你可以@UiThread 从@MainThread方法中调用方法,反之亦然。但是,在系统应用程序在不同线程上具有多个视图的情况下,UI 线程可能与主线程不同。因此,你应该 @UiThread 使用 @MainThread.
在同一进程中运行的所有组件都在UI线程中实例化。
此外,Android UI toolkit不是线程安全的。因此,你不能从工作线程操作UI—你必须从UI线程对用户界面执行所有操作。因此,Android的单线程模型只有两条规则:
- 不要阻塞UI线程;
- 不要在非UI线程访问 UI 。
3.1.1 阻塞UI线程
如果所有事情都发生在UI线程中,那么执行长时间操作(如网络访问或数据库查询)将阻塞整个UI。
发生ANR的原因:
- Activity超过5秒无响应;
- BroadcastReceiver超过10秒无响应。
3.1.2 Worker Thread操作UI
@Override protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread); //Worker Thread(工作线程) new Thread(new Runnable() { @Override public void run() { //操作UI线程 Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show(); } }).start(); }
运行后直接报错:
2021-10-12 14:47:47.495 4122-4247/com.scc.demo E/AndroidRuntime: FATAL EXCEPTION: Thread-7 Process: com.scc.demo, PID: 4122 java.lang.RuntimeException: Can't toast on a thread that has not called Looper.prepare() at android.widget.Toast$TN.<init>(Toast.java:895) at android.widget.Toast.<init>(Toast.java:205) at android.widget.Toast.makeText(Toast.java:597) at android.widget.Toast.makeText(Toast.java:566) at com.scc.demo.actvitiy.ThreadActivity$1.run(ThreadActivity.java:18) at java.lang.Thread.run(Thread.java:919)
3.2 Worker Thread(工作线程)
因不能阻塞主线程,但是有些耗时操作(如加载图片、网络请求等)非即时相应的则可以通过工作线程来执行。
注意,你不能从UI线程或"主"线程以外的任何线程更新UI。
为了解决这个问题,Android提供了几种从其他线程访问UI线程的方法:
- Activity.runOnUiThread(Runnable)
- View.post(Runnable)
- View.postDelayed(Runnable, long)
3.2.1 样例:子线程访问UI线程
public class ThreadActivity extends ActivityBase{ TextView tvName; @Override protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread); tvName = findViewById(R.id.tv_name); tvName.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { csThread(); startThread(); } }); } private void csThread(){ //Worker Thread(工作线程) new Thread(new Runnable() { @Override public void run() { //这样写直接报错 tvName.setText("我是Worker Thread---行路难!行路难!"); // ------强大的分割线------ // 下面几种方式都没问题 //第一种:Activity.runOnUiThread(Runnable) runOnUiThread(new Runnable() { @Override public void run() { tvName.setText("我是Worker Thread---行路难!行路难!"); Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show(); } }); //第二种:View.post(Runnable) tvName.post(new Runnable() { @Override public void run() { tvName.setText("我是Worker Thread---行路难!行路难!"); Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show(); } }); //第三种:View.postDelayed(Runnable, long) tvName.postDelayed(new Runnable() { @Override public void run() { tvName.setText("我是Worker Thread---行路难!行路难!"); Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show(); } },1000); //第四种:Handler new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { tvName.setText("我是Worker Thread---行路难!行路难!"); Toast.makeText(ThreadActivity.this,"我是Worker Thread",Toast.LENGTH_SHORT).show(); } }); } }).start(); } }
子线程直接操作主线程报错信息:
理论上应该拿 3.1.2 Worker Thread 操作UI 时的报错信息。既然都能通过这种方式解决,就多举一个。
2021-10-12 16:02:51.754 8635-8676/com.scc.demo E/AndroidRuntime: FATAL EXCEPTION: Thread-2 Process: com.scc.demo, PID: 8635 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8798) at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1606) at android.view.View.requestLayout(View.java:25390) ... at android.widget.TextView.checkForRelayout(TextView.java:9719) at android.widget.TextView.setText(TextView.java:6311) ... at com.scc.demo.actvitiy.ThreadActivity$2.run(ThreadActivity.java:31) at java.lang.Thread.run(Thread.java:923)
3.2.2 Android提供了几种从其他线程访问UI线程的方法源码
Activity.runOnUiThread(Runnable) public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action); } else { action.run(); } } View.post(Runnable) public boolean post(Runnable action) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.post(action); } getRunQueue().post(action); return true; } View.postDelayed(Runnable, long) public boolean postDelayed(Runnable action, long delayMillis) { final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { return attachInfo.mHandler.postDelayed(action, delayMillis); } getRunQueue().postDelayed(action, delayMillis); return true; }
你会发现他们都是使用 Handler 来完成的。所以在 3.2.1 的样例中咱可以使用 new Handler() 来完成更新 UI。
3.3 线程的状态
- new:新建状态,new出来,还没有调用start。
- Runnable:可运行状态,调用start进入可运行状态,可能运行也可能没有运行,取决于操作系统的调度。
- Blocked:阻塞状态,被锁阻塞,暂时不活动,阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
- Waiting:等待状态,不活动,不运行任何代码,等待线程调度器调度,wait sleep。
- Timed Waiting:超时等待,在指定时间自行返回。
- Terminated:终止状态,包括正常终止和异常终止。
3.4 开启线程的三种方式
- 1:继承Thread重写run方法。
- 2:实现Runnable重写run方法。
- 3:实现Callable重写call方法。
private void startThread(){ //第一种:继承Thread重写run方法 new MyThread().start(); //第二种:实现Runnable重写run方法 new Thread(new MyRunanble()).start(); //第三种:实现Callable重写call方法 FutureTask<Integer> ft = new FutureTask<Integer>(new MyCallable()); new Thread(ft).start(); } class MyThread extends Thread{ @Override public void run() { MLog.e(this.getClass().getName()); } } class MyRunanble implements Runnable{ @Override public void run() { MLog.e(this.getClass().getName()); } } class MyCallable implements Callable { @Override public Object call() throws Exception { MLog.e(this.getClass().getName()); return null; } }
小结
实现Callable和实现Runnable类似,但是功能更强大,具体表现在:
- 可以在任务结束后提供一个返回值,Runnable不行。
- call方法可以抛出异常,Runnable的run方法不行。
- 可以通过运行Callable得到的Fulture对象监听目标线程调用call方法的结果,得到返回值,(fulture.get(),调用后会阻塞,直到获取到返回值)。
3.5 run()和start()方法区别
- run():方法只是线程的主体方法,和普通方法一样,不会创建新的线程。
- start():只有调用start()方法,才会启动一个新的线程,新线程才会调用run()方法,线程才会开始执行。
3.6 wait、notify、notifyAll
- wait():释放obj的锁,导致当前的线程等待,直接其他线程调用此对象的notify()或notifyAll()方法。
- notify(): 唤醒在此对象监视器上等待的单个线程
- notifyAll(): 通知所有等待该竞争资源的线程
注意:当要调用wait()或notify()/notifyAll()方法时,一定要放到synchronized(obj)代码中,否则会报错java.lang.IllegalMonitorStateException。当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此等待线程虽被唤醒,但仍无法获得obj锁,直到调用线程退出synchronized块,释放obj锁后,其他等待线程才有机会获得锁继续执行。
3.7 join、sleep、wait
- join()方法在等待的过程中释放对象锁。
- sleep()方法在睡眠时不释放对象锁,
- wait():释放对象锁
3.8 线程阻塞
- 1:线程执行了Thread.sleep(int millsecond)方法,放弃CPU,睡眠一段时间,一段时间过后恢复执行;
- 2:线程执行一段同步代码,但无法获得相关的同步锁,只能进入阻塞状态,等到获取到同步锁,才能恢复执行;
- 3:线程执行了一个对象的wait()方法,直接进入阻塞态,等待其他线程执行notify()/notifyAll()操作;
- 4:线程执行某些IO操作,因为等待相关资源而进入了阻塞态,如System.in,但没有收到键盘的输入,则进入阻塞态。
- 5:线程礼让,Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或更高优先级的线程,但并不会使线程进入阻塞态,线程仍处于可执行态,随时可能再次分得CPU时间。
- 6:线程自闭,join()方法,在当前线程调用另一个线程的join()方法,则当前线程进入阻塞态,直到另一个线程运行结束,当前线程再由阻塞转为就绪态。
- 7:线程执行suspend()使线程进入阻塞态,必须resume()方法被调用,才能使线程重新进入可执行状态。
3.9 线程中断
使用 interrupt()中断,但调用interrupt()方法只是传递中断请求消息,并不代表要立马停止目标线程。然后通过抛出InterruptedException来唤醒它。
public class Thread { // 中断当前线程 public void interrupt(); // 判断当前线程是否被中断 public boolen isInterrupt(); // 清除当前线程的中断状态,并返回之前的值 public static boolen interrupted(); }
3.10 线程池ThreadPoolExecutor
线程池的工作原理:线程池可以减少创建和销毁线程的次数,从而减少系统资源的消耗。
当一个任务提交到线程池时:
- 1:首先判断核心线程池中的线程是否已经满了,如果没满,则创建一个核心线程执行任务,否则进入下一步。
- 2:判断工作队列是否已满,没有满则加入工作队列,否则执行下一步。
- 3:判断线程数是否达到了最大值,如果不是,则创建非核心线程执行任务,否则执行饱和策略,默认抛出异常。
3.11 线程池的种类
FixedThreadPool:可重用固定线程数的线程池,只有核心线程,没有非核心线程,核心线程不会被回收,有任务时,有空闲的核心线程就用核心线程执行,没有则加入队列排队。
SingleThreadExecutor:单线程线程池,只有一个核心线程,没有非核心线程,当任务到达时,如果没有运行线程,则创建一个线程执行,如果正在运行则加入队列等待,可以保证所有任务在一个线程中按照顺序执行,和FixedThreadPool的区别只有数量。
CachedThreadPool:按需创建的线程池,没有核心线程,非核心线程有Integer.MAX_VALUE个,每次提交任务如果有空闲线程则由空闲线程执行,没有空闲线程则创建新的线程执行,适用于大量的需要立即处理的并且耗时较短的任务。
ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,用于延时执行任务或定期执行任务,核心线程数固定,线程总数为Integer.MAX_VALUE。
3.12 保证线程安全
线程安全性体现在三个方法:
原子性:提供互斥访问,同一时刻只能有一个线和至数据进行操作。
JDK中提供了很多atomic类,如AtomicInteger\AtomicBoolean\AtomicLong,它们是通过CAS完成原子性。 JDK提供锁分为两种:synchronized依赖JVM实现锁,该关键字作用对象的作用范围内同一时刻只能有一个线程进行操作。另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性是ReentrantLock。
可见性:一个线程对主内存的修改及时被其他线程看到。
JVM提供了synchronized和volatile,volatile的可见性是通过内存屏障和禁止重排序实现的,volatile会在写操作时,在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存;会在读操作时,在读操作前加一条load指令,从内存中读取共享变量。
有序性:指令没有被编译器重排序。
可通过volatile、synchronized、Lock保证有序性。
3.12 volatile、synchronized、Lock、ReentrantLock
volatile:解决变量在多个线程间的可见性,但不能保证原子性,只能用于修饰变量,不会发生阻塞。volatile能屏蔽编译指令重排,不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。多用于并行计算的单例模式。volatile规定CPU每次都必须从内存读取数据,不能从CPU缓存中读取,保证了多线程在多CPU计算中永远拿到的都是最新的值。
synchronized:互斥锁,操作互斥,并发线程过来,串行获得锁,串行执行代码。解决的是多个线程间访问共享资源的同步性,可保证原子性,也可间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。可用来修饰方法、代码块。会出现阻塞。synchronized发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。非公平锁,每次都是相互争抢资源。
lock:一个接口,lock可以让等待锁的线程响应中断。在发生异常时,如果没有主动通过unLock()去释放锁,则可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
ReentrantLoc:可重入锁,锁的分配机制是基于线程的分配,而不是基于方法调用的分配。ReentrantLock有tryLock方法,如果锁被其他线程持有,返回false,可避免形成死锁。对代码加锁的颗粒会更小,更节省资源,提高代码性能。ReentrantLock可实现公平锁和非公平锁,公平锁就是先来的先获取资源。ReentrantReadWriteLock用于读多写少的场合,且读不需要互斥场景。