一、什么是多线程
在操作系统中,一个应用程序被看做是一个进程,而进程内部又可以开启多个线程。
每个线程都会在操作系统中的任务调度器等待被调度,实际处理任务的是CPU,同一时刻能够处理的线程数取决于CPU的核心数。
若线程数超过了CPU的核心数,那么这些线程会轮流拥有CPU的时间片(取决于时间片轮转调度算法)。
串行:同一时刻只能有一个任务执行,其它任务在等待。
并行:两个线程在同一时刻可以一起执行任务。
并发:两个线程在多个时刻交替执行,但这个间隔非常短,几乎是同时执行。
线程间的通信
方法名 | 描述 |
---|---|
join() | 在线程中调用另一个线程的join(),阻塞当前线程,等待目标线程结束。 |
wait()、notify()、notifyAll() | 调用wait()使线程等待某个条件,线程等待时会被挂起。当其他线程使条件满足时,其他线程调用notify()或notifyAll()来唤醒挂起的线程。 |
await()、signal()、signalAll() | juc类库中提供了Condition类来实现线程之间的协调,可通过Condition实例调用await()使线程等待,其他线程调用signal()或signalAll()唤醒等待的线程。 await()可以设置等待的条件,比wait()更灵活 |
二、线程的创建方式
操作系统实现线程主要有三种方式:
- 内核线程(Kernel-Level Thread,KLT),内核线程指的是直接由操作系统内核支持的线程,程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口叫轻量级进程(Light Weight Process,LWP)。轻量级进程与内核线程之间是1:1的关系。HotSpot就是采用这种方式。
- 用户线程(User Thread,UT),用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。进程与用户线程之间是1:N的关系。
- 混合实现(N:M):用户线程加轻量级进程的混合实现,在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。用户线程与轻量级进程之间是M:N的关系。
JDK提供了四种方式可以创建线程
- Thread类
- Runnable接口
- Callable接口
-
1. Thread 类
创建一个类并继承Thread类,重写Thread类的run方法(run方法里的代码就是线程执行的代码),调用Thread类的start方法使线程启动。
缺点:Java具有单继承的特性,一个对象只能继承一个类,相较于实现Runable接口,扩展性稍差。public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("创建的线程:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) throws IOException {
//创建一个MyThread实例并调用start方法启动线程
new MyThread().start();
}
}
2. Runable 接口
创建一个类,实现Runable接口,实现Runable接口中的run方法(run方法里的代码就是线程执行的代码)。在使用时以实现Runable接口类的实例作为参数创建一个Thread(Thread线程启动后会执行实例的run方法中的程序),再调用Thread类的start方法启动线程。
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("创建的线程:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) throws IOException {
//创建一个线程,创建一个MyRunnable做为线程的参数,调用线程的start方法启动线程
new Thread(new MyRunnable()).start();
}
}
3. Callable 接口
与Runable不同,Callable接口的实例需要配合FutureTask类使用,它的特点是可以通过FutureTask类的get方法获取线程执行完成后的返回值(此方法是阻塞方法,会阻塞当前线程)。与Runable相同之处是,都需要创建Thread类实例并调用start方法启动线程。 ```java public class MyCallable implements Callable
{ @Override public String call() throws Exception { for (int i = 0; i < 5; i++) {
System.out.println("创建的线程:" + Thread.currentThread().getName());
} return Thread.currentThread().getName() + “ thread over”; }
public static void main(String[] args) throws IOException, ExecutionException, InterruptedException { //创建一个MyCallable MyCallable myCallable = new MyCallable(); FutureTask
futureTask = new FutureTask<>(myCallable); //将FutureTask作为Thread的参数,并调用start启动线程 new Thread(futureTask).start(); //FutureTask的get方法是获取Callable的返回值,是阻塞方法(需要等待线程执行完成) System.out.println(futureTask.get()); }
}
<a name="v8fm2"></a>
## 4. Executor 线程池
JDK中的JUC包(java.util.concurrent)提供了线程池的方式创建线程
```java
public class ExecutorDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程池执行 Runnable匿名内部类实例:" + Thread.currentThread().getName());
}
});
executorService.execute(() -> System.out.println("线程池执行 lambda表达式创建的Runnable匿名内部类:" + Thread.currentThread().getName()));
}
}
在阿里巴巴Java开发手册中【强制】规定了不允许使用Executors创建线程池,因为可能会导致OutOfMemoryError,其原文如下:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
通过以上说明我们可以得知不建议使用Executors创建线程池的原因是需要规避资源耗尽的风险,那么如何去规避呢?我们可以通过ThreadPoolExecutor类来创建一个定制化的线程池。
7个参数配置项如下:
- corePoolSize:线程核心数5,当线程数超过这个数值时,会回收超过存活时间未活动的线程
- maximumPoolSize:最大线程数10,总线程数不能超过这个数
- keepAliveTime:存活时间30,当线程没有工作时能存活的时间
- unit:存活时间的单位
- workQueue:工作队列,当所有核心线程都在忙,则先把任务加入工作队列
- threadFactory(非必填):创建线程的工厂,所有线程通过工厂创建,缺省值DefaultThreadFactory
handler(非必填):拒绝策略,当线程数满了,工作队列也满了之后任务的拒绝策略,缺省值AbortPolicy ```java public class ExecutorDemo {
public static void main(String[] args) {
//创建线程池
ExecutorService executorService = new ThreadPoolExecutor(
5,
10,
30,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1000),
new BasicThreadFactory.Builder().build(),
new ThreadPoolExecutor.DiscardPolicy());
//通过线程池执行代码
executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
}
}
<a name="HxTY8"></a>
# 三、多线程安全
<a name="uw7dj"></a>
## 可见性(volatile)
看过Java虚拟机的同学应该知道,线程所使用的内存是在虚拟机栈中,是线程隔离的,线程对共享变量操作时,会将共享变量复制一份到线程的工作内存中(栈),计算完成后再写回主内存中(堆)。<br />![线程工作流程图](https://cdn.nlark.com/yuque/0/2022/png/25938361/1645519836532-97e08ec9-daa5-4ae4-b656-f21c6aac1632.png#clientId=u6051282e-cfb8-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u37422f33&margin=%5Bobject%20Object%5D&originHeight=264&originWidth=749&originalType=url&ratio=1&rotation=0&showTitle=true&status=done&style=none&taskId=ucd3b4486-f30a-4daa-b0ba-e7f157965d9&title=%E7%BA%BF%E7%A8%8B%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B%E5%9B%BE "线程工作流程图")<br />线程对共享变量的操作:从主内存Load到工作内存,进行运算,完成后再Store回主内存;这个操作在多线程情况下是不安全的,因为线程的执行受调度器控制,所以每个线程Load和Store操作的时机和顺序是不可预测的。<br />![多线程工作流程图](https://cdn.nlark.com/yuque/0/2022/png/25938361/1645519823259-9607f0a5-3b9c-4559-b12b-5eebcdf2e6bc.png#clientId=u6051282e-cfb8-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=R1TGw&margin=%5Bobject%20Object%5D&originHeight=530&originWidth=997&originalType=url&ratio=1&rotation=0&showTitle=true&status=done&style=none&taskId=u9d45e944-5eaa-445a-b199-e88a84842f3&title=%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B%E5%9B%BE "多线程工作流程图")<br />一个程序案例:创建A、B两个线程和一个boolean值共享变量flag值为true,线程A进行循环工作,当flag为false则退出,通过线程B去修改flag为false,先启动线程A,稍等后启动线程B。当你运行此程序时会发现线程A出现无法退出的情况,这是因为线程A对线程B的修改不具有可见性,无法感知flag在主内存中被修改为false,线程A的工作内存中flag的值还是为true。<br />volatile关键字可用于解决可见性问题,当线程A读取共享变量flag后,线程B修改了flag值,此时线程A拷贝的flag将失效,线程A对flag值进行任何操作,都必须立刻从主内存重新Load。<br />volatile通过lock指令实现,可规避指令重排序。
<a name="rMwbW"></a>
## 原子性(synchronized)
假设A和B线程同时对v进行加一操作(其中一种可能的执行顺序):<br />A Load v=1;B Load v=1;A还没将运算完的结果Store回主内存,B又读取了v。<br />A Store v=2;B Store v=2;v的结果不符合预期的值1+1+1=3。<br />我们预期的Load v=1;newV=v+1;Store v=newV;在多线程中并不是一个原子性操作。
```java
public class SynchronizedDemo {
static int v = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
for (int i = 0; i < 100000; i++) {
v++;
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100000; i++) {
v++;
}
}).start();
Thread.sleep(1000);//等待两个线程执行完成
System.out.println(v);//v的值小于正确值200000
}
}
Java提供了synchronized关键字来解决这一问题,synchronized是非公平锁,也就是说竞争锁的线程是无序执行的,这里还可以深入到锁粗化以及操作系统调度算法。
synchronized通过monitorenter、monitorexit指令实现。
public class SynchronizedDemo {
static int v = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
//不建议在循环内写同步代码块,因为会重复申请锁
for (int i = 0; i < 100000; i++) {
//对SynchronizedDemo的Class对象进行加锁,只有持有锁的线程能执行同步代码块
synchronized (SynchronizedDemo.class) {
v++;
}
//JVM会在代码块执行完成后自动释放(申请100000次则需要释放100000次)
}
}).start();
new Thread(() -> {
//对SynchronizedDemo的Class对象进行加锁,只有持有锁的线程能执行同步代码块
synchronized (SynchronizedDemo.class) {
for (int i = 0; i < 100000; i++) {
v++;
}
}
//JVM会在代码块执行完成后自动释放
}).start();
Thread.sleep(1000);
//不使用synchronized同步代码块时v的值小于正确值200000
//使用synchronized同步代码块时v的值等于正确值200000
System.out.println(v);
}
}
若需要同时锁定多个共享资源、更大范围的代码块、更灵活的控制锁,Java中JUC包(java.util.concurrent)中Lock接口提供了扩展实现。
有序性
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前
半句是指“线程内似表现为串行的语义”(Within-Thread As-If-SerialSemantics)
后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本
身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对
其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。
参考文献
jdk的内置锁
cyc2018 Java并发
《深入理解Java虚拟机》第三版