使用场景
- 通过并行计算提高程序执行性能。
- 需要等待网络、I/O响应导致耗费大量的执行时间,可以采用异步线程的方式来减少阻塞。
多线程创建
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口(待返回值)
并发基础
线程的状态
- NEW:初始状态,线程被构建,但是还没有调用start方法。
- RUNNABLED:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为“运行中”。
- 阻塞状态,表示线程进入等待状态。又分为等待阻塞、同步阻塞和其他阻塞。
- TIME_WAITING:超时等待状态,超时以后自动返回。
- TERMINATED:终止状态,表示当前线程执行完毕。
线程终止
interrupt方法,当其他线程通过调用当前线程的interrupt方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。
这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。
线程的安全问题
- 可见性
缓存一致性问题,多核心CPU情况下存在指令并行执行,而各个CPU核心之间的数据不共享从而导致缓存一致性问题。
解决方案:总线锁、缓存锁。
- 原子性
多线程对共享资源的访问。 - 有序性
编译器、处理器执行优化、指令重排。
解决办法
内存模型
内存模型定义了共享内存系统中多线程程序读写操作行为的规范,来屏蔽各种硬件和操作系统的内存访问差异,来实现Java程序在各个平台下都能达到一致的内存访问效果。
Java内存模型定义了线程和内存的交互方式,在JMM抽象模型中,分为主内存、工作内存。主内存是所有线程共享的,工作内存是每个线程独有的。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存中的变量。并且不同的线程之间无法访问对方工作内存中的变量,线程间的变量值的传递都需要通过主内存来完成。
总的来说,JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。
JMM怎么解决原子性、可见性、有序性的问题?
在Java中提供了一系列和并发处理相关的关键字,比如volatile、Synchronized、final、juc等,这些就是Java内存模型封装了底层的实现后提供给开发人员使用的关键字,在开发多线程代码的时候,我们可以直接使用synchronized等关键词来控制并发,使得我们不需要关心底层的编译器优化、缓存一致性的问题了,所以在Java内存模型中,除了定义了一套规范,还提供了开放的指令在底层进行封装后,提供给开发人员使用。
- 原子性
在java中提供了两个高级的字节码指令monitorenter和monitorexit,在Java中对应的Synchronized来保证代码块内的操作是原子的。 - 可见性
ava中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。 - 有序性
可以使用synchronized和volatile来保证多线程之间操作的有序性,volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。
synchronized的应用方式
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
- 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
wait和notify
调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态进入等待队列并且释放锁;然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。
同步锁
在java5以后,增加了JUC的并发包且提供了Lock接口用来实现锁的功能,它提供了与synchroinzed关键字类似的同步功能,只是它比synchronized更灵活,能够显示的获取和释放锁。
Lock与Synchronized
- 从层次上,一个是关键字、一个是类, 这是最直观的差异.
- 从使用上,lock具备更大的灵活性,可以控制锁的释放和获取; 而synchronized的锁的释放是被动的,当出现异常或者同步代码块执行完以后,才会释放锁。
- lock可以判断锁的状态、而synchronized无法做到。
- lock可以实现公平锁、非公平锁; 而synchronized只有非公平锁。锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
并发工具
CountDownLatch
countdownlatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。从命名可以解读到countdown是倒数的意思,类似于我们倒计时的概念。
countdownlatch提供了两个方法,一个是countDown,一个是await,countdownlatch初始化的时候需要传入一个整数,在这个整数倒数到0之前,调用了await方法的程序都必须要等待,然后通过countDown来倒数。
Semaphore
semaphore也就是我们常说的信号灯,semaphore可以控制同时访问的线程个数,通过acquire获取一个许可,如果没有就等待,通过release释放一个许可,有点类似限流的作用。
Atomic原子操作
在Java5以后,提供了原子操作类,这些原子操作类提供了一种简单、高效以及线程安全的更新操作。而由于变量的类型很多,所以Atomic一共提供了12个类分别对应四种类型的原子更新操作,基本类型、数组类型、引用类型、属性类型.
线程池
JDK 为我们内置了几种常见线程池的实现,均可以使用 Executors 工厂类创建。为了更好的控制多线程,JDK提供了一套线程框架Executor,帮助开发人员有效的进行线程控制。它们都在java.util.concurrent包中,是JDK并发包的核心。
其中有一个比较重要的类:Executors,他扮演着线程工厂的角色,我们通过Executors可以创建特定功能的线程池。
- newFixedThreadPool:该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交时,若线程池
中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行。 - newSingleThreadExecutor: 创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
- newCachedThreadPool:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若有任务,用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在60秒后自动回收。
- newScheduledThreadPool: 创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器。
submit和execute的区别
- execute只能接受Runnable类型的任务。
- submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null。