Java/Android 进程与线程之 多线程开发(二)

多线程开发

Java多线程开发

1、线程状态

  • 1、NEW 新建状态

当用new操作符创建一个线程后,如Thread thread = new Thread(),此时线程处在新建状态。 当一个线程处于新建状态时,线程中的任务代码还没开始运行。
这里的开始执行具体指调用线程中start方法。(一个线程只能start一次,不能直接调用run方法,只有调用start方法才会开启新的执行线程,接着它会去调用run。在start之后,线程进入RUNNABLE状态,之后还可能会继续转换成其它状态。)

  • 2、RUNNABLE 就绪状态(可执行状态)

a、一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当调用了线程对象的start()方法即启动了线程,此时线程就处于就绪状态。
b、处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他就绪线程竞争CPU,只有获得CPU使用权才可以运行线程

  • 3、BLOCKED 阻塞状态

线程在获取锁失败时(因为锁被其它线程抢占),它会被加入锁的同步阻塞队列,然后线程进入阻塞状态(Blocked)。处于阻塞状态(Blocked)的线程放弃CPU使用权,暂时停止运行。待其它线程释放锁之后,阻塞状态(Blocked)的线程将在次参与锁的竞争,如果竞争锁成功,线程将进入就绪状态(Runnable) 。
注意区分BLOCKED 状态与一般的I/O阻塞:BLOCKED状态特指被 synchronized 块阻塞,即是跟线程同步有关的一个状态。

  • 4、WAITING 等待状态(条件等待状态)
    当线程的运行条件不满足时,通过锁的条件等待机制(调用锁对象的wait()或显示锁条件对象的await()方法)让线程进入等待状态(WAITING)。处于等待状态的线程将不会被cpu执行,除非线程的运行条件得到满足后,其可被其他线程唤醒,进入阻塞状态(Blocked)。调用不带超时的Thread.join()方法也会进入等待状态。
    产生条件
    (1)一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的 Object.notify() 或 Object.notifyAll()。
    (2)一个调用了 Thread.join 方法的线程会等待指定的线程结束。

  • 5、TIMED_WAITING 限时等待状态
    限时等待是WAITING等待状态的一种特例,主要是在时限参数和sleep方法的不同。线程在等待时我们将设定等待超时时间,如超过了我们设定的等待时间,等待线程将自动唤醒进入阻塞状态(Blocked)或就绪状态(Runnable) 。在调用Thread.sleep()方法、带有超时设定的Object.wait()方法、带有超时设定的Thread.join()方法等,线程会进入限时等待状态(TIMED_WAITING)。
    产生条件
    (1)Thread.sleep
    (2)带时限(timeout)的 Object.wait
    (3)带时限(timeout)的 Thread.join

  • 6、TERMINATED 死亡状态
    线程执行完了(completed execution)或者因异常退出了run()方法(exited),该线程结束生命周期

线程阻塞

特点
线程放弃CPU的使用,暂停运行。只有等阻塞原因消除后回复运行;或是被其他线程中断导致该线程退出阻塞状态,同时跑出InterruptedException.

阻塞状态
BLOCKED状态 无法获取同步锁 :synchronic
WAITING状态(TIMED_WAITING状态) 不满足运行条件 :wait/notify、sleep
RUNNABLE状态 正在JVM中执行,占用某个资源 :阻塞式 I/O 操作

阻塞原因
(1)Thread.sleep(int millsecond) 调用 sleep 的线程会在一定时间内将 CPU 资源给其他线程执行,超过睡眠事件后唤醒。与是否持有同步锁无关。进程处于 TIMED_WAITING 状态
(2)线程执行一段同步代码(Synchronic)代码,但无法获取同步锁:同步锁用于实现线程同步执行,未获得同步锁而无法进入同步块的线程处于 BLOCKED 状态
(3)线程对象调用 wait 方法,进入同步块的线程发现运行条件不满足,此时会释放锁,并释放CPU,等待其他线程norify。此时线程处于 WAITING 状态
(4)执行阻塞式I/O操作,等待相关I/O设备(如键盘、网卡等),为了节省CPU资源,释放CPU。此时线程处于RUNNABLE状态。

线程协作
作用

这种协作关系的存在,线程A可以避免在条件不满足时的盲目尝试,也为线程B的顺利执行腾出了资源;同时,在条件满足时,又能及时得到通知。协作关系的存在使得彼此都能受益。

wait() / notify

(1) 当获得锁的线程A进入同步块后发现条件不满足时,应该调用 wait()方法,这时线程A释放锁,并进入所谓的 wait set 中。这时,线程A不再活动,不再参与调度,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程A状态即是 WAITING
(2) 当另一个线程B执行动作使线程A执行条件满足后,它还要执行一个特别的动作,也即是“通知(notify)”处于WAITING状态的线程A,即是把它从 wait set 中释放出来,重新进入到调度队列(ready queue)中。
(3) 如果是 notify,则选取所通知对象的 wait set 中的一个线程释放;
如果是 notifyAll,则释放所通知对象的 wait set 上的全部线程。
(4) 但被通知线程A并不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行

wait()与wait(1000)区别
没有参数的wait()等价于wait(0),表示线程永久等下去,等到天荒地老,除非收到通知。这种完全将再次活动的命运交给通知者可能会导致该线程永远等下去,无法得到执行的机会(当通知者准备执行notify时因某种原因被杀死,持有的锁也释放,此时线程执行的条件满足了,但等待的线程却因收不到通知从而一直处于等待状态)
此时可设置带有参数的wait(1000),等待1秒,相当于等待两个通知,取决于哪个先到:

如果在1000毫秒内,线程A收到了线程B的通知而唤醒,则这个闹钟随之失效;
如果超过了1000毫秒还没收到通知,则闹钟将线程A唤醒。

join()

join 的机制中并没有显式的 wait/notify 的调用,但可以视作是一种特殊的,隐式的 wait/notify 机制。
假如有 a,b 两个线程,在 a 线程中执行 b.join(),相当于让 a 去等待 b,此时 a 停止执行,等 b 执行完了,系统内部会隐式地通知 a,使 a 解除等待状态,恢复执行。

Thread.sleep()

进入 TIMED_WAITING 状态的另一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
这种情况下就是完全靠“自带闹钟”来通知。(sleep方法不会等待协作进程的通知)
sleep方法没有任何同步语义,与锁无关:sleep方法不会等待协作进程的通知,当线程调用sleep方法时带了锁,则sleep期间锁仍为线程所拥有

wait 与 sleep 的区别与联系

wait和sleep均能使线程处于等待状态

  • 定义
    wait方法定义在Object里面,基于对象锁,所有的对象都能使用
    (Java里面每一个对象都有隐藏锁,也叫监视器(monitor)。当一个线程进入一个synchronized方法的时候它会获得一个当前对象的锁。)
    sleep方法定义在Thread里面,是基于当前线程
  • 条件
    wait必须在同步环境(synchronized方法)下使用,否则会报IllegalMonitorStateException异常
    sleep方法可在任意条件下使用
  • 功能
    wait/notify一起使用,用于线程间的通信。wait用于让线程进入等待状态,notify则唤醒正在等待的线程。
    sleep用于暂停当前线程的执行,它会在一定时间内释放CPU资源给其他线程执行,超过睡眠时间则会正常唤醒。
  • 锁的持有
    在同步环境中调用wait方法会释放当前持有的锁
    调用sleep则不会释放锁,一直持有锁(直到睡眠结束)

2、线程控制方法

JVM充分地利用现代多核处理器的强大性能。采用异步调用线程,提高使用性能,缺点就是会造成线程不安全。为了保证线程安全性,即确保Java内存模型的可见性、原子性和有序性。Java主要通过volatile、synchronized和ReentrantLock机制实现线程安全。

2.1 Synchronized

定义

synchronized 规定了同一个时刻只允许一条线程可以进入临界区(互斥性),同时还保证了共享变量的内存可见性。此规则决定了持有同一个对象锁的多个同步块只能串行执行。
Java中的每个对象都可以为锁。

  • 普通同步方法,锁是当前实例对象。
  • 静态同步方法,锁是当前类的class对象。
  • 同步代码块,锁是括号中的对象。

Java中的每个对象都有一个监视器,来监测并发代码的重入,线程进入同步块,线程获得内置锁。内置锁是一个互斥锁,以为着最多只有一个线程能够获取该锁。这个锁由JVM自动获取和释放,线程进入synchronized方法时获取该对象的锁,synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。

原子性与可见性

可见性:一个线程对共享变量的修改,更够及时的被其他线程看到
原子性:即不可再分了,不能分为多步操作。

锁的升级内部机制
1. 对象头和monitor

Java对象在内存中的存储结构主要有一下三个部分:

  • 对象头
  • 实例数据
  • 填充数据
    当创建一个对象时LockObject时,对象的Markword 存储锁的相关信息,包括指向轻量级锁指针、指向重量级锁指针、偏向线程ID 等。
    monitor是线程私有的数据结构,每一个线程都有一个可用monitor列表,同时还有一个全局的可用列表,先来看monitor的内部。
    在这里插入图片描述
  • Owner:初始时为NULL表示当前没有任何线程拥有该monitor,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
  • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor失败的线程。
  • RcThis:表示blocked或waiting在该monitor上的所有线程的个数。
  • Nest:用来实现重入锁的计数。
  • HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
  • Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值:0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

在 java 虚拟机中,线程一旦进入到被synchronized修饰的方法或代码块时,指定的锁对象通过某些操作将对象头中的LockWord指向monitor 的起始地址与之关联,同时monitor 中的Owner存放拥有该锁的线程的唯一标识,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。

2、偏向锁

偏向锁是jdk1.6引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。

在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:
Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.
如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码.
如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。
如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。
如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。

为什么如此做?
因为经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块的。即偏向锁是针对于一个线程而言的,线程获得锁之后就不会进行解锁操作,节省了很多开销。

3、轻量级锁(自旋锁)

当出现有两个线程来竞争锁的话,那么偏向锁就失效了,此时锁就会膨胀,升级为轻量级锁。

特点
轻量级锁主要是自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。

缺点
(1)如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在原地等待空消耗cpu。
(2)本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。

优化

我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁

4、重量级锁(互斥锁)

重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁

当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
这就是说为什么重量级线程开销很大的。互斥锁(重量级锁)也称为阻塞同步、悲观锁

可重入锁 ()和 不可重入锁(自旋锁)

可重入锁
定义
当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。

实现原理

重入锁的一种实现方法是为每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;
当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;
而如果同一个线程再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;
当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。

不可重入锁(自旋锁)
当一个线程调用print()方法时,获得了锁,这时就无法再调用doAdd()方法,这时必须先释放锁才能调用,所以称这种锁为不可重入锁,也叫自旋锁

类锁与对象锁

对象锁
对象锁是用于对象实例方法,或者一个对象实例上的

类锁
类锁是用于类的静态方法或者一个类的class对象上的

对象锁与类锁的区别

  • 对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步
  • 类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的

2.2 volatile

定义

对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。
如果需要在其他线程中立即可见,需要使用 volatile 关键字

原理

对于非volatile变量进行读写时,每个写成先从主存拷贝变量到线程缓存中,执行完操作再保存到主存中。

volatile变量保证每次读写变量都是不经过缓存而是直接从内存读写数据。volatile变量不会将对该变量的操作与其他内存操作一起重排序,能及时更新到主存;且因该变量存储在主存上,所以总会返回最新写入的值。

特性
  • 1、保证此变量对所有的线程的可见性,无法保证原子性。
    当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存更新
  • 2、禁止指令重排序优化。
    有volatile修饰的变量,赋值后多执行了一个“load and save”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)
  • 3、性能较低
    volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不乱序执行
  • 4、轻量级sychronized
    在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制

2.3 ReentrantLock

定义

ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力
主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术

公平锁模型

  • 初始化时,state=0,表示没有线程占用资源。线程A请求锁。
  • 线程A 获得锁,state原子性+1 并执行任务。线程B请求锁。
  • 线程B无法获得锁,生成节点进行排队(Node队列)。线程A再次请求锁。
  • 此时的线程A不需要排队,直接得到锁,执行任务,state原子性+1(可重入锁:一个线程在* * 获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加)。
  • 线程A释放了一次锁,则state原子性 -1,只有当线程A 将锁全部释放,state=0时,其他线程才有机会获取锁,此时会通知队列唤醒 线程B节点,使线程 B 可以参与竞争。
  • 若线程B竞争获得锁,则对应结点从队列中删除。

不公平锁模型
当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续乖乖休眠了。
即公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序(队列的先后顺序)来依次获得锁。而不公平锁则不用按照申请锁的时间顺序获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

Synchronized & ReentrantLock & Volatile 区别

1)Synchronized &ReentrantLock 区别

  • AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
  • synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。
  • 当然Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。

(2)Synchronized & Volatile 区别

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

3、基本线程类

3.1 Thread

3.1.1 Thread类的属性

Thread类实现了Runnable接口,在Thread类中,有一些比较关键的属性

public class Thread implements Runnable{
	private char name[];//表示Thread名字,可以通过Thread构造器中的参数指定线程的名字
	private int priority;//线程的优先级(最大值为10,最小值为1,默认为5)
	// 守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
	private boolean daemon = false;//该线程是否为守护线程
	private Runnable target;//要执行的任务
}
3.1.2 Thread类的方法
// start() 用来启动一个线程,实现多线程,当调用start方法后,系统会开启一个新线程用来执行用户定义的子任务,并为响应线程分配资源。这时线程处于就绪状态,但并没有运行,一旦得到cpu时间片,就开始执行run方法(run()称为线程体,包含要执行这个线程的内容,run()方法运行结束则线程终止)
public static Thread.start()

// run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
public static Thread.run()

// 当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)
// 调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
// 注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
public static Thread.yield() 

// sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
// 但是有一点要非常注意,sleep方法不会释放锁(相当于一直持有该对象的锁),也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
// 还有一点要注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
sleep(long millis)     //参数为毫秒
sleep(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒

// 在一个线程中调用other.join(),将等待other执行完后才继续本线程。  
// 假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间。  
join()
join(long millis)     //参数为毫秒
join(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒

// interrupt()是Thread类的一个实例方法,用于中断本线程。这个方法被调用时,会立即将线程的中断标志设置为“true”。所以当中断处于“阻塞状态”的线程时,由于处于阻塞状态,中断标记会被设置为“false”,抛出一个 InterruptedException。所以我们在线程的循环外捕获这个异常,就可以退出线程了。
// interrupt()并不会中断处于“运行状态”的线程,它会把线程的“中断标记”设置为true,所以我们可以不断通过isInterrupted()来检测中断标记,从而在调用了interrupt()后终止线程,这也是通常我们对interrupt()的用法。
public interrupte()
3.1.3 中断机制
  • java中的线程中断机制是一种协作机制。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。
  • public void interrupt();//每个线程都有个boolean类型的中断状态。当使用Thread的interrupt()方法时,线程的中断状态会被设置为true。
  • 当一个线程中断另一个线程时,被中断的线程不一定要立即停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方便的时候停止它正在做的事情。interrupt方法,就是告诉线程,我需要中断你,该方法调用之后,线程并不会立刻终止,而是在合适的时机终止。什么时机呢?
  • 机制一:如果该线程处在可中断状态下,(例如Thread.sleep(), Thread.join()或 Object.wait()),那么该线程会立即被唤醒,同时会收到一个InterruptedException,如果是阻塞在io上,对应的资源会被关闭。
  • 机制二:如果该线程处在不可中断状态下,即没有调用上述api,处于运行时的进程。那么java只是设置一下该线程的interrupt状态,其他事情都不会发生,如果该线程之后会调用行数阻塞API,那到时候线程会马会上跳出,并抛出InterruptedException,接下来的事情就跟第一种状况一致了。如果不会调用阻塞API,那么这个线程就会一直执行下去。在被中断线程中运行的代码以后可以轮询中断状态,看看它是否被请求停止正在做的事情。中断状态可以通过 Thread.isInterrupted()来读取,并且可以通过一个名为Thread.interrupted() 的操作读取和清除。

怎么利用中断机制结束线程?

public class InterruptedExample {

    public static void main(String[] args) throws Exception {
        InterruptedExample interruptedExample = new InterruptedExample();
        interruptedExample.start();
    }

    public void start() {
        MyThread myThread = new MyThread();
        myThread.start();

        try {
        //当Thread 处于 sleep 后处于阻塞状态,收到中断请求会跑出InterruptedException异常
            Thread.sleep(3000);
            myThread.cancel();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private class MyThread extends Thread{

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                // 线程循环执行打印一些信息,使用isInterrupted判断线程是否被中断,若中断则结束线程
                    System.out.println("test");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                	// 阻塞状态下的线程抛出异常后则会被终止
                    System.out.println("interrupt");
                    // 抛出InterruptedException后中断标志被清除(中断标志 重新设置为false)
                    // 标准做法是再次调用interrupt恢复中断,正确情景下为true
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("stop");
        }

        public void cancel(){
        //对线程调用interrupt()方法,不会真正中断正在运行的线程,
        //只是发出一个请求,由线程在合适时候结束自己。
            interrupt();
        }
    }
}

3.2 Runnable接口

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。


package com.thread;
 
public class RunnableThreadTest implements Runnable
{
 
	private int i;
	public void run()
	{
		for(i = 0;i <100;i++)
		{
			System.out.println(Thread.currentThread().getName()+" "+i);
		}
	}
	public static void main(String[] args)
	{
		for(int i = 0;i < 100;i++)
		{
			System.out.println(Thread.currentThread().getName()+" "+i);
			if(i==20)
			{
				RunnableThreadTest rtt = new RunnableThreadTest();
				new Thread(rtt,"新线程1").start();
				new Thread(rtt,"新线程2").start();
			}
		} 
	} 
}

3.3 Callable

通过Callable和FutureTask创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

package com.thread;
 
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class CallableThreadTest implements Callable<Integer>
{
 
	public static void main(String[] args)
	{
		// 创建Callable实现体的实例,使用FutureTask类包装Callable对象
		CallableThreadTest ctt = new CallableThreadTest();
		FutureTask<Integer> ft = new FutureTask<>(ctt);
		for(int i = 0;i < 100;i++)
		{
			System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
			if(i==20)
			{
			//使用FutureTask对象作为Thread对象的target创建并启动新线程
				new Thread(ft,"有返回值的线程").start();
			}
		}
		try
		{
		//调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
			System.out.println("子线程的返回值:"+ft.get());
		} catch (InterruptedException e)
		{
			e.printStackTrace();
		} catch (ExecutionException e)
		{
			e.printStackTrace();
		}
 
	}
 
	@Override
	public Integer call() throws Exception
	{
	// call 方法即为线程的执行体,并且拥有返回值
		int i = 0;
		for(;i<100;i++)
		{
			System.out.println(Thread.currentThread().getName()+" "+i);
		}
		return i;
	} 
}

在这里插入图片描述

4、高级多线程控制类

Java1.5提供了一个非常高效实用的多线程包:java.util.concurrent, 提供了大量高级工具,可以帮助开发者编写高效、易维护、结构清晰的Java多线程程序。

4.1 ThreadLocal类

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,副本之间相互独立,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
原理
ThreadLocal类中有1个Map(称:ThreadLocalMap):用于存储每个线程 & 该线程设置的存储在ThreadLocal变量的值
1、ThreadLocalMap的键Key = 当前ThreadLocal实例、值value = 该线程设置的存储在ThreadLocal变量的值
2、该key是 ThreadLocal对象的弱引用;当要抛弃掉ThreadLocal对象时,垃圾收集器会忽略该 key的引用而清理掉ThreadLocal对象

4.2 Semaphore — 控制并发线程数

在多线程对一个(多个)公共资源进行访问的场景下,
信号量是一个非负整数(表示可以并发访问公共资源的线程数),所有通过它的线程都会将该整数减一(可使用的公共资源数目-1),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一(Semaphore>0);要么一直等下去(Semaphore<=0),直到信号量大于0或超时。Release(释放)实际上是在信号量上执行加操作,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的公共资源。

解析:厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项

4.2 CAS

CAS:比较并交换
比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

缺点

循环时间长开销很大。

只能保证一个共享变量的原子操作。

ABA问题

CAS 的使用流程通常如下:1)首先从地址 V 读取值 A;2)根据 A 计算目标值 B;3)通过 CAS 以原子的方式将地址 V 中的值从 A 修改为 B。
但是在第1步中读取的值是A,并且在第3步修改成功了,我们就能说它的值在第1步和第3步之间没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题

Android多线程开发

1、复合使用

1.1 AsyncTask

简介

一个Android已封装好的轻量级异步类,属于抽象类,使用时需要实现子类。用于
实现多线程,
AsyncTask的实现原理 = 线程池 + Handler
其中:线程池用于线程调度、复用 & 执行任务;Handler 用于异步通信
其内部封装了2个线程池 + 1个Handler
在这里插入图片描述

方法介绍

在这里插入图片描述

方法执行顺序

在这里插入图片描述

使用步骤

步骤1:创建AsyncTask子类

  private class MyTask extends AsyncTask<Params, Progress, Result> {

        ....

      // 方法1:onPreExecute()
      // 作用:执行 线程任务前的操作
      // 注:根据需求复写
      @Override
      protected void onPreExecute() {
           ...
        }

      // 方法2:doInBackground()
      // 作用:接收输入参数、执行任务中的耗时操作、返回 线程任务执行的结果
      // 注:必须复写,从而自定义线程任务
      @Override
      protected String doInBackground(String... params) {

            ...// 自定义的线程任务

            // 可调用publishProgress()显示进度, 之后将执行onProgressUpdate()
             publishProgress(count);
              
         }

      // 方法3:onProgressUpdate()
      // 作用:在主线程 显示线程任务执行的进度
      // 注:根据需求复写
      @Override
      protected void onProgressUpdate(Integer... progresses) {
            ...

        }

      // 方法4:onPostExecute()
      // 作用:接收线程任务执行结果、将执行结果显示到UI组件
      // 注:必须复写,从而自定义UI操作
      @Override
      protected void onPostExecute(String result) {

         ...// UI操作

        }

      // 方法5:onCancelled()
      // 作用:将异步任务设置为:取消状态
      @Override
        protected void onCancelled() {
        ...
        }
  }

步骤2:创建Async子类的实例对象(任务实例)

  MyTask mTask = new MyTask();

步骤3:手动调用execute()从而执行异步线程任务

a.必须在UI线程中调用
b.同一个AsyncTask实例对象只能执行1次,若执行第2次会抛出异常
c.执行任务中,系统会系统会自动调用AsyncTask的一系列方法:onPreExecute() 、doInBackground()、onProgressUpdate() 、onPostExecute()

mTask.execute();

实例
需求:
点击按钮 则 开启线程执行线程任务
显示后台加载进度
加载完毕后更新UI组件
期间若点击取消按钮,则取消加载

    private class MyTask extends AsyncTask<String, Integer, String> {

        // 方法1:onPreExecute()
        // 作用:执行 线程任务前的操作
        @Override
        protected void onPreExecute() {
            text.setText("加载中");
            // 执行前显示提示
        }


        // 方法2:doInBackground()
        // 作用:接收输入参数、执行任务中的耗时操作、返回 线程任务执行的结果
        // 此处通过计算从而模拟“加载进度”的情况
        @Override
        protected String doInBackground(String... params) {

            try {
                int count = 0;
                int length = 1;
                while (count<99) {

                    count += length;
                    // 可调用publishProgress()显示进度, 之后将执行onProgressUpdate()
                    publishProgress(count);
                    // 模拟耗时任务
                    Thread.sleep(50);
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }

            return null;
        }

        // 方法3:onProgressUpdate()
        // 作用:在主线程 显示线程任务执行的进度
        @Override
        protected void onProgressUpdate(Integer... progresses) {

            progressBar.setProgress(progresses[0]);
            text.setText("loading..." + progresses[0] + "%");

        }

        // 方法4:onPostExecute()
        // 作用:接收线程任务执行结果、将执行结果显示到UI组件
        @Override
        protected void onPostExecute(String result) {
            // 执行完毕后,则更新UI
            text.setText("加载完毕");
        }

        // 方法5:onCancelled()
        // 作用:将异步任务设置为:取消状态
        @Override
        protected void onCancelled() {

            text.setText("已取消");
            progressBar.setProgress(0);

        }
    }
遇到问题

6.1)关于生命周期
问题:AsyncTask不与任何组件绑定生命周期
解决:在Activity 或 Fragment中使用 AsyncTask时,最好在Activity 或 Fragment的onDestory()调用 cancel(boolean);
6.2)关于内存泄露
问题:若AsyncTask被声明为Activity的非静态内部类,当Activity需销毁时,会因AsyncTask保留对Activity的引用 而导致Activity无法被回收,最终引起内存泄露
解决:AsyncTask应被声明为Activity的静态内部类

源码解析

执行任务前,通过 任务队列 线程池类(SerialExecutor)将任务按顺序放入到队列中;
通过同步锁 修饰execute()从而保证AsyncTask中的任务是串行执行的,之后的线程任务执行是 通过任务线程池类(THREAD_POOL_EXECUTOR) 进行的。
任务线程池类(THREAD_POOL_EXECUTOR)实际上是1个已配置好的可执行并行任务的线程池
调用THREAD_POOL_EXECUTOR.execute()实际上是调用线程池的execute()去执行具体耗时任务而该耗时任务则是步骤2中初始化 WorkerRunnable实例对象时复写的call()内容
在call()方法里,先调用 我们复写的doInBackground(mParams)执行耗时操作
再调用postResult(result), 通过 InternalHandler 类 将任务消息传递到主线程;根据消息标识(MESSAGE_POST_RESULT)判断,最终通过finish()调用我们复写的onPostExecute(result),从而实现UI更新操作

1.2 HandlerThread

介绍

HandlerThread是一个Android已封装好的轻量级异步类,用于实现多线程(在工作线程中执行耗时任务)及异步通信、消息传递(工作线程&主线程之间通信)从而保证线程安全
HandlerThread本质上是通过继承Thread类和封装Handler类的使用,从而使得创建新线程和与其他线程进行通信变得更加方便易用(不需要使用"任务线程(如继承Thread类)+Handler"复杂组合)

原理

内部原理 = Thread类 + Handler类机制
(1)通过继承Thread类,快速创建1个带有Looper对象的新工作线程
(2)通过封装Handler类,快速创建Handler&与其他线程进行通信

在这里插入图片描述

实例
public class MainActivity extends AppCompatActivity {

    Handler mainHandler,workHandler;
    HandlerThread mHandlerThread;
    TextView text;
    Button button1,button2,button3;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 显示文本
        text = (TextView) findViewById(R.id.text1);

        // 创建与主线程关联的Handler
        mainHandler = new Handler();

        /**
          * 步骤1:创建HandlerThread实例对象
          * 传入参数 = 线程名字,作用 = 标记该线程
          */
        mHandlerThread = new HandlerThread("handlerThread");

        /**
         * 步骤2:启动线程
         */
        mHandlerThread.start();

        /**
         * 步骤3:创建工作线程Handler & 复写handleMessage()
         * 作用:关联HandlerThread的Looper对象、实现消息处理操作 & 与其他线程进行通信
         * 注:消息处理操作(HandlerMessage())的执行线程 = mHandlerThread所创建的工作线程中执行
         */

        workHandler = new Handler(mHandlerThread.getLooper()){
            @Override
            // 消息处理的操作
            public void handleMessage(Message msg)
            {
                //设置了两种消息处理操作,通过msg来进行识别
                switch(msg.what){
                    // 消息1
                    case 1:
                        try {
                            //延时操作
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 通过主线程Handler.post方法进行在主线程的UI更新操作
                        mainHandler.post(new Runnable() {
                            @Override
                            public void run () {
                                text.setText("我爱学习");
                            }
                        });
                        break;

                    // 消息2
                    case 2:
                        try {
                            Thread.sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        mainHandler.post(new Runnable() {
                            @Override
                            public void run () {
                                text.setText("我不喜欢学习");
                            }
                        });
                        break;
                    default:
                        break;
                }
            }
        };

        /**
         * 步骤4:使用工作线程Handler向工作线程的消息队列发送消息
         * 在工作线程中,当消息循环时取出对应消息 & 在工作线程执行相关操作
         */
        // 点击Button1
        button1 = (Button) findViewById(R.id.button1);
        button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // 通过sendMessage()发送
                // a. 定义要发送的消息
                Message msg = Message.obtain();
                msg.what = 1; //消息的标识
                msg.obj = "A"; // 消息的存放
                // b. 通过Handler发送消息到其绑定的消息队列
                workHandler.sendMessage(msg);
            }
        });

        // 点击Button2
        button2 = (Button) findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // 通过sendMessage()发送
                // a. 定义要发送的消息
                Message msg = Message.obtain();
                msg.what = 2; //消息的标识
                msg.obj = "B"; // 消息的存放
                // b. 通过Handler发送消息到其绑定的消息队列
                workHandler.sendMessage(msg);
            }
        });

        // 点击Button3
        // 作用:退出消息循环
        button3 = (Button) findViewById(R.id.button3);
        button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mHandlerThread.quit();
            }
        });

    }    
}
问题
内存泄露

原因
Handler导致内存泄露:当Handler消息队列 还有未处理的消息 / 正在处理消息时,存在引用关系: “未被处理 / 正处理的消息 -> Handler实例 -> 外部类”
若出现 Handler的生命周期 > 外部类的生命周期 时(即 Handler消息队列 还有未处理的消息 / 正在处理消息 而 外部类需销毁时),将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露。
解决
将Handler子类设置为静态内部类+使用weakReference弱引用持有Activity实例

1.3 IntentService

介绍

Android里的一个封装类,继承四大组件之一Service,用于处理异步请求&实现多线程。线程任务需按顺序、在后台执行。适用于离线下载,不符合多个数据同时请求的场景(所有任务都在同一个Thread looper里执行)

原理

ntentService本质 = Handler + HandlerThread:
1、通过HandlerThread 单独开启1个工作线程:IntentService
2、创建1个内部 Handler :ServiceHandler
3、绑定 ServiceHandler 与 IntentService
4、通过 onStartCommand() 传递服务intent 到ServiceHandler 、依次插入Intent到工作队列中 & 逐个发送给 onHandleIntent()
5、通过onHandleIntent() 依次处理所有Intent对象所对应的任务
因此我们通过复写onHandleIntent() & 在里面 根据Intent的不同进行不同线程操作 即可

实例

步骤1:定义 IntentService的子类

public class myIntentService extends IntentService {

  /** 
    * 在构造函数中传入线程名字
    **/  
    public myIntentService() {
        // 调用父类的构造函数
        // 参数 = 工作线程的名字
        super("myIntentService");
    }

   /** 
     * 复写onHandleIntent()方法
     * 根据 Intent实现 耗时任务 操作
     **/  
    @Override
    protected void onHandleIntent(Intent intent) {

        // 根据 Intent的不同,进行不同的事务处理
        String taskName = intent.getExtras().getString("taskName");
        switch (taskName) {
            case "task1":
                Log.i("myIntentService", "do task1");
                break;
            case "task2":
                Log.i("myIntentService", "do task2");
                break;
            default:
                break;
        }
    }

    @Override
    public void onCreate() {
        Log.i("myIntentService", "onCreate");
        super.onCreate();
    }
   /** 
     * 复写onStartCommand()方法
     * 默认实现 = 将请求的Intent添加到工作队列里
     **/  
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("myIntentService", "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        Log.i("myIntentService", "onDestroy");
        super.onDestroy();
    }
}

步骤2:在Manifest.xml中注册服务

<service android:name=".myIntentService">
            <intent-filter >
                <action android:name="cn.scu.finch"/>
            </intent-filter>
        </service>

步骤3:在Activity中开启Service服务

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

            // 同一服务只会开启1个工作线程
            // 在onHandleIntent()函数里,依次处理传入的Intent请求
            // 将请求通过Bundle对象传入到Intent,再传入到服务里

            // 请求1
            Intent i = new Intent("cn.scu.finch");
            Bundle bundle = new Bundle();
            bundle.putString("taskName", "task1");
            i.putExtras(bundle);
            startService(i);

            // 请求2
            Intent i2 = new Intent("cn.scu.finch");
            Bundle bundle2 = new Bundle();
            bundle2.putString("taskName", "task2");
            i2.putExtras(bundle2);
            startService(i2);

            startService(i);  //多次启动
        }
    }

注意点
1、由于onCreate()只会调用一次 = 只会创建1个工作线程;
2、当多次调用 startService(Intent)时(即 onStartCommand()也会调用多次),其实不会创建新的工作线程,只是把消息加入消息队列中 & 等待执行。
3、所以,多次启动 IntentService 会按顺序执行事件

4、不建议通过 bindService() 启动 IntentService
采用 bindService()启动 IntentService的生命周期如下:

onCreate() ->> onBind() ->> onunbind()->> onDestory()

即,并不会调用onStart() 或 onStartcommand(),故不会将消息发送到消息队列,那么onHandleIntent()将不会回调,即无法实现多线程的操作
此时,你应该使用Service,而不是IntentService

在这里插入图片描述

2、高级使用

线程池(ThreadPool)

为什么用线程池
  • 线程池是一块缓存了一定线程数量的区域,用于复用线程和管理线程(如1、统一分配、调优&监控2、控制线程池的最大并发数)
  • 降低因线程创建&销毁带来的性能开销(重用缓存在线程池的线程)
  • 提高线程响应速度&执行效率:1、重用线程 = 不需创建线程,即可马上执行2、管理线程 = * * 优化线程执行顺序(避免大量线程间因互相抢占系统资源而到只阻塞现象)
  • 提高对线程的管理度
线程池参数介绍

在这里插入图片描述

使用流程
// 1. 创建线程池
   // 创建时,通过配置线程池的参数,从而实现自己所需的线程池
   Executor threadPool = new ThreadPoolExecutor(
                                              CORE_POOL_SIZE,
                                              MAXIMUM_POOL_SIZE,
                                              KEEP_ALIVE,
                                              TimeUnit.SECONDS,
                                              sPoolWorkQueue,
                                              sThreadFactory
                                              );
    // 注:在Java中,已内置4种常见线程池,下面会详细说明

// 2. 向线程池提交任务:execute()
    // 说明:传入 Runnable对象
       threadPool.execute(new Runnable() {
            @Override
            public void run() {
                ... // 线程执行任务
            }
        });

// 3. 关闭线程池shutdown() 
  threadPool.shutdown();
  
  // 关闭线程的原理
  // a. 遍历线程池中的所有工作线程
  // b. 逐个调用线程的interrupt()中断线程(注:无法响应中断的任务可能永远无法终止)

  // 也可调用shutdownNow()关闭线程:threadPool.shutdownNow()
  // 二者区别:
  // shutdown:设置 线程池的状态 为 SHUTDOWN,然后中断所有没有正在执行任务的线程
  // shutdownNow:设置 线程池的状态 为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
  // 使用建议:一般调用shutdown()关闭线程池;若任务不一定要执行完,则调用shutdownNow()
线程池分类

在这里插入图片描述

定长线程池(FixedThreadPool)
1、特点
只有核心线程 & 不会被回收、线程数量固定、任务队列无大小限制(超出的线程任务会在队列中等待)
2、应用场景
控制线程最大并发数
3、具体使用
通过 Executors.newFixedThreadPool() 创建

// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run(){
    System.out.println("执行任务啦");
     }
    };
        
// 3. 向线程池提交任务:execute()
fixedThreadPool.execute(task);
        
// 4. 关闭线程池
fixedThreadPool.shutdown();
定时线程池(ScheduledThreadPool)
1、特点
核心线程数量固定、非核心线程数量无限制(闲置时马上回收)
2、应用场景
执行定时 / 周期性 任务
3、具体使用
通过Executors.newScheduledThreadPool()创建


// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
       public void run(){
              System.out.println("执行任务啦");
          }
    };
// 3. 向线程池提交任务:schedule()
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务

// 4. 关闭线程池
scheduledThreadPool.shutdown();

可缓存线程池(CachedThreadPool)
1、特点
只有非核心线程、线程数量不固定(可无限大)、灵活回收空闲线程(具备超时机制,全部回收时几乎不占系统资源)、新建线程(无线程可用时)
任何线程任务到来都会立刻执行,不需要等待
2、应用场景
执行大量、耗时少的线程任务
3、具体使用
通过Executors.newCachedThreadPool()创建


// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run(){
        System.out.println("执行任务啦");
            }
    };

// 3. 向线程池提交任务:execute()
cachedThreadPool.execute(task);

// 4. 关闭线程池
cachedThreadPool.shutdown();

//当执行第二个任务时第一个任务已经完成
//那么会复用执行第一个任务的线程,而不用每次新建线程。

单线程化线程池(SingleThreadExecutor)
1、特点
只有一个核心线程(保证所有任务按照指定顺序在一个线程中执行,不需要处理线程同步的问题)
2、应用场景
不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作,文件操作等
3、使用
通过Executors.newSingleThreadExecutor()创建


// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run(){
        System.out.println("执行任务啦");
            }
    };

// 3. 向线程池提交任务:execute()
singleThreadExecutor.execute(task);

// 4. 关闭线程池
singleThreadExecutor.shutdown();

线程池执行原则
  • 1.如果线程池中的线程数未达到核心线程数,就会开启一个线程去执行任务;
  • 2.如果线程池中的线程数已经达到核心线程数,而任务队列workQueue未满,则会将任务添加到workQueue任务队列中;
  • 3.如果线程池中的线程数已经达到核心线程数但未超过最大线程数,而且任务队列workQueue已经满,则会开启非核心线程来执行任务;
  • 4.如果线程池中的线程数已经达到最大线程数,那么拒绝该任务,执行饱和策略,抛出RejectedExecutionException异常。

相关方法
ThreadPoolExecutor有两个方法供我们执行,execute()和submit()都可以执行任务。通常我们不需要返回值时使用execute()执行任务,需要返回值时使用submit(),submit()方法返回一个Future,Future可以获得结果,并且可以cancel()取消请求。

execute(): 执行任务,无返回值;
submit(): 执行任务,返回Future,可以获得结果,并且cancel()取消请求。内部也执行execute()的逻辑;
shutdown(): 关闭线程池,不影响已经提交的任务;
shutdownNow(): 关闭线程池,尝试终止正在执行的任务。

面试题总结

1、线程协作,wait与sleep区别。

2、锁(Lock)机制,锁的升级与锁的分类(区别)

3、原子性、可见性?线程同步,Synchronized、volatile与ReentrantLock 区别。

4、ThreadLocal原理,HandleThread、IntentService 原理

5、线程池优点,怎么实现,各参数意义。(结合Okhttp中线程池使用来讲解)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android线程通信可以通过多种方式实现。其中一种常见的方式是使用Handler和Looper机制。通过在主线程中创建Handler对象,可以将消息发送到主线程的消息队列中,然后主线程的Looper会不断地从消息队列中取出消息并处理。在子线程中,可以通过Handler的post方法将消息发送到主线程。这样就实现了主线程和子线程之间的通信。\[2\] 另外,如果需要在子线程之间进行通信,可以使用HandlerThread类。HandlerThread是一个带有Looper的线程,可以在其中创建Handler对象,并通过Handler发送消息到该线程的消息队列中。这样就可以实现子线程之间的通信。\[3\] 总结起来,Android线程通信可以通过Handler和Looper机制实现主线程和子线程之间的通信,也可以使用HandlerThread实现子线程之间的通信。这些机制可以帮助开发者更好地处理多线程编程中的同步问题。\[1\] #### 引用[.reference_title] - *1* [Android线程间通信原理以及多线程](https://blog.csdn.net/a734474820/article/details/125561136)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [【Android线程间通信——Handler消息机制](https://blog.csdn.net/qq_40265247/article/details/123898141)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [android线程间通信的几种方法_Android进程间和线程间通信方式](https://blog.csdn.net/Goals1989/article/details/127966389)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值