Java多线程
线程和进程的区别:
线程和进程都是操作系统进行资源分配和调度的基本单位,但它们的本质区别在于CPU调度方式。线程是由CPU进行调度的执行任务的基本单位,而进程则是由操作系统进行调度的独立运行的程序或指令集合。线程可以看作是进程中的一个执行流,它们共享进程的资源,如内存、文件描述符等。
并发:
并发是指在同一时间内,多个任务被快速轮换执行,使得宏观上具有多个线程或者进程同时执行的效果。在Java中,可以通过继承Thread类或实现Runnable接口来创建线程。
进程:
在操作系统中,一个运行的程序或者说一个动态的指令集合通常对应一个进程。进程是系统进行资源分配和调度的一个独立单位,也是拥有系统资源的基本单位。进程是系统中独立存在的实体,它可以拥有自己独立的资源,拥有自己私有的地址空间,进程之间不能直接访问其他进程的地址空间。
线程:
线程是CPU调度的基本单位,也就是说在一个进程中可以有多个并发程序执行流。线程拓展了进程的概念,使得任务的执行得到更加的细分,所以Thread有时候也被称为Lightweight Process。线程是进程的执行单元,但是线程不是分配系统资源的单位,它们共享所在进程的资源,包括共享内存、公有数据、全局变量、进程文件描述符、进程处理器、进程代码段、进程用户ID等等。
线程独立拥有自己的线程ID、堆栈、程序计数器、局部变量、寄存器组值、优先级、信号屏蔽码、错误返回码等等,线程是独立运行的,其执行是抢占式的。线程共享进程资源,线程之间的通信要进程之间的通信来得容易得多。此外,线程的创建和销毁的开销也远远小于进程的系统开销。
线程池:
虽然线程的创建销毁的开销相对较小,但是频繁得创建和销毁也会消耗有限的资源,从而带来性能上的浪费,也不够高效。因此线程池的出现就是为了解决这一问题,即在初始状态创建并维护一定数量的空闲线程,当有需要执行的任务,就交付给线程中的一个线程,任务执行结束后,该线程也不会死亡,而是回到线程池中重新变为空闲状态。线程池的好处包括减少线程频繁创建销毁的资源开销,同时能够有效控制系统中并发线程的数量,防止系统性能的剧烈下降。
线程创建/启动的三种方法:
1. 继承Thread类创建多线程:每次创建的Thread对象并不能共享线程类的实例变量。
2. 实现Runnable接口创建多线程:实现Runnable接口的类可以作为参数传递给Thread类的构造函数,从而创建一个新的线程。这种方式下,每个线程都有自己的Runnable对象和对应的实例变量。
3. 使用Callable和Future创建多线程:Callable接口与Runnable接口类似,但它可以返回结果。通过将Callable对象传递给Thread类的构造函数,可以创建一个新的线程来执行Callable任务。Future接口则用于获取Callable任务的结果。
重构后的代码如下:
```java
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线程执行体,多个线程可以共享一个实例变量。
```java
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();
}
}
```
线程的生命周期包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。
1. 新建和就绪状态:当程序使用`new`关键字创建一个线程对象时,该线程处于新建状态。此时,JVM为其分配内存并初始化成员变量。当程序调用`start()`方法后,线程进入就绪状态,JVM为其创建方法调用栈和PC计数器。
2. 运行状态:当线程获得CPU时间片并开始执行时,线程进入运行状态。在运行状态下,线程会不断地执行其`run()`方法中的代码。
3. 阻塞状态:当线程遇到`sleep()`、`wait()`或`join()`等阻塞方法时,线程会进入阻塞状态。在阻塞状态下,线程会暂停执行,直到被唤醒或者超时。
4. 死亡状态:当线程执行完毕或者因异常而终止时,线程会进入死亡状态。在死亡状态下,线程的资源会被回收。
下面是一个简单的Java示例,展示了如何使用`Callable`和`Future`创建线程以及获取返回值:
```java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThirdThread {
public static void main(String[] args) {
// lambda表达式 + functionInterface类型转换
// Callable: 有返回值
Future
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 (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
```
线程的状态可以分为以下几种:
1. 运行状态:当就绪状态的线程获得了CPU时,程序就处于运行状态。
2. 阻塞状态:线程在以下情况下会进入阻塞状态:
- 调用sleep()方法,主动放弃所占有的CPU资源。
- 调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞。
- 试图获得一个同步监视器,但是该同步监视器被其他线程所持有。
- 等待某个通知notify,通常与wait配合使用。
- 调用suspend()方法挂起,该方法容易造成死锁,不建议使用。
3. 就绪状态:当发生以下情况时,线程进入就绪状态,但是线程什么时候进入运行状态,需要根据系统调度来决定:
- sleep()方法的线程经过了指定的sleep的时间。
- 阻塞式IO方法返回值。
- 成功获得了同步监视器。
- 线程获得了其他线程发出的通知,被唤醒。
- 挂起的线程调用了Resume()方法恢复。
4. 死亡状态:线程执行体执行结束,以及抛出一个未捕获的Exception或Error,或者直接调用stop()方法结束该线程。可以通过线程对象的isAlive()方法来判断线程对象的状态(新建或者死亡都会返回false)。
注意:抢占式策略系统会给每个执行的线程一个小的时间段来处理任务,当该时间段用完之后,系统会剥夺该线程所占用的资源,让其他线程获得执行的机会。在系统调度时,还会考虑到线程的优先级问题。
线程控制有以下几种方法:
1. join():让一个线程等待另一个线程,当在某个线程执行流中调用其他线程的join()方法,该线程将被阻塞,知道join线程执行完毕为止。
2. 后台线程:后台线程又称为Daemon Thread,守护线程,JVM的垃圾回收线程就是典型的后台线程。特征是:如果所有前台线程都死亡,那么后台线程自动死亡。调用Thread对象的setDaemon(true)可以将指定线程设置为后台线程,注意需要在Start()之前调用,主线程默认为前台线程,前台线程创建的子线程默认为前台线程,后台线程创建的子线程默认为后台线程。
3. sleep():sleep(ms)是Thread类的静态方法,让当前线程暂停millis毫秒,并进入阻塞状态,睡眠状态的线程不会释放同步监视器,在此期间该线程不会获得执行的机会。注意使用sleep方法时需要捕捉InterruptedException或者抛出该异常。
以下是重构后的代码:
```java
public class SleepThread {
public static void main(String[] args) throws Exception {
// 注意异常
for (int i = 0; i < 5; i++) {
System.out.println("当前时间" + new Date());
Thread.
```java
public class DrawThread extends Thread {
private Account account;
private double drawAmount;
public DrawThread(String name, Account account, double drawAmount) {
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
@Override
public void run() {
synchronized (account) {
if (account.getBalance() >= drawAmount) {
System.out.println(getName() + "取钱成功");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
```
在Java中,为了避免死锁的出现,我们可以采取以下措施:
1. 使用`ReentrantLock`类来实现线程同步。`ReentrantLock`是一个可重入的互斥锁,它提供了与`synchronized`关键字类似的功能,但更加灵活。通过使用`ReentrantLock`,我们可以更好地控制锁的获取和释放,从而避免死锁的发生。
2. 避免嵌套锁。在多线程环境下,如果一个线程已经获得了一个锁,然后又试图获取另一个锁,这将导致死锁。因此,我们应该尽量避免嵌套锁的情况。
3. 按顺序加锁。在多个线程需要访问共享资源的情况下,我们可以要求线程按照一定的顺序加锁。这样可以确保每个线程在获得锁之后,都能按照预期的顺序执行。
4. 使用`tryLock()`方法尝试获取锁。`tryLock()`方法允许线程尝试获取锁,如果锁已经被其他线程占用,那么该方法会立即返回,而不会阻塞当前线程。这样可以避免因为等待锁而导致的死锁。
5. 设置锁的超时时间。当一个线程在一定时间内无法获取到锁时,它将放弃获取锁的操作。这样可以避免某些线程因为等待锁而导致的死锁。
下面是一个使用`ReentrantLock`的示例代码:
```java
class X {
private final ReentrantLock lock = new ReentrantLock(); // 需要定义线程安全的方法
public void foo() {
lock.lock(); // 加锁
try {
// 需要保证线程安全的代码
} finally {
lock.unlock(); // 使用finally块保证释放锁
}
}
}
```
关于死锁的问题,死锁是指在计算机系统中,两个或多个进程因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。产生死锁的四个必要条件包括:互斥条件、请求与保持条件、不剥夺条件和循环等待条件。在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。目前处理死锁的方法可归结为以下四种:预防死锁、避免死锁、检测死锁和解除死锁。
线程通信是指多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。
线程池是一种多线程处理形式,它创建一定数量的线程,将它们全部初始化之后,放入到一个池子中,以便重复利用。当有任务到达时就在这些线程当中取出一个执行。
使用线程池执行线程任务的步骤如下:
1. 调用`Executors`类的静态工厂方法创建一个`ExecutorService`对象,该对象代表一个线程池。例如,可以使用`newFixedThreadPool(6)`创建一个固定大小为6的线程池。
2. 创建`Runnable`实现类或者`Callable`实现类的实例,作为线程的执行任务。例如,可以创建一个简单的`Runnable`实现类,用于打印当前线程的名称和循环变量`i`的值。
```java
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);
}
};
}
}
```
3. 调用`ExecutorService`对象的`submit`方法来提交`Runnable`或者`Callable`实例。在这个例子中,我们向线程池中提交了两个相同的任务。
4. 当没有任务时,使用`shutdown()`方法来关闭线程池。这将等待所有已提交的任务完成,并释放与线程池关联的所有资源。
```java
pool.shutdown();
```
需要注意的是,上述代码中的线程池大小设置为6,但实际上只提交了两个任务。在实际应用中,可以根据需要调整线程池的大小以平衡性能和资源占用。
线程池:
ExecutorService 是一个代表线程池的接口,它可以执行 Runnable 和 Callable 对象所代表的线程。返回的 ExecutorService 对象可以用来执行多个任务,从而提高程序的执行效率。
ThreadLocal 类:
ThreadLocal 是 Java 中用于实现线程局部变量的类。它为每个使用该变量的线程提供一个变量值的副本,从而隔离多线程程序的竞争资源。ThreadLocal 类提供了三个 public 方法:get()、remove() 和 set(T value)。其中,get() 方法用于获取当前线程局部变量的值;remove() 方法用于删除当前线程局部变量中的值;set(T value) 方法用于设置当前线程局部变量的值。
示例代码:
```java
class Accout {
private 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中的多线程本质上也是Java的多线程,同时添加了一些不同的特性和使用的场景。其中,最主要的一个区别就是Android中主线程和子线程中的区分。
Android中的主线程是UI线程,负责运行四大组件并与用户实现交互,需要保持较高的反应速度,所以主线程不允许进行耗时的操作(比如说网络请求和访问),否则容易出现ANR现象。子线程则负责处理一些耗时的任务,而如果子线程中想要实现对UI的操作,则需要通过Android的handler消息机制。
为什么子线程中不允许对UI进行操作呢?因为Android的UI控件并不是线程安全,多线程的并发访问会带来UI控件的不可预期的状态,且考虑到加锁机制会带来性能上的问题,因此Android在设计初期就禁止子线程处理UI。UI操作时ViewRootImpl会对操作者所在的线程进行checkThread,如果非主线程,会抛出CalledFromWrongThreadException。
那么Android除了java原生的Thread/Runnable等线程形态,还有哪些包装过了的有特点的线程形式?
1. AsyncTask:AsyncTask封装了线程池和Handler,主要是为了方便开发者不去写自己的后台线程和定义Handler,而方便更新UI界面。AsyncTask是一个抽象的泛型类,使用时必须继承并实现它的子类,并且至少重写doInBackground(Params...)方法。
2. IntentService:IntentService从名字来看,可以知道它是一个服务,其内部采用HandlerThread执行任务,执行完毕后自动退出。其特点是它是Service,比起其他线程来说具有更高的优先级,不容易被系统杀死,而能够保证任务的执行。
3. HandlerThread:HandlerThread是一个具有消息循环loop的线程,也就是一开始就准备好了loop的线程,在其内部可以直接使用Handler。
AsyncTask是Android提供的一个轻量级的用于处理异步任务的类。它是一个对于Thread和Handle的辅助类,主要让开发者方便的使用UI Thread和后台Thread的操作(比如在后台线程下载文件,同时要在UI线程更新下载进度)。
AsyncTask的使用步骤有三个:
1. 创建AsyncTask的子类,并根据需要复写相关的方法。
2. 创建AsyncTask的子类实例对象。
3. 调用子类实例对象的execute()方法,执行异步任务。
```
private class DownloadFilesTask extends AsyncTask
// 在线程池中执行该方法必须返回计算结果给onPostExecute()方法
protected Long doInBackground(final 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));
// 如果cancel()方法被调用,则提前结束任务
if (isCancelled()) break;
}
return totalSize;
}
// 在主线程中调用执行,因此这里可以有UI操作。任务执行结束后会调用该方法
protected void onPostExecute(final Long result) {
showDialog("下载了 " + result + " 字节");
}
}
// 主线程调用execute方法,执行任务前会调用onPreExecute()方法完成一些准备工作。
// onPreExecute()方法在主线程中执行
new DownloadFilesTask().execute(url1, url2, url3);
```
这段代码是Android开发中的异步执行方法,具体包括以下几个部分:
1. onPreExecute(): 用于在主线程执行子线程之前进行一些操作,例如设置进度条等。通常在子线程启动后立即调用。
2. onPostExecute(Result): 用于在主线程中获取子线程执行的结果,并将结果传递给onPostExecute()方法的参数。通常在子线程完成后调用。
3. doInBackground(Params...): 用于在子线程中执行耗时的操作,例如网络请求、文件读写等。通常在onPreExecute()方法之后调用。
4. onProgressUpdate(Progress...): 用于在子线程中更新进度信息,以便在UI上显示进度条或其他进度提示。通常在doInBackground()方法中使用。
以下是重构后的代码:
```java
public abstract class AsyncTask
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
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory);
}
```
Java中的`java.util.concurrent.ThreadPoolExecutor`是一个线程池执行器,它可以管理和控制多个线程的执行。它提供了一个灵活的方式来处理并发任务,可以根据需要创建和销毁线程。
`ThreadPoolExecutor`的主要组成部分包括:
1. 核心线程数(corePoolSize):线程池中始终保持活跃的线程数量。即使线程处于空闲状态,也会一直保留在池中。
2. 最大线程数(maximumPoolSize):线程池中允许的最大线程数量。当任务队列已满且核心线程数已达到最大值时,新提交的任务将导致线程池中的线程数量超过最大值。
3. 空闲线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数时,多余的空闲线程在等待新任务的最长时间。超过这个时间后,这些空闲线程将被终止。
4. 任务队列(workQueue):用于存储等待执行的任务的阻塞队列。常用的有`ArrayBlockingQueue`、`LinkedBlockingQueue`等。
5. 拒绝策略(rejectedExecutionHandler):当任务无法提交给线程池时采取的策略。常见的拒绝策略有直接抛出异常、丢弃任务、返回默认结果等。
下面是一个简单的示例代码,演示如何使用`ThreadPoolExecutor`执行后台任务:
```java
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个具有2个核心线程和4个最大线程的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue
// 提交任务给线程池执行
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is being executed by thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " has completed execution by thread " + Thread.currentThread().getName());
});
}
// 关闭线程池(在实际应用中通常不需要显式关闭)
executor.shutdown();
}
}
```
上述代码创建了一个具有2个核心线程和4个最大线程的线程池,然后提交了10个任务给线程池执行。每个任务打印自己的ID以及执行它的线程名称,并模拟了一个耗时操作。最后,通过调用`shutdown()`方法关闭线程池。
重构后的代码如下:
```java
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
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的rejectedExecution方法来通知调用者。线程池的工作流程如下:
1. 如果线程池中线程的数目少于corePoolSize,就算线程池中有其他的空闲核心线程,线程池还是会重新创建一个核心线程;直到核心线程数目到达corePoolSize(常驻线程就位)。
2. 如果线程池中线程的数目大于或者等于corePoolSize,但是工作队列workQueue没有满,那么新的任务会放在队列workQueue中,按照FIFO的原则依次等待执行。当有核心线程处理完任务空闲出来后,会检查这个工作队列然后取出任务默默执行去。
3. 如果线程池中线程数目大于等于corePoolSize,并且工作队列workQueue满了,但是总线程数目小于maximumPoolSize,那么直接创建一个线程处理被添加的任务。
4. 如果工作队列满了,并且线程池中线程的数目到达了最大数目maximumPoolSize,那么就会用最后一个构造参数handler处理。默认的处理方式是直接丢掉任务,然后抛出一个异常。