13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

Android性能优化之Java线程机制与线程调度原理详解

本文转载自微信公众号「Android开发编程」,作者Android开发编程。转载本文请联系Android开发编程公众号。

创新互联主要从事网站制作、成都网站设计、网页设计、企业做网站、公司建网站等业务。立足成都服务托克托,十年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:18982081108

前言小计

在平时工作中如若使用不当会出现数据错乱、执行效率低(还不如单线程去运行)或者死锁程序挂掉等等问题,所以掌握了解多线程至关重要;

线程有很多优势:

1、提高多处理器的利用效率;

2、简化业务功能设计;

3、实现异步处理;

多线程的风险:

1、共享数据的线程安全性;

2、多线程执行时的活跃性问题;

3、多线程的所带来的性能损失问题;

多线程相对于其他知识点来讲,有一定的学习门槛,并且了解起来比较费劲;

线程的优势我们很清楚,线程的风险我们也都知道,但是要做好风险控制就没有那么简单了;

本文从基础概念开始到最后的并发模型由浅入深,讲解下线程方面的知识;

一、什么是线程?

1、线程简介

2、线程的四个属性

线程有编号、名字、类别以及优先级四个属性,除此之外,线程的部分属性还具有继承性,下面我们就来看看线程的四个属性的作用和线程的继承性;

①编号

线程的编号(id)用于标识不同的线程,每条线程拥有不同的编号;

注意事项:不能作为唯一标识,某个编号的线程运行结束后,该编号可能被后续创建的线程使用,因此编号不适合用作唯一标识,编号是只读属性,不能修改;

②名字

③类别

④优先级

作用:线程的优先级(Priority)用于表示应用希望优先运行哪个线程,线程调度器会根据这个值来决定优先运行哪个线程;

⑤取值范围

Java 中线程优先级的取值范围为 1~10,默认值是 5,Thread 中定义了下面三个优先级常量;

注意事项:不保证,线程调度器把线程的优先级当作一个参考值,不一定会按我们设定的优先级顺序执行线程;

⑥线程饥饿

优先级使用不当会导致某些线程永远无法执行,也就是线程饥饿的情况;

⑦继承性

线程的继承性指的是线程的类别和优先级属性是会被继承的,线程的这两个属性的初始值由开启该线程的线程决定;

假如优先级为 5 的守护线程 A 开启了线程 B,那么线程 B 也是一个守护线程,而且优先级也是 5 ;

这时我们就把线程 A 叫做线程 B 的父线程,把线程 B 叫做线程 A 的子线程;

3、线程的六个重要方法

线程的常用方法有六个,它们分别是三个非静态方法 start()、run()、join() 和三个静态方法 currentThread()、yield()、sleep() ;

下面我们就来看下这六个方法都有哪些作用和注意事项

①start()

② run()

③ join()

④Thread.currentThread()

⑤Thread.yield()

⑥ Thread.sleep(ms)

作用:sleep(ms) 方法是一个静态方法,用于使当前线程在指定时间内休眠(暂停)。

4、线程的六种状态

①线程的生命周期

线程的生命周期不仅可以由开发者触发,还会受到其他线程的影响,下面是线程各个状态之间的转换示意图;

我们可以通过 Thread.getState() 获取线程的状态,该方法返回的是一个枚举类 Thread.State;

线程的状态有新建、可运行、阻塞、等待、限时等待和终止 6 种,下面我们就来看看这 6 种状态之间的转换过程;

新建状态:当一个线程创建后未启动时,它就处于新建(NEW)状态;

②可运行状态:当我们调用线程的 start() 方法后,线程就进入了可运行(RUNNABLE)状态,可运行状态又分为预备(READY)和运行(RUNNING)状态;

③预备状态:处于预备状态的线程可被线程调度器调度,调度后线程的状态会从预备转换为运行状态,处于预备状态的线程也叫活跃线程,当线程的 yield() 方法被调用后,线程的状态可能由运行状态变为预备状态

④运行状态:运行状态表示线程正在运行,也就是处理器正在执行线程的 run() 方法;

⑤阻塞状态:当下面几种情况发生时,线程就处于阻塞(BLOCKED)状态,发起阻塞式 I/O 操作、申请其他线程持有的锁、进入一个 synchronized 方法或代码块失败;

⑥等待状态:一个线程执行特定方法后,会等待其他线程执行执行完毕,此时线程进入了等待(WAITING)状态;

⑦等待状态:下面的几个方法可以让线程进入等待状态;

可运行状态:下面的几个方法可以让线程从等待状态转变为可运行状态,而这种转变又叫唤醒;

⑧限时等待状态

⑨ 终止状态

当线程的任务执行完毕或者任务执行遇到异常时,线程就处于终止(TERMINATED)状态;

二、线程调度的原理

线程调度原理相关的对 Java 内存模型、高速缓存、Java 线程调度机制进行一个简单介绍;

1、Java 的内存模型

2、高速缓存

①高速缓存简介

现代处理器的处理能力要远胜于主内存(DRAM)的访问速率,主内存执行一次内存读/写操作需要的时间,如果给处理器使用,处理器可以执行上百条指令;

为了弥补处理器与主内存之间的差距,硬件设计者在主内存与处理器之间加入了高速缓存(Cache);

处理器执行内存读写操作时,不是直接与主内存打交道,而是通过高速缓存进行的;

高速缓存相当于是一个由硬件实现的容量极小的散列表,这个散列表的 key 是一个对象的内存地址,value 可以是内存数据的副本,也可以是准备写入内存的数据;

②高速缓存内部结构

从内部结构来看,高速缓存相当于是一个链式散列表(Chained Hash Table),它包含若干个桶,每个桶包含若干个缓存条目(Cache Entry);

③缓存条目结构

缓存条目可进一步划分为 Tag、Data Block 和 Flag 三个部分;

Tag:包含了与缓存行中数据对应的内存地址的部分信息(内存地址的高位部分比特)

Data: Block 也叫缓存行(Cache Line),是高速缓存与主内存之间数据交换的最小单元,可以存储从内存中读取的数据,也可以存储准备写进内存的数据;

Flag: 用于表示对应缓存行的状态信息

3、Java 线程调度机制

在任意时刻,CPU 只能执行一条机器指令,每个线程只有获取到 CPU 的使用权后,才可以执行指令;

也就是在任意时刻,只有一个线程占用 CPU,处于运行的状态;

多线程并发运行实际上是指多个线程轮流获取 CPU 使用权,分别执行各自的任务;

线程的调度由 JVM 负责,线程的调度是按照特定的机制为多个线程分配 CPU 的使用权;

线程调度模型分为两类:分时调度模型和抢占式调度模型;

①分时调度模型

分时调度模型是让所有线程轮流获取 CPU 使用权,并且平均分配每个线程占用 CPU 的时间片;

②抢占式调度模型

三、线程的安全性问题详解

线程安全问题不是说线程不安全,而是多个线程之间交错操作有可能导致数据异常;

下面我们就来看下与线程安全相关的竞态和实现线程安全要保证的三个点:原子性、可见性和有序性;

①原子性

②可见性

③ 有序性

四、实现线程安全

要实现线程安全就要保证上面说到的原子性、可见性和有序性;

常见的实现线程安全的办法是使用锁和原子类型,而锁可分为内部锁、显式锁、读写锁、轻量级锁(volatile)四种;

下面我们就来看看这四种锁和原子类型的用法和特点;

1、锁

是锁(Lock)的作用,让多个线程更好地协作,避免多个线程的操作交错导致数据异常的问题;

锁的五个特点:

2、 volatile 关键字

volatile 关键字可用于修饰共享变量,对应的变量就叫 volatile 变量,volatile 变量有下面几个特点;

3、原子类型

原子类型简介:

在 JUC 下有一个 atomic 包,这个包里面有一组原子类,使用原子类的方法,不需要加锁也能保证线程安全,而原子类是通过 Unsafe 类中的 CAS 指令从硬件层面来实现线程安全的;

这个包里面有如 AtomicInteger、AtomicBoolean、AtomicReference、AtomicReferenceFIeldUpdater 等;

我们先来看一个使用原子整型 AtomicInteger 自增的例子;

// 初始值为 1

AtomicInteger integer = new AtomicInteger(1);

// 自增

int result = integer.incrementAndGet();

// 结果为 2

System.out.println(result);

AtomicReference 和 AtomicReferenceFIeldUpdater 可以让我们自己的类具有原子性,它们的原理都是通过 Unsafe 的 CAS 操作实现的;

我们下面看下它们的用法和区别;

①、AtomicReference 基本用法

  
 
 
 
  1. class AtomicReferenceValueHolder { 
  2.   AtomicReference atomicValue = new AtomicReference<>("HelloAtomic"); 
  3. public void getAndUpdateFromReference() { 
  4.   AtomicReferenceValueHolder holder = new AtomicReferenceValueHolder(); 
  5.   // 对比并设值 
  6.   // 如果值是 HelloAtomic,就把值换成 World 
  7.   holder.atomicValue.compareAndSet("HelloAtomic", "World"); 
  8.   // World 
  9.   System.out.println(holder.atomicValue.get()); 
  10.   // 修改并获取修改后的值 
  11.   String value = holder.atomicValue.updateAndGet(new UnaryOperator() { 
  12.     @Override 
  13.     public String apply(String s) { 
  14.       return "HelloWorld"; 
  15.     } 
  16.   }); 
  17.   // Hello World   
  18.   System.out.println(value); 

② AtomicReferenceFieldUpdater 基本用法

AtomicReferenceFieldUpdater 在用法上和 AtomicReference 有些不同,我们直接把 String 值暴露了出来,并且用 volatile 对这个值进行了修饰;

并且将当前类和值的类传到 newUpdater ()方法中获取 Updater,这种用法有点像反射,而且 AtomicReferenceFieldUpdater 通常是作为类的静态成员使用;

  
 
 
 
  1. public class SimpleValueHolder { 
  2.   public static AtomicReferenceFieldUpdater valueUpdater 
  3.     = AtomicReferenceFieldUpdater.newUpdater( 
  4.       SimpleValueHolder.class, String.class, "value"); 
  5.   volatile String value = "HelloAtomic"; 
  6. public void getAndUpdateFromUpdater() { 
  7.   SimpleValueHolder holder = new SimpleValueHolder(); 
  8.   holder.valueUpdater.compareAndSet(holder, "HelloAtomic", "World"); 
  9.   // World 
  10.   System.out.println(holder.valueUpdater.get(holder)); 
  11.   String value = holder.valueUpdater.updateAndGet(holder, new UnaryOperator() { 
  12.     @Override 
  13.     public String apply(String s) { 
  14.       return "HelloWorld"; 
  15.     } 
  16.   }); 
  17.   // HelloWorld 
  18.   System.out.println(value); 

③AtomicReference 与 AtomicReferenceFieldUpdater 的区别

AtomicReference 和 AtomicReferenceFieldUpdater 的作用是差不多的,在用法上 AtomicReference 比 AtomicReferenceFIeldUpdater 更简单;

但是在内部实现上,AtomicReference 内部一样是有一个 volatile 变量;

使用 AtomicReference 和使用 AtomicReferenceFIeldUpdater 比起来,要多创建一个对象;

对于 32 位的机器,这个对象的头占 12 个字节,它的成员占 4 个字节,也就是多出来 16 个字节;

对于 64 位的机器,如果启动了指针压缩,那这个对象占用的也是 16 个字节;

对于 64 位的机器,如果没启动指针压缩,那么这个对象就会占 24 个字节,其中对象头占 16 个字节,成员占 8 个字节;

当要使用 AtomicReference 创建成千上万个对象时,这个开销就会变得很大;

这也就是为什么 BufferedInputStream 、Kotlin 协程 和 Kotlin 的 lazy 的实现会选择 AtomicReferenceFieldUpdater 作为原子类型;

因为开销的原因,所以一般只有在原子类型创建的实例确定了较少的情况下,比如说是单例,才会选择 AtomicReference,否则都是用 AtomicReferenceFieldUpdater;

4、 锁的使用技巧

五、线程的四个活跃性问题

1、死锁

死锁是线程的一种常见多线程活跃性问题,如果两个或更多的线程,因为相互等待对方而被永远暂停,那么这就叫死锁现象;

下面我们就来看看死锁产生的四个条件和避免死锁的三个方法;

2、死锁产生的四个条件

当多个线程发生了死锁后,这些线程和相关共享变量就会满足下面四个条件:

只要产生了死锁,上面的条件就一定成立,但是上面的条件都成立也不一定会产生死锁;

3、 避免死锁的三个方法

要想消除死锁,只要破坏掉上面的其中一个条件即可;

由于锁具有排他性,且无法被动释放,所以我们只能破坏掉第三个和第四个条件;

①、粗锁法

②锁排序法

锁排序法指的是相关线程使用全局统一的顺序申请锁;

假如有多个线程需要申请锁,我们只需要让这些线程按照一个全局统一的顺序去申请锁,这样就能破坏“循环等待资源”这个条件;

③tryLock

显式锁 ReentrantLock.tryLock(long timeUnit) 这个方法允许我们为申请锁的操作设置超时时间,这样就能破坏“占用并等待资源”这个条件;

④开放调用

开放调用(Open Call)就是一个方法在调用外部方法时不持有锁,开放调用能破坏“占用并等待资源”这个条件;

六、线程之间怎么协作?

线程间的常见协作方式有两种:等待和中断;

当一个线程中的操作需要等待另一个线程中的操作结束时,就涉及到等待型线程协作方式;

常用的等待型线程协作方式有 join、wait/notify、await/signal、await/countDown 和 CyclicBarrier 五种,下面我们就来看看这五种线程协作方式的用法和区别;

1、join

下面是 join() 方法的简单用法;

  
 
 
 
  1. public void tryJoin() { 
  2.   Thread threadA = new ThreadA(); 
  3.   Thread threadB = new ThreadB(threadA); 
  4.   threadA.start(); 
  5.   threadB.start(); 
  6. public class ThreadA extends Thread { 
  7.   @Override 
  8.   public void run() { 
  9.     System.out.println("线程 A 开始执行"); 
  10.     ThreadUtils.sleep(1000); 
  11.     System.out.println("线程 A 执行结束"); 
  12.   } 
  13. public class ThreadB extends Thread { 
  14.   private final Thread threadA; 
  15.   public ThreadB(Thread thread) { 
  16.     threadA = thread; 
  17.   } 
  18.   @Override 
  19.   public void run() { 
  20.     try { 
  21.       System.out.println("线程 B 开始等待线程 A 执行结束"); 
  22.       threadA.join(); 
  23.       System.out.println("线程 B 结束等待,开始做自己想做的事情"); 
  24.     } catch (InterruptedException e) { 
  25.       e.printStackTrace(); 
  26.     } 
  27.   } 

2、 wait/notify

下面是 wait/notify 使用的示例代码;

  
 
 
 
  1. final Object lock = new Object(); 
  2. private volatile boolean conditionSatisfied; 
  3. public void startWait() throws InterruptedException { 
  4.   synchronized (lock) { 
  5.     System.out.println("等待线程获取了锁"); 
  6.     while(!conditionSatisfied) { 
  7.       System.out.println("保护条件不成立,等待线程进入等待状态"); 
  8.       lock.wait(); 
  9.     } 
  10.     System.out.println("等待线程被唤醒,开始执行目标动作"); 
  11.   } 
  12. public void startNotify() { 
  13.   synchronized (lock) { 
  14.     System.out.println("通知线程获取了锁"); 
  15.     System.out.println("通知线程即将唤醒等待线程"); 
  16.     conditionSatisfied = true; 
  17.     lock.notify(); 
  18.   } 

3、 wait/notify 原理

4、notify()/notifyAll()

notify() 可能导致信号丢失,而 notifyAll() 虽然会把不需要唤醒的等待线程也唤醒,但是在正确性方面有保障;

所以一般情况下优先使用 notifyAll() 保障正确性;

一般情况下,只有在下面两个条件都实现时,才会选择使用 notify() 实现通知;

①只需唤醒一个线程

当一次通知只需要唤醒最多一个线程时,我们可以考虑使用 notify() 实现通知,但是光满足这个条件还不够;

在不同的等待线程使用不同的保护条件时,notify() 唤醒的一个任意线程可能不是我们需要唤醒的那个线程,所以需要条件 2 来排除;

②对象的等待集中只包含同质等待线程

同质等待线程指的是线程使用同一个保护条件并且 wait() 调用返回后的逻辑一致;

最典型的同质线程是使用同一个 Runnable 创建的不同线程,或者同一个 Thread 子类 new 出来的多个实例;

5、await/signal

wait()/notify() 过于底层,而且还存在两个问题,一是过早唤醒、二是无法区分 Object.wait(ms) 返回是由于等待超时还是被通知线程唤醒;

await/signal 基本用法

  
 
 
 
  1. private Lock lock = new ReentrantLock(); 
  2. private Condition condition = lock.newCondition(); 
  3. private volatile boolean conditionSatisfied = false; 
  4. private void startWait() { 
  5.   lock.lock(); 
  6.   System.out.println("等待线程获取了锁"); 
  7.   try { 
  8.     while (!conditionSatisfied) { 
  9.       System.out.println("保护条件不成立,等待线程进入等待状态"); 
  10.       condition.await(); 
  11.     } 
  12.     System.out.println("等待线程被唤醒,开始执行目标动作"); 
  13.   } catch (InterruptedException e) { 
  14.     e.printStackTrace(); 
  15.   } finally { 
  16.     lock.unlock(); 
  17.     System.out.println("等待线程释放了锁"); 
  18.   } 
  19. public void startNotify() { 
  20.   lock.lock(); 
  21.   System.out.println("通知线程获取了锁"); 
  22.   try { 
  23.     conditionSatisfied = true; 
  24.     System.out.println("通知线程即将唤醒等待线程"); 
  25.     condition.signal(); 
  26.   } finally { 
  27.     System.out.println("通知线程释放了锁"); 
  28.     lock.unlock(); 
  29.   } 

6、 awaitUntil() 用法

awaitUntil(timeout, unit) 方法;

如果是由于超时导致等待结束,那么 awaitUntil() 会返回 false,否则会返回 true,表示等待是被唤醒的,下面我们就看看这个方法是怎么用的;

  
 
 
 
  1. private void startTimedWait() throws InterruptedException { 
  2.   lock.lock(); 
  3.   System.out.println("等待线程获取了锁"); 
  4.   // 3 秒后超时 
  5.   Date date = new Date(System.currentTimeMillis() + 3 * 1000); 
  6.   boolean isWakenUp = true; 
  7.   try { 
  8.     while (!conditionSatisfied) { 
  9.       if (!isWakenUp) { 
  10.         System.out.println("已超时,结束等待任务"); 
  11.         return; 
  12.       } else { 
  13.         System.out.println("保护条件不满足,并且等待时间未到,等待进入等待状态"); 
  14.         isWakenUp = condition.awaitUntil(date); 
  15.       } 
  16.     } 
  17.     System.out.println("等待线程被唤醒,开始执行目标动作"); 
  18.   } finally { 
  19.       lock.unlock(); 
  20.   } 
  21. public void startDelayedNotify() { 
  22.   threadSleep(4 * 1000); 
  23.   startNotify(); 

7、 await/countDown

使用 join() 实现的是一个线程等待另一个线程执行结束,但是有的时候我们只是想要一个特定的操作执行结束,不需要等待整个线程执行结束,这时候就可以使用 CountDownLatch 来实现;

await/countDown 基本用法

  
 
 
 
  1. public void tryAwaitCountDown() { 
  2.   startWaitThread(); 
  3.   startCountDownThread(); 
  4.   startCountDownThread(); 
  5. final int prerequisiteOperationCount = 2; 
  6. final CountDownLatch latch = new CountDownLatch(prerequisiteOperationCount); 
  7. private void startWait() throws InterruptedException { 
  8.   System.out.println("等待线程进入等待状态"); 
  9.   latch.await(); 
  10.   System.out.println("等待线程结束等待"); 
  11. private void startCountDown() { 
  12.   try { 
  13.     System.out.println("执行先决操作"); 
  14.   } finally { 
  15.     System.out.println("计数值减 1"); 
  16.     latch.countDown(); 
  17.   } 

8、 CyclicBarrier

有的时候多个线程需要互相等待对方代码中的某个地方(集合点),这些线程才能继续执行,这时可以使用 CyclicBarrier(栅栏);

CyclicBarrier 基本用法

  
 
 
 
  1. final int parties = 3; 
  2. final Runnable barrierAction = new Runnable() { 
  3.   @Override 
  4.   public void run() { 
  5.     System.out.println("人来齐了,开始爬山"); 
  6.   } 
  7. }; 
  8. final CyclicBarrier barrier = new CyclicBarrier(parties, barrierAction); 
  9. public void tryCyclicBarrier() { 
  10.   firstDayClimb(); 
  11.   secondDayClimb(); 
  12. private void firstDayClimb() { 
  13.   new PartyThread("第一天爬山,老李先来").start(); 
  14.   new PartyThread("老王到了,小张还没到").start(); 
  15.   new PartyThread("小张到了").start(); 
  16. private void secondDayClimb() { 
  17.   new PartyThread("第二天爬山,老王先来").start(); 
  18.   new PartyThread("小张到了,老李还没到").start(); 
  19.   new PartyThread("老李到了").start(); 
  20. public class PartyThread extends Thread { 
  21.   private final String content; 
  22.   public PartyThread(String content) { 
  23.     this.content = content; 
  24.   } 
  25.   @Override 
  26.   public void run() { 
  27.     System.out.println(content); 
  28.     try { 
  29.       barrier.await(); 
  30.     } catch (BrokenBarrierException e) { 
  31.       e.printStackTrace(); 
  32.     } catch (InterruptedException e) { 
  33.       e.printStackTrace(); 
  34.     } 
  35.   } 

Android 中常用的 7 种异步方式:Thread、HandlerThread、IntentService、AsyncTask、线程池、RxJava 和 Kotlin 协程;

总结:

1、线程有很多优势:

2、多线程的风险:

3、下次详解下Android中常用异步方式,从实际出发。


当前名称:Android性能优化之Java线程机制与线程调度原理详解
地址分享:http://cdbrznjsb.com/article/djchidi.html

其他资讯

让你的专属顾问为你服务