多线程编程基础

多线程编程基础

1.进程与线程

  • 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
  • 线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程]之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

2.线程的状态

线程状态

  • New:创建状态。线程被创建,但未调用 .start() 方法。
  • Runnable:可运行状态。新创建的线程调用 .start() 方法后可运行状态,但是否运行,还取决于操作系统分配给该线程的时间片。
  • Blocked:阻塞状态。线程调用同步方法时,未获得锁时,进入阻塞状态。
  • Waiting:等待状态。从等待状态恢复,需要线程调度器的激活。
  • Timed waiting:超时等待状态。可在指定时间后恢复。
  • Terminated:终止状态。进入终止状态的两种情况:1.run方法执行完毕正常退出;2.未捕获的异常导致线程终止。

3.多线程实现的三种方法

3.1 继承Thread类

  • TestThread.class
package thread;

public class TestThread extends Thread {
    @Override
    public void run() {
        System.out.println("TestThread!");
    }
}
  • Main.class
import thread.*;

public class Main {
    public static void main(String[] args) {
        Thread thread = new TestThread();
        thread.start();
    }
}

3.2 实现Runnable接口

  • TestRunnable.class
package thread;

public class TestRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("TestRunnable!");
    }
}
  • Main.class
import thread.*;

public class Main {
    public static void main(String[] args) {
        TestRunnable testRunnable = new TestRunnable();
        Thread thread = new Thread(testRunnable);
        thread.start();
    }
}

3.3 实现Callable接口

  • Callable接口属于Executor框架中的功能类,比Runnable更强大。
  • 实现Callable接口,重写**call()**方法。
  • Callable可以在任务接受后提供一个返回值
  • Callable中的call()方法可以抛出异常
  • 运行Callable可以获得一个Future对象。Future对象表示异步计算的结果,提供检查计算是否完成的方法。

  • TestCallable.class
package thread;

import java.util.concurrent.Callable;

public class TestCallable implements Callable {
    @Override
    public String call() throws Exception {
        System.out.println("TestCallable!");
        return "success";
    }
}
  • Main.class
import thread.*;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
    public static void main(String[] args) {
        TestCallable testCallable = new TestCallable();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future future = executorService.submit(testCallable);

        try {
            //调用.get()方法时会阻塞当前线程,直到call()方法返回结果。
            System.out.println(future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.中断

4.1 interrupt

  • 中断线程并非终止线程。中断线程目的是引起线程注意,由被中断的线程决定如何响应中断。重要的线程一般不理会中断,大多情况下普通线程会将中断作为一个终止的请求。
  • 使用 interrupt 方法来请求中断线程。
  • 当一个线程调用 interrupt 方法时,线程的中断标识位被置为 true 。线程会周期性检查这个中断标识位,可以使用 Thread.currentThread().isInterrupt() 判断线程是否被中断。
while(!Thread.currentThread().isInterrupt()){
    //中断后的响应
}

4.2 被阻塞线程下的中断

  • 如果一个线程被阻塞,线程在检查中断标识位时如果发现为 true ,会在阻塞方法调用处抛出 InterruptedException 异常,且在异常抛出前,将线程中的中断标识位复位为 false
  • 在底层捕获 InterruptedException 异常后一般两种处理方式:
/**
* 方式一:
* 将标识位再次置为true。
* 外界通过Thread.currentThread().isInterrupt()判断后继续操作。
*/
void task(){
    ...
    try{
        sleep(1000);
    }catch(InterruptedException e){
        Thread.currentThread().interrupt();
    }
    ...
}
/**
* 方式二:
* 直接抛出让调用者处理这个异常。
*/
void task() throw InterruptedException{
    ...
    sleep(1000);
    ...
}

4.3 安全地终止线程

  • 通过中断方式控制线程终止。
  • TestRunnable.class
package thread;

public class TestRunnable implements Runnable {
    private long i;

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            i++;
            System.out.println("i=" + i);
        }
        System.out.println("STOP!");
    }
}
  • Main.class
import thread.*;

import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        TestRunnable testRunnable=new TestRunnable();
        Thread thread = new Thread(testRunnable,"testRunnable");
        thread.start();
        TimeUnit.MILLISECONDS.sleep(10);
        thread.interrupt();
    }
}

  • 通过boolean方式控制线程终止。
  • TestRunnable.class
package thread;

public class TestRunnable implements Runnable {
    private long i;
    private volatile boolean on = true;

    @Override
    public void run() {
        while (on) {
            i++;
            System.out.println("i=" + i);
        }
        System.out.println("STOP!");
    }

    public void cancel() {
        on = false;
    }
}
  • Main.class
import thread.*;

import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        TestRunnable testRunnable=new TestRunnable();
        Thread thread = new Thread(testRunnable,"testRunnable");
        thread.start();
        TimeUnit.MILLISECONDS.sleep(10);
        testRunnable.cancel();
    }
}

5.同步

5.1 重入锁

  • 重入锁 ReentrantLock 支持一个线程对资源的重复加锁。
Lock mLock = new ReentrantLock();
mLock.lock();
try{
    ...
}
finally{
  mLock.unlock();  
}

  • 条件对象又叫条件变量
  • 可以使用条件对象来管理那些获得了锁,但是因为条件限制不能完整执行完流程的线程。
private Lock mLock;
private Condition mCondition;
...
//实例化
mLock = new ReentrantLock();
mCondition = mLock.newCondition();
...
//在阻塞地方
while(条件){
    ...
    mCondition.await();
}
...
//在激活的地方
mCondition.signalAll();
...
  • .signalAll 方法会唤醒因这一条件对象被阻塞的所有线程,然后这些线程再通过竞争实现对对象的访问。
  • 一般都是使用 while( ) 条件语句对流程控制。

5.2 同步方法

  • Java中每一个对象都有一个内部锁,当一个方法使用 synchronized 关键字声明,那么该对象的锁将保护整个方法。
  • synchronized 关键字声明的方法含有1个相关条件。wait() 方法等价于 condition.await()notifyAll() 方法等价于 condition.notifyAll() 等。
public synchronized void method(){
    ...
}
//等价于
Lock mLock = new ReentrantLock();
public void method(){
    mLock.lock();
    try{
        ...
    }finally{
        mLock.unlock();
    }
}

5.3 同步代码块

  • 不推荐。同步代码块比较脆弱。
  • 一般实现同步最好使用 java.util.concurrent 包下提供的类,比如阻塞队列。
synchronized(obj){
    ...
}

5.4 一些概念

  • Java 内存模型

    • Java中的堆内存用来存储对象实例,堆内存是被所有线程共享的运行时内存区域,存在内存可见性问题。
    • 局部变量、方法定义的参数不会再线程间共享,则不会受到内存模型的影响。
    • 线程之间的共享变量存储在主存之中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。(本地内存是抽象概念,并不存在,实际涵盖缓存、写缓冲区、寄存器等区域)
    • Java内存模型控制线程之间的通信,决定一个线程对主存共享变量的写入何时对另一个线程可见。
  • A线程与B线程通信:

    • 线程A把线程A本地内存中更新过的共享变量刷新到主存中去。
    • 线程B到主存中读取线程A更新过的共享变量。

Java内存模型抽象示意图


  • 原子性:对基本数据类型变量的读取和赋值操作是原子性操作,是不可中断的。
  • java.util.concurrent.atomic 包中存在很多高效的机器级指令,其操作具备原子性。
  • 可见性:一个线程的修改的状态对另一个线程是可见的。volatile 修饰的共享变量会被立即更新到主存中,具备可见性。普通共享变量变量被修改,并不会被立即写入内存,写入时间不确定,无法保证可见性。
  • 有序性:Java内存模型允许编译器和处理器对指令进行重排序。重排序不会影响单线程程序执行,但会影响多线程并发执行的正确性。可以通过 volatilesynchronizedLock 来保证有序性。

5.5 volatile

  • 为读写一个或者两个实例域使用同步方法开销过大;volatile 关键字为实例域的同步访问提供了免锁机置。

  • 禁止使用指令重排序。即线程修改共享变量的值之后,立即更新到主存。

  • volatile 保证有序性,但是不保证原子性

  • 使用 volatile 关键字需要具备的两个条件:

    • 对变量的操作不会依赖于当前值。
    • 该变量没有包含在具有其他变量的的不变式中。
  • volatile 使用场景举例。

//状态标志
volatile boolean stop;
...
public void shutdown(){
    stop = true;
}
public void doWork(){
    while(!stop){
        ...
    }
}
//双重检查模式(DLC)
public class Singleton{
    private volatile static Singleton instance = null;
    public static Singleton getInstance(){
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

6. 阻塞队列

6.1 阻塞队列简介

  • 当队列中没有数据,消费者端的所有线程都会被阻塞,直到有数据放入队列。

  • 当队列中填满数据,生产者端的所有线程都会被阻塞,直到队列中存在空位。

  • 阻塞队列中的核心方法

    • 存数据
    • offer(E e):如果可以,将e添加到阻塞队列中。返回值boolean。不阻塞当前执行方法的线程。
    • offer(E e, long timeout, TimeUnit unit):在指定时间内不能加入阻塞队列返回false。
    • put(E e):如果阻塞队列满了,则阻塞调用方法的线程,直到阻塞队列有空间再唤醒。

    • 取数据
    • poll(long timeout, TimeUnit unit):在指定时间内取队首元素。
    • take():取队首元素,取不到则阻塞调用方法的线程,直到新元素添加到队列中。
    • drainTo(Collection<? super E> c):一次性取出队列中存在的所有元素。

6.2 Java中的阻塞队列

  • ArrayBlockingQueue:

    • 数组实现的有界阻塞队列。
    • FIFO
    • 默认不保证线程公平地访问队列。
  • LinkedBlockingQueue

    • 链表实现的有界阻塞队列。
    • FIFO
    • 对于生产者端和消费者端分别采用了独立的锁来控制数据同步。(高效
    • 默认大小(Integer.MAX_VALUE),注意可能会占满内存。
  • PriorityBlockingQueue

    • 支持优先级排序的无界阻塞队列。
    • 可自定义实现 **compareTo()**方法排序。
    • 可初始化时指定构造参数 Comparator 排序。
    • 不能保证同优先级元素的顺序。
  • DelayQueue

    • 支持延时获取元素的无界阻塞队列。
    • 通过PriorityQueue实现。
    • 创建元素时可指定到期时间,到期后才可以取走该元素。
  • SynchronousQueue

    • 不存储元素的阻塞队列。
    • 每个插入操作必须等待另一个线程的移除操作。
    • 每个移除操作必须等待另一个线程的插入操作。
  • LinkedTransferQueue:

    • 链表结构的无界阻塞TransferQueue队列。

    • 实现了TransferQueue接口。

    • 部分重要方法:

      • transfer(E e):若当前存在一个正在等待的消费者线程,则立刻将元素传递给消费者;如果没有消费者等待,将元素插到队尾,并进入阻塞状态,直到有消费者线程取走该元素。
      • tryTransfer(E e):若当前存在一个正在等待的消费者线程,则立刻将元素传递给消费者;如果没有消费者等待,返回false,不进入队列。
      • tryTransfer(E e, long timeout, TimeUnit unit):在 tryTransfer(E e) 方法基础上增加了等待时间,超时后返回false。
  • LinkedBlockingDeque

    • 链表实现的双向阻塞队列。
    • 支持双端插入和移除操作,减少了一半的竞争。

7.线程池

7.1 Java线程池

  • 多线程任务提交给 Runnable / Callable
  • Executor 框架用来处理多线程任务,其中核心部分 ThreadPollExecutor 是线程池的核心实现类。

7.2 ThreadPollExecutor

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    ...
}
  • corePoolSize:核心线程数。默认情况下当任务提交时才创建前程;调用 prestartAllCoreThreads 方法,会提前创建并启动所有的核心线程来等待任务。

  • maximumPoolSize:线程池允许创建的最大线程数。

  • keepAliveTime:非核心线程闲置超时时间。任务多且短时适度增大keepAliveTime会提高线程利用率。**allowCoreThreadTimeOut **属性值为 true 时,keepAliveTime 也会应用到核心线程上。

  • unit:时间单位。

  • workQueue:任务队列。

  • threadFactory:线程工厂。可为线程设置名字。

  • handler:饱和策略。

    • 默认 AbordPolicy。
    • AbordPolicy:抛弃任务,并抛出 RejectedExecutionException 异常。
    • DiscardPolicy:抛弃任务,不做任何动作。
    • CallerRunsPolicy:用调用者线程来处理任务。该策略提供简单的反馈控制,能够减缓新任务的提交速度。
    • DiscardOldestPolicy:丢弃队列第一个任务。

7.3 线程池的处理流程和原理

  • 线程池的处理流程图

线程池的处理流程

  • 线程池执行示意图

线程池执行示意图

7.4 线程池种类

  • 4种常见的线程池:newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutornewScheduledThreadPool
7.4.1 newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • 可重用固定线程数的线程池。
  • 只有固定的核心线程数,没有非核心线程。
  • LinkedBlockingQueue:默认 Integer.MAX_VALUE。

newFixedThreadPool

7.4.2 newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • 根据需要创建线程的线程池。
  • 没有核心线程,非核心线程是无界的。
  • SynchronousQueue。
  • 适用于大量的需要立即处理且耗时较短的任务。

newCachedThreadPool

7.4.3 newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  • 使用单个工作线程的线程池。
  • LinkedBlockingQueue。

newSingleThreadExecutor

7.3.4 newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
  • 能够定时和周期性执行任务的线程池。

  • 当执行newScheduledThreadPool 的 scheduleAtFixedRatescheduleWithFixedDelay方法时,会向DelayedWorkQueue添加一个实现 RunnableScheduledFuture 接口的ScheduledFutureTask(任务包装类),并检查运行的线程是否达到 corePoolSize。

    • 没有达到核心线程数:新建线程并启动线程,然后去DelayedWorkQueue中取ScheduledFutureTask,然后再去执行任务。(而非直接启动线程后执行任务)
    • 达到核心线程数:将任务添加到DelayedWorkQueue中。
  • DelayedWorkQueue会将任务进项排序,先要执行的任务放在队列前。

  • 当任务执行完,会将ScheduledFutureTask中的time变量改为下次要执行的时间并放回到DelayedWorkQueue中。

newScheduledThreadPool

8.参考资料

  • Android 进阶之光
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值