JUC
在Java 5.0 提供了 java.util.concurrent (简称 JUC )包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的 Collection 实现等。
一、多线程回顾
线程和进程
程序是完成特定任务、用某种语言编写的一段代码,程序是静态的,程序运行后变为一个进程,进程是动态的。一个进程内可以有多个线程同时执行。进程是所有线程的集合,每一个线程是进程中的一条执行路径。
多线程的使用优势
提高应用程序的响应;提高 CPU 的利用率;改善冗长、复杂的程序结构;使用线程可以耗时任务放到后台去处理。
多线程的创建方式
方式一:继承 Thread 类
- 创建一个继承 Thread 类的子类
- 重写 Thread 类的 run() --> 将此线程执行的操作声明在 run() 中
- 创建 Thread 类的子类的对象
- 通过此对象调用 start():① 启动当前线程 ② 调用当前线程的 run()
方式二:实现 Runnable 接口
- 创建一个实现 Runnable 接口的类
- 实现类去实现 Runnable 中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
- 通过 Thread 类的对象调用 start()
// 当线程只需要创建一次时,可以使用匿名内部类的方式创建
new Thread(new Runnable() {
@Override
public void run() {
// 逻辑代码
}
}).start();;
方式三:实现 Callable 接口
1.创建一个实现 Callable 的实现类
2.实现 call 方法,将此线程需要执行的操作声明在 call() 中
3.创建 Callable 接口实现类的对象
4.将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象
5.将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()
6.获取 Callable 中 call 方法的返回值,get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call() 的返回值。
// 匿名内部类
Callable<Object> callable = new Callable<Object>() {
@Override
public Object call() throws Exception {
// 逻辑代码
return "该值可以被FutureTask.get()拿到";
}
};
FutureTask<Object> futureTask = new FutureTask<>(callable);
new Thread(futureTask, "线程A").start();
try {
Object object = futureTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
Callable 接口 比 Runnable 强大之处:
1. call() 方法相比 run() 方法有返回值。
2. call() 方法可以将异常抛出(throws)。
3. 支持泛型的返回值。
4. Callable 接口一般用于配合 ExecutorService 使用。
方式四:使用线程池
并发情况下,大量的线程创建和销毁对资源的消耗很大,因此创建即用即取的线程池,可以避免频繁创建和销毁,方便线程管理,实现重复利用,提高性能。
线程池的常用属性:
corePoolSize:核心池的大小,默认情况下核心线程一直存活,即使处于闲置状态也不会受存keepAliveTime
限制。
maximumPoolSize:最大线程数,超过这个数的线程将被阻塞。
keepAliveTime:线程没有任务时最多保持多长时间后会终止。
步骤:
-
创建线程池并指定线程数量。
ThreadPoolExecutor 是线程池的真正实现,它通过构造方法的一系列参数,来构成不同配置的线程池
-
执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接口实现类的对象
-
关闭线程池
ExecutorService pool = Executors.newFixedThreadPool(10); // 指定池中的线程数量
ThreadPoolExecutor service = (ThreadPoolExecutor) pool;
// 设置线程池的属性
service.setCorePoolSize(5); // 核心池的大小,默认情况下核心线程会一直存活。
service.setKeepAliveTime(2, TimeUnit.SECONDS); // 设置闲置线程 2 秒后关闭。
service.setMaximumPoolSize(20); // 再次设置最大线程数,如果小于构造器中的值,则该值优先。
service.execute(new Thread(new Runnable())); // 适合用于实现 Runnable 接口的类
service.submit(new THread(new Callable<V>()));// 适合用于实现 Callable 接口的类
service.shutdown();
线程的常用方法
Thread 中常用 API | 解释 |
---|---|
start() | 启动线程 |
currentThread() | 获取当前线程对象 |
getID() | 获取当前线程ID |
getName() | 获取当前线程名称,Thread-编号 该编号从0开始 |
setName() | 设置当前线程的名字 |
sleep(long mill) | 休眠线程 |
stop() | 停止线程 |
yield() | 释放 cpu 的操作 |
join() | 加塞,谁调 join() 谁先执行,执行完该线程后,其他线程才能执行。 |
isAlive() | 判断线程是否还活着 |
线程的优先级
线程的优先级控制:
MAX_PRIORITY(10)
MIN _PRIORITY (1)
NORM_PRIORITY (5)
常用方法:
getPriority() **:**返回线程优先级
setPriority(int newPriority) **:**设置线程的优先级
子线程创建时继承父线程的优先级。
线程的生命周期
线程的状态
线程的分类
Java 中的线程分为两类:一种是守护线程(后台线程),一种是用户线程(前台线程)。它们唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在 start() 方法前调用 thread.setDaemon(true) 可以把一个用户线程变成一个守护线程。当主线程不存在或主线程停止时,守护线程也会停止。
Java垃圾回收就是一个典型的守护线程。若 JVM 中都是守护线程,当前 JVM 将退出。
线程的停止
1.设置退出标志,使线程正常退出,也就是当 run() 方法完成后线程终止。
最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。
2.使用 interrupt() 方法中断线程
线程处于阻塞状态:
如使用了 sleep ,同步锁的 wait , socket 中的 receiver , accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt() 方法时,会抛出 **InterruptException **异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
线程未处于阻塞状态:
使用 **isInterrupted() **判断线程的中断标志来退出循环。当使用 interrupt() 方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
3.使用 stop 方法强行终止线程(该方法已被废弃,使用它是极端不安全的)
二、线程安全问题
当多个线程同时共享同一个全局变量或静态变量,记性写的操作时,可能会发生数据冲突,造成线程安全问题。
同步机制
方式一:同步代码块
同步监视器可以是任意类的对象
实现 Runnable 的方式同步监视器可以用 this;继承 Thread 的方式同步监视器不能使用 this
继承 Thread 的方式在实现线程安全的问题上,共享数据必须 static 修饰保证只有一份,同步监视器不能使用 this,因为可能会创建多个对象。
synchronized (同步监视器/锁) {
// 操作共享数据的代码
}
方式二:同步方法
在实现 Runnable 的方式中,同步方法不用加 static,默认的同步监视器是 this
在继承 Thread 的方式中,同步方法必须加 static,共享数据也是 static,默认的同步监视器就是 (当前类.class)
权限修饰符 synchronized 返回值类型 方法名(形参列表){
// 操作共享数据的代码
}
常见问题
① 什么是静态同步函数,它使用什么锁?
使用 static 和 synchronized 关键字同时修饰的函数为静态同步函数。静态同步函数使用的锁是该函数所属字节码文件对象,即类.class。
② 同步代码块与同步函数区别?
同步代码使用自定义锁(明锁),同步函数使用this锁。
③什么是多线程中的死锁?
死锁指不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
使用 Lock 解决线程安全问题
从 JDK5.0 开始,Java提供了 java.util.concurrent.locks.Lock 接口,通过显式定义同步锁对象来实现同步。
ReentrantLock 重入锁,是实现 Lock 接口的一个类,也是在实际编程中使用频率很高的一个锁。
相比 synchronized 的由系统自动获取锁和释放锁,Lock 需要手动实现加锁和释放锁,因此也更加灵活。
class Thread implements Runnable{
private ReentrantLock lock =new ReentrantLock(true); // true 代表开启公平模式
@Override
public void run() {
try {
lock.lock();
//操作资源
} finally {
lock.unlock();
}
}
}
三、线程的通信
线程阻塞与唤醒
wait() // 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify() // 一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个。
notifyAll() // 一旦执行此方法,就会唤醒所有被wait的线程。
/*
说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
*/
【面试题】sleep() 和 wait() 的异同?
相同点:sleep() 和 wait() 都可以使得当前的线程进入阻塞状态。
不同点:
1.两个方法声明的位置不同:Thread 类中声明 sleep() , Object类中声明 wait()。
2.调用的要求不同:sleep() 可以在当前线程的任何场景下调用。 wait() 必须使用在同步代码块或同步方法中。
3.关于是否释放锁:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait() 会释放锁。
使用 Lock 后的线程通信
Condition 的功能类似于在传统的线程技术中的,Object.wait() 和 Object.notify() 的功能。
Condition.await() 类似于 wait。
Condition.signal() 类似于 notify。
class Thread implements Runnable{
private ReentrantLock lock = new ReentrantLock(true); // true 代表开启公平模式
private Condition condition = lock.newCondition(); // 创建 condition 对象
@Override
public void run() {
try {
lock.lock();
conditoin.signal(); // 唤醒线程
//操作资源
try {
condition.await(); // 阻塞线程
} catch (InterruptedException e1) {
e1.printStackTrace();
}
} finally {
lock.unlock();
}
}
}
四、JUC 工具类
ReentrantReadWriteLock
JAVA 的并发包提供了读写锁 ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。
CountDownLatch
CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞。其它线程调用countDown 方法会将计数器减 1 (调用 countDown 方法的线程不会阻塞),当计数器的值变为0时,因 await 方法阻塞的线程会被唤醒,继续执行。
CyclicBarrier
CyclicBarrier 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。线程进入屏障通过 CyclicBarrier 的 await() 方法。
Semaphore
acquire(获取) 当一个线程调用 acquire 操作时,获取信号量(信号量减1),如果当前信号量为0,则会一直等到有线程释放信号量,或超时。
release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
锁的划分
① 特质上分:共享锁和排他锁
② 用途上分: 读锁和写锁
③ 数据库: 表锁和行锁
④ 世界观划分: 悲观锁(真锁)和乐观锁(假锁)
才会继续干活。线程进入屏障通过 CyclicBarrier 的 await() 方法。
Semaphore
acquire(获取) 当一个线程调用 acquire 操作时,获取信号量(信号量减1),如果当前信号量为0,则会一直等到有线程释放信号量,或超时。
release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
锁的划分
① 特质上分:共享锁和排他锁
② 用途上分: 读锁和写锁
③ 数据库: 表锁和行锁
④ 世界观划分: 悲观锁(真锁)和乐观锁(假锁)
⑤ 是否明显: 显式锁和隐式锁
来源:CSDN
作者:马本不想再等了
链接:https://blog.csdn.net/qq_42180284/article/details/103944817