线程与进程的区别
线程是操作系统能够进行运算调度最小的单元,是进程的执行单元。线程是进程内部创建和调度的,共享进程的资源;进程具有独立的地址空间,而线程共享进程的地址空间。进程之间通信需要特殊的机制,如管道、消息队列,而线程之间通信可以通过共享内存直接访问。进程切换开销较大,涉及到地址空间的切换,而线程切换开销较小,因为线程共享地址空间和其他资源。
线程(Thread)是计算机程序中的执行单元,是进程中的一个实体。
创建线程通过
1.继承 Thread 类:创建一个类并继承 Thread 类,重写 run() 方法作为线程的执行逻辑,并通过调用 start() 方法来启动线程。
2.实现 Runnable 接口:创建一个类实现 Runnable 接口,实现 run() 方法,并通过创建 Thread 对象,将 Runnable 对象作为参数传递给 Thread 构造函数,并调用 start() 方法启动线程。
3.实现Callable接口:创建一个类实现Callable接口,重写call()方法
新建状态(New):
就绪状态(Runnable):
运行状态(Running):
阻塞状态(Blocked):
终止状态(Terminated):
线程的状态之间存在转换:
sleep():使当前线程休眠指定时间。
join():等待该线程执行完毕。
yield():暂停当前线程的执行,让出 CPU 资源。
interrupt():中断线程的执行。
isAlive():判断线程是否存活。
线程池可以降低线程生命周期的系统开销问题,加快响应速度;统筹内存和CPU的使用,避免资源使用不当;可以统一管理资源,创建线程的方式有很多种,我了解的有:1、固定大小的线程池,可控制并发的线程数,超出的线程会在工作队列中等待。2、带有缓存的线程池。3、可以执行延迟任务的线程池。
线程池的创建有七个参数分别是:核心线程数,最大线程数,工作队列,线程工厂,存活时间,存活时间单位,拒绝策略
线程池的工作原理:
接收到任务,首先判断一下核心线程是否已满,如果未满则创建一个新的线程执行任务,如果核心线程已满,工作队列未满,将线程存储到工作队列当中,等待核心线程获取执行;如果工作队列已满且线程数小于最大线程数,则创建一个新的线程去处理任务;如果线程数超过了最大线程数,按照四种拒绝策略处理任务,四种拒绝策略分别是:1.提交最早的线程自己去执行该任务,2.默认拒绝策略,会抛出异常,3.直接丢弃任务,没有任何异常抛出,4,丢弃最老任务,其实就是把最早进入的工作丢掉,然后把新任务加入到工作队列当中。
如何保证线程安全的
在多线程环境下,确保数据的一致性和避免竞态条件是非常重要的。以下是几种常用的方法和技术来实现这一目标:
使用同步机制:
使用原子操作类:
使用锁机制:
使用并发容器:
使用线程安全的类:
使用线程间的通信:
1. 锁的类型
锁分为乐观锁、悲观锁、synchronized
乐观锁是每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
悲观锁每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放
2.lock和synchronize的区别synchronized
都是解决线程安全的工具,synchronize是java中的同步关键字;而lock是J.U.C包中提供的接口
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
2.lock和synchronize的区别
这个问题我从四个方面来回答
第一个,从功能角度来看,lock和synchronize都是java中用来解决线程安全问题的一个工具,
第二个,从特性来看,首先synchronize是java中的同步关键字;而lock是J.U.C包中提供的接口而这个接口它有很多的实现类其中就包括reentrantLock这样一个重入锁的实现,其次synchronize可以通过两种方式去控制锁的力度,一种是把synchronize关键字修饰在方法层面,另一张种是修饰在代码块上,并且我们可以通过synchronize加锁对象的生命周期来控制锁的作用范围,比如锁对象是静态对象或者类对象那么这个锁就属于全局锁;如果锁对象是普通实例对象,那么这个锁的范围取决于这个实例的生命周期。lock中锁的力度是通过它里面提供的lock( )方法和unlock( )方法来决定的,包裹在两个方法之间的代码是可以保证线程安全的,而锁的作用域取决于lock实例的生命周期。
lock比synchronize的灵活性更高,lock可以自主的去决定什么时候加锁,什么时候释放锁,只需要调用lock()和unlock()方法就可以了。同时lock还提供了非阻塞的竞争锁的方法,叫trylock(),这个方法可以通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁,而synchronize由于是关键字所以他无法去实现非阻塞竞争锁的方法,另外synchronize锁的释放是被动的,就是当synchronize同步代码块执行结束以后或者代码出现异常的时候才会被释放。
最后lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时候如果已经有其他线程正在排队或者等待锁释放那么当前竞争锁的线程是无法插队的;而非公平锁就是不管是否有线程在排队等待锁他都会去尝试竞争一次锁,synchronize只提供了一种非公平锁的实现。
第三个,从性能方面来看,synchronize和lock在性能方面相差不大。在实现上会有一定的区别,synchronize引入了偏向锁,轻量级锁,重量级锁,以及锁升级的机制去实现锁的优化,而lock中用到了自旋锁的方式,去实现性能优化,以上就是我对这个问题的理解。
3.说一下死锁
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
产生死锁的原因(1) 因为系统资源不足。(2) 进程运行推进的顺序不合适。(3) 资源分配不当等。
怎样防止死锁
1、尽量使用try lock( )防范,设置超时时间,超时则关闭,防止死锁
2、使用安全类concurrent 代替自己手写锁
3、减少锁的使用粒度,避免几个功能共用一把锁
4、减少同步代码块
要证明 HashMap 不是线程安全的,可以通过以下步骤进行测试:
创建多个线程:创建多个并发线程,每个线程都尝试在同一个 HashMap 上执行并发读写操作。
并发写入操作:在每个线程中,进行并发写入操作,即向 HashMap 中添加新的键值对。
并发读取操作:同时在其他线程中进行并发读取操作,即从 HashMap 中获取键对应的值。
观察结果:观察在多线程环境下,是否出现以下情况:
如果在测试中出现了以上情况,即可得出结论:HashMap 不是线程安全的。
示例代码如下所示:
import java.util.HashMap;
public class HashMapThreadSafetyTest {
private static final int NUM_THREADS = 10;
private static final int NUM_OPERATIONS = 10000;
private static HashMap<Integer, Integer> hashMap = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
// 创建多个并发线程
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(new HashMapWriter());
threads[i].start();
}
// 等待所有线程执行完毕
for (int i = 0; i < NUM_THREADS; i++) {
threads[i].join();
}
// 输出 HashMap 的大小
System.out.println("HashMap size: " + hashMap.size());
}
static class HashMapWriter implements Runnable {
@Override
public void run() {
for (int i = 0; i < NUM_OPERATIONS; i++) {
// 并发写入操作
hashMap.put(i, i);
// 并发读取操作
for (int j = 0; j < NUM_OPERATIONS; j++) {
Integer value = hashMap.get(j);
if (value == null || !value.equals(j)) {
System.out.println("HashMap is not thread-safe");
return;
}
}
}
}
}
}
在上述示例中,我们创建了多个并发线程,在每个线程中进行并发写入和读取操作。如果出现了数据不一致的情况,即可得出结论:HashMap 不是线程安全的。
当线程池指定了核心线程数和最大线程数,并且有并发请求到达时,线程池的运作如下:
核心线程处理请求:如果当前线程池中的线程数量小于核心线程数,线程池会创建新的线程来处理请求,直到达到核心线程数。
任务队列存储请求:如果当前线程池中的线程数量已达到核心线程数,而且任务队列还有剩余容量,线程池会将新的请求任务放入任务队列中等待执行。
创建新的线程处理请求:如果当前线程池中的线程数量已达到核心线程数,并且任务队列已满,而且当前线程池中的线程数量还未达到最大线程数,线程池会创建新的线程来处理请求。
拒绝策略处理请求:如果当前线程池中的线程数量已达到最大线程数,并且任务队列已满,而且没有空闲线程可用,线程池会根据指定的拒绝策略来处理新的请求。常见的拒绝策略包括抛出异常、直接丢弃任务、丢弃队列中最早的任务、或者在调用者线程中执行任务等。
空闲线程回收:如果线程池中的线程数量超过核心线程数,并且有空闲线程,那么超过空闲线程存活时间的空闲线程将被终止,以减少资源消耗。
总结起来,线程池在接收到并发请求时,首先会创建核心线程来处理请求,然后将多余的请求放入任务队列中。如果任务队列已满,线程池会创建新的线程来处理请求,直到达到最大线程数。如果线程池中的线程数量已达到最大线程数并且没有空闲线程可用,根据指定的拒绝策略来处理新的请求。同时,空闲线程会根据存活时间进行回收,以减少资源消耗。这样可以在并发请求的情况下,合理利用线程池中的线程资源,并根据负载情况自动调整线程数量,以提高系统的并发处理能力。
线程池的创建参数可以控制线程池的大小、线程的存活时间、任务队列的容量等。常见的线程池创建参数包括以下几个:
核心线程数(corePoolSize):线程池中保留的核心线程数,即初始创建的线程数量。这些线程会一直存活,即使没有任务需要执行。
最大线程数(maximumPoolSize):线程池中允许创建的最大线程数。当任务数量超过核心线程数且任务队列已满时,线程池会创建新的线程,直到达到最大线程数。超过最大线程数的任务将按照线程池的拒绝策略进行处理。
空闲线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数,并且这些线程处于空闲状态时,空闲线程的存活时间。超过存活时间的空闲线程将被终止,以减少资源消耗。
任务队列(workQueue):用于存放等待执行的任务的队列。线程池中的线程会从任务队列中取出任务并执行。
线程工厂(threadFactory):用于创建线程的工厂类。可以自定义线程工厂来创建具有自定义属性的线程。
这些参数在线程池创建时通过构造函数或者对应的设置方法进行配置。在线程创建时,这些参数会影响线程池的行为:
这些参数的设置会影响线程池的行为,例如线程的数量、任务的排队等,以满足应用程序的需求。在运行时,线程池会根据这些参数动态调整线程的创建和销毁,以及任务的执行。