一、多线程的三种实现方式
1. 继承Thread类,重写run函数方法
2. 实现Runnable接口,重写run函数方法
3. 实现Callable接口,重写call函数方法,ExecutorService、Callable、Future实现有返回结果的多线程
Callable和Runnable的不同之处:
- Callable接口可以返回结果,而Runnable接口不能返回结果。
- Callable接口可以抛出异常,而Runnable接口不能抛出异常。
二、如何停止一个线程
1. 创建一个标识(flag),当线程完成你所需要的工作后,可以将标识设置为退出标识。
2. 使用Thread的interrupt()方法和nterrupted()方法,两者配合break退出循环,或者return来停止线程,有点类似标识(flag)。
3. 可以使用try-catch语句,在try-catch语句中抛出异常,强行停止线程进入catch语句,这种方法可以将错误向上抛,使线程停止事件得以传播。
三、Thread 线程状态和相关方法
可运行(runnable):线程对象创建后,线程调用start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu的使用权。
运行(running):可运行状态(runnable)的线程获得了cpu使用权,执行程序代码。
阻塞(block):线程因为某种原因放弃了cpu使用权,即让出了cpu使用权,暂时停止运行,直到线程进入可运行(runnable)状态,才有机会再次获得cpu使用权转到运行(running)状态。阻塞的情况分三种:
1. 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
2. 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
3. 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期,且死亡的线程不可再次复生。
sleep和wait的区别:
- sleep()是Thread类的方法,wait()是Object类中的方法;
守护线程是指为其他线程的运行提供服务的线程。在Java中,可以通过调用setDaemon(boolean on)方法来设置线程的Daemon模式。当参数为true时,表示将线程设置为守护线程;当参数为false时,表示将线程设置为用户模式。
实现守护线程的方法有以下几种:
1. 使用wait()、notify()和notifyAll()方法:
当一个线程调用wait()方法时,它会释放所有持有的对象锁,并进入等待状态。只有当另一个线程调用notify()或notifyAll()方法唤醒该线程后,它才会重新进入可运行状态。
2. 使用sleep()方法:
sleep()方法使线程进入睡眠状态,这是一个静态方法。在调用此方法时,需要捕获InterruptedException异常。当线程被唤醒后,它会进入Runnable状态,等待JVM调度。
3. 使用join()方法:
join()方法使一个线程中断,当IO操作完成时,线程会回到Runnable状态,等待JVM的调度。
4. 使用synchronized关键字:
当用synchronized关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。需要注意的是,synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
同步方法:给一个方法增加synchronized修饰符之后就可以使它成为同步方法。这个方法可以是静态方法和非静态方法,但是不能是抽象类的抽象方法,也不能是接口中的接口方法。当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了,在此期间,其他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导致它释放了该对象的同步锁之后。在一个对象被某个线程锁定之后,其他线程是可以访问这个对象的所有非同步方法的。
同步块:同步块是通过锁定一个指定的对象,来对同步块中包含的代码进行同步;而同步方法是对这个方法块里的代码进行同步,而这种情况下锁定的对象就是同步方法所属的主体对象自身。如果这个方法是静态同步方法呢?那么线程锁定的就不是这个类的对象了,也不是这个类自身,而是这个类对应的java.lang.Class类型的对象。同步方法和同步块之间的相互制约只限于同一个对象之间,所以静态同步方法只受它所属类的其它静态同步方法的制约,而跟这个类的实例(对象)没有关系。如果一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块。synchronized 关键字用于保护共享数据。
2. 使用特殊域变量(volatile)实现线程同步
volatile关键字为域变量的访问提供了一种免锁机制,相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值,不能用来修饰final类型的变量。
3. 使用重入锁ReentrantLock类实现线程同步
ReentrantLock类是可重入、互斥、实现了Lock接口的锁。创建一个ReentrantLock实例的方法是:ReentrantLock()。获得锁的方法是:lock()。释放锁的方法是:unlock(),通常在finally代码释放锁。
4. 使用ThreadLocal局部变量实现线程同步
如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。前者采用以“空间换时间”的方法,后者采用以“时间换空间”的方式。
5. 使用阻塞队列LinkedBlockingQueue实现线程同步
LinkedBlockingQueue是一个基于已连接节点的、范围任意的blocking queue。队列是先进先出的顺序(FIFO)。
LinkedBlockingQueue类常用方法:
- LinkedBlockingQueue():创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue
- put(E e):在队尾添加一个元素,如果队列满则阻塞
- size():返回队列中的元素个数
- take():移除并返回队头元素,如果队列空则阻塞
代码实例:实现商家生产商品和买卖商品的同步。当队列满时,add()方法会抛出异常,offer()方法返回false,put()方法会阻塞。
6. 使用原子变量AtomicXxx实现线程同步
Xxx 可以是 String、Integer等。原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即这几种行为要么同时完成,要么都不完成。
AtomicInteger类常用方法:
- AtomicInteger(int initialValue):创建具有给定初始值的新的AtomicInteger
- addAndGet(int delta):以原子方式将给定值与当前值相加
- get():获取当前值
原子操作主要有:对于引用变量和大多数原始变量(long和double除外)的读写操作;对于所有使用volatile修饰的变量(包括long和double)的读写操作。
7. 使用线程池进行管理及优化
1. Android HandlerThread
HandlerThread 是用于处理消息队列的线程。它的源码中,Looper.loop()的核心代码是一个无限循环,退出循环的条件是:msg.target == null;也就是说,如果我们向此 looper 的 MessageQueue 发送一个 target 为 null 的消息,就可以停止这个线程的运行。停止 HandlerThread 的方法就是使用 quit 方法,具体调用形式如下:
重构后的内容如下:
1. 停止消息处理线程:
```java
mHandlerThread.getLooper().quit();
```
2. 使用线程池管理线程:
```java
new Thread(new Runnable() {
}).start();
```
这样创建的匿名对象存在一些问题,因为它是匿名的,无法对其进行管理。如果需要多次执行这个操作,就可能创建多个线程,占用系统资源。使用线程池的好处是可以重复利用存在的线程,减少系统的开销,并可以执行定时和并发数的控制。
线程池的作用是限制系统中执行线程的数量。根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果。少了会浪费系统资源,多了会造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要使用线程池?首先,它可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。其次,可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存而把服务器瘫痪(每个线程需要大约1MB内存,线程开得越多,消耗的内存也就越大,最后死机)。
Java通过Executors提供了四种线程池:
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程;若无可回收,则新建线程。
- newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数;超出的线程会在队列中等待。
- newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行。