android判断当前时分秒在制定范围内

您所在的位置:网站首页 手机卡显示仅限紧急呼叫怎么回事 android判断当前时分秒在制定范围内

android判断当前时分秒在制定范围内

2022-11-06 06:05| 来源: 网络整理| 查看: 265

文章目录 (一)Java 多线程开发1.1)线程状态1.2)线程控制方法(1.2.1)Synchronized(1.2.2)Volatile(1.2.3)ReentrantLock 1.3)基本线程类(1.3.1)Thread 类(1.3.2)Runnable 接口(1.3.3)Callable 接口 1.4)高级多线程控制类(1.4.1)ThreadLocal类(1.4.2)原子类(AtomicInteger、AtomicBoolean……)(1.4.3)容器类(1.4.4)Semaphore —— 控制并发线程数(1.4.5)Java 线程池 1.5)Java 并发模型1.6)Java 线程安全1.7)Java 断点续传 (二)Android 多线程开发2.1)基础使用1、继承Thread类2、实现Runnable接口3、Handler 2.2)复合使用1、AsyncTask2、HandlerThread3、IntentService 2.3)高级使用线程池(ThreadPool) 2.4)对比2.5)其他1、Synchronized2、ThreadLocal

(一)Java 多线程开发 1.1)线程状态

(1)Java线程 6 种状态 public static enum Thread.State 用来描述一个线程的状态,包括6种状态 注:这里的六中状态是JVM层面的状态,与OS底层的线程状态不同(OS底层线程状态有5种,如下图) Java 的线程状态倾向于描述线程,而OS的线程状态倾向于描述CPU。 在这里插入图片描述 而对于Java而言,Java的线程类型都来源于Thread 类下的 State 这一内部枚举类中所定义的状态:

NEW 新建状态 当用new操作符创建一个线程后,如Thread thread = new Thread(),此时线程处在新建状态。 当一个线程处于新建状态时,线程中的任务代码还没开始运行。 这里的开始执行具体指调用线程中start方法。(一个线程只能start一次,不能直接调用run方法,只有调用start方法才会开启新的执行线程,接着它会去调用run。在start之后,线程进入RUNNABLE状态,之后还可能会继续转换成其它状态。)RUNNABLE 就绪状态(可执行状态) 也被称为“可执行状态”。一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当调用了线程对象的start()方法即启动了线程,此时线程就处于就绪状态。 处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他就绪线程竞争CPU,只有获得CPU使用权才可以运行线程。比如在单核心CPU的计算机系统中,不可能同时运行多个线程,一个时刻只能有一个线程处于运行状态。对与多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度执行。 除了调用start()方法后让线程变成就绪状态,一个线程阻塞状态结束后也可以变成就绪状态,或者从运行状态变化到就绪状态。 对于Java虚拟机的RUNNABLE状态,包含OS的Ready、Running。(由于现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片”方式进行抢占式轮转调度,其中上下文切换过程很快,因此ready与running状态切换很快,对于RUNNABLE状态,就没有切换的意义了。) 以及部分waiting状态(即OS状态下的阻塞式I/O操作),这些状态可统一归纳为RUNNABLE状态的官方定义: 处于 runnable 状态下的线程正在 Java 虚拟机中执行,但它可能正在等待来自于操作系统的其它资源,比如处理器或者其他I/O设备等。(CPU、硬盘、网卡等资源,若在为线程服务,就认为线程在"执行")BLOCKED 阻塞状态 线程在获取锁失败时(因为锁被其它线程抢占),它会被加入锁的同步阻塞队列,然后线程进入阻塞状态(Blocked)。处于阻塞状态(Blocked)的线程放弃CPU使用权,暂时停止运行。待其它线程释放锁之后,阻塞状态(Blocked)的线程将在次参与锁的竞争,如果竞争锁成功,线程将进入就绪状态(Runnable) 。 注意区分BLOCKED 状态与一般的I/O阻塞:BLOCKED状态特指被 synchronized 块阻塞,即是跟线程同步有关的一个状态。 进程同步 线程同步机制用于解决多线程之间竞争关系——争夺锁。在ava 在语言级直接提供了同步的机制,也即是 synchronized 关键字: synchronized(expression) {……}

它的机制是这样的:对表达式(expresssion)求值(值的类型须是引用类型(reference type)),获取它所代表的对象,然后尝试获取这个对象的锁: 如果能获取锁,则进入同步块执行,执行完后退出同步块,并归还对象的锁(异常退出也会归还);如果不能获取锁,则阻塞在这里,直到能够获取锁。如果一个线程在同步块中,则其他想进入该同步块的进程被阻塞,处于该同步块的Entry Set中,处于BLOCKED状态。 BLOCKED状态官方定义如下:

一个正在阻塞等待一个监视器锁的线程处于这一状态。(A thread that is blocked waiting for a monitor lock is in this state.)

包括两种情况: (1)进入(enter)同步块时阻塞 一个处于 blocked 状态的线程正在等待一个监视器锁以进入一个同步的块或方法。 监视器锁用于同步访问,以达到多线程间的互斥。所以一旦一个线程获取锁进入同步块,在其出来之前,如果其它线程想进入,就会因为获取不到锁而阻塞在同步块之外,这时的状态就是 BLOCKED。 (2)wait 之后重进入(reenter)同步块时阻塞 一个处于 blocked 状态的线程正在等待一个监视器锁,在其调用 Object.wait 方法之后,以再次进入一个同步的块或方法。 过程如下:

调用 wait 方法必须在同步块中,即是要先获取锁并进入同步块,这是第一次 enter。而调用 wait 之后则会释放该锁,并进入此锁的等待队列(wait set)中。当收到其它线程的 notify 或 notifyAll 通知之后,等待线程并不能立即恢复执行,因为停止的地方是在同步块内,而锁已经释放了,所以它要重新获取锁才能再次进入(reenter)同步块,然后从上次 wait 的地方恢复执行。这是第二次 enter,所以叫 reenter。但锁并不会优先给它,该线程还是要与其它线程去竞争锁,这一过程跟 enter 的过程其实是一样的,因此也可能因为锁已经被其它线程据有而导致 BLOCKED。

这两种情况可总结为:当因为获取不到锁而无法进入同步块时,线程处于 BLOCKED 状态。BLOCKED状态可以看做特殊的WAITING,表示等待同步锁的状态。如果有线程长时间处于 BLOCKED 状态,要考虑是否发生了死锁(deadlock)的状况。

WAITING 等待状态(条件等待状态) 当线程的运行条件不满足时,通过锁的条件等待机制(调用锁对象的wait()或显示锁条件对象的await()方法)让线程进入等待状态(WAITING)。处于等待状态的线程将不会被cpu执行,除非线程的运行条件得到满足后,其可被其他线程唤醒,进入阻塞状态(Blocked)。调用不带超时的Thread.join()方法也会进入等待状态。 一个正在无限期等待另一个线程执行一个特别的动作的线程处于这一状态。

一个线程进入 WAITING 状态是因为调用了以下方法:

不带时限的 Object.wait 方法不带时限的 Thread.join 方法

然后会等其它线程执行一个特别的动作,比如:

一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的 Object.notify() 或 Object.notifyAll()。一个调用了 Thread.join 方法的线程会等待指定的线程结束。

进程协作 可以看出,WAITING状态所涉及的不是一个线程的独角戏,相反,它涉及多个线程,具体地讲,这是多个线程间的一种协作机制。wait/notify与join都是线程间的一种协作机制。下面分别介绍wait/notify场景与join场景 (1)wait/notify场景 当获得锁的线程A进入同步块后发现条件不满足时,应该调用 wait()方法,这时线程A释放锁,并进入所谓的 wait set 中。这时,线程A不再活动,不再参与调度,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程A状态即是 WAITING。 现在的问题是:线程A什么时候才能再次活动呢?显然,最佳的时机是当条件满足的时候。 (此时可能存在多个类似线程A这种条件不满足的线程无法执行,与线程B争夺锁资源从而导致饥饿状态) 当另一个线程B执行动作使线程A执行条件满足后,它还要执行一个特别的动作,也即是“通知(notify)”处于WAITING状态的线程A,即是把它从 wait set 中释放出来,重新进入到调度队列(ready queue)中。 如果是 notify,则选取所通知对象的 wait set 中的一个线程释放; 如果是 notifyAll,则释放所通知对象的 wait set 上的全部线程。 但被通知线程A并不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。(这也即是所谓的 “reenter after calling Object.wait”,即BLOCKED状态。)

如果能获取锁,线程A就从 WAITING 状态变成 RUNNABLE 状态;否则,从 wait set 出来,又进入 entry set,线程A就从 WAITING 状态又变成 BLOCKED 状态。

综上,这是一个协作机制,需要两个具有协作关系的线程A、B分别执行wait和notify。显然,这种协作关系的存在,线程A可以避免在条件不满足时的盲目尝试,也为线程B的顺利执行腾出了资源;同时,在条件满足时,又能及时得到通知。协作关系的存在使得彼此都能受益。 这里的协作机制也即经典的消费者-生产者问题 (2)join场景 从定义中可知,除了 wait/notify 外,调用 join 方法也会让线程处于 WAITING 状态。 join 的机制中并没有显式的 wait/notify 的调用,但可以视作是一种特殊的,隐式的 wait/notify 机制。 假如有 a,b 两个线程,在 a 线程中执行 b.join(),相当于让 a 去等待 b,此时 a 停止执行,等 b 执行完了,系统内部会隐式地通知 a,使 a 解除等待状态,恢复执行。 换言之,a 等待的条件是 “b 执行完毕”,b 完成后,系统会自动通知 a。

TIMED_WAITING 限时等待状态 限时等待是WAITING等待状态的一种特例,主要是在时限参数和sleep方法的不同。线程在等待时我们将设定等待超时时间,如超过了我们设定的等待时间,等待线程将自动唤醒进入阻塞状态(Blocked)或就绪状态(Runnable) 。在调用Thread.sleep()方法、带有超时设定的Object.wait()方法、带有超时设定的Thread.join()方法等,线程会进入限时等待状态(TIMED_WAITING)。 一个正在限时等待另一个线程执行一个动作的线程处于这一状态。 带指定的等待时间的等待线程所处的状态。一个线程处于这一状态是因为用一个指定的正的等待时间(为参数)调用了以下方法中的其一: Thread.sleep带时限(timeout)的 Object.wait带时限(timeout)的 Thread.join

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

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

(2)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则不会释放锁,一直持有锁(直到睡眠结束) TERMINATED 死亡状态 线程执行完了(completed execution)或者因异常退出了run()方法(exited),该线程结束生命周期。

总结:

BLOCKED状态和WAITING状态对比 BLOCKED是同步(synchronized)机制下被动阻塞等待获取同步锁的状态,处于running(OS意义下)状态的线程可通过加同步锁(Synchronized)被动进入BLOCKED状态。 WAITING是异步(wait/notify)机制下主动等待条件满足后获取通知的状态,处于running(OS意义下)状态的线程可主动调用object.wait或者sleep,或者join(join内部调用的是sleep,所以可看成sleep的一种)进入WAITING状态。JVM层面进程状态与OS层面进程状态对比 在这里插入图片描述导致线程阻塞(OS意义下的阻塞waiting状态)的原因 线程阻塞的特点 线程放弃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状态。

(2)Java线程 状态转换 “Java 线程状态的改变通常只与自身显式引入的机制有关。如果 JVM 中的线程状态发生改变了,通常是自身机制引发的。比如 synchronize 机制有可能让线程进入BLOCKED 状态,sleep,wait等方法则可能让其进入 WATING 之类的状态。 在这里插入图片描述

1.2)线程控制方法

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

(1.2.1)Synchronized

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

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

synchronized 是应用于同步问题的人工线程调度工具。Java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内(线程进入同步块),监视器发挥作用,线程获得内置锁。内置锁是一个互斥锁,以为着最多只有一个线程能够获取该锁。这个锁由JVM自动获取和释放,线程进入synchronized方法时获取该对象的锁,synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。 wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某个对象的监视器)。 当某个线程wait之后,其他执行该同步快的线程可以进入该同步块执行。 当某个线程并不持有监视器的使用权时(如上图中5的状态,即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。 在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。 锁的内部机制:从偏向锁到重量级锁 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. 偏向锁 当线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操作,将线程ID插入到Markword中,同时修改偏向锁的标志位。 此时偏向锁标志位为1。 偏向锁是jdk1.6引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。 也就是说: 在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤: Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致. 如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码. 如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。 如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。 如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。 即偏向锁是针对于一个线程而言的,线程获得锁之后就不会进行解锁操作,节省了很多开销。为什么要这样做呢?因为经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块的。这也是为什么会有偏向锁出现的原因。在Jdk1.6中,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景。 下述代码中,当线程访问同步方法method1时,会在对象头(SynchronizedTest.class对象的对象头)和栈帧的锁记录中存储锁偏向的线程ID,下次该线程在进入method2,只需要判断对象头存储的线程ID是否为当前线程,而不需要进行CAS操作进行加锁和解锁(因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟)。

public class SynchronizedTest { private static Object lock = new Object(); public static void main(String[] args) { method1(); method2(); } synchronized static void method1() {} synchronized static void method2() {} }

3. 轻量级锁 当出现有两个线程来竞争锁的话,那么偏向锁就失效了,此时锁就会膨胀,升级为轻量级锁。锁撤销升级为轻量级锁之后,那么对象的Markword也会进行相应的的变化。下面先简单描述下锁撤销之后,升级为轻量级锁的过程:

线程在自己的栈桢中创建锁记录 LockRecord。将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。将锁记录中的Owner指针指向锁对象。将锁对象的对象头的MarkWord替换为指向锁记录的指针。

轻量级锁主要是自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。自旋锁有一些问题: (1)如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在原地等待空消耗cpu。 (2)本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。 基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。

4. 重量级锁 轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。 主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。 这就是说为什么重量级线程开销很大的。互斥锁(重量级锁)也称为阻塞同步、悲观锁 因此可做个总结: 线程可以通过两种方式锁住一个对象:

通过膨胀一个处于无锁状态(状态位001)的对象获得该对象的锁;对象处于膨胀状态(状态位00),但LockWord指向的monitor的Owner字段为NULL,则可以直接通过CAS原子指令尝试将Owner设置为自己的标识来获得锁。

获取锁(monitorenter)的大概过程:

对象处于无锁状态时(LockWord的值为hashCode等,状态位为001),线程首先从monitor列表中取得一个空闲的monitor,初始化Nest和Owner值为1和线程标识,一旦monitor准备好,通过CAS替换monitor起始地址到LockWord进行膨胀。如果存在其它线程竞争锁的情况而导致CAS失败,则回到monitorenter重新开始获取锁的过程即可。对象已经膨胀,monitor中的Owner指向当前线程,这是重入锁的情况(reentrant),将Nest加1,不需要CAS操作,效率高。对象已经膨胀,monitor中的Owner为NULL,此时多个线程通过CAS指令试图将Owner设置为自己的标识获得锁,竞争失败的线程则进入第4种情况。对象已经膨胀,同时Owner指向别的线程,在调用操作系统的重量级的互斥锁之前自旋一定的次数,当达到一定的次数如果仍然没有获得锁,则开始准备进入阻塞状态,将rfThis值原子加1,由于在加1的过程中可能被其它线程破坏对象和monitor之间的联系,所以在加1后需要再进行一次比较确保lock word的值没有被改变,当发现被改变后则要重新进行monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。

释放锁(monitorexit)的大概过程:

检查该对象是否处于膨胀状态并且该线程是这个锁的拥有者,如果发现不对则抛出异常。检查Nest字段是否大于1,如果大于1则简单的将Nest减1并继续拥有锁,如果等于1,则进入到步骤3。检查rfThis是否大于0,设置Owner为NULL然后唤醒一个正在阻塞或等待的线程再一次试图获取锁,如果等于0则进入到步骤4。缩小(deflate)一个对象,通过将对象的LockWord置换回原来的HashCode等值来解除和monitor之间的关联来释放锁,同时将monitor放回到线程私有的可用monitor列表。

重入锁 & 非重入锁 可重入锁指同一个线程可以再次获得之前已经获得的锁,避免产生死锁。 当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。 下面为可重入锁与非可重入锁的实现区别 不可重入锁

public class Lock{ private boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } public synchronized void unlock(){ isLocked = false; notify(); } }

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

public class Lock{ boolean isLocked = false; Thread lockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException{ Thread thread = Thread.currentThread(); while(isLocked && lockedBy != thread){ wait(); } isLocked = true; lockedCount++; lockedBy = thread; } public synchronized void unlock(){ if(Thread.currentThread() == this.lockedBy){ lockedCount--; if(lockedCount == 0){ isLocked = false; notify(); } } } }

lockBy:保存已经获得锁实例的线程,在lock()判断调用lock的线程是否已经获得当前锁实例,如果已经获得锁,则直接跳过while,无需等待。 lockCount:记录同一个线程重复对一个锁对象加锁的次数。否则,一次unlock就会解除所有锁,即使这个锁实例已经加锁多次了。 两种锁举例

public class Count{ Lock lock = new Lock(); public void print(){ lock.lock(); doAdd(); lock.unlock(); } public void doAdd(){ lock.lock(); //do something lock.unlock(); } }

对于不可重入锁,当一个线程调用print()方法时,获得了锁,这时就无法再调用doAdd()方法,这时必须先释放锁才能调用,所以称这种锁为不可重入锁,也叫自旋锁。 对于可重入锁,可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。 第一个线程执行print()方法,得到了锁,使lockedBy等于当前线程,也就是说,执行的这个方法的线程获得了这个锁,执行add()方法时,同样要先获得锁,因不满足while循环的条件,也就是不等待,继续进行,将此时的lockedCount变量,也就是当前获得锁的数量加一,当释放了所有的锁,才执行notify()。如果在执行这个方法时,有第二个线程想要执行这个方法,因为lockedBy不等于第二个线程,导致这个线程进入了循环,也就是等待,不断执行wait()方法。只有当第一个线程释放了所有的锁(一共两个锁:print方法一个锁+add方法一个锁),执行了notify()方法,第二个线程才得以跳出循环,继续执行。 java中常用的可重入锁

synchronizedjava.util.concurrent.locks.ReentrantLock

注意: 这里要区别,同一个对象的多方法都加入synchronized关键字时,线程A 访问 (synchronized)object.A,线程B 访问 (synchronized)object.B时,必须等线程A访问完A,线程B才能访问B;此结论同样适用于对于object中使用synchronized(this)同步代码块的场景;synchronized锁定的都是当前对象!

lock 机制(单独使用)实现线程竞争 在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程(通过竞争获取到lock实例的线程)能执行该块内容。

//通过lock锁定 public class Thread1 implements Runnable { Object lock; public void run() { synchronized(lock){ ..do something } } } //直接用于方法 public class Thread1 implements Runnable { public synchronized void run() { ..do something } }

synchronized使用 (1)同步方法 synchronized关键字修饰的方法 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 此时该内置锁为对象锁/类锁。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。

public synchronized void save(){}

注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类 (1.1)对象锁 对象锁是用于对象实例方法,或者一个对象实例上的 (1.2)类锁 类锁是用于类的静态方法或者一个类的class对象上的 对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。 (1.3)对象锁与类锁的区别 synchronized是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,注意这里是“类的当前实例”,类的两个不同实例就没有这种约束了。那么static synchronized恰好就是要控制类的所有实例的访问了,static synchronized是限制线程同时访问jvm中该类的所有实例同时访问对应的代码快。实际上,在类中某方法或某代码块中有 synchronized,那么在生成一个该类实例后,该类也就有一个监视快,放置线程并发访问该实例synchronized保护快,而static synchronized则是所有该类的实例公用一个监视快了,也就是两个的区别了,也就是synchronized相当于this.synchronized,而staticsynchronized相当于Something.synchronized.

pulbic class Something(){ public synchronized void isSyncA(){} public synchronized voidisSyncB(){} public static synchronizedvoid cSyncA(){} public static synchronizedvoid cSyncB(){} }

那么,假如有Something类的两个实例a与b,那么下列组方法何以被1个以上线程同时访问呢

a. x.isSyncA()与x.isSyncB() b. x.isSyncA()与y.isSyncA() c. x.cSyncA()与y.cSyncB() d. x.isSyncA()与Something.cSyncA() a,都是对同一个实例的synchronized域访问,因此不能被同时访问 b,是针对不同实例的,因此可以同时被访问 c,因为是staticsynchronized,所以不同实例之间仍然会被限制,相当于Something.isSyncA()与 Something.isSyncB()了,因此不能被同时访问。 d,是可以被同时访问的,答案理由是synchronzied的是实例方法与synchronzied的类方法由于锁定(lock)不同的原因。 个人分析也就是synchronized 与static synchronized 相当于两帮派,各自管各自,相互之间就无约束了,可以被同时访问。后面一部分将详细分析synchronzied是怎么样实现的。

结论:

synchronized static是某个类的范围,synchronized static cSync{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。synchronized 是某实例的范围,synchronized isSync(){}防止多个线程同时访问这个实例中的synchronized 方法。类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。

(2)同步代码块 synchronized关键字修饰的语句块 但用Synchronized修饰同步方法有缺陷: 当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题。 因此用synchronized修饰代码块,缩小同步范围,减少了风险。 因此采用同步代码块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。 代码如:

synchronized(object){ }

注:同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

public class TestSynchronized { public void test1() { synchronized(this) { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } } public synchronized void test2() { int i = 5; while( i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static void main(String[] args) { final TestSynchronized myt2 = new TestSynchronized(); Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } }, "test1" ); Thread test2 = new Thread( new Runnable() { public void run() { myt2.test2(); } }, "test2" ); test1.start();; test2.start(); // TestRunnable tr=new TestRunnable(); // Thread test3=new Thread(tr); // test3.start(); } }

执行结果

test2 : 4 test2 : 3 test2 : 2 test2 : 1 test2 : 0 test1 : 4 test1 : 3 test1 : 2 test1 : 1 test1 : 0

上述的代码,第一个方法时用了同步代码块的方式进行同步,传入的对象实例是this,表明是当前对象,当然,如果需要同步其他对象实例,也不可传入其他对象的实例;第二个方法是修饰方法的方式进行同步。因为第一个同步代码块传入的this,所以两个同步代码所需要获得的对象锁都是同一个对象锁,下面main方法时分别开启两个线程,分别调用test1和test2方法,那么两个线程都需要获得该对象锁,另一个线程必须等待。上面也给出了运行的结果可以看到:直到test2线程执行完毕,释放掉锁,test1线程才开始执行。(可能这个结果有人会有疑问,代码里面明明是先开启test1线程,为什么先执行的是test2呢?这是因为java编译器在编译成字节码的时候,会对代码进行一个重排序,也就是说,编译器会根据实际情况对代码进行一个合理的排序,编译前代码写在前面,在编译后的字节码不一定排在前面,所以这种运行结果是正常的, 这里是题外话,最主要是检验synchronized的用法的正确性) synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。 一个类的对象锁和另一个类的对象锁是没有关联的,当一个线程获得A类的对象锁时,它同时也可以获得B类的对象锁。

wait/notify 机制实现线程协作 wait/notify机制:在Java中,可以通过配合调用Object对象的wait()方法和notify()方法或notifyAll()方法来实现线程间的通信。 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。 当线程执行wait()方法时候,会将当前进程阻塞,释放当前的锁,然后让出CPU,进入等待状态。(直到接到通知或被中断为止) 只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,从wait()方法中继续往下执行。 要注意

notify唤醒阻塞的线程后,线程会接着上次的执行继续往下执行。wait/notify必须在同步方法或同步快中调用。wait()方法释放当前线程的锁,因此如果当前线程没有持有适当的锁,则抛出IllegalMonitorStateException异常。notify()方法调用前,线程也必须要获得该对象的对象级别锁,的如果调用notify()时没有持有适当的锁,也会抛出IllegalMonitorStateException。notify与notifyall区别与联系 notify 与 notifyall 都是用于唤醒被 wait 的线程 notify 调用后,如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知,并使它等待获取该对象的对象锁。但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的wait线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll。 notifyAll使所有原来在该对象上wait的线程统统退出wait的状态(即全部被唤醒,不再等待notify或notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll线程退出调用了notifyAll的synchronized代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出synchronized代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。notify后,当前线程不会马上释放该对象锁,wait所在的线程并不能马上获取该对象锁,要等到程序退出synchronized代码块后,当前线程才会释放锁,wait所在的线程也才可以通过竞争获取该对象锁

典型案例:生产者-消费者问题 两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。

/** * 生产者生产出来的产品交给店员 */ public synchronized void produce() { if(this.product >= MAX_PRODUCT) { try { wait(); System.out.println("产品已满,请稍候再生产"); } catch(InterruptedException e) { e.printStackTrace(); } return; } this.product++; System.out.println("生产者生产第" + this.product + "个产品."); notifyAll(); //通知等待区的消费者可以取出产品了 } /** * 消费者从店员取产品 */ public synchronized void consume() { if(this.product 创建线程的方法,这就是我叫你别理他的那个星期六!你还看! ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);

源码:

public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }

(3)ScheduledThreadPool() 定长线程池:

支持定时及周期性任务执行。

创建方法:

//nThreads => 最大线程数即maximumPoolSize ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);

源码:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } //ScheduledThreadPoolExecutor(): public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); }

(4)SingleThreadExecutor() 单线程化的线程池:

有且仅有一个工作线程执行任务所有任务按照指定顺序执行,即遵循队列的入队出队规则

创建方法:

ExecutorService singleThreadPool = Executors.newSingleThreadPool();

源码:

public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())); } Java 线程池的停止 Executor框架提供了Java线程池的能力,ExecutorService扩展了Executor,提供了管理线程生命周期的关键能力。其中,ExecutorService.submit返回了Future对象来描述一个线程任务,它有一个cancel()方法。 下面的例子扩展了上面的InterruptedExample,要求线程在限定时间内得到结果,否则触发超时停止。 public class InterruptByFuture { public static void main(String[] args) throws Exception { ExecutorService es = Executors.newSingleThreadExecutor(); Future task = es.submit(new MyThread()); try { //限定时间获取结果 task.get(5, TimeUnit.SECONDS); } catch (TimeoutException e) { //超时触发线程中止 System.out.println("thread over time"); } catch (ExecutionException e) { throw e; } finally { boolean mayInterruptIfRunning = true; task.cancel(mayInterruptIfRunning); } } private static class MyThread extends Thread { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { System.out.println("count"); Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("interrupt"); Thread.currentThread().interrupt(); } } System.out.println("thread stop"); } public void cancel() { interrupt(); } } }

Future的get方法可以传入时间,如果限定时间内没有得到结果,将会抛出TimeoutException。此时,可以调用Future的cancel()方法,对任务所在线程发出中断请求。 cancel()有个参数mayInterruptIfRunning,表示任务是否能够接收到中断。 mayInterruptIfRunning=true时,任务如果在某个线程中运行,那么这个线程能够被中断; mayInterruptIfRunning=false时,任务如果还未启动,就不要运行它,应用于不处理中断的任务 要注意,mayInterruptIfRunning=true表示线程能接收中断,但线程是否实现了中断不得而知。线程要正确响应中断,才能真正被cancel。 线程池的shutdownNow()会尝试停止池内所有在执行的线程,原理也是发出中断请求。

1.5)Java 并发模型

(1)并发模型 (1.1)并发与并行 并发程序是指在运行中有两个及以上的任务同时在处理,与之相关的概念并行,是指在运行中有两个及以上的任务同时执行,差别是在于处理和执行。在单核CUP中两个及以上任务的处理方式是让它们交替的进入CUP执行,这种对执行的处理方式就是并发。 并行只能发生在多核CUP中,每个CUP核心拿到一个任务同时执行,并行是并发的一个子集 与串行程序相比并发编程的优点: (1)提高硬件资源的利用率(特别是IO资源),提高系统的响应速度、减少客户端等待、增加系统吞吐量 (2)解决特定领域的问题,比如GUI系统,WEB服务器等。 (1.2)线程并发实现 并发实现包括三种:进程并发、线程并发与协程并发。 其中线程并发是Java的并发模型方式。 在操作系统中线程是包含在进程中的消费资源较少、运行迅速的最小执行单元,根据操作系统内核是否对线程可感知,把线程分为内核线程和用户线程。

基于内核线程 使用内核线程的一种高级接口–轻量级进程(Light Weight Process,LWP)实现的线程(通常意义上的线程),它与内核线程是一对一的关系。线程的创建,初始化,同步,切换(用户态、内核态)都需要内核调度器(Scheduler)进行调度,消耗内核资源,每一个轻量级进程都需要一个内核线程对应,所以这线程能创建的数量是也是有限的。基于用户线程 建立在用户空间的上的线程,内核对此无感知。线程的创建、调度在用户态完成,不需要系统内核支援。由于没有系统内核的支援,所有的线程操作都需要用户程序自己处理。线程的创建、切换和调度都是需要考虑的问题,而且由于操作系统只把处理器资源分配到进程,如“阻塞如何处理”,“多处理器系统中如何将线程映射到其它处理器上”这类问题解决起来将会异常困难,甚至不可能完成。基于用户线程和内核线程混合 即使用内核线程(轻量级进程),也使用用户线程。用户线程依然建立在用户空间上,线程的创建、调度、处理器映射能够得到内核线程的支援,实现简单。用户线程与轻量级进程(内核线程)是N:M的对应关系,可以支持大规模的并发。

(1.3)线程并发通信 线程间通过协作才能合力完成一个任务,协作的基础就是通信。常用的线程间通信的方式有两种。

共享内存 设置一个共享变量,多个线程都可以对共享变量进行操作,共享变量就行通信的中介。共享内存通信方式实现简单,数据的共享还使得线程间的通信不需要数据的传送,直接对内存进行访问,加快了程序的执行效率。 但是多个线程操作同一个共享变量,势必会造成“数据争用”。竞争条件下必须让共享变量进入临界区进行保护,否则会产生数据不一致。消息传递

(2)Java并发模型——线程模型 (2.1)简介 每一个JAVA线程都对应者一个内核线程,所以线程的创建、调度、上下文切换都需要系统内核的支持,会消耗系统资源。 JAVA线程间通信是通过共享内存实现的,锁在线程并发中有着举足轻重地位,使用JAVA多线程时需要非常小心的处理同步问题。

(2.2)问题 Java并发编程需要面对两个问题:

资源消耗问题: 包括线程的创建、上下文切换对资源的消耗,锁的互斥操作对资源的消耗,常用的解决方法有池化资源,根据计算类型保有适量线程,锁优化策略等。线程安全问题: 线程安全问题,要想让并发程序正确的执行,需要解决原子性,可见性、有序性的问题,常用的保障线程安全的方法有加锁、不共享状态、不可变对象。 “线程与锁”模型是JAVA语言的并发模型。这也是大多数语言都支持的模型,由于其基本接近硬件本身运行的模式,可以解决的问题领域很多有着很高的运行效率,一直都是并发编程的首选。缺点是使用这样模型需要开发者时刻警惕线程安全问题,处理复杂的线程协作问题,关注计算资源的开销问题。

(2.3)并发机制

锁机制 比如synchronized或者ReentrantLockCAS算法 读的时候记录结果,写的时候检查是不是还是刚才读到的,如果是,那么说明读和写之间没有其他线程修改它的值,这段代码是原子执行的,可以进行修改操作;如果不是,那么说明其他线程修改了它的值,这段代码并没有原子执行,此时需要使用循环,重新读取,再检查,直至保证原子执行。如Volatile。 这种方式和锁有一些类似,都可以保证代码的原子执行,但是使用锁会涉及到一些线程的挂起和上下文切换问题,需要消耗资源,但是CAS仅是轮询,不涉及JVM级别。书中提到低度和中度竞争的情况下,CAS的代价是低于锁的,在高度竞争的情况下,CAS的代价是高于锁的(毕竟轮询也需要消耗资源,占用CPU),但高度竞争这种情况是比较少的。在一些细粒度的并发操作上,推荐还是使用CAS。

(2.4)并发工具

基础类:Synchronized、Volatile、Finaljava.util.concurrent包:原子类(atomic)、显示锁(ReentrantLock)、同步模式(CountDownLatch)、线程安全容器(ConcurrentHashMap、CopyOnWriteArrayList、Queue、TransferQueue) 感谢Doug Lea在Java 5中提供了他里程碑式的杰作java.util.concurrent包,它的出现让Java的并发编程有了更多的选择和更好的工作方式。Doug Lea的杰作主要包括以下内容: 更好的线程安全的容器线程池和相关的工具类可选的非阻塞解决方案显示的锁和信号量机制 1.6)Java 线程安全

(1)什么是Java线程安全 多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。 线程安全有以下几种实现方式 (2)如何保证Java线程安全

不可变 不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。 不可变的类型: final 关键字修饰的基本数据类型String枚举类型Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。 互斥同步 synchronizedReentrantLock。 非阻塞同步 互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 CAS 随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。 乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。AtomicIntegerABA 无同步方案 栈自闭 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。线程本地存储(ThreadLocal)可重入代码(Reentrant Code) 这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。 1.7)Java 断点续传

(1)原理 在下载行为出现中断的时候,记录下中断的位置信息,然后在下次行为开始的时候,直接从记录的这个位置开始下载内容,而不再从头开始。 分为两步:

当“上传(下载)的行为”出现中断,我们需要记录本次上传(下载)的位置(position)。当“续”这一行为开始,我们直接跳转到postion处继续上传(下载)的行为。

(2)代码

import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; public class Test { // step1:首先,我们定义了一个变量position,记录在发生中断的时候,已完成读写的位置。(这是为了方便,实际来说肯定应该讲这个值存到文件或者数据库等进行持久化) private static int position = -1; public static void main(String[] args) { // 源文件与目标文件 File sourceFile = new File("D:/", "test.txt"); File targetFile = new File("E:/", "test.txt"); // 输入输出流 FileInputStream fis = null; FileOutputStream fos = null; // 数据缓冲区 byte[] buf = new byte[1]; try { fis = new FileInputStream(sourceFile); fos = new FileOutputStream(targetFile); // 数据读写 while (fis.read(buf) != -1) { fos.write(buf); // step2:然后在文件读写的while循环中,我们去模拟一个中断行为的发生。这里是当targetFile的文件长度为3个字节则模拟抛出一个我们自定义的异常。(我们可以想象为实际下载中,已经上传(下载)了”x”个字节的内容,这个时候网络中断了,那么我们就在网络中断抛出的异常中将”x”记录下来)。 if (targetFile.length() == 3) { position = 3; throw new FileAccessException(); } } } catch (FileAccessException e) { //step3:开启”续传“行为,即keepGoing方法. keepGoing(sourceFile,targetFile, position); } catch (FileNotFoundException e) { System.out.println("指定文件不存在"); } catch (IOException e) { // TODO: handle exception } finally { try { // 关闭输入输出流 if (fis != null) fis.close(); if (fos != null) fos.close(); } catch (IOException e) { e.printStackTrace(); } } } private static void keepGoing(File source,File target, int position) { // step3.1:我们起头让线程休眠10秒钟,这正是为了让我们运行程序看到效果。 try { Thread.sleep(10000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } // step3.2:在“续传”行为开始后,通过RandomAccessFile类来包装我们的文件,然后通过seek将指针指定到之前发生中断的位置进行读写就搞定了。 (实际的文件下载上传,我们当然需要将保存的中断值上传给服务器,这个方式通常为 try { RandomAccessFile readFile = new RandomAccessFile(source, "rw"); RandomAccessFile writeFile = new RandomAccessFile(target, "rw"); readFile.seek(position); writeFile.seek(position); // 数据缓冲区 byte[] buf = new byte[1]; // 数据读写 while (readFile.read(buf) != -1) { writeFile.write(buf); } } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } class FileAccessException extends Exception { }

(3)实现结果 运行程序,那么文件就会开启“由D盘上传到E盘的过程”,我们首先点开E盘,会发现的确多了一个test.txt文件,打开它发现内容如下: 在这里插入图片描述 这个时候我们发现内容只有“abc”。这是在我们预料以内的,因为我们的程序模拟在文件上传了3个字节的时候发生了中断。 等待10秒钟过去,然后再点开该文件,发现内容的确已经变成了“abc”,由此也就完成了续传。 在这里插入图片描述

(二)Android 多线程开发

在这里插入图片描述

2.1)基础使用 1、继承Thread类

(1)简介 Thread类是Java中实现多线程的具体类,封装了所需线程操作。在Android开发中用于实现多线程。 注:线程对象&运行线程区别 线程对象是运行线程的实体,用来控制线程行为的唯一方式。线程对象通过线程类实例化创建,负责控制线程的状态,如:运行、睡眠、挂起/停止。 优点 实现简单:只要继承Thread类&复写run()即可实现多线程操作 缺点 局限性大:必须集成Thread类(Java规定单继承,即集成Thread类后不可继承其他类) 不适合资源共享:一个线程=一个实例对象,相对独立无法资源共享 消耗资源:Thread线程=一次性消费品&一个耗时任务。执行完一个耗时操作后,线程会被自动销毁,如果有100个耗时任务则必须开100个线程。多次创建&销毁线程,耗费系统资源 (2)使用 2.1)使用步骤 在这里插入图片描述 2.2)常规使用

// 步骤1:创建线程类 (继承自Thread类) class MyThread extends Thread{ // 步骤2:复写run(),内容 = 定义线程行为 @Override public void run(){ ... // 定义的线程行为 } } // 步骤3:创建线程对象,即 实例化线程类 MyThread mt=new MyThread(“线程名称”); // 步骤4:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起 / 停止 // 此处采用 start()开启线程 mt.start();

2.3)匿名类使用

// 步骤1:采用匿名类,直接 创建 线程类的实例 new Thread("线程名称") { // 步骤2:复写run(),内容 = 定义线程行为 @Override public void run() { // 步骤3:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起 / 停止 }.start();

2.4)常规&匿名类使用区别 在这里插入图片描述

2、实现Runnable接口

(1)简介 一个与多线程相关的抽象接口,仅定义1个方法=run(),在Android开发中用于实现多线程 适合资源共享:Runnable可被多个线程(Thread实例)共享,适合多线程处理同一资源的情况 灵活:一个类可以继承多个接口,避免集成THread类导致的单继承局限性

补充:Java进阶知识——接口与继承区别 (1)关键字:接口interface,继承extends (2)定义:接口:对功能的描述,继承:具体描述一种类 (3)结构:接口只能定义全局常量、抽象方法,继承可以定义属性方法、常量、变量等等 (4)接口可以实现"多继承",继承只能"单继承" (5)实现接口的类一定要实现接口的抽象方法,继承的类可以调用、重载父类的任意方法

(2)使用 2.1)使用步骤 在这里插入图片描述 注: Java中真正能创建新线程的只有Thread类对象 通过实现Runnable的方式,最终还是通过Thread类对象来创建线程 所以对于 实现了Runnable接口的类,称为 线程辅助类;Thread类才是真正的线程类 2.2)常规使用

// 步骤1:创建线程辅助类,实现Runnable接口 class MyThread implements Runnable{ .... @Override // 步骤2:复写run(),定义线程行为 public void run(){ } } // 步骤3:创建线程辅助对象,即 实例化 线程辅助类 MyThread mt=new MyThread(); // 步骤4:创建线程对象,即 实例化线程类;线程类 = Thread类; // 创建时通过Thread类的构造函数传入线程辅助类对象 // 原因:Runnable接口并没有任何对线程的支持,我们必须创建线程类(Thread类)的实例,从Thread类的一个实例内部运行 Thread td=new Thread(mt); // 步骤5:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起 / 停止 // 当调用start()方法时,线程对象会自动回调线程辅助类对象的run(),从而实现线程操作 td.start();

2.3)匿名类使用

// 步骤1:通过匿名类 直接 创建线程辅助对象,即 实例化 线程辅助类 Runnable mt = new Runnable() { // 步骤2:复写run(),定义线程行为 @Override public void run() { } }; // 步骤3:创建线程对象,即 实例化线程类;线程类 = Thread类; Thread mt1 = new Thread(mt, "窗口1"); // 步骤4:通过 线程对象 控制线程的状态,如 运行、睡眠、挂起 / 停止 mt1.start();

(3)继承Thread类&实现Runnable接口对比 在这里插入图片描述 Android中Thread/Runnable方法使用了java原生的Thread/Runnable的线程形态,详细用法可参考上文Java多线程开发。

3、Handler

见陈小云Android学习之旅:第十章 进程间的通信 之 Handler机制(二)

2.2)复合使用 1、AsyncTask

(1)简介 一个Android已封装好的轻量级异步类,属于抽象类,使用时需要实现子类。用于 实现多线程,如在工作线程中执行耗时任务 异步通信、消息传递,如实现工作线程&主线程(UI线程)之间的通信,即:将工作线程的执行结果传递给主线程,从而在主线程中指向相关UI操作(保证线程安全)

public abstract class AsyncTask { ... }

不需使用"任务线程(如Thread类)+Handler"复杂组合,方便实现异步通信 采用线程池的缓存线程+复用线程,避免频繁创建&销毁线程所带来的系统资源开销 (2)类定义 AsyncTask类属于抽象类,即使用时需 实现子类

public abstract class AsyncTask { ... } // 类中参数为3种泛型类型 // 整体作用:控制AsyncTask子类执行线程任务时各个阶段的返回类型 // 具体说明: // a. Params:开始异步任务执行时传入的参数类型,对应excute()中传递的参数 // b. Progress:异步任务执行过程中,返回下载进度值的类型 // c. Result:异步任务执行完成后,返回的结果类型,与doInBackground()的返回值类型保持一致 // 注: // a. 使用时并不是所有类型都被使用 // b. 若无被使用,可用java.lang.Void类型代替 // c. 若有不同业务,需额外再写1个AsyncTask的子类 }

(3)核心方法 AsyncTask 核心 & 常用的方法如下: 在这里插入图片描述 方法执行顺序如下 在这里插入图片描述 (4)使用步骤 步骤1:创建AsyncTask子类 a. 继承AsyncTask类 b. 为3个泛型参数指定类型;若不使用,可用java.lang.Void类型代替 c. 根据需求,在AsyncTask子类内实现核心方法

private class MyTask extends AsyncTask { .... // 方法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子类的实例对象(任务实例) AsyncTask子类的实例必须在UI线程中创建

MyTask mTask = new MyTask();

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

mTask.execute();

(5)实例分析 5.1)实例需求 点击按钮 则 开启线程执行线程任务 显示后台加载进度 加载完毕后更新UI组件 期间若点击取消按钮,则取消加载 5.2)代码实现

private class MyTask extends AsyncTask { // 方法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>分析1 mWorker = new WorkerRunnable() { // 在任务执行线程池中回调:THREAD_POOL_EXECUTOR.execute() // 下面会详细讲解 public Result call() throws Exception { // 添加线程的调用标识 mTaskInvoked.set(true); Result result = null; try { // 设置线程的优先级 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // 执行异步操作 = 耗时操作 // 即 我们使用过程中复写的耗时任务 result = doInBackground(mParams); Binder.flushPendingCommands(); } catch (Throwable tr) { mCancelled.set(true);// 若运行异常,设置取消的标志 throw tr; } finally { // 把异步操作执行的结果发送到主线程 // 从而更新UI,下面会详细讲解 postResult(result); } return result; } }; // 2. 初始化FutureTask变量 = 1个FutureTask ->>分析2 mFuture = new FutureTask(mWorker) { // done()简介:FutureTask内的Callable执行完后的调用方法 // 作用:复查任务的调用、将未被调用的任务的结果通过InternalHandler传递到UI线程 @Override protected void done() { try { // 在执行完任务后检查,将没被调用的Result也一并发出 ->>分析3 postResultIfNotInvoked(get()); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { throw new RuntimeException("An error occurred while executing doInBackground()", e.getCause()); } catch (CancellationException e) { //若 发生异常,则将发出null postResultIfNotInvoked(null); } } }; } /** * 分析1:WorkerRunnable类的构造函数 */ private static abstract class WorkerRunnable implements Callable { // 此处的Callable也是任务; // 与Runnable的区别:Callable存在返回值 = 其泛型 Params[] mParams; } /** * 分析2:FutureTask类的构造函数 * 定义:1个包装任务的包装类 * 注:内部包含Callable 、增加了一些状态标识 & 操作Callable的接口 */ public FutureTask(Callable callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; } // 回到调用原处 /** * 分析3:postResultIfNotInvoked() */ private void postResultIfNotInvoked()(Result result) { // 取得任务标记 final boolean wasTaskInvoked = mTaskInvoked.get(); // 若任务无被执行,将未被调用的任务的结果通过InternalHandler传递到UI线程 if (!wasTaskInvoked) { postResult(result); } }

3、总结 创建了1个WorkerRunnable类 的实例对象 & 复写了call()方法 创建了1个FutureTask类 的实例对象 & 复写了 done() 步骤3:手动调用execute(Params…params) 1、具体使用

mTask.execute();

2、源码分析

public final AsyncTask execute(Params... params) { return executeOnExecutor(sDefaultExecutor, params); // ->>分析1 } /** * 分析1:executeOnExecutor(sDefaultExecutor, params) * 参数说明:sDefaultExecutor = 任务队列 线程池类(SerialExecutor)的对象 */ public final AsyncTask executeOnExecutor(Executor exec,Params... params) { // 1. 判断 AsyncTask 当前的执行状态 // PENDING = 初始化状态 if (mStatus != Status.PENDING) { switch (mStatus) { case RUNNING: throw new IllegalStateException("Cannot execute task:" + " the task is already running."); case FINISHED: throw new IllegalStateException("Cannot execute task:" + " the task has already been executed " + "(a task can be executed only once)"); } } // 2. 将AsyncTask状态设置为RUNNING状态 mStatus = Status.RUNNING; // 3. 主线程初始化工作 onPreExecute(); // 4. 添加参数到任务中 mWorker.mParams = params; // 5. 执行任务 // 此处的exec = sDefaultExecutor = 任务队列 线程池类(SerialExecutor)的对象 // ->>分析2 exec.execute(mFuture); return this; } /** * 分析2:exec.execute(mFuture) * 说明:属于任务队列 线程池类(SerialExecutor)的方法 */ private static class SerialExecutor implements Executor { // SerialExecutor = 静态内部类 // 即 是所有实例化的AsyncTask对象公有的 // SerialExecutor 内部维持了1个双向队列; // 容量根据元素数量调节 final ArrayDeque mTasks = new ArrayDeque(); Runnable mActive; // execute()被同步锁synchronized修饰 // 即说明:通过锁使得该队列保证AsyncTask中的任务是串行执行的 // 即 多个任务需1个个加到该队列中;然后 执行完队列头部的再执行下一个,以此类推 public synchronized void execute(final Runnable r) { // 将实例化后的FutureTask类 的实例对象传入 // 即相当于:向队列中加入一个新的任务 mTasks.offer(new Runnable() { public void run() { try { r.run(); } finally { scheduleNext();->>分析3 } } }); // 若当前无任务执行,则去队列中取出1个执行 if (mActive == null) { scheduleNext(); } } // 分析3 protected synchronized void scheduleNext() { // 1. 取出队列头部任务 if ((mActive = mTasks.poll()) != null) { // 2. 执行取出的队列头部任务 // 即 调用执行任务线程池类(THREAD_POOL_EXECUTOR)->>继续往下看 THREAD_POOL_EXECUTOR.execute(mActive); } } }

3、总结: 执行任务前,通过 任务队列 线程池类(SerialExecutor)将任务按顺序放入到队列中; 通过同步锁 修饰execute()从而保证AsyncTask中的任务是串行执行的,之后的线程任务执行是 通过任务线程池类(THREAD_POOL_EXECUTOR) 进行的。 继续往下分析:THREAD_POOL_EXECUTOR.execute()

/** * 源码分析:THREAD_POOL_EXECUTOR.execute() * 说明: * a. THREAD_POOL_EXECUTOR实际上是1个已配置好的可执行并行任务的线程池 * b. 调用THREAD_POOL_EXECUTOR.execute()实际上是调用线程池的execute()去执行具体耗时任务 * c. 而该耗时任务则是步骤2中初始化WorkerRunnable实例对象时复写的call() * 注:下面先看任务执行线程池的线程配置过程,看完后请回到步骤2中的源码分析call() */ // 步骤1:参数设置 //获得当前CPU的核心数 private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); //设置线程池的核心线程数2-4之间,但是取决于CPU核数 private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4)); //设置线程池的最大线程数为 CPU核数*2+1 private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; //设置线程池空闲线程存活时间30s private static final int KEEP_ALIVE_SECONDS = 30; //初始化线程工厂 private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); public Thread newThread(Runnable r) { return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); } }; //初始化存储任务的队列为LinkedBlockingQueue 最大容量为128 private static final BlockingQueue sPoolWorkQueue = new LinkedBlockingQueue(128); // 步骤2: 根据参数配置执行任务线程池,即 THREAD_POOL_EXECUTOR public static final Executor THREAD_POOL_EXECUTOR; static { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); // 设置核心线程池的 超时时间也为30s threadPoolExecutor.allowCoreThreadTimeOut(true); THREAD_POOL_EXECUTOR = threadPoolExecutor; } // 请回到步骤2中的源码分析call()

至此,我们回到步骤2中的源码分析call()

/** * 步骤2的源码分析:AsyncTask的构造函数 */ public AsyncTask() { // 1. 初始化WorkerRunnable变量 = 一个可存储参数的Callable对象 mWorker = new WorkerRunnable() { public Result call() throws Exception { // 添加线程的调用标识 mTaskInvoked.set(true); Result result = null; try { // 设置线程的优先级 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // 执行异步操作 = 耗时操作 // 即 我们使用过程中复写的耗时任务 result = doInBackground(mParams); Binder.flushPendingCommands(); } catch (Throwable tr) { mCancelled.set(true);// 若运行异常,设置取消的标志 throw tr; } finally { // 把异步操作执行的结果发送到主线程 // 从而更新UI ->>分析1 postResult(result); } return result; } }; .....// 省略 } /** * 分析1:postResult(result) */ private Result postResult(Result result) { @SuppressWarnings("unchecked") // 创建Handler对象 ->> 源自InternalHandler类—>>分析2 Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult(this, result)); // 发送消息到Handler中 message.sendToTarget(); return result; } /** * 分析2:InternalHandler类 */ private static class InternalHandler extends Handler { // 构造函数 public InternalHandler() { super(Looper.getMainLooper()); // 获取的是主线程的Looper() // 故 AsyncTask的实例创建 & execute()必须在主线程使用 } @Override public void handleMessage(Message msg) { AsyncTaskResult result = (AsyncTaskResult) msg.obj; switch (msg.what) { // 若收到的消息 = MESSAGE_POST_RESULT // 则通过finish() 将结果通过Handler传递到主线程 case MESSAGE_POST_RESULT: result.mTask.finish(result.mData[0]); ->>分析3 break; // 若收到的消息 = MESSAGE_POST_PROGRESS // 则回调onProgressUpdate()通知主线程更新进度的操作 case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break; } } } /** * 分析3:result.mTask.finish(result.mData[0]) */ private void finish(Result result) { // 先判断是否调用了Cancelled() // 1. 若调用了则执行我们复写的onCancelled() // 即 取消任务时的操作 if (isCancelled()) { onCancelled(result); } else { // 2. 若无调用Cancelled(),则执行我们复写的onPostExecute(result) // 即更新UI操作 onPostExecute(result); } // 注:不管AsyncTask是否被取消,都会将AsyncTask的状态变更为:FINISHED mStatus = Status.FINISHED; }

总结 任务线程池类(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更新操作 7.3)Async源码总结 在这里插入图片描述

2、HandlerThread

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

步骤1:创建HandlerThread实例对象 //传入参数 = 线程名字,作用 = 标记该线程 HandlerThread mHandlerThread = new HandlerThread("handlerThread"); 步骤2:启动线程 mHandlerThread.start() 步骤3:创建工作线程Handler&复写handleMessage() // 作用:关联HandlerThread的Looper对象、实现消息处理操作 & 与其他线程进行通信 // 注:消息处理操作(HandlerMessage())的执行线程 = mHandlerThread所创建的工作线程中执行 Handler workHandler = new Handler( handlerThread.getLooper() ) { @Override public boolean handleMessage(Message msg) { ...//消息处理 return true; } }); 步骤4:使用工作线程Handler向工作线程的消息队列发送消息 // 在工作线程中,当消息循环时取出对应消息 & 在工作线程执行相关操作 // a. 定义要发送的消息 Message msg = Message.obtain(); msg.what = 2; //消息的标识 msg.obj = "B"; // 消息的存放 // b. 通过Handler发送消息到其绑定的消息队列 workHandler.sendMessage(msg); 步骤5:结束线程,即停止线程的消息循环 mHandlerThread.quit();

(3)实例

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(); } }); } }

(4)源码解析 4.1)工作原理 内部原理 = Thread类 + Handler类机制 (1)通过继承Thread类,快速创建1个带有Looper对象的新工作线程 (2)通过封装Handler类,快速创建Handler&与其他线程进行通信 4.2)源码解析 步骤1:创建HandlerThread的实例对象 1、具体使用

HandlerThread mHandlerThread = new HandlerThread("handlerThread");

2、源码解析

public class HandlerThread extends Thread { // 继承自Thread类 int mPriority; // 线程优先级 int mTid = -1; // 当前线程id Looper mLooper; // 当前线程持有的Looper对象 // HandlerThread类有2个构造方法 // 区别在于:设置当前线程的优先级参数,即可自定义设置 or 使用默认优先级 // 方式1. 默认优先级 public HandlerThread(String name) { // 通过调用父类默认的方法创建线程 super(name); mPriority = Process.THREAD_PRIORITY_DEFAULT; } // 方法2. 自定义设置优先级 public HandlerThread(String name, int priority) { super(name); mPriority = priority; } ... }

3、总结 HandlerThread类继承自Thread类 创建HandlerThread类对象 = 创建Thread类对象 + 设置线程优先级 = 新开1个工作线程 + 设置线程优先级 步骤2:启动线程 1、具体使用

mHandlerThread.start();

2、源码解析

/** * 源码分析:此处调用的是父类(Thread类)的start(),最终回调HandlerThread的run() */ @Override public void run() { // 1. 获得当前线程的id mTid = Process.myTid(); // 2. 创建1个Looper对象 & MessageQueue对象 Looper.prepare(); // 3. 通过持有锁机制来获得当前线程的Looper对象 synchronized (this) { mLooper = Looper.myLooper(); // 发出通知:当前线程已经创建mLooper对象成功 // 此处主要是通知getLooper()中的wait() notifyAll(); // 此处使用持有锁机制 + notifyAll() 是为了保证后面获得Looper对象前就已创建好Looper对象 } // 4. 设置当前线程的优先级 Process.setThreadPriority(mPriority); // 5. 在线程循环前做一些准备工作 ->>分析1 // 该方法实现体是空的,子类可实现 / 不实现该方法 onLooperPrepared(); // 6. 进行消息循环,即不断从MessageQueue中取消息 & 派发消息 Looper.loop(); mTid = -1; } } /** * 分析1:onLooperPrepared(); * 说明:该方法实现体是空的,子类可实现 / 不实现该方法 */ protected void onLooperPrepared() { }

3、总结 1、为当前工作线程(即步骤1创建的线程)创建1个Looper对象 & MessageQueue对象 2、通过持有锁机制来获得当前线程的Looper对象 3、发出通知:当前线程已经创建mLooper对象成功 4、工作线程进行消息循环,即不断从MessageQueue中取消息 & 派发消息 步骤3:创建工作线程Handler & 复写handleMessage() 1、具体使用

Handler workHandler = new Handler( handlerThread.getLooper() ) { @Override public boolean handleMessage(Message msg) { ...//消息处理 return true; } });

2、源码解析

/** * 源码分析:handlerThread.getLooper() * 作用:获得当前HandlerThread线程中的Looper对象 */ public Looper getLooper() { // 若线程不是存活的,则直接返回null if (!isAlive()) { return null; } // 若当前线程存活,再判断线程的成员变量mLooper是否为null // 直到线程创建完Looper对象后才能获得Looper对象,若Looper对象未创建成功,则阻塞 synchronized (this) { while (isAlive() && mLooper == null) { try { // 此处会调用wait方法去等待 wait(); } catch (InterruptedException e) { } } } // 上述步骤run()使用 持有锁机制 + notifyAll() 获得Looper对象后 // 则通知当前线程的wait()结束等待 & 跳出循环 // 最终getLooper()返回的是在run()中创建的mLooper对象 return mLooper; }

3、总结 在获得HandlerThread工作线程的Looper对象时存在一个同步的问题:只有当线程创建成功 & 其对应的Looper对象也创建成功后才能获得Looper的值,才能将创建的Handler 与 工作线程的Looper对象绑定,从而将Handler绑定工作线程 解决方案:即保证同步的解决方案 = 同步锁、wait() 和 notifyAll(),即 在run()中成功创建Looper对象后,立即调用notifyAll()通知 getLooper()中的wait()结束等待 & 返回run()中成功创建的Looper对象,使得Handler与该Looper对象绑定 步骤4:使用工作线程Handler向工作线程的消息队列发送消息 1、具体使用

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

2、源码解析 源码分析:workHandler.sendMessage(msg) 此处的源码即Handler的源码,故不作过多描述 步骤5:结束线程,即停止线程的消息循环 1、具体使用

mHandlerThread.quit();

2、源码解析

/** * 源码分析:mHandlerThread.quit() * 说明: * a. 该方法属于HandlerThread类 * b. HandlerThread有2种让当前线程退出消息循环的方法:quit() 、quitSafely() */ // 方式1:quit() // 特点:效率高,但线程不安全 public boolean quit() { Looper looper = getLooper(); if (looper != null) { looper.quit(); return true; } return false; } // 方式2:quitSafely() // 特点:效率低,但线程安全 public boolean quitSafely() { Looper looper = getLooper(); if (looper != null) { looper.quitSafely(); return true; } return false; } // 注:上述2个方法最终都会调用MessageQueue.quit(boolean safe)->>分析1 /** * 分析1:MessageQueue.quit(boolean safe) */ void quit(boolean safe) { if (!mQuitAllowed) { throw new IllegalStateException("Main thread not allowed to quit."); } synchronized (this) { if (mQuitting) { return; } mQuitting = true; if (safe) { removeAllFutureMessagesLocked(); // 方式1(不安全)会调用该方法 ->>分析2 } else { removeAllMessagesLocked(); // 方式2(安全)会调用该方法 ->>分析3 } // We can assume mPtr != 0 because mQuitting was previously false. nativeWake(mPtr); } } /** * 分析2:removeAllMessagesLocked() * 原理:遍历Message链表、移除所有信息的回调 & 重置为null */ private void removeAllMessagesLocked() { Message p = mMessages; while (p != null) { Message n = p.next; p.recycleUnchecked(); p = n; } mMessages = null; } /** * 分析3:removeAllFutureMessagesLocked() * 原理:先判断当前消息队列是否正在处理消息 * a. 若不是,则类似分析2移除消息 * b. 若是,则等待该消息处理处理完毕再使用分析2中的方式移除消息退出循环 * 结论:退出方法安全与否(quitSafe() 或 quit()),在于该方法移除消息、退出循环时是否在意当前队列是否正在处理消息 */ private void removeAllFutureMessagesLocked() { final long now = SystemClock.uptimeMillis(); Message p = mMessages; if (p != null) { // 判断当前消息队列是否正在处理消息 // a. 若不是,则直接移除所有回调 if (p.when > now) { removeAllMessagesLocked(); } else { // b. 若是正在处理,则等待该消息处理处理完毕再退出该循环 Message n; for (;;) { n = p.next; if (n == null) { return; } if (n.when > now) { break; } p = n; } p.next = null; do { p = n; n = p.next; p.recycleUnchecked(); } while (n != null); } } }

4.3)总结 在这里插入图片描述 (5)问题&解决 5.1)内存泄露 1、问题

In Android, Handler classes should be static or leaks might occur.

2、原因 Handler导致内存泄露:当Handler消息队列 还有未处理的消息 / 正在处理消息时,存在引用关系: “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 若出现 Handler的生命周期 > 外部类的生命周期 时(即 Handler消息队列 还有未处理的消息 / 正在处理消息 而 外部类需销毁时),将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露 3、解决 将Handler子类设置为静态内部类+使用weakReference弱引用持有Activity实例 5.2)连续发送消息 1、问题 当你连续点击3下时,发现并无按照最新点击的按钮操作显示,而是按顺序的一个个显示出来 2、原因 使用HandlerThread时只是开了一个工作线程,当你点击了n下后,只是将n个消息发送到消息队列MessageQueue里排队,等候派发消息给Handler再进行对应的操作

3、IntentService

(1)介绍 Android里的一个封装类,继承四大组件之一Service,用于处理异步请求&实现多线程。线程任务需按顺序、在后台执行。适用于离线下载,不符合多个数据同时请求的场景(所有任务都在同一个Thread looper里执行) (2)使用 步骤1:定义 IntentService的子类 传入线程名称、复写onHandleIntent()方法

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中注册服务

步骤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); //多次启动 } }

(3)源码解析 3.1)工作原理 在这里插入图片描述 若启动IntentService 多次,那么 每个耗时操作 则 以队列的方式 在 IntentService的 onHandleIntent回调方法中依次执行,执行完自动结束 3.2)源码解析 问题1:IntentService如何单独开启1个新的工作线程 IntentService源码中的 onCreate()方法

@Override public void onCreate() { super.onCreate(); // 1. 通过实例化andlerThread新建线程 & 启动;故 使用IntentService时,不需额外新建线程 // HandlerThread继承自Thread,内部封装了 Looper HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); thread.start(); // 2. 获得工作线程的 Looper & 维护自己的工作队列 mServiceLooper = thread.getLooper(); // 3. 新建mServiceHandler & 绑定上述获得Looper // 新建的Handler 属于工作线程 ->>分析1 mServiceHandler = new ServiceHandler(mServiceLooper); } /** * 分析1:ServiceHandler源码分析 **/ private final class ServiceHandler extends Handler { // 构造函数 public ServiceHandler(Looper looper) { super(looper); } // IntentService的handleMessage()把接收的消息交给onHandleIntent()处理 @Override public void handleMessage(Message msg) { // onHandleIntent 方法在工作线程中执行 // onHandleIntent() = 抽象方法,使用时需重写 ->>分析2 onHandleIntent((Intent)msg.obj); // 执行完调用 stopSelf() 结束服务 stopSelf(msg.arg1); } } /** * 分析2: onHandleIntent()源码分析 * onHandleIntent() = 抽象方法,使用时需重写 **/ @WorkerThread protected abstract void onHandleIntent(Intent intent);

问题2:IntentService 如何通过onStartCommand() 将Intent 传递给服务 & 依次插入到工作队列中

/** * onStartCommand()源码分析 * onHandleIntent() = 抽象方法,使用时需重写 **/ public int onStartCommand(Intent intent, int flags, int startId) { // 调用onStart()->>分析1 onStart(intent, startId); return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; } /** * 分析1:onStart(intent, startId) **/ public void onStart(Intent intent, int startId) { // 1. 获得ServiceHandler消息的引用 Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; // 2. 把 Intent参数 包装到 message 的 obj 发送消息中, //这里的Intent = 启动服务时startService(Intent) 里传入的 Intent msg.obj = intent; // 3. 发送消息,即 添加到消息队列里 mServiceHandler.sendMessage(msg); }

3.3)源码总结 IntentService本质 = Handler + HandlerThread: 1、通过HandlerThread 单独开启1个工作线程:IntentService 2、创建1个内部 Handler :ServiceHandler 3、绑定 ServiceHandler 与 IntentService 4、通过 onStartCommand() 传递服务intent 到ServiceHandler 、依次插入Intent到工作队列中 & 逐个发送给 onHandleIntent() 5、通过onHandleIntent() 依次处理所有Intent对象所对应的任务 因此我们通过复写onHandleIntent() & 在里面 根据Intent的不同进行不同线程操作 即可 3.4)注意事项 注意事项1:工作任务队列 = 顺序执行 即 若一个任务正在IntentService中执行,此时你再发送1个新的任务请求,这个新的任务会一直等待直到前面一个任务执行完毕后才开始执行 原因: 1、由于onCreate()只会调用一次 = 只会创建1个工作线程; 2、当多次调用 startService(Intent)时(即 onStartCommand()也会调用多次),其实不会创建新的工作线程,只是把消息加入消息队列中 & 等待执行。 3、所以,多次启动 IntentService 会按顺序执行事件 若服务停止,则会清除消息队列中的消息,后续的事件不执行 注意事项2:不建议通过 bindService() 启动 IntentService 原因:

// 在IntentService中,onBind()`默认返回null @Override public IBinder onBind(Intent intent) { return null; }

采用 bindService()启动 IntentService的生命周期如下:

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

即,并不会调用onStart() 或 onStartcommand(),故不会将消息发送到消息队列,那么onHandleIntent()将不会回调,即无法实现多线程的操作 此时,你应该使用Service,而不是IntentService (4)对比 4.1)与Service对比 在这里插入图片描述 4.2)与其他线程对比 在这里插入图片描述

2.3)高级使用 线程池(ThreadPool)

(1)介绍 线程池是一块缓存了一定线程数量的区域,用于复用线程和管理线程(如1、统一分配、调优&监控2、控制线程池的最大并发数) 降低因线程创建&销毁带来的性能开销(重用缓存在线程池的线程) 提高线程响应速度&执行效率:1、重用线程 = 不需创建线程,即可马上执行2、管理线程 = 优化线程执行顺序(避免大量线程间因互相抢占系统资源而到只阻塞现象) 提高对线程的管理度 注:传统多线程方式(集成Thread类 & 实现Runnable接口)的问题 1、每次新建/销毁线程对象消耗资源、响应速度慢 2、线程缺乏统一管理,容易出现阻塞情况 (2)工作原理 2.1核心参数 在这里插入图片描述 上述6个参数的配置 决定了 线程池的功能,具体设置时机 = 创建 线程池类对象时 传入 ThreadPoolExecutor类 = 线程池的真正实现类 开发者可根据不同需求 配置核心参数,从而实现自定义线程池

// 创建线程池对象如下 // 通过 构造方法 配置核心参数 Executor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory ); // 构造函数源码分析 public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue, ThreadFactory threadFactory )

2.2内部原理逻辑 在这里插入图片描述 (3)使用流程

// 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() 作者:Carson_Ho 链接:https://www.jianshu.com/p/0e4a5e70bf0e 来源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

(4)常见的4类功能线程池 4.1定长线程池(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();

4.2定时线程池(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();

4.3可缓存线程池(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(); //当执行第二个任务时第一个任务已经完成 //那么会复用执行第一个任务的线程,而不用每次新建线程。

4.4单线程化线程池(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(); 2.4)对比

在这里插入图片描述

2.5)其他 1、Synchronized

一.设计目的 (1)JDK为何要设计锁 即同一时刻最多只有1个线程执行被Synchronized修饰的方法/代码,其他线程必须等待当前线程执行完该方法/代码块后才能执行该方法/代码块 (2)应用场景 多线程编程中,有可能会出现多个线程同时访问一个共享、可变资源(临界资源)的情况,这种资源可能是:对象、变量、文件等。由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问 加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问) 1、修饰 实例方法 / 代码块时,(同步)保护的是同一个对象方法的调用 & 当前实例对象 2、修饰 静态方法 / 代码块时,(同步)保护的是 静态方法的调用 & class 类对象 二.设计原理 (1)加锁对象 1、同步实例方法,锁是当前实例对象 2、同步类方法,锁是当前对象 3、同步代码块,锁是括号里的对象 (2)加锁原理 1、依赖 JVM 实现同步 2、底层通过一个监视器对象(monitor)完成, wait()、notify() 等方法也依赖于 monitor 对象 3、监视器锁(monitor)的本质 依赖于 底层操作系统的互斥锁(Mutex Lock)实现

(object){ //monitorenter进入同步块 //业务逻辑 //monitorexit退出同步块 }

(3)JVM加锁过程 在这里插入图片描述 三.具体使用 Synchronized 用于 修饰 代码块、类的实例方法 & 静态方法 (1)使用规则 在这里插入图片描述 (2)锁的类型&等级 1、类型 Synchronized会修饰代码块、类的实例方法&静态方法 在这里插入图片描述 2、区别 在这里插入图片描述 (3)使用方式

/** * 对象锁 */ public class Test{ // 对象锁:形式1(方法锁) public synchronized void Method1(){ System.out.println("我是对象锁也是方法锁"); try{ Thread.sleep(500); } catch (InterruptedException e){ e.printStackTrace(); } } // 对象锁:形式2(代码块形式) public void Method2(){ synchronized (this){ System.out.println("我是对象锁"); try{ Thread.sleep(500); } catch (InterruptedException e){ e.printStackTrace(); } } } } /** * 方法锁(即对象锁中的形式1) */ public synchronized void Method1(){ System.out.println("我是对象锁也是方法锁"); try{ Thread.sleep(500); } catch (InterruptedException e){ e.printStackTrace(); } } /** * 类锁 */ public class Test{    // 类锁:形式1 :锁静态方法 public static synchronized void Method1(){ System.out.println("我是类锁一号"); try{ Thread.sleep(500); } catch (InterruptedException e){ e.printStackTrace(); } } // 类锁:形式2 :锁静态代码块 public void Method2(){ synchronized (Test.class){ System.out.println("我是类锁二号"); try{ Thread.sleep(500); } catch (InterruptedException e){ e.printStackTrace(); } } } }

四.特点 在这里插入图片描述

2、ThreadLocal

(1)简介 ThreadLocal是线程的局部变量,用于为每个线程提供1个特定空间(即该变量),以保存该线程所独享的资源。适用于隔离线程&放置线程间数据资源共享的场景。 注: a.使每个线程可独立地改变自己空间内的资源(设置、存储的值)而不会和其他线程资源冲突 b.1个变量只能被同一个进程读、写,若第2个线程同时执行1段含有1个ThreadLocal变量引用的代码,它们也无法访问到对方的ThreadLocal变量 (2)使用流程 2.1创建ThreadLocal变量

// 1. 直接创建对象 private ThreadLocal myThreadLocal = new ThreadLocal() // 2. 创建泛型对象 private ThreadLocal myThreadLocal = new ThreadLocal(); // 3. 创建泛型对象 & 初始化值 // 指定泛型的好处:不需要每次对使用get()方法返回的值作强制类型转换 private ThreadLocal myThreadLocal = new ThreadLocal() { @Override protected String initialValue() { return "This is the initial value"; } }; // 特别注意: // 1. ThreadLocal实例 = 类中的private、static字段 // 2. 只需实例化对象一次 & 不需知道它是被哪个线程实例化 // 3. 每个线程都保持 对其线程局部变量副本 的隐式引用 // 4. 线程消失后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用) // 5. 虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程只能访问到自己通过调用ThreadLocal的set()设置的值 // 即 哪怕2个不同的线程在同一个`ThreadLocal`对象上设置了不同的值,他们仍然无法访问到对方的值

2.2访问ThreadLocal变量

// 1. 设置值:set() // 需要传入一个Object类型的参数 myThreadLocal.set("初始值”); // 2. 读取ThreadLocal变量中的值:get() // 返回一个Object对象 String threadLocalValue = (String) myThreadLocal.get();

(3)具体使用

public class ThreadLocalTest { // 测试代码 public static void main(String[] args){ // 新开2个线程用于设置 & 获取 ThreadLoacl的值 MyRunnable runnable = new MyRunnable(); new Thread(runnable, "线程1").start(); new Thread(runnable, "线程2").start(); } // 线程类 public static class MyRunnable implements Runnable { // 创建ThreadLocal & 初始化 private ThreadLocal threadLocal = new ThreadLocal(){ @Override protected String initialValue() { return "初始化值"; } }; @Override public void run() { // 运行线程时,分别设置 & 获取 ThreadLoacl的值 String name = Thread.currentThread().getName(); threadLocal.set(name + "的threadLocal"); // 设置值 = 线程名 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + ":" + threadLocal.get()); } } }

测试结果

线程1:线程1的threadLocal 线程2:线程2的threadLocal

从上述结果看出,在2个线程分别设置ThreadLocal值 & 分别获取,结果并未互相干扰 (4)实现原理 4.1)核心原理 ThreadLocal类中有1个Map(称:ThreadLocalMap):用于存储每个线程 & 该线程设置的存储在ThreadLocal变量的值 1、ThreadLocalMap的键Key = 当前ThreadLocal实例、值value = 该线程设置的存储在ThreadLocal变量的值 2、该key是 ThreadLocal对象的弱引用;当要抛弃掉ThreadLocal对象时,垃圾收集器会忽略该 key的引用而清理掉ThreadLocal对象 4.2)源码分析 如何设置 & 获取 ThreadLocal变量里的值

// ThreadLocal的源码 public class ThreadLocal { ... /** * 设置ThreadLocal变量引用的值 * ThreadLocal变量引用 指向 ThreadLocalMap对象,即设置ThreadLocalMap的值 = 该线程设置的存储在ThreadLocal变量的值 * ThreadLocalMap的键Key = 当前ThreadLocal实例 * ThreadLocalMap的值 = 该线程设置的存储在ThreadLocal变量的值 **/ public void set(T value) { // 1. 获得当前线程 Thread t = Thread.currentThread(); // 2. 获取该线程的ThreadLocalMap对象 ->>分析1 ThreadLocalMap map = getMap(t); // 3. 若该线程的ThreadLocalMap对象已存在,则替换该Map里的值;否则创建1个ThreadLocalMap对象 if (map != null) map.set(this, value);// 替换 else createMap(t, value);// 创建->>分析2 } /** * 获取ThreadLocal变量里的值 * 由于ThreadLocal变量引用 指向 ThreadLocalMap对象,即获取ThreadLocalMap对象的值 = 该线程设置的存储在ThreadLocal变量的值 **/ public T get() { // 1. 获得当前线程 Thread t = Thread.currentThread(); // 2. 获取该线程的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 3. 若该线程的ThreadLocalMap对象已存在,则直接获取该Map里的值;否则则通过初始化函数创建1个ThreadLocalMap对象 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; // 直接获取值 } return setInitialValue(); // 初始化 } /** * 初始化ThreadLocal的值 **/ private T setInitialValue() { T value = initialValue(); // 1. 获得当前线程 Thread t = Thread.currentThread(); // 2. 获取该线程的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 3. 若该线程的ThreadLocalMap对象已存在,则直接替换该值;否则则创建 if (map != null) map.set(this, value); // 替换 else createMap(t, value); // 创建->>分析2 return value; } /** * 分析1:获取当前线程的threadLocals变量引用 **/ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } /** * 分析2:创建当前线程的ThreadLocalMap对象 **/ void createMap(Thread t, T firstValue) { // 新创建1个ThreadLocalMap对象 放入到 Thread类的threadLocals变量引用中: // a. ThreadLocalMap的键Key = 当前ThreadLocal实例 // b. ThreadLocalMap的值 = 该线程设置的存储在ThreadLocal变量的值 t.threadLocals = new ThreadLocalMap(this, firstValue); // 即 threadLocals变量 属于 Thread类中 ->> 分析3 } ... } /** * 分析3:Thread类 源码分析 **/ public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null; // 即 Thread类持有threadLocals变量 // 线程类实例化后,每个线程对象拥有独立的threadLocals变量变量 // threadLocals变量在 ThreadLocal对象中 通过set() 或 get()进行操作 ... }

(5)补充 5.1)ThreadLocal如何做到线程安全 1、每个线程拥有自己独立的ThreadLocals变量(指向ThreadLocalMap对象 ) 2、每当线程 访问 ThreadLocals变量时,访问的都是各自线程自己的ThreadLocalMap变量(键 - 值) 3、ThreadLocalMap变量的键 key = 唯一 = 当前ThreadLocal实例 5.2)与同步机制的区别 在这里插入图片描述 #(三)Java/Android 多线程开发联系/区别



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3