Android 进阶之光 第四章 多线程编程

第四章 多线程编程

线程基础

  • 进程与线程
  1. 进程:可以被看做是程序的实体, 是系统进行资源分配和调度的基本单位.
  2. 线程:是操作系统调度的最小单元, 也叫轻量级进程
  • 使用多线程的优点
  1. 可以减少程序的响应时间。如果某个操作很耗时, 能够避免陷入长时间的等待, 从而有着更好的交互性.
  2. 线程较之进程, 创建和切换的开销更小, 在共享数据方面的效率非常高.
  3. 更高利用多CPU或多核设备的性能
  4. 简化程序结构, 便于理解维护.
  • 线程的状态
  1. New
  2. Runnable(可运行状态, 不是立即运行, 取决于系统的决定)
  3. Blocked(当调用同步方法而未获取锁时会进入阻塞状态)
  4. Waiting(暂时不活动, 不运行任何代码, 消耗最少的资源)
  5. Timed waiting(与waiting相比, 能在指定的时间自行返回)
  6. Terminated
  7. 线程的状态
  • 创建线程
    创建线程的方法一般有三种:
  1. 继承Thread类, 重写run方法(本质为实现Runnable接口的一个实例)
    1.创建继承Thread类的子类, 并重写run方法(执行体).
    2.创建子类的实例
    3.调用实例对象的start方法启动线程
  2. 实现Runnable接口, 并实现接口的run方法
    1.自定义一个实现Runnable接口的类, 实现run方法
    2.创建上面的类的对象, 并将其作为参数去创建一个Thread子类的实例.Thread mThread = new Thread(参数)
    3.调用Thread.start().
  3. 实现Callable接口, 重写call方法
    是Executor框架中的功能类, 与Runnable接口的功能类似, 但提供了比Runnable更强大的功能, 主要表现在以下3点:
    1.任务结束后提供一个返回值,
    2.call方法可以抛出异常
    3.运行了Callback后可以得到一个Future对象, 该对象利用Future.get方法监视目标线程调用call方法的情况, 但会一直阻塞直到call方法返回结果
	public class TestCallable{
		//创建线程类
		public String class MyTestCallable implements Callable{
			public String call() throws Exception{
			return "hello world";
			}
		}
		
		public static void main(String[] args){
			MyTestCallable mMyTestCallable = new MyTestCallable();
			ExecutorService mExecutorService = Executors.newSingleThreadPool();
			Future mfuture = mExecutorService.submit(mMyTestCallable);
			try{
				//等待线程结束, 返回结果
				System.out.println(mfuture.get());
			}catch(Exception e){
				e.printStackTrace();
			}
		}
	}
  • 理解中断
    中断作为一个终止的请求, 被中断的线程不一定会终止. 中断是为了引起线程的注意.
  1. 线程调用interrupt方法对中断标志位进行置位(true).
  2. 线程会不断检测这个标志位, 判断线程是否应该被中断(比较重要的线程不会理会中断)
  3. 想要知道线程是否被置位可以调用Thread.currentThread().isInterrupted()
  4. 若一个线程被阻塞, 则无法检测中断状态. (阻塞状态时, 线程如果发现中断标志位为true, 则会在阻断方法调用处抛出InterruptedException异常, 并将中断标志位复位(false))
  5. InterruptedException异常时要进行处理
    不能生吞中断: 既不抛出异常, 也不进行处理(仅仅做记录也是不行的). 处理 InterruptedException
    处理的方式有:
    1.使用try-catch来捕获异常. 在catch子句中调用Thread.currentThread.interrupt()来设置中断状态(保留中断发生的证据), 让外界通过判断Thread.currentThread().isInterrupted()来决定是否终止线程还是继续下去.
    2.可以先在try-catch进行一些处理, 然后再将异常抛出.
    3.方法直接抛出异常, 让调用者捕获
  • 安全的终止线程(利用中断的相关方法)
  1. 利用thread.interrupt()方法对中断置位来控制
  2. 利用中断判断方法Thread.currentThread().isInterrupted()run方法中进行while判断
  3. 或者用volatile修饰的boolean型变量来控制

同步

  • 同步
    Java中的同步指的是通过人为的控制和调度,保证共享资源多线程访问成为线程安全,来保证结果的准确。
  1. sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
  2. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
  3. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
  4. sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
  5. wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
  • 重入锁(ReentrantLock)与条件对象
    支持重进入的锁, 表示该锁支持一个线程对锁本身的重复加锁(其他的线程此时不能对该锁加锁)
  1. 加锁的次数要和解锁的次数要相同, 否则其他线程对此锁无法加锁
  2. 条件对象condition中提供了condition.await()方法(进入阻塞状态并放弃锁)和condition.signalAll()方法(将该条件对象上等待的线程唤醒, 解除阻塞, 这里说明一下并不是立即激活)
  3. 重入锁看这里
    1.可以实现公平锁(默认为非公平锁)
    2.可响应中断或者 更好的获取锁 限时等待的方法tryLock()去解决死锁
  • 上面链接中关于condition的使用
import java.util.concurrent.locks.*;
//对重入锁和条件对象的测试
public class HelloWorld {

	static ReentrantLock lock = new ReentrantLock();
	static Condition condition = lock.newCondition();
	public static void main(String[] args) throws InterruptedException {

        lock.lock();
        new Thread(new SignalThread()).start();
        System.out.println("主线程等待通知");
        try {
        	System.out.println("主线程的前try");
            condition.await(); //释放锁, 等唤醒后需要重新获取锁
            System.out.println("主线程的后try");
        } finally {
        	System.out.println("主线程的final");
            lock.unlock();
        }
        System.out.println("主线程恢复运行");
    }
    static class SignalThread implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
            	System.out.println("子线程的前try");
                condition.signal();
                System.out.println("子线程通知");
            } finally {
            	System.out.println("子线程的final");
                lock.unlock();
            }
        }
    }
}
  • 上面链接中关于tryLock的使用
public class ReentrantLockTest {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
        thread.start();
        thread1.start();
    }

    static class ThreadDemo implements Runnable {
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
            try {
                while(!firstLock .tryLock()){
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                while(!secondLock .tryLock()){
                    firstLock .unlock();
                    TimeUnit.MILLISECONDS.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"正常结束!");
            }
        }
    }
}
  • 同步方法
  1. 使用synchronized关键字来编写, 与重入锁相似, synchronized关键字直接修饰方法, 将整个方法进行保护.
  2. 每一个对象是有内部锁的
  3. 直接使用wait()notifyAll()方法, 与condition作用相似.
  • 同步代码块
    主要是使用对象的锁. 同步代码块非常脆弱, 通常不建议使用.
	private Object lock = new Object();
	synchronized(lock){
		//执行相关操作
	}
  • volatile
    下面1和2是基本概念
  1. java内存模型
    1.[内存模型的图]
    2.堆内存是被所有线程共享的运行时内存区域. java内存模型定义了线程和主存之间的抽象关系: 线程之间的共享变量存储在主存中, 每个线程都有一个私有的本地内存(实际不存在), 本地内存中存储了该线程共享变量的副本.
  2. 原子性, 可见性, 有序性
    1.原子性: 原子性操作是不可被中断的, 要么执行完毕, 要么就不执行. 对基本数据类型的读取和赋值操作就是原子性操作. 而如果一个语句有多个操作时, 就不是原子性操作.
    2.可见性: 一个线程修改的结果, 另一个线程能够马上看到. 普通的共享变量不能保证可见性, 因为普通的共享变量被修改后不会被立即写入主存. 而当一个共享变量被volatile修饰时, 他会保证修改的值立即被更新到主存.
    3.有序性: java内存模型中允许编译器和处理器对指令进行重排序(分为编译期重排序和运行时重排序, 不会影响单线程执行的正确性, 优化程序性能), 但会影响到多线程并发执行的正确性.
  3. volatile关键字
    1.保证可见性,禁止重排序.
    当一个共享变量被volatile修饰后, 具有两种含义: 一, 该共享变量具有可见性(强制一个线程中被修饰的值发生修改后立即写入主存, 并使其他线程工作缓存中该值的缓存无效. 防止线程因做其他的事情没把值写入主存), 二, 禁止使用重排序.
    2.不保证原子性
    3.保证有序性
    禁止重排序有两重含义: 一, 程序执行到volatile时, 在其前面的操作已经全部执行完毕, 并且结果会对后面的操作可见, 后面的操作都还没有执行. 二, 在进行指令优化时, volatile前面的语句不能在它后面zhixing, 后面的语句也不能在它前面执行
  4. 正确使用volatile
    volatile在某些情况下性能优于synchronized, 但是无法替代, 因为volatile无法保证操作的原子性.
    1.使用volatile需要严格遵循两个使用条件, 一, 即变量真正独立于(不能使用)其他变量和自己以前的值, 即不能自增, 自减, 因为不能保证原子性. 二, 没有包含在其他变量的不变式中(两个线程都会通过用于保护不变式的检查导致错误), 比如 [0, 10], 一个线程修改最大值为5, 同时另一个线程修改最小值为6, 此时区间就变成了[6, 5]. 很明显就错了
    2.使用场景
    1.状态标志, 使用volatile修饰Boolean类型的变量, 不依赖于程序内的任何其他状态
    2.双重检查锁定模式(DCL) 参考链接

public class Singleton {
	private static volatile Singleton instance;
 
	private Singleton() {
	}
 
	public static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}

阻塞队列

  • 阻塞队列简介
  1. 阻塞队列就是生产者存放元素的容器, 常见的阻塞场景:
    1.队列没有数据, 消费者端的所有线程都会自动挂起(阻塞), 直到有数据放入队列,线程被唤醒
    2.队列中数据填满了, 生产者端的所有线程都会自动挂起, 直到队列中有空的位置, 线程被唤醒
  1. BlockingQueue的核心方法:
    放入数据
    1.offer(anObject): 如果阻塞队列可以容纳, 返回true, 否则返回false, 并且将anObject放入阻塞队列. (本方法不会阻塞当前执行方法的线程)
    2.offer(E, long, TimeUnit): 在指定时间内如果不能往队列中添加, 则返回false
    3.put(anObject): 如果阻塞队列没有空间, 则调用此方法的线程被阻断, 直到有空间再继续.
    获取数据
    1.poll(time): 在等待的时间内都不能取到排在队列首位的对象就返回null.
    2.poll(long, TimeUnit): 在指定时间内不能取出数据, 返回false
    3.take(): 为空则阻断
    4.drainTo(): 取出所有的可用数据对象(还可以指定个数), 可以提高数据获取效率, 无需多次分批加锁. 释放锁
  • java中的阻塞队列(P205)
  1. ArrayBlockingQueue: 由数据结构组成的有界阻塞队列
  2. LinkedBlockingQueue: 由链表结构组成的有界阻塞队列
  3. PriorityBlockingQueue: 支持优先级排序的无界阻塞队列
  4. DelayQueue: 使用优先级队列实现的无界阻塞队列
  5. SynchronousQueue: 不储存元素的阻塞队列
  6. LinkedTransferQueue: 由链表结构组成的无界阻塞队列
  7. LinkedBlockingQueue: 由链表结构组成的双向阻塞队列
  • 实现原理
    其实就是利用了可重入锁和条件对象, 相关可以看上面condition的使用示例, 然后挑一个阻塞队列源码看看就可以.
  • 使用场景
  1. 生产者-消费者模式
    生产者-消费者模式可以使用阻塞队列实现(更为便捷), 或者也可以使用同步方法(Synchronized)实现, 自己处理好同步和线程通信问题,
  2. 线程池

线程池

每个线程的创建和销毁都需要一定的开销, 如果每次执行一个任务都要开一个新线程去执行, 会消耗大量的资源. 同时线程又需要进行管理. 所以此时线程池就有存在性了.
一般任务的提交是Runnable或者Callable, 而用Executor框架来处理任务. 线程池的核心实现类就是Executor框架中的ThreadPoolExecutor.

  • ThreadPoolExcutor
    拥有四个构造方法, 最长的构造方法有以下参数
  1. corePoolSize: 核心线程数. 默认情况下线程池是空的, 只有任务提交时才会创建线程. 如果调用线程池的prestartAllcoreThread方法, 线程池会提前创建并启动所有的核心线程来等待任务.
  2. maximumPoolSize: 线程池允许创建的最大线程数.
  3. keepAliveTime: 非核心线程闲置的超时时间, 超过这个时间则回收. 如果任务很多, 并且每个任务执行的时间很短, 则可以调大这个来提高线程的利用率.
  4. TimeUnit: 上一个参数的时间单位, 有DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS等.
  5. workQueue: 任务队列. 类型为BlockingQueue.
  6. ThreadFactory: 线程工厂. 可以给每个创建出来的线程命名, 一般无需设置该参数.
  7. RejectedExecutionHandler: 饱和后策略.
    1.默认为AbordPolicy: 表示无法处理新任务, 并抛出RejectedExecutionException异常
    2.CallerRunsPolicy: 调用者所在的线程来处理任务, 能够减缓新任务的提交速度.
    3.DiscardPolicy: 不能执行的任务, 会将该任务删除
    4.DiscardOldestPolicy: 丢弃队列最近的任务, 并执行当前的任务
  • 线程池的处理流程
  1. 线程池执行示意图
  2. 执行流程: 执行execute后, 未达到核心线程数, 就创建核心线程数处理任务. 达到就将任务放入任务队列, 如果任务队列也满了, 就创建非核心线程去处理任务, 当超过了最大线程数时, 此时执行饱和策略.
  • 线程池的种类
    通过直接或者间接的配置ThreadPoolExecutor的参数可以创建不同类型的线程池.比较常用的有四种
  1. FixedThreadPool: 是可重用固定线程数的线程池。只有核心线程, 核心线程数量固定并且不会被回收. 内部采用了无界的阻塞队列, LinkedBlockingQueue. 函数为ExecutorService newFixedThreadPool(int 核心线程数)
  2. CachedThreadPool: 是一个根据需要创建线程的线程池。没有核心线程, 非核心线程无界, 提交的任务如果大于线程池中线程处理现成的速度, 就会不断地创建新线程, 等待任务最长时长为60s, 采用不储存元素的SynchronousQueue. 比较适合大量需要立即处理并且耗时比较少的任务. 函数为ExecutorService newCacheThreadPool()
  3. SingleThreadExecutor: 是使用单个工作线程的线程池,只有一个核心线程, 采用了无界阻塞队列LinkedBlockingQueue, 能够确保所有的任务在一个线程中按顺序逐一执行. 函数为ExecutorService newSingleThreadExecutor():
  4. ScheduledThreadPool: 是一个能实现定时和周期性任务的线程池. 有核心线程数, 队列为无界的DelayWorkQueue, 该队列会对任务进行排序, 未达到核心线程时会启动核心线程, 该线程会去取该队列中的ScheduledFutureTask然后执行, 执行完任务后, 会将ScheduledFutureTask中time变量改为下次要执行的时间并放回到队列中.函数为ScheduledExecutorService newScheduledThreadPool(int 核心线程数)

AsyncTask的原理

  • 源码分析
  1. Android3.0之前的AsyncTask内部使用了ThreadPoolExecutor, 核心线程5个, 线程池允许创建的最大线程数是128, 队列大小为10, 所以最多支持138个任务. 等待新任务的最长时间是1s, 可并行.
  2. Android7.0版本的AsyncTask内部有一个串行的线程池SerialExecutor(保证一个时间段只有一个任务执行, 不会出现超过任务数而执行饱和策略), 后面会通过postResult方法创建message, 最后处理后执行onPostExecute方法, 得到异步任务执行后的结果. 其核心线程和线程池允许创建的最大线程数都是由CPU的核数来计算出来. 采用的阻塞队列还是LinkedBlockingQueue, 容量设定为128.
  3. 如果想在3.0 及以上使用并行的线程处理, 可以使用以下代码
	asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, " ");
	//也可以传入自定义的线程池
	Executor exec = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 
		0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
	asyncTask.executeOnExecutor(exec, " ");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值