线程的创建方式:
- 继承Thread类:
- 最直接的方式,重写Thread类的run()方法
- run()方法中不定义任何逻辑,需要重写来进行定义具体的业务逻辑
- 实例化该类后,通过start()启动线程【start()方法内部会自动执行run()方法】
- 缺点:会导致不能继承其他的类,限制性较大。
- 最直接的方式,重写Thread类的run()方法
class MyThread extends Thread{
@Override
public void run(){
具体逻辑.........
}
}
public static void main(){
MyThread myThread = new MyThread();
myThread.start();
}
- 实现Runnable接口:
- 大体与Thread类的功能差不多,但是是通过接口实现的方式。
class MyThread implements Runnable{
@Overrid
public void run(){
具体逻辑......
}
}
public static void main(){
Thread t=new Thread(new MyRunnable);
t.start();
}
- 实现Callable接口
- 可以有返回值,并且可以多线程处理一份资源
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 线程执行的代码, 这里返回一个整型结果
return 1;
}
}
public static void main(String[] args) {
MyCallable task = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(task);//将callable类包装进FutureTask
Thread t = new Thread(futureTask);
t.start();
try {
Integer result = futureTask.get(); // 获取线程执行结果
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
- 使用线程池(Excutors):
- 一种规范管理多线程的方式,避免了线程创建和销毁的开销
Class Task implement Runnable{
@Override
public void run(){
具体逻辑.............
}
}
public static void main(){
ExecutorService executor= new Executors.newFixedThreadPool(10)//设置线程池大小:10
for(int i;i<10;i++){
executor.submit(new Task());
}
executor.shutdown;
}
如何终止线程:
- 使用sleep()休眠线程,然后调用interrupt方法使线程标记为中断状态,在线程提交完此次任务后自我了断
- 使用stop()暴力停止:但是会有很大不好的后果,已被废弃
- 使用interrupt方法标记为中断状态,在run方法中判断线程状态,如果是中断则return或手动抛出异常
线程的状态:
- New:刚刚被创建,还未start(),初始状态
- Runnable:调用了start(),正在运行状态
- Blocked:正在等待锁,阻塞状态
- Waitting:正在等待其他线程完成指定动作,等待状态
- Timed_waitting:人为设置了等待时间的等待状态W
- Terminated:线程完成任务,终止状态
Sleep和Wait的区别:
- 锁处理:Sleep不释放锁,抱着锁睡觉(但会释放CPU)。而wait方法会释放锁
- 使用位置:sleep可以在任何位置使用,而wait必须在synchronzed代码块/方法中使用
- 唤醒条件:sleep睡醒后就苏醒,而wait必须要等待notify()来唤醒
- 用途:sleep用于暂停线程,而wait用于线程间的通信与协作
- Wait和Blocked的区别:
- Wait是主动暂停,用于与其他线程的写作。而Blocked是抢锁失败,进入阻塞
- Wait需要显式的唤醒,而Blocked会主动去抢锁
线程间的通信方式:
- wait()、notify()、notifyAll():
- 最基础的通信方式
- Lock()和Condition接口:
- 提供了一种比synchronzed更轻量灵活的锁机制
- volatile关键字:
- 用volatile关键字修饰的变量再被更改后会立即通知其他线程,保证变量的可见性
- Semaphore信号量:一个计数的信号量,可以控制特定资源的访问
优雅的停止线程的方式:
不应该暴力的停止(如stop)。而是应该通过逻辑控制停止
- 通过共享标志变量来控制:
- 用volatile关键字修饰一个变量,当工作线程检测到为false时停止(设置中断标志)
- 直接使用线程中断机制:
- 通过Tread.interrupt()来标识中断标志,当线程执行完当前任务则会自动中断。
如何保证线程安全:
- synchronized关键字:同步代码块或方法,确保同一时间只能有一个线程进入代码块
- volatile关键字:作用于变量,确保所有线程都能及时看到该变量的值
- 使用ReentrantLock可重入锁:实现Lock接口。比synchronized更加强大的锁机制,就是那种需要fianlly解锁的那种
- 使用JUC提供的原子类:如AtomicInterger
- 使用并发安全的集合:如ConcurrentHashMapper;CopyOnWriteArryayList;ConcurrentLinkedQueue等
Java中常用的锁:
- Synchronized内置锁:
- 是一种可重入锁,公平锁
- 当没有其他线程竞争时,会使用无锁、偏向锁、轻量级锁、重量级锁
- ReentrantLock可重入锁:
- 默认非公平锁
- 比起Synchronized更加灵活功能高级,如定时锁等待,是否公平
- ReadWriteLock读写锁:
- 允许多个读取者但是只允许一个写入者,用于读远多于写的情况
- 自旋锁(思想):线程在等待锁时会持续循环检查锁是否可用,通常用于锁等待时间很短的情况,通常用CAS替代
- 锁的公平性:
- 公平:按先后顺序来
- 不公平:按线程获取锁的CAS来
- 非公平锁的吞吐量更大:如果直接抢到锁就不需要睡觉了,线程的状态来回转换很耗时耗性能
synchronized的四种量级:
- 无锁:没有任何线程持有锁
- 偏向锁:在锁内部维护一个线程ID,如果该线程再次获取锁则直接进入。当有线程竞争锁时升级为轻量级锁
- 轻量级锁:当线程轻微竞争时,其他线程需要自旋来等待锁的释放,自旋不断尝试获取锁,占用CPU
- 后期Java废弃了偏向锁,因为现代应用持续的并发都比较高,偏向锁的撤销反而浪费性能
- 重量级锁:当线程激烈竞争时,其他线程获取不到锁会直接进入阻塞状态。
ThreadLocal:
在Thread内部维护了一个ThreadLocalMap字段,以map的形式存储数据,以做到内部数据不污染Thread本身,具体内部存储什么数据由开发者定义。
比如在Thread Local的工具类中创建:
private static ThreadLocal<Long> userIdHolder = new ThreadLocal<>();
会创建一个userIdHolder的Key,而具体的value通过userIdHolder.set设置
若再创建一个private static ThreadLocal<Long> userNameHolder = new ThreadLocal<>();
同理会在ThreadLocalMap内部创建一个userNameHold的Key,以为当前线程以一种解耦的方式赋成员变量
注意:key是弱引用,而value是强引用,所以当业务逻辑走完时必须要remove清理强引用value,防止OOM
形成死锁的四个条件:
- 互斥:首先此份资源不允许多个线程使用
- 持有并等待:占着茅坑不拉屎,线程A持有了资源1,后续需要资源2,但是资源2被线程BNTR了,线程1死等而不释放资源1
- 不可剥夺:线程持有了此资源,必须完成任务才肯释放资源
- 环路等待:两个线程获取资源的顺序形成了环形。
线程池:
主要是为了统一管理多线程,并且减少频繁的线程创建和销毁的性能浪费。
根据阿里巴巴Java开发手册,只允许使用ThreadPoolExecutore,可以通过参数配置来实现其他线程池的功能。
- Executores:最简单便捷的线程池,但是默认核心线程数无界,可能会导致线程过多而OOM,所以更加推荐使用ThreadPoolExecutor手动配置
- ThreadPoolExecutore:需要指定7个核心线程参数
- 核心线程数:IO密集型任务一般选择CPU核心数的两倍
- 最大线程数:只有当任务队列爆满才会扩容线程数
- 存活时间:当线程数>核心线程数,空闲时间大于存活时间的线程会被销毁
- 任务队列:用于指定任务队列
- 拒绝策略:
- 直接抛异常:一种“快速失败”的思想,可用于如验证码、抢单场景
- 调用者执行(CallerRunsPolicy):让提交任务的线程自己执行,可能会堵塞主线程,不推荐
- DiscardPolicy:直接丢弃新任务
- DiscardOldestPolicy:直接丢弃最老任务,与上一个都是用于容忍度较高的场景,比如点赞。
- ScheduledThreadPool:支持定时或周期性的执行任务
- FixedThreadPool:无最大线程数,线程数固定,因为默认使用LinkedBlockQueue的无界队列而不便于修改,被许多公司禁止使用
- CacheThreadPool:无最大线程数,可能会导致OOM,被禁止使用
- SingleThreadPool:只有单个线程,当线程阻塞时才会新建线程,适合低并发且强调任务提交顺序
任务队列:
- LinkedBlockQueue(链表阻塞队列):
- 默认无界,吞吐量大,适合IO密集型任务
- 内部使用了两把锁,允许并行出入队列
- 链表节点的创建消除对内存有压力
- ArrayBlockQueue(数组阻塞队列):
- 必须指定大小,因此内存稳定,避免OOM
- 内部只有一把锁,出入对互斥。因此吞吐量更低
- 继承了数组的特性,所以无法动态扩容
- SynchronousQueue(同步移交队列):
- 不存储元素,每一个任务必须等待消费,否则会阻塞生产者
- 适合任务量大但处理极速的场景,吞吐量大
- PriorityBlockingQueue(优先级队列):
- 无界,支持按照优先级排列任务
- 适合VIP用户等场景
- DelayQueue(延迟队列):
- 当指定延迟时间到了才允许进行处理
- 用于订单超时未支付等场景
任务队列爆满的应对措施:
- 查找根因,是代码问题还是SQL问题,查询慢SQL
- 重写扩容线程逻辑:默认的逻辑是核心线程繁忙会将任务丢尽阻塞队列。当阻塞队列满了才会去根据最大线程数扩大线程数量。可以参考Tomcat的做法重写逻辑,当核心线程繁忙后立即扩大线程数
- 自定义拒绝策略:参考开源方法,将任务丢入消息队列中或持久化到数据库,后续再捞出来做消息补偿。而非选择常用的CallerRunsPolicy,这可能会导致主线程繁忙甚至是Tomcat直接挂掉
线程池种类:
- Executor



