Java 语言内置了多线程支持:一个 Java 程序实际上是一个 JVM 进程,主线程来执行 main() 方法,在 main() 方法内部,我们可以启动多个线程;
线程的创建方式:
- 继承 Thread 覆写 run() 方法,创建 Thread 实例, new MyThread();
- 实现 Runnable 接口,new Thread(new MyRunnable());
- 实现 Callable 接口;重写 call() 方法;
相比 run() ,可以有返回值;
方法可以抛异常;
支持泛型的返回值;
需要借助 FutureTask 类获取返回结果; - 使用线程池;
继承关系:
FutureTask 类 —实现–》RunnableFuture 接口 —继承—》 Runnable 和 Future 接口;
class NumThread implements Callable<Integer>{
@override
public Integer call() throws Exception{}
}
psvm{
NumThread numThread = new NumThread();
FutureTask<Integer> futureTask = new FutureTask<>(numThread);
new Thread(futureTask).start();
}
设置线程优先级
Thread.setPriority(int n) # 1-10, 默认是5;
操作系统对高优先级线程;
Java 线程的状态:
- New :新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行 run() 方法;
- Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting :运行中的线程,因为执行 sleep() 方法正在计时等待;
- Terminated:线程已终止,因为 run() 方法执行完毕;
线程终止的原因:
- 正常终止:run() 方法执行到 return 语句返回;
- 意外终止:run方法因为未捕获的异常导致线程终止;
- 强制终止:对线程的Thread实例调用 stop()方法强制终止;
join()
等待该线程结束,然后才继续往下执行自身线程;
有一个重载方法 join(long) 可以指定一个等待时间,超过时间后就不再等待;
线程池
线程池接口:public interface ExecutorService extends Executor
ExecutorService : 真正的线程池接口,常见子类:ThreadPoolExecutor;
void execute( Runnable command ):执行任务/命令,没有返回值,一般用来执行Runnable;
Futuresubmit(Callable task):执行任务,有返回值,一般用来执行 Callable;
void shutdown() :关闭连接池;
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池;
- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
线程池的核心参数:
ThreadPoolExecutor(
int corePoolSize, # 核心线程数;
int maximumPoolSize, # 最大线程数
long keepAliveTime # 非核心线程的空闲时间,直到只剩下 corePoolSize 个线程为止;
TimeUnit unit, # 时间单位
BlockingQueue workQueue, # 工作队列
ThreadFactory threadFactory # 线程工厂 ,通常使用默认的工厂:Executors.defaultThreadFactory()
RejectedExecutionHandler handler # 拒绝策略,队列满了,并且工作线程大于等于线程池的最大线程数,如何拒绝新来的任务;
)
提交任务 —》 核心线程池 --》核心线程池满了之后,任务被放入任务队列 --》 任务队列已满,扩容线程池 --》扩容至最大线程数仍然有任务提交进来,就执行拒绝策略;
不使用 jdk 提供的线程池的原因:
请求队列长度为 Integer 的最大值;
最大线程数量为 int 最大值; 很容易内存溢出;
四种拒绝策略:
AbortPolicy(默认):直接抛出 RejectedExecutionException异常阻止系统正常运行;
CallerRunsPolicy: “调用者运行”,将任务回退到调用者;
DiscardOldestPolicy:抛弃队列中等待最久的任务,将当前任务加入队列;
DiscardPolicy:默默丢弃无法处理的任务;
ThreadLocal
在一个线程中,横跨若干方法调用,需要传递的对象,我们称之为上下文 Context;
给每个方法增加一个 context 参数非常麻烦,如果调用链中有无法修改源码的第三方库,参数就无法传递;
Java 标准库提供了 ThreadLocal , 它可以在一个线程中传递同一个对象;
ThreadLocal 通常是以静态字段的形式进行初始化:
static ThreadLocal<User> threadLocal = new ThreadLocal<>();
try{
threadLocal.set(user);
threadLocal.get();
}finally{
threadLocal.remove();
}
ThreadLocal 可以看成是一个全局 Map<Thread,Object>, 线程获取 ThreadLocal 变量时,总是使用 Thread自身(Thread.currentThread())作为 key;
注意:ThreadLocal 一定要在 finally 中清除;
中断线程
中断线程就是其它线程给该线程发一个信号,该线程收到信号后结束执行 run() 方法,使得自身线程能立刻结束运行;
方式一:对目标线程调用 interrupt()
在其它线程中对目标线程调用 interrupt() 方法,目标线程需要反复使用 isInterrupted() 检测自身状态是否是 interrupted 状态,如果是,就立刻结束运行;
interrupt() 方法仅仅向目标线程发出了“中断请求”,至于目标线程是否能立刻响应,要看具体代码;
如果目标线程通过 join() (或者sleep)等方法处于等待状态,对目标线程调用 interrupt(),join方法会立刻抛出 InterruptedException ;
因此目标线程只要捕获到 join() 方法抛出的 InterruptedException, 就说明有其他线程对其调用了 interrupt()方法,通常情况下该线程应该立刻结束运行;
总结:目标线程检测到 isInterrupted()
为 true
或者捕获了 InterruptedException
都应该立刻结束自身线程;
方式二:设置标志位
psvm{
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; //设置标志位为 false
}
class HelloThread extends Thread{
public volatile boolean running = true;
public void run(){
while(running){
}
}
}
注意:线程间共享变量需要使用 volatile 关键字标记,确保每个线程都能读取到更新后的变量值;
Java 内存模型中,变量的值保存在主内存中,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中;如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的
volatile 关键字的作用是告诉虚拟机:
每次访问变量时,总是获取主内存的最新值;
每次修改变量后,立刻回写到主内存;
守护进程
对于定时任务,我们经常设置为守护线程;当其它非守护线程结束时,JVM 进程结束;
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出;JVM退出时,不会关心守护线程是否已结束;
设置守护线程:
在调用 start() 方法前,调用 setDaemon(true) 把该线程标记为守护线程;
注意:
守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
线程同步
在多线程模型下,对共享变量进行读写时,必须保证:一个线程执行时,其它线程必须等待;
加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只能有一个线程执行;
synchronized
Java 中提供了 synchronized 关键字对一个对象(必须是线程间的共享实例)进行加锁;
synchronized 保证了代码块在任意时刻最多只有一个线程能执行。
synchronized 语句块结束后会自动释放锁;
使用 synchronized
的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized
结束处正确释放锁;
两种原子操作:
1、基本类型赋值(long 和 double 除外),int n =m ;
2、引用类型赋值: List list = anotherList;
如果是多行赋值语句,就必须保证是同步操作;
同步方法
把 synchronized 逻辑封装到一个方法中;
用synchronized
修饰的方法就是同步方法,它表示整个方法都必须用this
实例加锁
如果一个类被设计为允许多线程正确访问,那么这个类就是 线程安全的, 如 StringBuffer;
# 下面两种写法等价
public void add(int n ){
synchronized(this){
count += n;
}
}
public synchronized void add(int n){
count += n ;
}
### 对 static 方法添加 synchronized , 锁住的是该类的 Class 实例;
public synchronized static void test(int n){}
可重入锁
能被同一个线程反复获取的锁,就叫做可重入锁。
synchronized 是可重入锁,每获取一次锁内部计数器就加1,每退出 synchronized 块,记录 -1,减到0的时候,才会真正释放锁;
多线程协调运行
多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务;
synchronized+Wait/Notify
wait()方法必须在当前获取的锁对象上调用;
调用 wait方法后,线程进入等待状态,wait 方法不会返回,直到被其他线程唤醒后,wait才会返回,继续执行下一条语句;
特点:
- wait 是定义在 Object 类的一个 native 方法;
- 必须在 synchronized 块中才能调用 wait 方法;
- 调用wait() 方法时,会释放线程获得的锁;
- 被唤醒后(wait方法返回),线程会重新尝试获取锁;
- 在相同的锁对象上调用 notify() 就可以唤醒等待的线程,然后从 wait() 方法返回;
- 使用
notifyAll()
将唤醒所有当前正在this
锁等待的线程,而notify()
只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性); - 要在
while()
循环中调用wait()
,而不是if
语句;被唤醒重新获取锁后要重新判断一次
ReentrantLock + Condition(await/signalAll)
并发包: java.util.concurrent ;
java.util.concurrent.locks 提供的 ReentrantLock (Lock接口的实现类)用于替代 synchronized加锁;
ReentrantLock默认是非公平锁,需要创建的时候设置为true标识公平锁。
与synchronized的区别:
1、synchronized 是java语言层面的语法(需要jvm实现),锁是自动添加和释放的,不需要考虑异常,而 ReentrantLock 是 java 代码实现的锁(API层面的互斥锁),必须手动加锁,并在 finally 中正确释放锁(加锁和解锁的次数要一致);
2、synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断, 并且ReentrantLock 可以尝试获取锁:
3、ReentrantLock还可以实现公平锁。所谓公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁。而synchronized只能是非公平锁。
补充:
- synchronized:偏向锁(偏向第一个线程,效率最高) —> 如果有线程竞争升级为轻量级锁(自旋锁) —>
自旋10次升级为重量级锁(悲观锁) - ReentrantLock可以尝试获取锁
lock.tryLock(1,TimeUnit.SECONDS);
在尝试获取锁的时候,等待1秒。如果1秒后仍未获取到锁,tryLock()
返回false
,程序就可以做一些额外处理,而不是无限等待下去。
Condition
synchronized 可以配合 wait 和 notify 来实现线程同步;
ReentrantLock 使用 Condition 对象来实现线程同步;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
condition.await();
condition.signalAll()
# await() 可以在等待指定时间后,如果没有被其它线程通过 signal() 或 signalAll()唤醒,可以自己醒来;
condition.await(1,TimeUnit.SECOND)
注意:
唤醒线程从await()
返回后需要重新获得锁。
使用Condition
时,引用的Condition
对象必须从Lock
实例的newCondition()
返回,这样才能获得一个绑定了Lock
实例的Condition
实例。
BlockingQueue
java.util.concurrent下的一个接口(Queue的子接口),是为了解决多线程中数据高效安全传输而提出的。
好处:
我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了;
提供了生产者-消费者模型的一种实现方案;
被阻塞的情况主要有如下两种:
- 当队列满了的时候进行入队列操作
- 当队列空了的时候进行出队列操作
接口的实现类
BlockingQueue 接口:获取元素时可能会让线程变成等待状态 ;java.util.concurrent
- ArrayBlockingQueue : 由数组结构组成的有界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列;
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue: 支持优先级排序的无界阻塞队列;
常用方法:
抛异常 | true/false/null | 阻塞 | 阻塞+超时退出返回true/false | |
---|---|---|---|---|
插入 | add(e) | offer | put | off(e,time,unit) |
移除 | remove | poll | take | poll(time,unit) |
检查 | element | peek | / | / |
ReadWriteLock
允许多个线程同时读,但只要有一个线程在写,其它线程就必须等待;
适用于读多写少的场景;
ReadWriteLock rwlock = new ReentrantReadWriteLock();
Lock rlock = rwlock.readLock();
Lock wlock = rwlock.writeLock();
锁降级
锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。
public void test(){
rwlock.writeLock().lock();
System.out.println("获取到写锁。。。。");
rwlock.readLock().lock();
System.out.println("获取到读锁----------");
rwlock.writeLock().unlock();
System.out.println("释放写锁==============");
rwlock.readLock().unlock();
System.out.println("释放读锁++++++++++++++++");
}
读写锁的特点:
1、支持公平/非公平策略
2、支持可重入
- 同一读线程在获取了读锁后还可以获取读锁
- 同一写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁
3、支持锁降级,不支持锁升级:写锁里面可以获取读锁,读锁里面不能获取写锁
4、Condition条件支持
写锁可以通过newCondition()
方法获取Condition对象。但是读锁是没法获取Condition对象,读锁调用newCondition()
方法会直接抛出UnsupportedOperationException
。
5、读写锁如果使用不当,很容易产生“饥饿”问题:
在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。
“饥饿”问题的解释:
当线程 A 持有读锁读取数据时,线程 B 要获取写锁修改数据就只能到队列里排队。此时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁,而要执行写操作线程 B 就要等线程 C 释放读锁。由于该场景下读操作远远大于写的操作,此时可能会有很多线程来读取数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等待下去,最终导致饥饿。
————————————————
Concurrent 集合
java.util.concurrent 包提供的并发集合类:
List ArrayList CopyOnWriteArrayList
Map HashMap ConcurrentHashMap
Set HashSet CopyOnWriteArraySet
Queue LinkedList ArrayBlockingQueue/LinkedBlockingQueue
Atomic
java.util.concurrent.atomic 包提供了一组原子操作的封装类;
AtomicInteger
增加值并返回新值: int addAndGet(int delta);
加1后返回新值: int incrementAndGet();
获取当前值: int get();
用 CAS 方式设置: int compareAndSet(int expect, int update);
如果AtomicInteger
的当前值是expect
,那么就更新为update
,返回true
。如果AtomicInteger
的当前值不是expect
,就什么也不干,返回false
Atomic 类通过“无锁” 的方式实现的线程安全访问。主要利用了 CAS: Compare and Set;
Semaphore
信号量的两个作用:
- 多个共享资源的互斥使用;
- 用来控制并发线程数;
信号量中的两种操作:
acquire(获取):当线程调用 acquire 操作时,它要么成功获取信号量(信号量减一),要么一直等下去;,直到有线程 释放信号量;
release (释放):会将信号量的值加1,然后唤醒等待线程;
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(4);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"\t抢到了车位");
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"\t离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
CountDownLatch
两个方法:
countDown() # 计数器减1;
await() # 只有计数器减为 0 ,main线程才能结束,否则要一直等待;
一个场景:所有人都离开了教室才打扫卫生;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t离开教室");
//计数器减一
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//只有计数器减为 0 ,main线程才能结束,否则要一直等待;
countDownLatch.await();
System.out.println("main 线程结束");
}
}
CyclicBarrier
机器7颗龙珠才召唤神龙;
一个线程完成任务后,就调用 barrier.await(), 当完成任务的线程达到一个数量后就执行总任务(相当于满足某条件下的一个回调);
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(7, () -> {
System.out.println("集齐龙珠召唤神龙");
});
for (int i = 1; i <=7; i++) {
final int temInt = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t收集到第:" + temInt + "颗龙珠");
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
面试
JMM : java 内存模型,主要是为了解决代码在不同的硬件生产商和操作系统下,内存访问逻辑的差异带来的线程安全问题;
目的:让同样的代码在不同平台下能够达到相同的访问结果;
线程访问主内存中的共享变量,会先在本地内存(工作内存)中保存一个副本;
工作内存线程独享;工作内存负责与线程交互,也负责与主内存交互;
JMM对共享内存的操作做了如下规定:
线程对共享内存的所有操作都必须在自己的工作内存中进行;
不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成;
volatile变量,用来确保将工作内存中变量的更新操作通知到其他线程
CAS
CAS : Compare And Swap,
是解决多线程并发安全问题的一种乐观锁算法;因为它在对共享变量更新之前,会先比较当前值是否与更新前的值一致,如果一致则更新,如果不一致则循环执行(称为自旋锁),直到当前值与更新前的值一致为止,才执行更新。
缺点:
开销大:在并发量比较高的情况下,如果反复尝试更新某个变量,却又一直更新不成功,会给CPU带来较大的压力
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性;
AQS
抽象队列同步器、它是实现同步器的基础组件(框架)
juc下面Lock(ReentrantLock、ReentrantReadWriteLock等)的实现以及一些并发工具类(Semaphore、CountDownLatch、CyclicBarrier等)就是通过AQS来实现的。
AQS维护了一个volatile语义(支持多线程下的可见性)的共享资源变量state和一个FIFO(first-in-first-out)线程等待队列(多线程竞争state资源被阻塞时,会进入此队列)。
线程会获取state的状态值,如果是 0 就尝试通过 CompareAndSetState(0,1)设置为1,设置成功就获取到了锁;
AQS将大部分的同步逻辑均已经实现好,继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码就可以,主要有以下方法:
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。