java 并发过关斩将
并发 是指什么呢? 从语文角度理解就是同一时间一起执行某些操作,那么在java语言的角度则可以认为是 (并发是指程序中多个任务在同一时间段内交替执行。虽然这些任务可能并不真正“同时”运行(尤其是在单核处理器上),但通过线程调度,它们看起来像是并行运行的)
多线程
java 中的并发 定然离不开多线程这个词语,那么我们需要搞清楚,线程是什么?他的内存形式是什么样的?由此引出
JMM(java 的内存模型)
java 中对解决多线程问题制定了一套规则也就是JMM ,解决了 可见性,原子性,有序性的问题
图结构(翻自 小林coding)

简单来说,jmm就是通过定义主内存(共享内存)和工作内存(线程自己的缓存),规定变量必须从主内存加载到工作内存才能操作,改完再写回主内存中,通过volatile(确保可见性,规定当数据修改必须立马写入主内存中,而不是再线程自己缓存中保存),synchronized(确保可见性,如其单词,就是添加一个锁,确保此时只能有占有锁资源的线程使用,确保了原子性)。
多线程的定义
那么什么是多线程?多线程就是指多个线程同步运行,这些线程拥有共享的内存空间,也就是上图的主内存,但是这些线程之间确负责不同的操作处理,执行不同的任务调度,那么他们就要对这些共享内存有一套合理的规则,否则会造成资源的抢占和程序的崩溃
线程安全与解决策略
线程安全就是指多个线程访问共享内存数据事,能够保证一致性和正确性,不会产生数据的竞争或者冲突,更不会因为抢占资源导致死锁,阻塞,能让程序一直进行
解决办法与策略:
1.使用 synchronized 确保原子性。确保同一时间只能有一个线程对共享资源进行修改
2.使用 vilatile 字段修饰,确保数据更改后及时写入共享内存,所有线程访问的结果都是一样的
3.继承 lock接口或者 ReentrantLock 接口,他们比synchronized 提供了更强大的锁定机制,有更高的性能
4.原子操作类:使用java 并发库中提供的原子操作类方法,或者数据库等,如redis 的 incr ,AtomicLong 等,他们本身符合原子性操作无需额外加锁
5.使用并发集合:ConcurrentHashMap ,CopyOnWirteArrayList 等,这些集合内部已经确保了容器安全
6.线程局部资源:每个线程可以有属于自己的本地资源,将变量变成线程自己的局部资源可以避免多线程的抢占
Java中如何创建多线程
说了这么多线程的安全解决策略,那么如何创建多线程呢?
1.继承 Thread 类:
优点:代码实现简单,只需要继承接口,然后实现 run 方法,通过创建类,然后进行 .strat(); 就可以启动线程
缺点:由于java 特性只能继承一个父类,那么此类就无法继承其他父类
2. 实现 runable 接口:
优点:类知识实现了接口,还可以继承其他类,实现其他接口,适合多个线程处理同一份资源的现象,可以将数据进行分离,符合java 面向对象的思想
缺点:代码编程较为复杂,如果需要访问当前线程,必须使用 Thread.currentThread() 方法
3. 实现callable 接口
优点:可以拥有返回值,执行线程后可以有返回结果,其余与 runable 一致
缺点:代码变成复杂,创建线程不能直接传入 callable 对象,而是需要实现包装进入 FeatureTask 从而加入线程 Thread 类中,进行线程的启动
4.使用线程池:
优点:可以重用预定设置的线程,避免了线程的频繁创建和开销,显著提高了程序的性能和减小了开销,可以自定义控制线程的数量,通过合理配置线程大小,可以最大 CPU 利用率和系统吞吐量
缺点:增加了程序的复杂度,当线程出现崩溃时,不好排查问题,不合理的配置会导致线程的死锁,资源过分消耗等问题
线程的六大状态
1. NEW :线程未启动,即创建了线程,但是还没有执行启动
2. RUNNAMBLE:就绪状态,调用了start(),正在执行
3. BLOCKED:等待监视器锁时,陷入阻塞状态
4. WAITING:等待状态的线程正在等待另外一线程执行特定的操作
5. TIMED_WAITING: 具有指定等待时间的等待状态
6. TERMINATED: 线程完成执行,终止状态
不同的线程如何通信?通信方式有哪些?
-
通过共享变量,访问共享变量来互相通信,也可能共享文件,需要注意数据竞争导致的数据不一致问题
-
同步机制:
-
synchronized:java 中 object 类中 有 wait() ,notify(),notifyAll(); 基于对象的监视器机制
-
wait():使线程进入等待状态,直到其他线程使用 notify() 或 notifyAll();
-
notify():唤醒在此对象监视器上的等待操作的单个线程
-
notifyAll(): 唤醒在此对象监视器上的等待操作的所有线程
-
-
2.lock 和 condition 接口:Lock 提供了比 synchronized 更为灵活的锁机制
1.await():使线程进入等待状态,直到其他线程使用 notify() 或 notifyAll()
2.signal():唤醒在此对象监视器上的等待操作的单个线程
3.signalALl():唤醒在此对象监视器上的等待操作的所有线程
3.volatile: volatile 关键可以保证变量的可见性,确保线程修改的共享数据可以及时写会共享变量,确保所有的线程访问的数据都是一样的
4.CountDownLatch: 同步辅助类,允许一个或多个线程等待其他线程完成操作
5.CycliBarrier:同步辅助类,允许一组线程相互等待,知道所有线程都到达某个公众屏障点
6.Semaphere: Seamphore 是计数信号量,可以控制同时访问特定资源的数量
java 中有哪些常用的锁?在什么场景使用?
1.内置锁(synchronized):java 内存中的自带锁 ,用于方法或者代码块,当线程进入synchronized代码快时,他会获取关联对象的锁,当线程离开代码快或者方法时,锁会被释放,如果当锁被一个线程拿走,其他线程也尝试获取这个锁就会发生堵塞,知道锁被释放会转成runable状态去竞争
2.ReentrantLock:比内置锁有更丰富高级的功能,如,锁等待,定时锁等待,公平锁选项等、ReentrantLock 通过lock() 和 unlock() 来占有锁和释放锁
3.读写锁(ReadWriteLock):允许多个线程读取,但是只能有一个写入者,适合读取多,写入少的场景,可以有搞并发性
4.乐观锁和悲观锁:悲观锁:通常是说访问数据前就锁定资源,假设了最坏的情况,即数据可能被其他线程修改,如内置锁,ReentrantLock,都是悲观锁 乐观锁:值不锁定资源,只在更新数据时检查数据是否已经被修改过,通常使用version 版本号或者时间戳来实现
5.自旋锁:这是一种机制,线程在进入等待阶段时,会持续循环的检查锁是否可用?而不是放弃cpu并且阻塞,通过 CAS机制来实现,短时间内可以提高性能,但是过度会影响资源和性能
CountDownLatch 的具体原理
让一个或多个线程等待其他线程完成后再进行线程的操作,核心就是通过一个Counter 计数器实现线程之间的协调,常用多任务的分阶段控制或主线程等待多个子线程就绪的场景
1.初始计数器:创建CountDownLatch 时指定一个初始计数值(N)
2.等待线程阻塞:调用 await()的线程会被阻塞,直到计数器为0
3.任务完成通知:其他线程完成任务后调用countDown(),使得计数器减1
4.唤醒等待线程:当计数器为0时,所有等待的线程会被唤醒
为什么非公平锁的吞吐量比公平锁大
公平锁执行过程中,需要将线程自己添加到等待队列的队尾并且休眠,等带线程释放锁时,会去唤醒等待队列的对首的线程去获取锁,是按照下锁的先后顺序来实现的,整个过程会经历运行状态 --- > 休眠状态----- > 运行状态,而每一次状态的改变都需要经历从用户态 ---> 内核态,转换比较耗时间
非公平锁,线程通过 cas 的方式获取锁,如果成功那么直接占有锁,如果失败才会进度等待队列,不用遵循先到先得的规矩,避免了锁的睡眠和恢复,减少用户态 -----> 内核态的转换
线程池的工作原理
线程池主要分成 核心线程池,线程池的最大容量,等待任务的队列,提交一个任务,如果核心线程池没满,就创建一个线程,满了,那就会进入等待队列中,如果等待队列也满了,那么就会,那么就会增加线程,如果到达最大线程数量,那么将会使用一些拒绝策略
流程图:

线程池的拒绝策略?
callerRunPolicy:使用线程调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的队列有空缺
AbortPolicy:直接抛出一个任务线程池拒绝的策略
DiscardPolicy:不做任何处理,静默拒绝提交的任务
DiscardOldestPolicy:抛弃最古老的任务,然后执行该任务
自定义拒绝策略:通过接口实现可以自定义拒绝任务策略
线程池的种类
ScheduledThreadPool:设置定期的执行任务,支持定时或者周期性任务
FixedThreadPool: 核心线程数和最大线程数一致,所以可以看成是固定线程数量的线程池
CacheThreadPool:可以称做可缓存线程池,它的特点在于线程数是几乎可以无限增加的,当线程闲置时可以对线程进行回收,线程数据会随着时间变化 ,内置一存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为 0.,实际不存储任何任务,只负责中转
singleThreadPool:使用唯一的线程去执行任务 ,原理和FixedThreadPool 一样,只不过线程只拥有一个,如果线程执行过程发生崩溃,线程池会创建一个新的线程去执行后续的任务,由于线程池只有一个线程,那么非常适合所有任务都需要按需提交的场景
为什么阿里云开发手册,需要手动去创建线程池
因为 上面的便捷式线程池,可能会导致资源耗尽,进而产生 OOM 的问题,实际开发场景应该根据自己的场景,并发情况去评估 线程池的几个参数应该怎么设置,从而达到最适合自己的效率也可以避免线程的过多消耗
线程池和多线程同步并发执行的优势
资源复用,降低开销:线程创建和销毁操作需要内核操作,开销较大。线程池会复用核心线程,避免频繁创建线程,尤其任务开销大时,减小资源的消耗
控制并发程度,避免资源耗尽:线程池可以配置最大线程数量和核心线程数据,避免高并发场景下创建过多的线程导致资源耗尽的可能
任务管理更加灵活:线程池支持任务排队,优先级调度,拒绝策略等,能应对任务峰值,更加多灵活的方式去控制并发程度
特有名词的解义与对比
sleep 和 wait 的区别
sleep 可以没有位置限制,可以在任何位置调用,无需拥有锁,调用后暂停执行的时间,也就是相当于睡一会,但是他不会释放自己的锁,所以它还是会占有cpu资源,然后sleep 到了规定的时间后会自己恢复成就绪状态执行
wait:只能在获取持有该对象锁之后的代码快或同步方法内使用,调用后,线程会释放其所占有的锁,进入等待状态,如果唤醒,需要 notify() 或者 notifyAll() 等线程使用其他方法进行唤醒
blocked 和waiting 的区别
blocked 状态通常是因为试图获取一个锁,但是由于竞争,没有争取到锁而自动进入的阻塞状态,等待锁可用时,blocked 就自己跳出blocked 状态去竞争锁,也就是整个状态转换都是自发进行的
waiting:这个状态是因为需要等待其他线程进行某些操作,这种状态下不会消耗cpu的资源,也不会参与锁的竞争,知道等待的线程完成操作,线程才能活跃
synchronized 和 reentrantLock 的区别
synchronized 可以用来修饰普通方法,代码快,静态方法,reentractLock只能在代码快
synchronized 会自己加锁,解锁,reentrantLock需要手动加锁,释放锁
~属于非公平锁,,~ 可以公平锁也可以非公平锁
什么是可重入锁
指的是一个线程获取到了锁之后,可以再次重复的获取该锁, 不会导致死锁或者其他问题,当线程持有该锁,再想获取该锁可以直接成功
AQS 是什么
AQS 是java 的一个抽象类,AQS用来构建锁,同步器,协作工具的工具类(框架)
核心思想,如果被请问的资源是处于空闲的,那么就把请求资源的线程设置为有效的工作线程,然后将资源变成锁定状态,如果被占用那么就需要一定的阻塞等待唤醒机制来唤醒,确保锁的分配,不会死锁。这个机制就是 CLH 队列的变体实现

核心三部分:state(状态),FIFO 队列(双向列表),协作工具(实现锁的占用,释放)等待的工具
CAS 是什么
CAS 是乐观锁机制,内部有三个操作数,内存位置(V),预期值(A),新值(B)。逻辑:如果内存位置V 的值等于预期值 A 的值,那么就将它更新 为B ,否则不做任何操作,整个做成符合原子性操作
CAS 的缺点
ABA 问题:是值CAS 更新中,当前读取到的是 A,然后准备赋值的时候任然是 A,但是实际情况下是 A 的值 可能被修改成了B,然后又修改成了A,CAS却会默认认为它没有修改。java中有AtomStampedReference 解决这个问题,他加入预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前标志是否等于预期标志(一般假如版本号)
循环时间长,开销大,自旋如果长时间不成功,会给CPU带来大消耗
只能保证一个共享变量的原子操做:如果有多个共享变量操作,不能保证原子性
什么是公平锁,什么是非公平锁
公平锁:严格遵循 FIFO(先来先得) 原则。线程按请求锁的顺序排队,新请求的线程必须加入等待队列末尾,只有轮到其位置时才能获取锁
特点:避免线程饥饿(Starvation),保证公平性;但性能开销较大
核心方法tryAcquire()中调用 hasQueuedPredecessors()`,检查当前线程是否是等待队列的首位
队列非空且当前线程不在队首,则获取锁失败,线程加入队列等待
非公平锁: 允许线程插队抢占锁。新线程可直接尝试获取锁(即使等待队列中有其他线程),成功则立即执行;失败才加入队列等待。
特点:吞吐量更高,但可能导致部分线程长时间无法获取锁(饥饿现象
直接通过 CAS(Compare-And-Swap) 尝试抢占锁,无视等待队列
优点:减少线程切换的开销,避免从运行态转到用户态
更多推荐


所有评论(0)