![c4e5ed5cbc9932291802480f9a9e6dd9.png](https://img-blog.csdnimg.cn/img_convert/c4e5ed5cbc9932291802480f9a9e6dd9.png)
线程与进程
进程是资源分配的最小单位,线程是CPU调度的最小单位。
进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是拥有资源和独立运行的最小单位,也是程序运行的最小单位。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。
一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间。
打个比方就是,CPU 相当于一个工厂,而进程相当于一个车间,线程相当于车间中的工人。
线程的实现方式
- Thread 和 Runnable
- Thread
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Thread started!");
}
};
thread.start();
-
- Runnable
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
Thread thread = new Thread(runnable);
thread.start();
- ThreadFactory
ThreadFactory factory = new ThreadFactory() {
int count = 0;
@Override
public Thread newThread(Runnable r) {
count ++;
return new Thread(r, "Thread-" + count);
}
};
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "started!");
}
};
Thread thread = factory.newThread(runnable);
thread.start();
Thread thread1 = factory.newThread(runnable);
thread1.start();
- Executor 和线程池
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);
executor.execute(runnable);
executor.execute(runnable);
线程的生命周期
线程的生命周期包括 5 个部分:新建、就绪、运行、阻塞、销毁。
![62aada492fda59dbd1cbcbf2cd9cdacd.png](https://img-blog.csdnimg.cn/img_convert/62aada492fda59dbd1cbcbf2cd9cdacd.png)
- 新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个Java内部状态。
- 就绪(RUNNABLE),就是调用的线程的start()方法后。表示该线程已经在JVM中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它CPU片段,在就绪队列里面排队。
- 运行,当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
- Java 的Thread.State 中,关于等待有三个常量表示。
- 阻塞(BLOCKED),阻塞表示线程在等待Monitor lock。比如,线程试图通过synchronized去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态
- 等待(WAITING),表示正在等待其他线程采取某些操作。比如调用 sleep()、wait() 、join() 之后,线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用 notify 或者 notifyAll() 方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
- 计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如wait或join等方法的指定超时版本
- 销毁(TERMINATED),如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
为什么一个线程不能 2 次 start()
一个线程只能启动一次,在第二次调用 start() 方法的时候,线程可能处于终止或者其他(非 NEW)状态,但是不论如何,都是不可以再次启动的
影响线程状态的因素
1、线程自身的方法。
sleep():在指定时间内让当前正在执行的线程暂定执行,但不会释放锁标志。
yield():暂停当前正在执行的线程对象,告诉调度器,主动让出 CPU;
join():等待该线程终止。
2、基于 Object 提供的 wait/notify/notifyAll 方法。如果我们持有某个对象的 Monitor 锁,调用 wait 会让当前线程处于等待状态,直到其他线程 notify 或者 notifyAll。所以,本质上是提供了 Monitor 的获取和释放的能力,是基本的线程间通信方式。
线程池
线程池是事先将多个线程对象放入到一个容器中,使用的时候直接去容器中拿,而不是new一个新的线程。节省了开辟子线程的时间,提高了写代码的执行效率。
1、线程池一共有几种。
1. newCachedThreadPool,可缓存的线程池,不固定线程数量,有空闲线程就复用,没有就新建,一定程度上减少了频繁创建/销毁小城,减少系统的开销。
2.newFixedThreadPool,固定数量的线程池,可控制线程的最大并发数,超出的线程会在队列中等待。
3.newSingleThreadPool,单线程的线程池,有且只有一个线程执行任务,所有的任务都按照指定的顺序执行,即遵循入队出队的规则。
4.newScheduleThreadPool,支持定时以制定周期循环执行任务的线程池
前三种线程池是 ThreadPoolExecutor 不同配置的实例,最后一种是 ScheduledThreadPoolExecutor 的实例。
2、线程池的原理
ThreadPoolExecutor 的创建有 7 个参数:核心线程数、最大线程数、线程存活时间、时间单位、工作队列、线程工厂、Handler。
从数据结构的角度来看,线程池主要参用了阻塞队列和HashSet的存储方式。
从任务执行的角度来看,线程池的机制是这样的。
1、如果正在运行的线程数 < coreSize,马上创建核心线程执行该task,不排队等待;
2、如果正在运行的线程数 >= coreSize,把该task放入阻塞队列;
3、如果队列已满 && 正在运行的线程数 < maximumPoolSize,创建新的非核心线程执行该task;
4、如果队列已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用handler的reject方法拒绝本次提交。
3、线程池的线程复用(了解)
ThreadPoolExecutor 中 executor 执行时,会把传入的 Runnable 通过 addWorker 方法,将 Runnable 包装成 Worker 对象,并将 Worker 添加到 workers 集合中,如果添加成功后,当执行 run 方法时,就会执行 worker 内部的run方法。
Worker 是 ThreadPoolExecutor 的内部类,它的构造方法制定了第一个要执行的任务 firstTask,并通过线程池的线程工厂创建线程。
当执行 run 方法时,最终会调用 runWorker 方法。 runWorker 方法是线程复用的核心。
// runWorker
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
Worker 继承了 AbstractQueueSynchronizer , 在执行每个任务之前,通过 lock 方法加锁,执行完后,通过 unlock 解锁,使用这种机制来防止运行中任务被中断。
在执行任务时,先尝试获取 firstTask,即构造方法传入的runnable
对象,然后尝试从getTask
方法获取任务队列中的任务。在任务执行前还要再次判断线程池是否已经处于STOP状态或者线程被中断。
woker 线程的执行流程就是首先执行初始化时分配给的任务,执行完成以后会尝试从阻塞队列中获取可执行的任务,如果指定时间内仍然没有任务可以执行,则进入销毁逻辑调用processWorkerExit()
方法。
注:这里只会回收 corePoolSize 与 maximumPoolSize 直接的那部分 woker
getTask()
方法通过一个循环不断轮询任务队列有没有任务到来,首先判断线程池是否处于正常运行状态,根据超时配置有两种方法取出任务:
- BlockingQueue.poll 阻塞指定的时间尝试获取任务,如果超过指定的时间还未获取到任务就返回null。
- BlockingQueue.take 这种方法会在取到任务前一直阻塞。
FixedThreadPool使用的是 take 方法,所以会线程会一直阻塞等待任务。CachedThreadPool使用的是 poll 方法,也就是说 CachedThreadPool 中的线程如果在60秒内未获取到队列中的任务就会被终止。
其实就是任务在并不只执行创建时指定的firstTask第一任务,还会从任务队列的中通过getTask()方法自己主动去取任务执行,而且是有/无时间限定的阻塞等待,保证线程的存活。
线程同步与线程安全
当多个线程同时对一个资源进行修改时,由于CPU时间调度上的问题,写入的数据可能回被多次的覆盖,所以就要使线程同步。
什么是线程安全?
线程安全就是当多个线程访问某个类时,不管运行环境采用哪种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这类都能表现出正确的行为,那这个类就是线程安全的。
线程安全的三特性
原子性
对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行。
可见性
一个线程修改了某个共享变量,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。
volatile 就是负责保证可见性的。
有序性
是保证线程内串行语义,避免指令重排等。
线程同步的方式有哪些?
1、synchronize
synchronized:保证方法内部或者代码块内部的资源(数据)的互斥访问。即同一时间、最多只有一个线程能获取到监视锁(Monitor)对象。当线程A尝试获取线程B持有的锁时,线程A只能等待或者阻塞,直到线程B释放锁。
synchronized 能够在方法或者代码块上加锁,默认的 monitor 是类对象,如果有多个 monitor 对象,它们之间不互斥。
synchronized 加锁的作用
1、保证监视资源的可见性。保证多线程环境下对监视资源的数据同步。即任何线程在获取到 monitor 后的第一时间,会先将共享内存中的数据复制到自己的缓存中;任何线程在释放 Monitor 的第一时间,会将缓存中的数据同步到共享内存中。
2、保证线程间操作的原子性。
同一时间只能由最多只能有一个线程访问,因此 synchronized 加锁的代码,要么不执行,要么全部执行完毕。
3、保证有序性,双重锁单例模式的指令重排问题,在高版本的 java 已经解决了这个问题。
解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序
public static Shop getInstance() {
if (sInstance == null) {
synchronized (Shop.class) {
if (sInstance == null) {
sInstance = new Shop(); // A
}
}
}
return sInstance;
}
在双重锁校验的单例模式中, sInstance = new Shop()
这句代码实际执行了三件事。
1、 给 Shop 的实例分配内存;
2、调用 Shop 的构造函数,初始化成员变量;
3、将 sInstance 的对象指向分配的内存空间。
但 Java 编译器允许处理器乱序执行,2、3的顺序是无法保证的。如果是 1-3-2 执行的顺序,当执行完 3 、2未执行之前,被切换到 B 线程,此时 sInstance 已经非空,B 会直接取走 sInstance,在使用时就会出错。
这就是指令重排。
解决办法也很简单:只需要给 instance 成员变量加上 volatile 关键字,就可以禁止指令重排序。
2、Lock 锁
使用 Lock 锁也能够实现多线程同步。
Lock定义了一系列的锁操作方法。它提供了一种无条件的、可轮询的、定时的以及可中断的锁获取方式,所有的加锁和解锁的方法都是显式调用的。
Lock 的子类有 ReentrantLock、ReentrantReadWriteLock。ReentrantLock 提供了与 synchronized 相同的互斥性和内存可见性。
Lock 锁需要手动释放锁,不然会一直持有锁对象,永不释放。
- 读写锁。
读写锁的加锁策略是,允许多个读操作同时进行,但每次只允许一个写操作。
3、 volatile
volatile 不能保证线程安全。
volatile 只能够保证修饰字段的原子性和同步性,但如果对该变量执行的是非原子操作线程依旧是不安全的。
变量被 volatile 修饰后,编译器与运行时会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排。
注意,volatile 只对基本类型的赋值和对象引用赋值的操作有效。
volatile 不能确保递增操作(count++)的原子性。volatile 只能保证可见性。
4、Atomic
Atomic包提供了一系列的 AtomicInteger、AtomicBoolean 等类,使用这些类来声明变量,可以保证对其的操作具有原子性来保证线程安全。
atomic 采用的是 CAS(CompareAndSwap)的操作原理,即无锁操作,一种乐观锁策略,原理就是多线程环境下各线程访问共享变量不会加锁阻塞排队,线程不会被挂起。通俗来讲就是一直循环对比,如果有访问冲突则重试,直到没有冲突为止。
至于 Android 中的多线程与线程间通信,我会单开一篇。