目录
一、线程、进程、程序
二、线程状态
三、线程的七大参数
四、线程有什么优缺点?
五、start 和 run 方法有什么区别?
六、wait 和 sleep的区别?
七、lock与synchronized的区别
八、Volatile关键字是线程安全的吗?底层原理是什么?
九、synchronized作用和底层原理?
十一、ThreadLocal是线程安全的吗?底层原理是什么?会存在内存泄露吗?
十二、HashMap和ConcurrentHashMap有什么区别?
十三、HashMap和HashTable有什么区别?
进程: 我们把运行中的程序叫做进程,每个进程都会占用内存与CPU资源,进程与进程之间互相独立.
线程: 线程就是进程中的一个执行单元,负责当前进程中程序的执行。一个进程可以包含多个线程。多线程可以提高程序的并行运行效率。
程序:是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
1.新建(New):创建线程对象时
2.就绪(Runnable):线程调用start方法,有执行资格没有执行权
3.运行:当就绪状态时抢到cpu的执行权之后,进入运行状态
4.阻塞(Blocked):当获取锁失败后,进入阻塞状态
5.等待(Waiting):等待被notify()方法唤醒
6.休眠(sleep):休眠一段时间,时间到了之后,进入就绪状态。
7.终止(Terminated):线程死亡
1.核心线程数(corePoolSize):表示保持活动状态的最小线程数。
2.最大线程数(maximumPoolSize):表示允许创建的最大线程数。
3.空闲时间(keepAliveTime):表示当线程数量超过核心线程数时,多余的空闲线程能够保持存活的时间。
4.阻塞队列(workQueue):表示用于存储等待执行的任务的阻塞队列。
5.单位(unit):表示空闲时间的单位,例如毫秒、秒等。
6.拒绝策略(rejectedExecutionHandler):表示当任务无法提交给线程池执行时采取的策略。
当线程任务进来时,首先尝试创建核心线程进行执行。如果线程池中的线程数量已经达到了核心线程数,
并且阻塞队列也已经满了,那么就会尝试创建新的线程进行执行。
如果线程池中的线程数量已经达到了最大线程数,并且阻塞队列也已经满了,那么就会执行拒绝策略。
示例:假设线程池的核心线程数为 10,最大线程数为 20,阻塞队列容量为 50,
则当有 100 个任务进入线程池时,会有 10 个核心线程立刻执行任务,50 个任务进入阻塞队列,
再有 10 个线程被创建并立即执行剩余的任务。最后还剩下 30 个任务无法执行,就会被拒绝执行。
优点:
1. 在多核CPU中,通过并行计算提高程序性能. 比如一个方法的执行比较耗时,现在把这个方法逻辑拆分,分为若干个线程并发执行,提高程序效率。
2. 可以解决网络等待、io响应导致的耗时问题。
3. 提高CPU的使用率.提高网络资源的利用率
缺点:
1. 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
2. 线程之间对共享资源的访问会造成资源安全问题;
3. 多线程存在上下文切换问题CPU 通过时间片分配算法来循环执行任务,所以本身就会占用cpu资源
调用start方法方可启动线程,而run方法只是thread类中的一个普通方法调用,还是在主线程里执行。
相同点:wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。
不同点:
sleep是Thread的静态方法,而 wait()都是 Object 的成员方法,每个对象都有。
sleep方法是暂停当前线程,相当于休眠一段时间,之后会自动唤醒,而wait()必须被notify 或者notifyall方法唤醒,不然会一直阻塞。
wait方法的调用必须先获取wait对象的锁,而sleep则无此限制wait方法执行后会释放对象锁
允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用);如果在 synchronized代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)。
语法层面:
1.synchronized是关键字,源码在jvm中,用c++ 语言实现
2..Lock 是接口,源码由jdk 提供,用java语言实现
3.使用 synchronized 时,退出同步代码块锁会由jvm自动释放,而使用Lock时,需要手动调用unlock方法释放锁,否则可能会导致死锁等问题。
功能层面
1. Lock 提供了更多的锁机制选择,例如公平锁、非公平锁、可重入锁、读写锁等多种类型;而 synchronized 只有一种类型,即独占锁(排他锁)。
2. Lock 应该优先考虑在高并发场景下使用,可以获得更好的性能和灵活性;而 synchronized 关键字则更适合用于简单的线程同步场景,易于使用和维护。
什么是重排序?
当一个变量被修改后,如果该变量的值没有被写入到主内存中,其他线程可能会读取到该变量的旧值,导致程序出错。
并发编程中有3大重要特性:
原子性:
一个操作或者多个操作,要么全部执行成功,要么全部执行失败。满足原子性的操作,中途不可被
中断。
可见性:
多个线程共同访问共享变量时,某个线程修改了此变量,其他线程能立即看到修改后的值。
有序性:
程序执行的顺序按照代码的先后顺序执行。(由于JMM模型中允许编译器和处理器为了效率,进行指令重排序的优化。指令重排序在单线程内表现为串行语义,在多线程中会表现为无序。那么多线程并发编程中,就要考虑如何在多线程环境下可以允许部分指令重排,又要保证有序性)
那么volatile关键字是如何保证可见性和有序性呢?
保证可见性:当一个变量被volatile修饰后,JVM会把工作内存中的最新变量值强制刷新到主内存中,会导致其他线程中的缓存失效。这样,其他线程使用缓存时,发现本地工作内存中此变量失效,便会从主内存中获取,这样获取到的值就是最新的值,实现了线程可见性。
线程:
线程是程序执行的最小单位,多个线程可以同时执行。在Java中,每个线程都有自己的工作内存
工作内存:
每个线程都有自己的工作内存,也称为线程的本地内存。工作内存是线程私有的,用于存储线程执行过程中的栈帧、局部变量等信息
主内存:
主内存是所有线程共享的内存区域。主内存中存储着所有的变量,包括静态变量、实例变量等。主内存是线程间进行数据交互的存储区域。
保证有序性:通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序的,从而保证了有序性。(屏蔽在多线程环境下CPU的指令重排)
总之,Volatile关键字可以保证可见性和有序性,但不保证原子性,因此volatile不是线程安全的。
1.Java关键字synchronized是用于实现多线程同步的机制,确保多个线程在访问共享资源时不会产生竞争和冲突,从而避免数据不一致的情况。其主要原理是基于Java对象的内部锁,即监视器锁(Monitor Lock),确保在同一时刻只有一个线程可以访问被保护的代码块或方法。
2.当一个线程尝试获取被synchronized关键字保护的资源时,如果该资源已被其他线程占用,该线程就会进入阻塞等待状态。当占用资源的线程释放该资源时,等待队列中的线程会竞争获取该资源,并且只有一个线程会成功获取到该资源,其他线程继续等待。
3.synchronized关键字保证了可见性和原子性,可见性是通过JVM底层的内存屏障来实现的,原子性则是通过监视器锁的互斥性来实现的。
在synchronized块内,线程获得了锁,它将会清空工作内存,从而使得该线程使用的变量能够从主内存中重新读取,同时也会把工作内存中的变量写回到主内存中。这样,其他线程就可以读取到最新的值,从而保证了可见性。
弱引用:只要垃圾回收机制一运行,不管 JVM 内存空间是否充足,都会回收该对象占用的内存。
ThreadLocal:为共享变量在每个线程中创建一个副本,每个线程都可以访问自己内部的副本变量。通过 threadlocal 保证线程的安全性。
在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap(其类似于 Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap 中元素的 key 为当前ThreadLocal 对象,而 value 对应线程的变量副本。ThreadLocal 本身并不存储值,它只是作为一个 key 保存到 ThreadLocalMap中,但是这里要注意的是它作为一个 key 用的是弱引用,因为没有强引用链,弱引用在 GC的时候可能会被回收。这样就会在 ThreadLocalMap 中存在一些key为null的键值对(Entry)。因为 key 变成 null 了,我们是没法访问这些 Entry 的,但是这些 Entry 本身是不会被清除的。如果没有手动删除对应 key 就会导致这块内存即不会回收也无法访问,也就是内存泄漏。使用完 ThreadLocal之后,记得调用 remove方法。
1.每个Thread维护一个ThreadLocalMap,这个ThreadLocalMap的key是ThreadLocal实例本身,value才是真正要存储的值Object
2.每个Thread线程内部都有一个ThreadLocalMap,Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
3.Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
4.对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
注意:在不使用线程池的前提下,即使不调用 remove 方法,因此是ThreadLocal是弱引用,线程的"变量副本"也会被gc回收,即不会造成内存泄漏的情况。
首先HashMap和ConcurrentHashMap都是Map接口的实现类。可以从以下三个方面分析它们的区别:
线程安全方面:由于HashMap没有加锁机制,线程不安全;ConcurrentHashMap内部加了分段锁,线程安全。
并发性能:由于ConcurrentHashMap采用了分段锁机制,因此可以支持多个线程同时进行读写操作,并且在高并发的场景下性能更好。而HashMap默认没有加锁,需要手动加锁,如果多个线程同时修改同一个HashMap对象,可以会出现锁竞争条件和死锁问题。
底层数据结构不同:HashMap底层使用数组+链表+红黑树,而ConcurrentHashMap底层使用分段锁(segment)机制实现。
ConcurrentHashMap底层分段锁原理:
JDK1.7的时候:ConcurrentHashMap为保证线程安全,使用的是分段锁机制--->首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当线程占用锁访问的数据时,其他段数据也能访问。但是,这种方式在高并发场景下仍然存在性能瓶颈,因为多个线程仍然需要竞争同一个锁。
JDK1.8之后:为提高效率,放弃了分段锁的设计,取代的是Node+CAS+Synchronized保证并发安全实现,ConcurrentHashMap将底层数据结构分成了多个小的桶(Segment),每个桶都维护了一个独立的哈希表,不同的线程可以同时访问不同的桶,从而避免了多个线程竞争同一个锁的情况,提高了并发性能和效率。syschronized只锁当前链表或红黑树的首节点,只要hash不冲突,效率就提高了。
1、线程安全性:
HashTable是线程安全的,HashMap是非线程安全的。因为ashTable的核心方法都加了synchronized同步锁,hashMap没有加锁。
2、null值的处理:
HashMap允许键和值为空(null),它可以存储null值以及使用null作为键值对。而HashTable不允许键或值为空(null),如果尝试存储Null值或使用null作为键,则会抛出NullPointerException异常。
3、遍历方式:
HashMap可以双向遍历,并可以进行删除操作;而HashTable只能单向遍历集合元素,不能进行删除操作。
4、初始容量和扩容机制:
Hashtable默认的初始容量是11,每次扩容时容量变为原来的两倍加一
HashMap默认的初始容量是16,每次扩容时容量变为原来的两倍