hello!小伙伴们大家好,依旧是你们熟悉的颜书凌,颜童鞋,大家现在好,非常感谢你们喜欢我的文章,我也会多多更新更优质的博客,话不多说,开始今天的硬货!
多线程
一、什么是线程
线程是系统能够调度的最小单元,在一个进程里面可以有多个线程,进程中负责程序执行的执行单元。一个进程中至少有一个线程。
二、线程和进程有什么区别?
一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。就比如说运行在我们电脑上的QQ,他是一个进程,但是里面是由很多线程在运行的。
三、如何在Java中实现线程?
创建线程有常用的种方式:
1、继承Thread类创建线程
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extend Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:
public class ThreadTest extends Thread{
@Override
public void run() {
System.out.println("我继承了Thread类实现创建线程");
super.run();
}
//new Thread( new ThreadTest()).start();
//另外一个类中
}
2、实现Runnable接口创建线程
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口,如下:
public class ThreadTest2 implements Runnable{
@Override
public void run() {
Thread thread = Thread.currentThread();
try {
System.out.println("模拟查sku详情数据");
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("当前线程Id是"+thread.getId());
}
//new Thread( new ThreadTest2()).start();
//另外一个类中
}
3、实现Callable接口通过FutureTask包装器来创建Thread线程
Callable接口(也只有一个方法)定义如下:
public class ThreadTest3 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1;
}
}
// try {
// Integer call = new ThreadTest3().call();
// System.out.println("线程三拿到的结果是:"+call);
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
4、使用线程池:
使用java.util.concurrent
包中的线程池框架,通过ExecutorService
来管理线程池,可以重用线程,并可以更好地控制线程的数量
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutorConfig threadPoolExecutorConfig = new ThreadPoolExecutorConfig();
ThreadPoolExecutor threadPoolExecutor = threadPoolExecutorConfig.threadPoolExecutor();
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取sku的基本信息,"+Thread.currentThread().getName());
return 1;
}, threadPoolExecutor);
Integer i = f1.get();
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取sku的图片信息获取,并且已经获取到任务2的spuID:" + i+"线程"+Thread.currentThread().getName());
return 2;
},threadPoolExecutor);
CompletableFuture<Integer> f3 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取spu的详情,并且已经获取到任务2的spuID:" + i+"线程"+Thread.currentThread().getName());
return 2;
},threadPoolExecutor);
CompletableFuture<Integer> f4 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取spu的规格参数信息,并且已经获取到任务2的spuID:" + i+"线程"+Thread.currentThread().getName());
return 2;
},threadPoolExecutor);
CompletableFuture<Integer> f5 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取spu的销售属性信息,并且已经获取到任务2的spuID:" + i+"线程"+Thread.currentThread().getName());
return 2;
},threadPoolExecutor);
CompletableFuture.allOf(f1,f2,f3,f4,f5);
System.out.println("等待完成查询返回数据");
}
}
四、多线程的特点:
解决多任务同时执行的需求,合理使用CPU资源。多线程的运行是根据CPU切换完成,如何切换由CPU决定,因此多线程运行具有不确定性。
异步,并行,高并发的场景都会跟线程有关系
五、多线程的原理:
多线程是一种并发编程的技术,它允许程序在同一个进程中同时执行多个独立的任务或操作。每个任务都由一个单独的线程来执行,而这些线程共享程序的资源和内存空间。
多线程的基本原理是通过将程序分成多个子任务,并创建对应数量的线程来同时执行这些子任务。每个线程都有自己的堆栈、寄存器和指令计数器等状态信息,可以独立地运行代码。不同线程之间可以进行通信和协调,通过锁、信号量、条件变量等机制来实现数据同步和互斥访问。
多线程在操作系统级别实现,通过操作系统提供的API(如POSIX标准中提供的pthread库)进行创建、管理和控制。在高级编程语言中也提供了相应的库或框架来支持多线程编程,如Java中的Thread类、C#中的Task类等
六、多线程的应用场景
- CPU密集型任务:需要进行大量计算或处理数据,占用大量CPU资源,例如图像、视频处理等。这种任务适合使用多线程技术,因为可以充分利用CPU资源并提高程序运行效率。
- I/O密集型任务:需要进行大量的输入输出操作,例如读取文件、网络通信等。这种任务相比CPU密集型任务更适合使用单线程或少量线程,因为在进行I/O操作时会阻塞CPU,此时如果开启过多线程反而会增加上下文切换的负担,导致程序运行效率变慢。
- 在GUI程序中,多线程的应用主要是为了提高用户体验和避免程序卡顿的问题。后台任务:当用户进行某些操作时,例如打开文件、导入数据等,这些操作可能需要耗费一定时间。如果在主线程中执行这些操作,则会导致GUI界面卡顿或无响应。因此可以使用一个后台线程来执行这些任务,使得主界面能够保持流畅。异步更新UI:当某个操作需要对UI进行更新时,例如下载进度条、播放音乐等,在主线程中更新UI可能会造成界面卡顿。因此可以使用一个单独的线程来进行UI更新,并通过回调机制将结果返回到主线程以更新UI。
- 多媒体处理:在图像编辑、视频剪辑等软件中,处理大量数据需要大量计算资源。使用多个线程分别处理不同部分的数据可以提高效率并且减少卡顿现象。
- 高并发服务器程序中多线程的应用。多线程的应用主要是为了提高服务器的并发处理能力和吞吐量。
- 在GUI程序中多线程的应用可以提高程序的效率和用户体验。如果是CPU密集型任务,则可以充分利用多核CPU的优势;如果是I/O密集型任务,则可以通过异步IO等方式来减少阻塞时间,并且避免过度使用多线程造成系统负荷过重。
七、多线程有三大特性:可见性,有序性,原子性
1、有序性:
从java源代码到最终实际执行的指令序列,会经历重排序(三重)——》》按照数据依赖关系指令重排(保证最终结果要一样)
源代码——>1、编译期优化重排序——>2、指令级并行重排序)——3、>内存系统重排序——>最终执行的指令序列
单线程环境栗确保程序最终执行结果和代码顺序执行结果相一致
处理器在进行重排序指令时是会考虑指令之间的数据依赖性
多线程环境中线程交替执行,结果无法预测
2、可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
3、原子性
一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响
多线程也有一定的缺点:
1、如果有大量的线程,会影响性能。
2、更多的线程需要更多的内存空间。
3、线程可能会给程序带来更多“bug”。
4、线程的中止需要考虑其对程序运行的影响。
5、通常块模型数据是在多个线程间共享的,需要考虑线程安全问题。
线程安全
一、什么是线程安全?
当多个线程访问某个类时,不管运行时环境采用何种类、调度方式或者这些线程将如何交替执行,并且在主要调用代码中不需要任何额外的同步和协同,这个类能表现出正确的行为,那么这个类就是线程安全的
那么怎么保证线程安全嘞
二、. 互斥同步(Synchronization)
互斥同步是通过锁来实现的,确保在同一时刻只有一个线程能够执行特定代码块。Java中使用synchronized
关键字来实现互斥同步。
public class SynchronizedExample {
private int counter = 0;
public synchronized void increment() {
counter++;
}
}
三. 使用原子类(Atomic Classes)
Java提供了一系列原子类,它们提供了一种简单、高效并且线程安全的方式来进行单一变量的操作。原子类的操作是不可分割的。
AtomicInteger的incrementAndGet方法是原子的,不会被其他线程中断,确保了线程安全。
public class AtomicExample {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet();
}
}
四、使用锁机制(Locking)
除了synchronized关键字外,Java还提供了ReentrantLock等锁类。锁提供了更灵活的同步控制。
ReentrantLock提供了显示的锁定和解锁过程,使得我们可以更加精确地控制线程的同步访问。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int counter = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}
}
五、 使用线程安全的集合类
Java提供了诸如ConcurrentHashMap、CopyOnWriteArrayList等线程安全的集合类,它们内部实现了线程安全。
ConcurrentHashMap使用了分段锁的机制,不同的段(Segment)上的操作是并发的,提高了并发性。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentMapExample {
private Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
public void addToMap(String key, int value) {
concurrentMap.put(key, value);
}
}
六、同步机制
采用Happens Before的第二、第三规则,实现顺序性、可见性、原子性
七、线程封闭
1、什么是线程封闭:
就是把对象封闭到一个线程里,只有这一个线程能够看到此对象。那么就算是这个对象不是线程安全的也不会出现不安全的问题,这就是线程封闭。
2、实现线程封闭的方法:
1.Ad-hoc线程封闭
这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现,是最糟糕的一种线程封闭。
2.栈封闭
栈封闭是我们最经常遇到的一种线程封闭。栈封闭就是指局部变量。多个线程访问同一个方法时,此方法的局部变量会被拷贝一份到我们的线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发的问题。所以能用局部变量就不要用全局变量,全局变量容易引起并发问题。
3.ThreadLocal类
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
3.1什么是threadlocal类
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
3.2ThreadLocal与Synchronized的区别
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本
,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
线程池
一、什么是线程池:
基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创
二、为什么要用线程池
- 降低资源消耗:线程池通过重复利用已创建的线程,减少了线程创建和销毁的开销,这有助于降低系统资源的消耗。
- 提高响应速度:线程池中的线程可以立即处理任务,无需等待新线程的创建,从而降低了任务的执行延迟,提高了系统的响应性能。
提高线程的可管理性:线程池允许对线程进行统一的分配、调优和监控,这有助于提高系统的稳定性和可扩展性。
- 控制并发度:通过限制同时执行的线程数量,线程池可以帮助避免资源耗尽的问题,从而控制并发度,避免系统过载和资源竞争。
提供任务队列:线程池通常提供一个任务队列,用于存储待执行的任务,这样可以有效地管理任务的调度和执行顺序。
提高性能:减少线程的创建和销毁开销,避免频繁地创建和销毁线程,从而提高程序的性能。
避免过度切换:如果不使用线程池,系统可能会创建大量同类线程,导致内存消耗完毕或出现“过度切换”的问题。
综上所述,使用线程池可以帮助提高系统的效率、稳定性和响应性,同时减少资源消耗和管理复杂性
三、如何实现线程的复用
- 继承重写Thread类,在其start方法中添加不断循环调用传递过来的Runnable对象
- 不让线程停止,任务执行完了挂起
- 建线程对象所带来的性能开销,节省了系统的资源。
四、线程池的7大核心参数
1、corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。
2、maximumPoolSize 线程池最大线程数量(非核心线程)
当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列(后面会介绍)中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
3、keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
4、unit 空闲线程存活时间单位
keepAliveTime的计量单位
5、workQueue 工作队列(阻塞队列)
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个
任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
6、threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
7、handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
- AbortPolicy。这是线程池的默认策略。当任务被拒绝时,它会抛出`RejectedExecutionException`异常。这种方式直接告知任务被拒绝,开发者可以根据业务逻辑决定是否重试或放弃提交新任务。
- DiscardPolicy。在这种策略下,当新任务被提交时,如果线程池无法处理,将被直接丢弃,不会有任何通知。这种策略适用于对任务执行不敏感的场景,因为它可能会导致数据丢失。
- DiscardOldestPolicy。如果线程池未关闭且无法执行新任务,则会丢弃任务队列中的最老任务(即存活时间最长的任务),然后可能尝试重新提交被拒绝的任务。这种策略旨在腾出空间给新提交的任务,但同样存在数据丢失的风险。
- CallerRunsPolicy。当有新任务提交且线程池无法执行时,该策略会让提交任务的线程直接执行该任务。这提供了最完善的处理方式,因为它避免了任务丢失,但可能会因为任务提交速度过快而导致程序阻塞。
每种策略都有其适用的场景和潜在问题。选择哪种策略取决于具体应用的需求和对任务执行可靠性的要求
五、线程池的使用
1、我们先自定义一个线程池(线程池参数如何设置后续我会再发一个博文,这里就不具体解释了)
@Configuration
public class ThreadPoolExecutorConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(){
//获取本机的cpu核数
int coreSize = 3*Runtime.getRuntime().availableProcessors();
//最大线程数
int maxSize = coreSize+30;
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(coreSize, maxSize, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100));
return poolExecutor;
}
}
2、代码实现
ThreadPoolExecutorConfig threadPoolExecutorConfig = new ThreadPoolExecutorConfig();
ThreadPoolExecutor threadPoolExecutor = threadPoolExecutorConfig.threadPoolExecutor();
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取sku的基本信息,"+Thread.currentThread().getName());
return 1;
}, threadPoolExecutor);
Integer i = f1.get();
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取sku的图片信息获取,并且已经获取到任务2的spuID:" + i+"线程"+Thread.currentThread().getName());
return 2;
},threadPoolExecutor);
CompletableFuture<Integer> f3 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取spu的详情,并且已经获取到任务2的spuID:" + i+"线程"+Thread.currentThread().getName());
return 2;
},threadPoolExecutor);
CompletableFuture<Integer> f4 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取spu的规格参数信息,并且已经获取到任务2的spuID:" + i+"线程"+Thread.currentThread().getName());
return 2;
},threadPoolExecutor);
CompletableFuture<Integer> f5 = CompletableFuture.supplyAsync(() -> {
System.out.println("获取spu的销售属性信息,并且已经获取到任务2的spuID:" + i+"线程"+Thread.currentThread().getName());
return 2;
},threadPoolExecutor);
CompletableFuture.allOf(f1,f2,f3,f4,f5);
System.out.println("等待完成查询返回数据");
}
到这里就结束了,这篇文章一万多个字,希望大家认真观看并学习,可以给凌弟一个暴击三连吗,你们的支持就是凌弟最大的动力,跪谢!!