Android 多线程使用及原理(Handler、线程池)

目录

线程与进程

线程

进程(Process)

Java Thread

线程使用

继承 Thread,重写 run 方法

ThreadFactory

线程池

什么是线程池

为什么使用线程池

使用线程池有哪些优势

ThreadPoolExecutor

Executors

线程池的状态

线程安全

volatile

synchronized

synchronized object

ReentrantLock

ReentrantReadWriteLock

总结

handler

handler简介

handler使用场景

handler作用

Handler、Message、Message Queue、Looper解释

Handler

Message

MessageQueue

Looper


  1. 线程与进程

  1. 线程

线程是操作系统进行运算调度的最小单元

  • 操作系统线程

    • POSIX(可移植操作系统接口)线程

    • Windows 线程

  • 语言提供线程访问

    • Java Thread

    • C thrd_create

    • C++ std::thread

  • Android 中调用线程的方式

    • 可通过语言 Java 使用线程

    • Java JNI 调用 C++、C 线程接口使用线程

    • Java JNI 直接调用 Linux c api pthread 使用线程

  1. 进程(Process)

是操作系统进行资源分配的最小单位。一个进程是一个程序的一次执行过程。每启动一个进程,操作系统就会为它分配一块独立的内存空间,用于存储PCB、数据段、程序段等资源。每个进程占有一块独立的内存空间。

  • 结构

    • 控制块(PCB)

    • 数据段

    • 程序段

状态:操作系统创建进程时,进程处于创建态,CPU调度进程时,进程处于运行态,此时其它已创建的和时间片到的进程就处于就绪态,当然还有些进程在进行磁盘、网络等IO时就处于阻塞态,操作系统销毁进程时,进程就处于终止态。另外,进程还具有静止就绪态和静止阻塞态,处于这两种状态,说明这个进程被操作系统挂起了,操作系统挂起进程,是为了观察和分析进程。

  1. Java Thread

  1. 线程使用

继承 Thread,重写 run 方法
Thread thread = new Thread(){
    @Override
    public void run() {
        super.run();
        System.out.println("hello, extend thread");
    }
};
// native
thread.start();

构造方法中传递 Runnable

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("hello, runnable thread");
    }
};
Thread thread = new Thread(runnable);
thread.start();
ThreadFactory
AtomicInteger count = new AtomicInteger();
ThreadFactory factory = new ThreadFactory() {
    @Override
    public Thread newThread(Runnable runnable) {
        return new Thread(runnable, "thread name - " + count.incrementAndGet());
    }
};
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("hello, factory runnable thread: " + Thread.currentThread().getName());
    }
};
Thread thread = factory.newThread(runnable);
thread.start();
  1. 线程池

  1. 什么是线程池

线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("hello, executor");
    }
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);
  1. 为什么使用线程池

使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处;

  1. 使用线程池有哪些优势

线程和任务分离,提升线程重用性;

控制线程并发数量,降低服务器压力,统一管理所有线程;

提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;

  1. ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {}
  • corePoolSize 指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去

  • maximumPoolSize 指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量

  • keepAliveTime 当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁

  • unit keepAliveTime的单位

  • workQueue 任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种

    • SynchronousQueue 直接提交队列,创建的线程数大于maximumPoolSize时,直接执行了拒绝策略抛出异常

    • ArrayBlockingQueue 有界任务队列,若大于maximumPoolSize,则执行拒绝策略

    • LinkedBlockingQueue 无界任务队列,若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。

    • PriorityBlockingQueue 优先任务队列,Runnable 需实现 Comparable 接口,按优先级进行了重新排列执行

  • threadFactory 线程工厂,用于创建线程,一般用默认即可

  • handler 拒绝策略;当任务太多来不及处理时,如何拒绝任务

    • AbortPolicy 该策略会直接抛出异常,阻止系统正常工作

    • CallerRunsPolicy 如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行

    • DiscardOledestPolicy 该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交

    • DiscardPolicy 该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失

  1. Executors

  • newFixedThreadPool 固定线程数量的线程池:核心线程数与最大线程数相等

  • newSingleThreadExecutor 单个线程数量的线程池

  • newCachedThreadPool 接近无限大线程数量的线程池

  • newScheduledThreadPool 带定时调度功能的线程池

虽然 JDK 提供了快速创建线程池的方法,但其实不推荐使用 Executors 来创建线程池,因为从上面构造线程池的代码可以看出,newFixedThreadPool 线程池由于使用了 LinkedBlockingQueue,队列的容量默认无限大,实际使用中出现任务过多时会导致内存溢出;newCachedThreadPool 线程池由于核心线程数无限大,当任务过多的时候会导致创建大量的线程,可能机器负载过高进程死掉。

  1. 线程池的状态

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;
  • RUNNING:线程池创建时就是这个状态,能够接收新任务,以及对已添加的任务进行处理。

  • SHUTDOWN:调用 shutdown 方法,线程池就会转换成 SHUTDOWN 状态,此时线程池不再接收新任务,但能继续处理已添加的任务到队列中。

  • STOP:调用 shutdownNow 方法,线程池就会转换成 STOP 状态,不接收新任务,也不能继续处理已添加的任务到队列中任务,并且会尝试中断正在处理的任务的线程。

  • TIDYING:SHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态;线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池会变为 TIDYING 状态;线程池在 STOP 状态,线程池中执行中任务为空时,线程池会变为 TIDYING 状态。

  • TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会转变为 TERMINATED 状态。

  1. 线程安全

是指在多线程环境下,代码能够正确地执行,且不会因为多个线程的同时访问而导致数据不一致或其他不期望的行为。

  1. volatile

Handler主要用于异步消息的处理,通过消息队列,进行线程间的通讯。 这种机制通常用来处理相对耗时比较长的操作。

handler示意图

  • 描述volatile 关键字用于声明变量,使其在多个线程中保持可见性。当一个线程修改了 volatile 变量的值,其他线程能够立即看到这个变化。

  • 使用场景:适用于简单标志变量的使用,例如状态标志或配置开关,不适用于复合操作(如自增、自减等)。

  • 示例

    private volatile boolean running = true;
    
    public void stop() {
        running = false;
    }
    
    public void run() {while (running) {// do work
        }
    }
  • synchronized

  • 描述synchronized 关键字用于方法或代码块,使得同一时间只有一个线程能够执行被 synchronized 修饰的代码块,从而确保线程安全。

  • 使用场景:适用于需要确保代码块或整个方法在同一时刻只能由一个线程执行的情况。

  • 示例

    public synchronized void increment() {
        count++;
    }
    
    public void print() {synchronized (this) {
            System.out.println(count);
        }
    }
  • synchronized object

  • 描述synchronized object 是指在特定对象上加锁,确保某段代码在同一时刻只能由一个线程执行,其他线程必须等待该对象的锁被释放。

  • 使用场景:适用于需要对特定对象进行同步的情况,而不是整个类或方法。

  • 示例

    private final Object lock = new Object();
    
    public void increment() {synchronized (lock) {
            count++;
        }
    }
  • ReentrantLock

  • 描述ReentrantLock 是一个可重入的互斥锁,比 synchronized 提供了更强大的功能,例如尝试加锁、超时加锁、中断加锁等。

  • 使用场景:适用于需要更灵活的锁机制,或者需要条件对象(Condition)来实现复杂的线程间通信的情况。

  • 示例

    private final ReentrantLock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();try {
            count++;
        } finally {
            lock.unlock();
        }
    }

  • ReentrantReadWriteLock

  • 描述ReentrantReadWriteLock 提供了一个读写锁机制,允许多个线程同时进行读操作,但写操作是互斥的,且写操作与读操作也是互斥的。

  • 使用场景:适用于读操作远多于写操作的场景,可以提高并发性能。

  • 示例

    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    
    public void increment() {
        writeLock.lock();try {
            count++;
        } finally {
            writeLock.unlock();
        }
    }
    
    public int getCount() {
        readLock.lock();try {return count;
        } finally {
            readLock.unlock();
        }
    }

    总结

  • volatile:保证变量的可见性,但不保证操作的原子性。

  • synchronized:确保代码块或方法在同一时刻只能被一个线程执行,保证互斥访问。

  • synchronized object:在特定对象上加锁,保证特定对象的互斥访问。

  • ReentrantLock:提供了更灵活的锁机制,包括尝试加锁、超时加锁等。

  • ReentrantReadWriteLock:提供了读写锁机制,提高并发性能,适用于读多写少的场景。

  • handler

  • handler简介

  • Handler是一套 Android 消息传递机制,主要用于线程间通信。

  • 用最简单的话描述: handler其实就是主线程在起了一个子线程,子线程运行并生成Message,Looper获取message并传递给Handler,Handler逐个获取子线程中的Message.

  • Binder/Socket用于进程间通信,而Handler消息机制用于同进程的线程间通信

  • 可以说只要有异步线程与主线程通信的地方就一定会有 Handler。

  • 在多线程的应用场景中,将工作线程中需更新UI的操作信息 传递到 UI主线程,从而实现 工作线程对UI的更新处理,最终实现异步消息的处理

  • 使用Handler消息传递机制主要是为了多个线程并发更新UI的同时,保证线程安全

  1. handler使用场景

1,子线程通过handler更新ui

2,线程间的通讯,子线程间也可以通过handler来实现通讯

3,安卓系统内部使用。Android系统中遍布着Handler。事件监测(卡顿,生命周期切换,anr监测…)

  1. handler作用

  • 保证 APP 循环运行

  • 线程间的通信

  • 发送延时任务

  • 性能优化

  1. Handler、Message、Message Queue、Looper解释

  • Message 代表一个行为what或者一串动作Runnable, 每一个消息在加入消息队列时,都有明确的目标Handler

  • MessageQueue 以队列的形式对外提供插入和删除的工作, 其内部结构是以双向链表的形式存储消息的

  • Looper Looper是循环的意思,它负责从消息队列中循环的取出消息然后把消息交给Handler处理

  • Handler 消息的真正处理者, 具备获取消息、发送消息、处理消息、移除消息等功能

  1. Handler
  1. Handler唯一绑定一个线程,同时也绑定其MessageQueue、Looper,属于多对一的关系,即一个线程可以对应多个Handler

  2. 调度Message/Runnable,

  3. 进行线程间通信

// 绑定 Looper
Handler handler = new Handler(); // Looper.myLooper()
Handler handler = new Handler(Looper.getMainLooper());
// 一个 Looper 绑定多个 Handler
Looper looper = Looper.getMainLooper();
Handler handler1 = new Handler(looper);
Handler handler2 = new Handler(looper);
Handler handler3 = new Handler(looper);
// 处理消息
Handler handler = new Handler(){
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
};
// 线程间通信
Handler handler = new Handler(Looper.getMainLooper());
Executors.newSingleThreadExecutor().execute(new Runnable() {
    @Override
    public void run() {
        // ....
        handler.post(new Runnable() {
            @Override
            public void run() {
                // main
            }
        });
    }
});
  1. Message

具体的消息类

  1. Message是可以被发送给Handler来进行处理的一个类

  2. 这个类里面包含了一些描述信息(其实就是它的what属性),和一些数据对象(其实就是它的data属性)

  3. 获取Message,建议使用Message.obtain()或者Handler.obtainMessage()方法来获取,以实现Message的复用效果

    Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            switch (msg.what){
                case 1:
                    ...
                    break;
            }
        }
    };
    Message message = Message.obtain();
    message.what;
    message.obj;
    message.arg1;
    message.arg2;
    message.callback;
    handler.sendMessage(message);
  4. MessageQueue
  5. 它持有一个会被Looper分发的Message的列表

  6. Message由Handler添加,而非MessageQueue直接添加

  7. 可以通过Looper.myQueue()来获取当前线程的MessageQueue

  8. IdleHandler

  9. MessgaeQueue是一个单链表构成的优先级队列,元素为Message,根据Message的when属性来确定优先级。由Handler来添加Message,由Looper来取出Message

    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            // 队列为空,做一些优先级更低的事儿
            // gc
            // 埋点
            // 延迟初始化
            return false;
        }
    });

  1. 异步消息

一般由于优先级更高的消息

Message message = Message.obtain();
message.setAsynchronous(true);
MessageQueue#next()
// Stalled by a barrier.  Find the next asynchronous message in the queue.
do {
    prevMsg = msg;
    msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
  1. 退出

// safe true,移除未来加入的消息,已有消息会执行完
// safe false,移除所有消息
// 退出消息循环
void quit(boolean safe)
  1. Looper

可以理解为一个消息循环器,从MessageQueue中取出消息,交给Handler执行。创建方式是prepare,开启循环的方式是调用loop方法

  1. 此类是用来开启,一个线程中,Message的循环的

  2. 线程默认是没有消息循环的,需要手动创建,可以通过调用prepareloop,来开启循环

  3. 与Looper交互最多的是Handler

Handler handler;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    Thread thread = new Thread("test"){
        @Override
        public void run() {
            super.run();
            Looper.prepare();
            handler = new Handler(Looper.myLooper());
            Looper.loop();
        }
    };
    thread.start();
}
HandlerThread thread = new HandlerThread("test");
Looper looper = thread.getLooper();
Handler handler = new Handler(looper);
  1. epoll 阻塞唤醒机制

epoll 是 Linux 中的高效的 I/O 多路复用机制,它会监听一个或多个文件描述符的多个事件类型,其中之一是文件描述符的写入事件。

epoll能够实现阻塞、唤醒的原理,就是能够监听管道这个文件描述符的读操作和写操作,实现阻塞和唤醒。

比如,一个MessageQueue中,没有了消息,那么就应该被阻塞了,那么怎么保持一直阻塞的?什么时候会被唤醒?答案就是:epoll可以监听,pipe的文件描述符的写操作。

当有新的消息写入时,epoll就可以监听到,从而通知线程,实现唤醒。没有写入时,就能保持一直阻塞。

因此,可以这么理解:在没有数据可读时,MessageQueue主要使用了 native 层的 epoll 机制来监听文件描述符(通常是pipe)的写入事件,以实现持续的阻塞和唤醒。

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值