Java 多线程
线程和进程的区别
线程和进程的本质:由CPU进行调度的并发式执行任务,多个任务被快速轮换执行,使得宏观上具有多个线程或者进程同时执行的效果。
进程:在操作系统来说,一个运行的程序或者说一个动态的指令集合通常对应一个进程Process,它是系统进行资源分配和调度的一个独立单位,也是拥有系统资源的基本单位。进程是系统中独立存在的实体,它可以拥有自己独立的资源,拥有自己私有的地址空间,进程之间不能直接访问其他进程的地址空间。
线程:线程是CPU调度的基本单位,也就是说在一个进程中可以有多个并发程序执行流,线程拓展了进程的概念,使得任务的执行得到更加的细分,所以Thread有时候也被称为Lightweight Process。线程是进程的执行单元,但是线程不是分配系统资源的单位,它们共享所在进程的资源,包括共享内存,公有数据,全局变量,进程文件描述符,进程处理器,进程代码段,进程用户ID等等。
线程独立拥有自己的线程ID,堆栈,程序计数器,局部变量,寄存器组值,优先级,信号屏蔽码,错误返回码等等,线程是独立运行的,其执行是抢占式的。线程共享进程资源,线程之间的通信要进程之间的通信来得容易得多。此外,线程的创建和销毁的开销也远远小于进程的系统开销。
线程和线程池
- 线程池:虽然线程的创建销毁的开销相对较小,但是频繁得创建和销毁也会消耗有限的资源,从而带来性能上的浪费,也不够高效。因此线程池的出,现就是为了解决这一问题,即在初始状态创建并维护一定数量的空闲线程,当有需要执行的任务,就交付给线程中的一个线程,任务执行结束后,该线程也不会死亡,而是回到线程池中重新变为空闲状态。
- 线程池的好处:减少线程频繁创建销毁的资源开销,同时能够有效控制系统中并发线程的数量,防止系统性能的剧烈下降。
线程创建/启动的三种方法
- 继承Thread类创建多线程,此时每次创建的Thread对象并不能共享线程类的实例变量,也就是下面程序中的i。
public class FirstThread extends Thread{
private int i;
@Override
public void run() {
for(i=0;i<10;i++)
System.out.println(getName()); // 继承自Thread
}
public static void main(String[] args) {
new FirstThread().start();
new FirstThread().start(); // 注意启动线程需要用Start
}
}
- 实现Runnable接口创建线程类,Runnable接口是一个函数式接口(可以使用Lambda表达式),通常做法是重写接口中的run方法,此时方法体即为线程执行体,使用Runnable接口实现类的实例作为Thread的target来创建Thread对象,此时因为使用一个共同的target线程执行体,多个线程可以共享一个实例变量。
public class SecondThread implements Runnable{
private int i;
@Override
public void run() {
for(;i<10;i++) {
System.out.println(Thread.currentThread().getName() + " "+ i);
}
}
public static void main(String[] args) {
SecondThread targetRunnable = new SecondThread();
new Thread(targetRunnable,"线程1").start();
new Thread(targetRunnable).start();
}
}
- 使用Callable和Future创建线程:Callable类似于Runnable,提供一个Call()方法作为线程执行体,并且可以有返回值,以及抛出异常,那么我们如何拿到返回值,java提供了future接口,在接口里定义了一些公共方法来控制关联它的Callable任务,然后java还贴心的给了FutureTask类,该类实现了Future接口和Runnable接口,所以FutureTask的实例就可以作为Thread的Target,所以通常做法是创建Callable接口实现类,并对该实现类的实例使用FutureTask来包装。
public class ThridThread {
public static void main(String[] args) {
// lambda 表达式 + functionInterface 类型转换
// Callbable: 有返回值
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{
int i =0;
for(;i<100;i++) {
System.out.println(Thread.currentThread().getName() + " "+ i);
}
return i;
});
new Thread(task,"有返回值的线程").start();
try {
System.out.println("子线程的返回值"+task.get());
}catch(Exception e) {
e.printStackTrace();
}
}
}
线程的生命周期
线程的生命周期包括:新建New,就绪Runnable,运行Running,阻塞Blocked,和死亡Dead 5种状态。
新建和就绪状态:程序使用new关键字之后,该线程就处于新建状态,jvm为其分配内存,并初始化成员变量。程序调用start() 方法之后,该线程就处于就绪状态,jvm为其创建方法调用栈和PC计数器。
-
运行和阻塞状态:如果就绪状态的线程获得了CPU,那么程序就处于运行状态。
当发生如下情况时,线程将进入阻塞状态。- 线程调用sleep()方法,主动放弃所占有的CPU资源。
- 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
- 线程试图获得一个同步监视器,但是该同步监视器被其他线程所持有
- 线程等待某个通知notify,notify通常与wait配合使用
- 线程调用suspend(),挂起,该方法容易造成死锁,不建议使用。
当发生如下情况时,线程进入就绪状态,但是线程什么时候进入运行状态,需要根据系统调度来决定。
- sleep()方法的线程经过了指定的sleep的时间
- 阻塞式IO方法返回值
- 成功获得了同步监视器
- 线程获得了其他线程发出的通知,被唤醒
- 挂起的线程调用了Resume()方法恢复。
- 线程死亡:线程执行体执行结束,以及抛出一个未捕获的Exception或Error,或者直接调用stop()方法结束该线程。可以通过线程对象的isAlive()方法,来判断线程对象的状态(新建或者死亡都会返回false)
注意:抢占式策略系统:系统会给每个执行的线程一个小的时间段来处理任务,当该时间段用完之后,系统会剥夺该线程所占用的资源,让其他线程获得执行的机会。在系统调度时,还会考虑到线程的优先级问题。
线程控制
join()-线程:让一个线程等待另一个线程,当在某个线程执行流中调用其他线程的join()方法,该线程将被阻塞,知道join线程执行完毕为止。
后台-线程:后台线程又称为Daemon Thread,守护线程,JVM的垃圾回收线程就是典型的后台线程。特征是:如果所有前台线程都死亡,那么后台线程自动死亡。调用Thread对象的setDaemon(true)可以将指定线程设置为后台线程,注意需要在Start()之前调用,主线程默认为前台线程,前台线程创建的子线程默认为前台线程,后台线程创建的子线程默认为后台线程。
sleep()-线程:sleep(ms)是Thread类的静态方法,让当前线程暂停millis毫秒,并进入阻塞状态,睡眠状态的线程不会释放同步监视器,在此期间该线程不会获得执行的机会。注意使用sleep方法时需要捕捉InterruptedException或者抛出该异常。
public class SleepThread {
public static void main(String[] args) throws Exception{ // 注意异常
for(int i =0;i<5;i++) {
System.out.println("当前时间"+new Date());
Thread.sleep(1000);
}
}
}
yield():线程让步,也是Thread的静态方法,使得正在执行的线程暂停,但不会阻塞线程,只是交出CPU的控制权,将线程转为就绪状态,让系统调度器重新调度一次。当某个线程调用yield方法暂停后,只有优先级与当前线程相同,或者优先级比当前线程更高的线程才有可能获得执行机会。
改变线程优先级:setPriority(int newPriority),高优先级的线程能获得更多的执行机会。
线程同步
线程的同步的意义在于线程安全,也就是说有多个线程并发访问同一个对象,而线程调度的不确定性可能带来潜在的安全问题。
同步监视器:java多线程引入同步监视器来解决同步问题,任何时刻只能有一个线程获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定。java允许任何对象作为同步监视器,通常我们使用可能被并发访问的共享资源作为同步监视器。
同步代码块:显式指定同步监视器。
public class DrawThread extends Thread {
private Account account;
private double drawaccout;
public DrawThread(String name,Account account,double drawaccount) {
super(name);
this.account = account;
this.drawaccout= drawaccount;
}
public void run() {
synchronized(account) {
if(account.getBlance()>=drawaccount) {
System.out.println(getName()+"取钱成功");
try {
Thread.sleep(1);
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
- 同步方法:隐式指定同步监视器,使用synchronized关键字来修饰某个方法,此时该方法(非static方法)无需显式指定同步监视器,同步监视器默认为this,也就是调用该方法的对象。
public synchronized void draw(double amount) {
……
}
- 同步监视器的释放:线程会在以下几种情况下释放同步监视器的锁定。
- 当前线程的同步方法,同步代码块执行结束或者在执行中遇到break,return等终止了代码块的执行
- 同步代码块或者方法中出现未处理的Error或者Exception,导致异常结束
- **当前线程执行同步代码块或者同步方法时,程序中执行了同步监视器的wait()方法,wait是object的方法,范围是该object实例所在的线程
- 同步锁:lock,更加强大的线程同步机制,通过显式定义锁对象来实现同步,也就是Lock对象,线程在访问共享资源之前,需要先获得锁对象。线程安全控制中比较常用的是ReetrantLock可重入锁。一个线程可以对已经加锁的ReetrantLock再度加锁。
class X{
private final ReentrantLock lock = new ReentrantLock();
//需要定义线程安全的方法
public void foo() {
lock.lock();//加锁
try {
// 需要保证线程安全的代码
}
finally {
lock.unlock();//使用finally块保证释放锁
}
}
}
-
死锁的问题:
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。目前处理死锁的方法可归结为以下四种:
预防死锁:这是一种较简单和直观的事先预防的方法。方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法,已被广泛使用。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率和系统吞吐量降低。
避免死锁:该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。
检测死锁:这种方法并不须事先采取任何限制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中将已发生的死锁清除掉。
解除死锁:这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。
java中应该避免死锁的出现。
线程安全是以牺牲程序运行效率为代价的,因此在注意线程安全的同时,也要注意不要滥用锁和同步方法,尽量只对那些会改变竞争资源的方法进行同步。同时还要根据单线程和多线程运行环境来提供线程不安全和线程安全两种版本,JDK提供的StringBuilder,StringBuffer就是一个例子。
线程通信
-
Object类提供的wait(),notify(),notifyAll()三个方法,由同步监视器来调用,对于同步方法,其同步监视器是默认实例this,可以再同步方法中直接调用这三个方法。
- wait(): 当前线程等待或者等待若干ms,当前线程自动释放同步监视器,线程进入等待状态(阻塞),直到其他线程调用了该同步监视器的notify()或者notifyAll方法。
- notify():唤醒在同步监视器上等待的单个线程,若有多个线程等待,则任意选择其中一个。
- notifyAll():唤醒在此同步监视器上等待的所有线程。
使用Condition控制线程通信:使用Lock对象来保证同步问题时,我们可以使用Condition类来释放Lock以及唤醒其他等待线程。
private final Lock lock = new ReentrantLock();
// Condition实例绑定在一个Lock对象上
private final Condition cond = lock.newCondition();
public void Draw(double drawamount) {
lock.lock();
try {
if(!flag)
cond.await();//导致当前线程等待
else {
// ...
cond.signalAll();// 唤醒其他线程
}
}catch(InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
- 使用阻塞队列(BlockingQueue)控制线程通信:当生产者线程试图向Blocking Queue中放入元素时,如果队列已满,则该线程被阻塞,当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。
线程池
- 使用线程池执行线程任务的步骤是:
- 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池。
- 创建Runnable实现类或者Callable实现类的实例,作为线程的执行任务。
- 调用ExecutorService对象的submit方法来提交Runnable或者Callable实例。
- 当没有任务时,使用shutdown()方法来关闭线程池。
public class Testjava{
public static void main(String[] args)
throws Exception{
ExecutorService pool = Executors.newFixedThreadPool(6);
Runnable target = ()->{
for(int i=0;i<100;i++) {
System.out.println(Thread.currentThread().getName()
+ "的i值为:"+ i);
}
};
// 向线程池中提交两个线程
pool.submit(target);
pool.submit(target);
pool.shutdown();
}
}
-
Executors是一个工厂类,它包含了如下几个静态工厂方法来创建线程池。
- newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程
- newFixedThreadPool(int nThreads):创建一个可重用的,具有固定线程数的线程池
- newSingleThreadExecutor():创建一个单线程线程池
- newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它将在指定延迟后执行线程任务
- newSingleThreadScheduledExecutor():创建一个延迟执行的单线程线程池
- newWorkingStealingPool(int parallelism):创建持有足够的线程的线程池来支持给定的并行级别,以充分支持多CPU并行能力。
- newWorkingStealingPool():根据CPU个数设置并行级别。
12367 返回ExecutorService对象,代表一个线程池,可以执行Runnable以及Callable对象所代表的线程,45返回的ScheduledExecutorService对象。
【Java8源码分析】线程池-Executor与ExecutorService的全面剖析
线程相关类
- ThreadLocal:Thread Local Variable线程局部变量,为每个使用该变量的线程提供一个变量值的副本,从而隔离多线程程序的竞争资源。ThreadLocal类的用法很简单,它提供了三个public方法:
- T get():返回此线程局部变量中当前线程副本的值
- void remove():删除此线程局部变量中当前线程的值
- void set(T value):设置副本值
class Accout{
private ThreadLocal<String> name = new ThreadLocal<>();
public Accout(String str) {
this.name.set(str);
}
public String getname() {
return name.get();
}
public void setname(String str) {
this.name.set(str);
}
}
注意:ThreadLocal与其他同步机制都是为了解决访问同一资源冲突问题而出现的,但是侧重的领域不同,同步机制为实现多个线程对相同资源访问的并发安全性,ThreadLocal则是隔离多个线程之间的数据共享,从而避免竞争。
- 线程安全的集合类:
- ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap都是线程不安全的,如果需要在多线程中对上述集合类进行存取,需要使用Collections提供的静态方法将其包装成线程安全的类。
- 更好的方法是使用 java.util.concurrent包下提供的大量支持高效并发访问的集合接口和实现类。如ConcurrentHashMap,ConcurrentLinkedQueue,ConcurrentLinkedDeque
Android多线程
Android中的多线程本质上也是Java的多线程,同时添加了一些不同的特性和使用的场景。其中,最主要的一个区别就是Android中主线程和子线程中的区分,Android中的主线程是UI线程,负责运行四大组件并与用户实现交互,需要保持较高的反应速度,所以主线程不允许进行耗时的操作(比如说网络请求和访问),否则容易出现ANR现象,子线程则负责处理 一些耗时的任务,而如果子线程中想要实现对UI的操作,则需要通过Android的handler消息机制。
为什么子线程中不允许对UI进行操作呢
因为Android的UI控件并不是线程安全,多线程的并发访问会带来UI控件的不可预期的状态,且考虑到加锁机制会带来性能上的问题,因此Android在设计初期就禁止子线程处理UI。UI操作时ViewRootImpl会对操作者所在的线程进行checkThread,如果非主线程,会抛出CalledFromWrongThreadException。
那么Android除了java原生的Thread/Runnable等线程形态,还有哪些包装过了的有特点的线程形式?
- AsyncTask: AsyncTask 封装了线程池和Handler,主要是为了方便开发者不去写自己的后台线程和定义Handler,而方便更新UI界面。
- IntentService: IntentService从名字来看,可以知道它是一个服务,其内部采用HandlerThread执行任务,执行完毕后自动退出。其特点是它是Service,比起其他线程来说具有更高的优先级,不容易被系统杀死,而能够保证任务的执行。
- HandlerThread: HandlerThread是一个具有消息循环loop的线程,也就是一开始就准备好了loop的线程,在其内部可以直接使用Handler。
AsyncTask
很棒的参考:你真的了解AsyncTask
AsyncTask 是一个轻量级的一部任务类,在线程池中执行后台任务。然后将任务的进度和最终结果传递给主线程,并在主线程中更新UI。
AsyncTask是一个抽象的泛型类,使用时必须继承并实现它的子类,并且至少重写doInBackground(Params...)方法。
// 三个泛型参数 不需要传递参数时,可以用void代替
public abstract class AsyncTask<Params,Progress,Result>
- AsyncTask的使用
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
//在线程池中执行 该方法必须返回计算结果给onPostExecute()
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
// 可以使用该方法返回任务的进度,该方法会调用onProgressUpdate()
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
if (isCancelled()) break;
}
return totalSize;
}
// 被主线程调用执行 因此这里可以有UI操作
protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}
// 在主线程中调用执行 任务执行结束后,会调用该方法,因此这里可以有UI操作
protected void onPostExecute(Long result) {
showDialog("Downloaded " + result + " bytes");
}
}
// 主线程调用execute方法,执行任务前,会调用[1] onPreExecute()完成一些准备工作
// onPreExecute()是在主线程中执行
new DownloadFilesTask().execute(url1, url2, url3);
// 也可以调用cancel来取消任务的执行
-
AsyncTask的使用注意事项:
- AsyncTask的类必须在主线程中加载,这一点液晶在Android4.1以上版本上自动完成
- AsyncTask 对象必须在主线程中创建
- execute方法必须在主线程中调用
- 不要直接调用
onPreExecute()
,onPostExecute(Result)
,doInBackground(Params...)
,onProgressUpdate(Progress...)
- 一个AsyncTask对象只能执行一次,否则会运行报错。
-
AsyncTask实现原理:
- AsyncTask中有两个线程池:SerialExecutor用于任务的排队,THREAD_POOL_EXECUTOR用于真正执行任务,定义了一个InternalHandler用于对UI进行操作,该handler是一个静态的Handler对象,所以想要对UI进行操作,这个handler对象必须在UI线程中创建,也就是为什么AsyncTask类为什么要在UI线程加载。
- ThreadPoolExecutor是真正建立线程池的类,它在AsyncTask类中创建线程池的参数如下:
public abstract class AsyncTask<Params, Progress, Result> {
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();//CPU数
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
}
// 核心线程数 = CPU数+1
// 最大线程数 = CPU数*2 + 1
// 非核心线程的超时时间为1秒
// 任务队列的容量为128
Android 开发手册写明:AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the
java.util.concurrent
package such asExecutor
,ThreadPoolExecutor
andFutureTask
HandlerThread
- HandlerThread 是Thread的子类,它的特点是可以使用Handler的Thread。它的run方法体如下所示,外界可以通过handler来通知HandlerThread来处理某些事情,从这可以看出Handler的用处不仅仅局限于处理UI相关的问题。
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized(this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid =-1;
}
IntentService
- IntentService封装了HandlerThread和Handler,但是它继承了Service,所以导致它的优先级比单纯线程要高,所以IntentService适合执行一些高优先级的后台任务。
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize: 线程池的核心线程数,默认情况下, 核心线程会在线程池中一直存活, 即使处于闲置状态. 但如果将allowCoreThreadTimeOut设置为true的话, 那么核心线程也会有超时机制, 在keepAliveTime设置的时间过后, 核心线程也会被终止.
- maximumPoolSize: 最大的线程数, 包括核心线程, 也包括非核心线程, 在线程数达到这个值后,新来的任务将会被阻塞.
- keepAliveTime: 超时的时间, 闲置的非核心线程超过这个时长,讲会被销毁回收, 当allowCoreThreadTimeOut为true时,这个值也作用于核心线程.
- unit:超时时间的时间单位.
- workQueue:线程池的任务队列, 通过execute方法提交的runnable对象会存储在这个队列中.
- threadFactory: 线程工厂, 为线程池提供创建新线程的功能.
- handler: 任务无法执行时,回调handler的rejectedExecution方法来通知调用者.
如果线程池中线程的数目少于corePoolSize,就算线程池中有其他的没事做的核心线程,线程池还是会重新创建一个核心线程;直到核心线程数目到达corePoolSize(常驻线程就位)
如果线程池中线程的数目大于或者等于corePoolSize,但是工作队列workQueue没有满,那么新的任务会放在队列workQueue中,按照FIFO的原则依次等待执行;(当有核心线程处理完任务空闲出来后,会检查这个工作队列然后取出任务默默执行去)
如果线程池中线程数目大于等于corePoolSize,并且工作队列workQueue满了,但是总线程数目小于maximumPoolSize,那么直接创建一个线程处理被添加的任务。
如果工作队列满了,并且线程池中线程的数目到达了最大数目maximumPoolSize,那么就会用最后一个构造参数handler处理;**默认的处理方式是直接丢掉任务,然后抛出一个异常。