(1)Java线程 6 种状态
public static enum Thread.State 用来描述一个线程的状态,包括6种状态
注:这里的六中状态是JVM层面的状态,与OS底层的线程状态不同(OS底层线程状态有5种,如下图)
Java 的线程状态倾向于描述线程,而OS的线程状态倾向于描述CPU。
而对于Java而言,Java的线程类型都来源于Thread 类下的 State 这一内部枚举类中所定义的状态:
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 方法之后,以再次进入一个同步的块或方法。
过程如下:
这两种情况可总结为:当因为获取不到锁而无法进入同步块时,线程处于 BLOCKED 状态。BLOCKED状态可以看做特殊的WAITING,表示等待同步锁的状态。如果有线程长时间处于 BLOCKED 状态,要考虑是否发生了死锁(deadlock)的状况。
一个线程进入 WAITING 状态是因为调用了以下方法:
然后会等其它线程执行一个特别的动作,比如:
进程协作
可以看出,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、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。
(1)带参数的wait(n)
没有参数的wait()等价于wait(0),表示线程永久等下去,等到天荒地老,除非收到通知。这种完全将再次活动的命运交给通知者可能会导致该线程永远等下去,无法得到执行的机会(当通知者准备执行notify时因某种原因被杀死,持有的锁也释放,此时线程执行的条件满足了,但等待的线程却因收不到通知从而一直处于等待状态)
此时可设置带有参数的wait(1000),等待1秒,相当于等待两个通知,取决于哪个先到:
(2)sleep
进入 TIMED_WAITING 状态的另一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。
这种情况下就是完全靠“自带闹钟”来通知。(sleep方法不会等待协作进程的通知)
sleep方法没有任何同步语义,与锁无关:sleep方法不会等待协作进程的通知,当线程调用sleep方法时带了锁,则sleep期间锁仍为线程所拥有。
补充:wait 与 sleep 的区别与联系
wait和sleep均能使线程处于等待状态
总结:
(2)Java线程 状态转换
“Java 线程状态的改变通常只与自身显式引入的机制有关。如果 JVM 中的线程状态发生改变了,通常是自身机制引发的。比如 synchronize 机制有可能让线程进入BLOCKED 状态,sleep,wait等方法则可能让其进入 WATING 之类的状态。
JVM充分地利用现代多核处理器的强大性能。采用异步调用线程,提高使用性能,缺点就是会造成线程不安全。为了保证线程安全性,即确保Java内存模型的可见性、原子性和有序性。Java主要通过volatile、synchronized实现线程安全。
synchronized 规定了同一个时刻只允许一条线程可以进入临界区(互斥性),同时还保证了共享变量的内存可见性。此规则决定了持有同一个对象锁的多个同步块只能串行执行。
Java中的每个对象都可以为锁。
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的内部
在 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也会进行相应的的变化。下面先简单描述下锁撤销之后,升级为轻量级锁的过程:
轻量级锁主要是自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗cpu的,就相当于在执行一个啥也没有的for循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。自旋锁有一些问题:
(1)如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在原地等待空消耗cpu。
(2)本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。
基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。
4. 重量级锁
轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。
主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
这就是说为什么重量级线程开销很大的。互斥锁(重量级锁)也称为阻塞同步、悲观锁
因此可做个总结:
线程可以通过两种方式锁住一个对象:
获取锁(monitorenter)的大概过程:
释放锁(monitorexit)的大概过程:
重入锁 & 非重入锁
可重入锁指同一个线程可以再次获得之前已经获得的锁,避免产生死锁。
当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
下面为可重入锁与非可重入锁的实现区别
不可重入锁
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中常用的可重入锁
注意:
这里要区别,同一个对象的多方法都加入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是怎么样实现的。
结论:
(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()方法中继续往下执行。
要注意
典型案例:生产者-消费者问题
两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。
/**
* 生产者生产出来的产品交给店员
*/
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 <= MIN_PRODUCT)
{
try
{
wait();
System.out.println("缺货,稍候再取");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
System.out.println("消费者取走了第" + this.product + "个产品.");
this.product--;
notifyAll(); //通知等待去的生产者可以生产产品了
}
1.Java多线程内存模式 与 重排序
在JAVA多线程环境下,对于每个Java线程除了共享的虚拟机栈外和Java堆之外,还存在一个独立私有的工作内存,工作内存存放主存中变量的值的拷贝。每个线程独立运行,彼此之间都不可见,线程的私有堆内存中保留了一份主内存的拷贝,只有在特定需求的情况下才会与主存做交互(复制/刷新)。
当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作
每一个操作都是原子的,即执行期间不会被中断。
对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。
如果需要在其他线程中立即可见,需要使用 volatile 关键字。
在有些场景下多线程访问程序变量会表现出与程序制定的顺序不一样。因为编译器可以以优化的名义改变每个独立线程的顺序,从而使处理器不按原来的顺序执行线程。一个Java程序在从源代码到最终实际执行的指令序列之间,会经历一系列的重排序过程。
对于多线程共享同一内存区域这一情况,使得每个线程不知道其他线程对数据做了怎样的修改(数据修改位于线程的私有内存中,具有不可见性),从而导致执行结果不正确。因此必须要解决这一同步问题。
2.volatile原理
对于非volatile变量进行读写时,每个写成先从主存拷贝变量到线程缓存中,执行完操作再保存到主存中。需要进行load/save操作。
而volatile变量保证每次读写变量都是不经过缓存而是直接从内存读写数据。省去了load/save操作。volatile变量不会将对该变量的操作与其他内存操作一起重排序,能及时更新到主存;且因该变量存储在主存上,所以总会返回最新写入的值。
因此volatile定义的变量具有以下特性:
3.volatile实例
class Bank {
//需要同步的变量加上volatile
private volatile int account = 100;
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
account += money;
}
}
4. Volatile、Synchronized 与 Final 对比
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。
lock: 在java.util.concurrent包内。共有三个实现:
//ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。
区别如下:
ReenreantLock类实现原理
轻松学习java可重入锁(ReentrantLock)的实现原理
ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。
ReentrantLock结构如下
volatile int state | 表示临界资源占有状态 |
---|---|
Thread | 正在执行的线程 |
Node 双向队列 | 处于等待阻塞的节点 |
即公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序(队列的先后顺序)来依次获得锁。而不公平锁则不用按照申请锁的时间顺序获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
ReenreantLock类的常用方法
class Bank {
private int account = 100;
//需要声明这个锁
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
ReenreantLock & Synchronized 的选择
比较类型 | Synchronized | ReenreantLock |
---|---|---|
锁的实现 | JVM | JDK |
等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情 | 不可中断 | 可中断 |
公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。 | 非公平 | 公平/非公平(默认) |
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
总结:Synchronized & ReentrantLock & Volatile 区别
(1)Synchronized &Volatile 区别
AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。
当然Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。
(2)Synchronized & ReentrantLock 区别
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
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;//要执行的任务
}
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()
Java中断机制
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();
}
}
}
线程执行方法与状态的联系
Thread类实现案例
(1)继承Thread类,重写该类的run()方法,run方法的方法体代表了线程要完成的任务,因此run方法可称为执行体
(2)创建Thread子类的实例,即创建线程对象
(3)调用线程对象的start方法启动线程
package com.thread;
public class FirstThreadTest extends Thread{
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
public void run()
{
for(;i<100;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args)
{
for(int i = 0;i< 100;i++)
{
// 调用100次main主线程
System.out.println(Thread.currentThread().getName()+" : "+i);
if(i==20)
{
// 当主线程调用到20时,执行100次子线程-1,子线程-2
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
实现结果
main : 18
main : 19
main : 20
Thread-0 0
Thread-0 1
main : 21
Thread-0 2
Thread-1 0
main : 22
main : 23
Thread-1 1
Thread-0 3
(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();
}
}
}
}
通过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
{
public static void main(String[] args)
{
// 创建Callable实现体的实例,使用FutureTask类包装Callable对象
CallableThreadTest ctt = new CallableThreadTest();
FutureTask 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;
}
}
创建线程的三种方式及对比
创建方式 | 使用方式 | 优势 | 劣势 |
---|---|---|---|
Thread | 继承Thread类创建线程类,并重写run方法 | 编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。 | 线程类已经继承了Thread类,所以不能再继承其他父类。 |
Runnable接口 | 实现Runnable接口创建线程类,并实现run方法 | 线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。 | 编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。 |
Callable接口 | 实现Callable接口创建线程类,并用FutureTask类包装Callable对象,并实现call方法 | 同上 | 同上 |
Java1.5提供了一个非常高效实用的多线程包:java.util.concurrent, 提供了大量高级工具,可以帮助开发者编写高效、易维护、结构清晰的Java多线程程序。
用处:保存线程的独立变量。对一个线程类(继承自Thread)
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,副本之间相互独立,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。
实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。
主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。
ThreadLocal 类的常用方法
public class Bank{
//使用ThreadLocal类管理共享变量account
private static ThreadLocal account = new ThreadLocal(){
@Override
protected Integer initialValue(){
return 100;
}
};
public void save(int money){
account.set(account.get()+money);
}
public int getAccount(){
return account.get();
}
}
如果使用atomic wrapper class如atomicInteger,或者使用自己保证原子的操作,则等同于synchronized
//返回值为boolean
AtomicInteger.compareAndSet(int expect,int update)
该方法可用于实现乐观锁,考虑文中最初提到的如下场景:a给b付款10元,a扣了10元,b要加10元。此时c给b2元,但是b的加十元代码约为:
if(b.value.compareAndSet(old, value)){
return ;
}else{
//try again
// if that fails, rollback and log
}
AtomicReference
对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。
这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号
简介
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。
信号量的特性如下:
在多线程对一个(多个)公共资源进行访问的场景下,
信号量是一个非负整数(表示可以并发访问公共资源的线程数),所有通过它的线程都会将该整数减一(可使用的公共资源数目-1),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一(Semaphore>0);要么一直等下去(Semaphore<=0),直到信号量大于0或超时。Release(释放)实际上是在信号量上执行加操作,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的公共资源。
在java中,还可以设置该信号量是否采用公平模式,如果以公平方式执行,则线程将会按到达的顺序(FIFO)执行,如果是非公平,则可以后请求的有可能排在队列的头部。
Java 使用
Semaphore(int permits, boolean fair)
//创建具有给定的许可数和给定的公平设置的Semaphore。
Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java并发库Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。
Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
实现案例
模拟上述实例,创建一个 同一时间最多只有5个线程访问 的连接池。
package SemaPhore;
import java.util.Random;
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
//线程池
ExecutorService executor = Executors.newCachedThreadPool();
//定义信号量,只能5个线程同时访问
final Semaphore semaphore = new Semaphore(5);
//模拟20个线程同时访问
for (int i = 0; i < 20; i++) {
final int NO = i;
Runnable runnable = new Runnable() {
public void run() {
try {
//获取许可
semaphore.acquire();
//availablePermits()指的是当前信号灯库中有多少个可以被使用
System.out.println("线程" + Thread.currentThread().getName() +"进入,当前已有" + (5-semaphore.availablePermits()) + "个并发");
System.out.println("index:"+NO);
Thread.sleep(new Random().nextInt(1000)*10);
System.out.println("线程" + Thread.currentThread().getName() + "即将离开");
//访问完后,释放
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
}
};
executor.execute(runnable);
}
// 退出线程池
executor.shutdown();
}
}
例如:
记创建线程消耗时间T1,执行任务消耗时间T2,销毁线程消耗时间T3
如果T1+T3>T2,那么是不是说开启一个线程来执行这个任务太不划算了!
正好,线程池缓存线程,可用已有的闲置线程来执行新任务,避免了T1+T3带来的系统开销
我们知道线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况。运用线程池能有效的控制线程最大并发数,避免以上的问题
比如:延时执行、定时循环执行的策略等 运用线程池都能进行很好的实现
Executor接口是Executor框架的一个最基本的接口,Executor框架的大部分类都直接或间接地实现了此接口。 只有一个方法
void execute(Runnable command):
在未来某个时间执行给定的命令。该命令可能在新的线程、已入池的线程或者正调用的线程中执行,这由 Executor 实现决定。
新建一个线程池时,一般只用5个参数的构造函数。
(3.2)添加任务
通过ThreadPoolExecutor.execute(Runnable command)方法即可向线程池内添加一个任务
(3.3)执行策略
(1)CachedThreadPool()
可缓存线程池:
创建方法:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
(2)FixedThreadPool()
定长线程池:
创建方法:
//nThreads => 最大线程数即maximumPoolSize
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);
//threadFactory => 创建线程的方法,这就是我叫你别理他的那个星期六!你还看!
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()));
}
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)并发模型
(1.1)并发与并行
并发程序是指在运行中有两个及以上的任务同时在处理,与之相关的概念并行,是指在运行中有两个及以上的任务同时执行,差别是在于处理和执行。在单核CUP中两个及以上任务的处理方式是让它们交替的进入CUP执行,这种对执行的处理方式就是并发。
并行只能发生在多核CUP中,每个CUP核心拿到一个任务同时执行,并行是并发的一个子集
与串行程序相比并发编程的优点:
(1)提高硬件资源的利用率(特别是IO资源),提高系统的响应速度、减少客户端等待、增加系统吞吐量
(2)解决特定领域的问题,比如GUI系统,WEB服务器等。
(1.2)线程并发实现
并发实现包括三种:进程并发、线程并发与协程并发。
其中线程并发是Java的并发模型方式。
在操作系统中线程是包含在进程中的消费资源较少、运行迅速的最小执行单元,根据操作系统内核是否对线程可感知,把线程分为内核线程和用户线程。
(1.3)线程并发通信
线程间通过协作才能合力完成一个任务,协作的基础就是通信。常用的线程间通信的方式有两种。
(2)Java并发模型——线程模型
(2.1)简介
每一个JAVA线程都对应者一个内核线程,所以线程的创建、调度、上下文切换都需要系统内核的支持,会消耗系统资源。
JAVA线程间通信是通过共享内存实现的,锁在线程并发中有着举足轻重地位,使用JAVA多线程时需要非常小心的处理同步问题。
(2.2)问题
Java并发编程需要面对两个问题:
(2.3)并发机制
(2.4)并发工具
(1)什么是Java线程安全
多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。
线程安全有以下几种实现方式
(2)如何保证Java线程安全
(1)原理
在下载行为出现中断的时候,记录下中断的位置信息,然后在下次行为开始的时候,直接从记录的这个位置开始下载内容,而不再从头开始。
分为两步:
(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”,由此也就完成了续传。
(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();
(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多线程开发。
见陈小云Android学习之旅:第十章 进程间的通信 之 Handler机制(二)
(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<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)问题&解决
6.1)关于生命周期
问题:AsyncTask不与任何组件绑定生命周期
解决:在Activity 或 Fragment中使用 AsyncTask时,最好在Activity 或 Fragment的onDestory()调用 cancel(boolean);
6.2)关于内存泄露
问题:若AsyncTask被声明为Activity的非静态内部类,当Activity需销毁时,会因AsyncTask保留对Activity的引用 而导致Activity无法被回收,最终引起内存泄露
解决:AsyncTask应被声明为Activity的静态内部类
线程任务执行结果丢失
问题:当Activity重新创建时(屏幕旋转 / Activity被意外销毁时后恢复),之前运行的AsyncTask(非静态的内部类)持有的之前Activity引用已无效,故复写的onPostExecute()将不生效,即无法更新UI操作
解决:在Activity恢复时的对应方法 重启 任务线程
(7)源码分析
7.1)原理介绍
AsyncTask的实现原理 = 线程池 + Handler
其中:线程池用于线程调度、复用 & 执行任务;Handler 用于异步通信
其内部封装了2个线程池 + 1个Handler,具体介绍如下:
7.2)源码分析
根据AsyncTask使用步骤讲解
步骤1:创建AsbncTask子类
该类复写的方法在后续源码中调用
步骤2:创建AsyncTask子类的实例对象(任务实例)
1、具体使用
MyTask mTask = new MyTask();
2、源码分析:AsyncTask的构造函数
/**
* 源码分析:AsyncTask的构造函数
*/
public AsyncTask() {
// 1. 初始化WorkerRunnable变量 = 一个可存储参数的Callable对象 ->>分析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源码总结
(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);
}
}
}
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再进行对应的操作
(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)与其他线程对比
(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 )
// 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();
一.设计目的
(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();
}
}
}
}
(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 多线程开发联系/区别