多线程基础探索

一个Android应用在创建的时候会开启一个线程,我们叫它主线程或者UI线程,在开发中,我们为了避免主线程被耗时操作阻塞从而产生ANR,我们就需要去开启子线程去访问网络或数据库等耗时操作。

  • 线程基础
  • 同步
  • 阻塞队列
  • 线程池

一.线程基础

1.进程与线程

(1)进程:进程是程序的实体,是受操作系统管理的基本运行单元,它也是线程的容器。

(2)线程:是操作系统调度的最小单元,也叫作轻量级进程。在一个进程中可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

2.线程的状态

在这里插入图片描述
如图所示线程分为六种不同的状态:

(1)New:新创建状态。线程被创建,还没有调用start方法,在线程运行之前还有一些基础工作要做。

(2)Runnable:可运行状态。一旦调用start方法,线程就处于可运行状态,一个可运行状态的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行时间。

(3)Blocked:阻塞状态。表示线程被锁阻塞,它暂时不活动。

(4)Waiting:等待状态。线程暂时不活动,并且不运行任何代码,这消耗最小的资源,直到线程调度器重新激活它。

(5)Timed waiting:超时等待状态。和等待状态不同的是,它是可以在指定的时间自行返回的。

(6)Terminated:终止状态。表示当前线程执行完毕。两种情况可以导致线程终止,第一种是run方法执行完毕正常终止,第二种是因为异常导致run方法异常终止。

3.创建线程

多线程的实现一般有以下3种方法:

(1)继承Thread类,重写run()方法

public class TestThread extends Thread{
    @Override
    public void run() {
        super.run();
    }
}
TestThread testThread = new TestThread();
testThread.start();

(2)实现Runnable接口,并实现该接口的run()方法

new Thread(new Runnable() {
    @Override
    public void run() {
    }
}).start();

(3)实现Callable接口,重写call()方法

//与Runnable接口相比,主要表现为3点:
@1
@2
@3
public class TestCallable implements Callable{
    @Override
    public String call() throws Exception {//@1 可以抛出异常
        return "测试代码";//@2 在任务接收后,提供返回值
    }
}

TestCallable testCallable = new TestCallable();
ExecutorService executorService = Executors.newSingleThreadExecutor();
 Future future = executorService.submit(testCallable);//@3 Future对象表示异步计算的结果,用来监视目标线程调用call()方法的情况。
    try {
        future.get();//获取结果时,当前线程会阻塞,直到call()方法返回结果
    } catch (ExecutionException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

4.理解中断

中断有3个重要的方法:

(1)interrupt:该方法用来请求中断线程,当一个线程调用interrupt时,线程的中断标识位会置位,即中断标识位为true。

(2)isInterrupted:该方法用来检测中断标识位,以判断线程是否被中断。

  while (!Thread.currentThread().isInterrupted()) {
            // do something
        }

(3)Thread.interrupted():该方法用来对中断标识位进行复位。但是如果一个线程被阻塞,就无法检测中断状态。如果一个线程处于阻塞状态,线程检查中断标识位时如果发现中断标识位为true,则会在阻塞方法调用处抛出InterruptedException异常,并且在抛出异常前将线程的中断标识位复位,即重新设置为false。需要注意的是被中断的线程不一定终止。

5.安全的终止线程

(1)通过interrupt()方法中断

Thread thread = new Thread(new Runnable() {
          private long i;
          @Override
          public void run() {
              while (!Thread.currentThread().isInterrupted()) {
                  i++;
                  Log.d("TAG","i=="+i);
              }
              Log.d("TAG","stop");
          }
      });
            thread.start();
        try {
            TimeUnit.MILLISECONDS.sleep(10);//睡眠10秒,是为了给线程时间来感知中断
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
            thread.interrupt();

(2)用boolean变量来控制中断,这里用到了volatile关键字

public static class MoonRunnbale implements Runnable {
    private long i;
    private volatile boolean on = true;
    @Override
    public void run() {
        while (on) {
            i++;
            Log.d("TAG","i=="+i);
        }
        Log.d("TAG","stop");
    }
    public void cancel(){
        on = false;
    }
}

MoonRunnbale moonRunnbale = new MoonRunnbale();
Thread moonThread = new Thread(moonRunnbale, "MoonThread");
moonThread.start();
try {
    TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
    e.printStackTrace();
}
moonRunnbale.cancel();

6.sleep和wait

(1)sleep来自Thread类的静态方法,wait来自Object类。

(2)sleep方法没有释放锁,wait方法释放了锁,使其他线程可以使用同步代码快或同步方法。sleep不出让系统资源,wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU,一般wait不加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify和notifyAll唤醒等待池中的线程,才会进入就绪队列等待OS分配系统资源,sleep(millseconds)可以指定时间使他自动唤醒,如果时间不到,只能使用interrupt()强行中断。

(3)使用范围,wait、notify和notifyAll只能在同步代码块和同步方法中使用,sleep可以在任何地方使用。

(4)sleep必须捕获异常,wait、notify和notifyAll不需要捕获异常。

二.同步

在多线程应用中,两个或两个以上的线程需要共享对同一个数据的存取,如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通称为竞争条件。比如火车站卖火车票,每个窗口相当于一个线程,这么多线程共用所有的火车票资源,如果不使用同步就无法保证其原子性。所以当一个线程使用火车票资源时,我们就交给它一把锁,等它把事情做完后再把锁给另外一个要用这个资源的线程。

1.重入锁与条件对象

(1)重入锁:ReentrantLock,该锁支持一个线程对资源的重复加锁。

   ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        try {
          ...
        }finally {
            reentrantLock.unlock();
        }

(2)条件对象:条件对象是用来管理那些已经获得了一个锁但是不能做有用工作的线程,这里不能做有用工作,是因为执行逻辑代码,可能要满足某些条件。条件对象又称之为条件变量。下面通过一个例子来说明,现在假设一个场景需要支付宝转账:

public class Alipay {
    private double[] accounts;
    private final ReentrantLock reentrantLock;
    private final Condition condition;
    //传入支付宝账户数量和每个账户的账户金额
    public Alipay(int n, double money) {
        accounts = new double[n];
        reentrantLock = new ReentrantLock();//重入锁
        condition = reentrantLock.newCondition();//条件对象
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = money;
        }
    }
    //from转账方,to接收方,amount转账金额
    public void transfer(int from, int to, int amount) throws InterruptedException {
        reentrantLock.lock();//加锁
        try {
            while (accounts[from] < amount) {//当转账余额不足
                //阻塞当前线程,并放弃锁
                condition.await();
            }
            //转账操作
            accounts[from] = accounts[from] - amount;
            accounts[to] = accounts[to] + amount;
            condition.signalAll();//重新激活因为这一条件而等待的所有线程
        }finally {
            reentrantLock.unlock();//释放锁
        }
    }
}

2.同步方法

使用synchronized关键字申明的方法是同步方法,上面的支付宝转账例子可以使用synchronized来实现同步,如下:

public synchronized void transfer(int from, int to, int amount) throws InterruptedException {//每一个对象都有一个内部锁
    
        while (accounts[from] < amount) {//当转账余额不足
            wait();
        }
        //转账操作
        accounts[from] = accounts[from] - amount;
        accounts[to] = accounts[to] + amount;
        notifyAll();
    }

3.同步代码块

以上面的支付宝转账为例:

public class Alipay {
    private double[] accounts;
    private Object lock = new Object();
    //传入支付宝账户数量和每个账户的账户金额
    public Alipay(int n, double money) {
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = money;
        }
    }
    //from转账方,to接收方,amount转账金额
    public  void transfer(int from, int to, int amount)  {
        synchronized (lock) {//创建Object类,是为了使用Object类所持有的锁
            //转账操作
            accounts[from] = accounts[from] - amount;
            accounts[to] = accounts[to] + amount;
        }
    } 
}

4.volatile

(1)java内存模型

内存模型的运用是为了屏蔽掉各种硬件和操作系统的内存访问差异,实现Java程序在各种平台下都能达到一致的内存访问效果,它是通过定义程序中各个变量的访问规则来实现的,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。需要注意的是,这里的变量指的是,实例字段、静态字段以及构成数组对象的元素,而局部变量和方法参数,因为是线程私有的,不会被共享。

内存模型结构图如下:

在这里插入图片描述
主内存:所有变量存储的位置,直接对应于物理硬件的内存。

工作内存:每个线程都要一个私有的工作内存,用于保存被该线程使用到的变量的主内存副本,需要注意的是工作内存是Java内存模型的一个抽象概念,其并不真实存在,它涵盖了缓存、写缓冲区、寄存器等区域。

注意:

a.线程中对所有变量的操作都是在工作内存中进行的,而不能直接读写主内存中的变量。

b.不同的线程之间不能直接访问对方工作内存中的变量,线程间变量值的传递必须通过主内存来完成。

(2)原子性、可见性和有序性

a.原子性:一个操作是不可被中断的,要么执行完毕,要么就不执行。像基本数据类型的访问读写就具备原子性,更大范围的原子性可以使用synchronized关键字来保证。

x = 3;  // 原子性操作
y = x;  //2个操作,先读取x值,再写入工作内存,不是原子性
x++;   //3个操作,先读取x,再加1,再写入工作内存,不是原子性

b.可见性:当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。它是通过变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现的。volatile能保证新值立即同步到主内存,且每次使用前从主内存刷新。synchronized对一个变量做unlock操作之前可以把此变量同步回主内存。

c.有序性:程序代码按照指令顺序执行。volatile关键字可以保证有序性,也可以通过synchronized和Lock来保证有序性。

(2)volatile

volatile关键字具有可见性和有序性,但是无法保证原子性,volatile有两个使用条件:

a.对变量的写操作不会依赖于当前值,也就是说不能自增、自减等操作。

b.该变量没有包含在具有其他变量的不变式中。

使用规则:变量真正独立于其它变量和自己以前的值。

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

三.阻塞队列

1.阻塞队列简介

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。常见的阻塞场景:

(1)当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞,直到有数据放入队列。

(2)当队列中数据填满的情况下,生产者端的所有线程都会被自动阻塞,直到队列中有空的位置,线程被自动唤醒。

2.BlockingQueue的核心方法

(1)放入数据

offer(anObject);//不阻塞当前执行方法的线程

offer(E o,long timeout,TimeUnit unit);//设定等待时间

put(anObject);//如果队列没有空间,会阻塞线程

(2)获取数据

poll(long timeout,TimeUnit unit);//指定时间内获取队首的数据

take();//获取队首的数据,如果队列为空,则阻断进入等待状态

drainTo();//一次性获取所有或指定的数据对象

3.Java中的阻塞队列

Java中提供了7个阻塞队列,如下:

ArrayBlockingQueue://由数组结构组成的有界阻塞队列

LinkedBlockingQueue://链表结构组成的有界阻塞队列

PriorityBlockingQueue://支持优先级排序的无界阻塞队列

DelayQueue://支持延时获取元素的无界阻塞队列

SynchronousQueue://不存储元素的阻塞队列

LinkedTransferQueue://链表无界阻塞队列

ArrayBlockingDeque://链表结构组成的双向阻塞队列

4.阻塞队列的使用

除了在线程池中使用阻塞队列,我们还可以在生产者-消费者模式中使用阻塞队列,这里对非阻塞队列实现生产者-消费者模式和阻塞队列实现生产者-消费者模式进行对比如下:

/*
非阻塞队列实现生产者-消费者模式
* */
public class QueueTest {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<>(queueSize);

    public static void main(String[] args) {
        QueueTest queueTest = new QueueTest();
        Producer producer = queueTest.new Producer();
        Consumer consumer = queueTest.new Consumer();
        producer.start();
        consumer.start();
    }
    
    class Consumer extends Thread{
        @Override
        public void run() {
            super.run();
            while (true) {
                synchronized (queue) {
                    while (queue.size() == 0) {
                        try {
                            Log.d("TAG","队列空,等待数据");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    //每次移走队首元素
                    queue.poll();
                    queue.notify();
                }
            }
        }
    }

    class Producer extends Thread{
        @Override
        public void run() {
            super.run();
            while (true) {
                synchronized (queue) {
                    while (queue.size() == queueSize) {
                        try {
                            Log.d("TAG","队列满,等待有空余空间");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    //每次插入一个元素
                    queue.offer(1);
                    queue.notify();
                }
            }
        }
    }
}
/*
使用阻塞队列实现生产者-消费者模式
好处:无需考虑同步和线程间通信的问题
* */
public class QueueTest {
    private int queueSize = 10;
    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(queueSize);

    public static void main(String[] args) {
        QueueTest queueTest = new QueueTest();
        Producer producer = queueTest.new Producer();
        Consumer consumer = queueTest.new Consumer();
        producer.start();
        consumer.start();
    }

    class Consumer extends Thread{
        @Override
        public void run() {
            super.run();
            while (true) {
                try {
                    queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Producer extends Thread{
        @Override
        public void run() {
            super.run();
            while (true) {
                try {
                    queue.put(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

四.线程池

1.ThreadPoolExecutor

可以通过ThreadPoolExecutor类来创建线程池,ThreadPoolExecutor类一共有4个构造方法,这里选择最多参数的构造方法如下:

public ThreadPoolExecutor(int corePoolSize,//核心线程数
                          int maximumPoolSize,//最大线程数
                          long keepAliveTime,//非核心线程闲置的超时时间
                          TimeUnit unit,//keepAliveTime参数的时间单位
                          BlockingQueue<Runnable> workQueue,//阻塞队列
                          ThreadFactory threadFactory,//线程工厂
                          RejectedExecutionHandler handler)//饱和策略,这是当任务队列和线程池都满了时所采取的应对策略

2.线程池的处理流程

在这里插入图片描述
如图所示可以得知线程池的处理流程主要分为3个步骤:

(1)提交任务后,线程池判断线程数是否达到了核心线程数,如果未达到核心线程数,则创建核心线程处理任务,否则执行下一步操作。

(2)接着线程池判断任务队列是否满了,如果没满,则将任务添加到任务队列中,否则执行下一步操作。

(3)接着因为任务队列满了,线程池就判断是否达到了最大线程数,如果未达到,则创建非核心线程处理任务,否则就执行饱和策略,默认会抛出RejectedExecutionException异常。

3.线程池的种类

通过直接或间接的配置ThreadPoolExecutor的参数可以创建不同类型的ThreadPoolExecutor,其中四种比较常见,如下:

(1)FixedThreadPool:可重用固定线程数的线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,//核心线程数和最大线程数一样,都是固定的
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

FixedThreadPool的corePoolSize和maximumPoolSize都设置为创建FixedThreadPool指定的参数nThreads,说明FixedThreadPool只有核心线程,并且数量是固定,没有非核心线程。

(2)CacheThreadPool:根据需要创建线程的线程池

public static ExecutorService newCachedThreadPool() {//没有核心线程,非核心线程是无界的,比较适用于大量的需要立即处理并且耗时较少的任务
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());

CacheThreadPool的corePoolSize为0,maximumPoolSize设置为Integer.MAX_VALUE,表示CacheThreadPool没有核心线程,非核心线程是无界的,keepAliveTime设置为60L,则空闲线程等待新任务的最长时间为60s,在此用了阻塞队列SynchronousQueue,它是一个不存储元素的阻塞队列,每一个插入操作必须等待另一个线程的移除操作,同样任何一个移除操作都等待另一个线程的插入操作。

(3)SingleThreadPool:使用单个工作线程的线程池

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));

(4)ScheduledThreadPool:实现定时和周期性任务的线程池

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());
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值