========================================================================
在Object类中有一些函数可以用于线程的等待与通知。
-
wait():当一个线程调用一个共享变量的 wait()方法时, 该调用线程会被阻塞挂起, 到发生下面几件事情之一才返回 :(1) 线程调用了该共享对象 notify()或者 notifyAll()方法;(2)其他线程调用了该线程 interrupt() 方法,该线程抛出InterruptedException异常返回。
-
wait(long timeout) :该方法相 wait() 方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的 timeout ms时间内被其它线程调用该共享变量的notify()或者 notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。
-
wait(long timeout, int nanos),其内部调用的是 wait(long timout)函数。
上面是线程等待的方法,而唤醒线程主要是下面两个方法:
-
notify() : 一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。 一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
-
notifyAll() :不同于在共享变量上调用 notify() 函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。
如果有这样的场景,需要等待某几件事情完成后才能继续往下执行,比如多个线程加载资源,需要等待多个线程全部加载完毕再汇总处理。Thread类中有一个join方法可实现。
Thread类中有一个静态态的 sleep 方法,当一个个执行中的线程调用了Thread 的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,获取到 CPU 资源后就可以继续运行。
Thread 有一个静态 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU 使用,但是线程调度器可以无条件忽略这个暗示。
当一个线程调用 yield 方法时, 当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 行权。
Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
-
void interrupt() :中断线程,例如,当线程A运行时,线程B可以调用钱程interrupt() 方法来设置线程的中断标志为 true 并立即返回。设置标志仅仅是设置标志, 线程A实际并没有被中断, 会继续往下执行。如果线程A因为调用了wait() 系列函数、 join 方法或者 sleep 方法阻塞挂起,这时候若线程 B调用线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
-
boolean isInterrupted() 方法: 检测当前线程是否被中断。
-
boolean interrupted() 方法: 检测当前线程是否被中断,与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。
========================================================================
上面整理了线程的创建方式和一些常用方法,可以用线程的生命周期把这些方法串联起来。
在Java中,线程共有六种状态:
| 状态 | 说明 |
| — | — |
| NEW | 初始状态:线程被创建,但还没有调用start()方法 |
| RUNNABLE | 运行状态:Java线程将操作系统中的就绪和运行两种状态笼统的称作“运行” |
| BLOCKED | 阻塞状态:表示线程阻塞于锁 |
| WAITING | 等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
| TIME_WAITING | 超时等待状态:该状态不同于 WAITIND,它是可以在指定的时间自行返回的 |
| TERMINATED | 终止状态:表示当前线程已经执行完毕 |
线程在自身的生命周期中, 并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变化如图示:
===========================================================================
使用多线程的目的是为了充分利用CPU,但要认识到,每个CPU同一时刻只能被一个线程使用。
为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换。
========================================================================
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
那么为什么会产生死锁呢? 死锁的产生必须具备以下四个条件:
-
互斥条件:指线程对己经获取到的资源进行它性使用,即该资源同时只由一个线程占用。如果此时还有其它线程请求获取获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
-
请求并持有条件:指一个 线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其它线程占有,所以当前线程会被阻塞,但阻塞 的同时并不释放自己已经获取的资源。
-
不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后才由自己释放该资源。
-
环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合 {T0,T1,T2,…… ,Tn} 中 T0 正在等待一 T1 占用的资源,Tl1正在等待 T2用的资源,…… Tn 在等待己被 T0占用的资源。
该如何避免死锁呢?答案是至少破坏死锁发生的一个条件。
其中,互斥这个条件我们没有办法破坏,因为用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?
-
对于“请求并持有”这个条件,可以一次性请求所有的资源。
-
对于“不可剥夺”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
-
对于“环路等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。
========================================================================
Java中的线程分为两类,分别为 **daemon 线程(守护线程)**和 user 线程(用户线程)。
在JVM 启动时会调用 main 函数,main函数所在的钱程就是一个用户线程。其实在 JVM 内部同时还启动了很多守护线程, 比如垃圾回收线程。
那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程束时, JVM会正常退出,而不管当前是否存在守护线程,也就是说守护线程是否结束并不影响 JVM退出。换而言之,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
===============================================================================
ThreadLocal是JDK 包提供的,它提供了线程本地变量,也就是如果你创建了ThreadLocal ,那么访问这个变量的每个线程都会有这个变量的一个本地副本,当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建 ThreadLocal 变量后,每个线程都会复制 到自己的本地内存。
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
下面来看一个ThreadLocal的使用实例:
public class ThreadLocalTest {
//创建ThreadLocal变量
static ThreadLocal localVar = new ThreadLocal();
//打印函数
static void print(String str) {
//打印当前线程本地内存中localVar变量值
System.out.println(str + “:” + localVar.get());
//清除前线程本地内存中localVar变量值
//localVar.remove();
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
public void run() {
//设置线程1中本地变量localVal的值
localVar.set(“线程1的值”);
//调用打印函数
print(“线程1”);
//打印本地变量的值
System.out.println(“线程1打印本地变量后:” + localVar.get());
}
});
Thread thread2 = new Thread(new Runnable() {
public void run() {
//设置线程2中本地变量localVal的值
localVar.set(“线程2的值”);
//调用打印函数
print(“线程2”);
//打印本地变量的值
System.out.println(“线程2打印本地变量后:” + localVar.get());
}
});
thread1.start();
thread2.start();
}
}
============================================================================
在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享 。
Java线程之间的通信由Java内存模型控制,Java内存模型决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看,Java内存模型定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是Java内存模型的 一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
Java内存模型的抽象示意如图:
在实际实现中线程的工作内存如下图:
=================================================================================
synchronized 块是 Java 提供的一种原子性内置锁, Java中的每个对象都可以把它当作同步锁来使用,这些 Java内置的使用者看不到的锁被称为内部锁,也作监视器锁。
线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块 被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块调用了该内置锁资源 wait系列方法时释放该内置锁。内置锁是排它锁,就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。
synchronized 的内存语义:这个内存语义就可以解决共享变量内存可见性问题,进入synchronized 块的内存语义是把在synchronized 块内使用到的变量从线程的工作内存中清除,这样在 synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。 退出 synchronized 块的内存语义是把在 synchronized 块内对共享变修改刷新到主内存。
=============================================================================
上面介绍了使用锁的方式可以解决共享内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销,对于解决内存可见性问题, Java 还提供了volatile种弱形式的同步,也就是使用 volatile 关键字, 该关键字可以确保对一个变量的更新对其他线程马上可见。
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存,当其它线程读取该共享变量,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
volatile虽然提供了可见性保证,但并不保证操作的原子性。
=================================================================================
所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。
例如在设计计数器一般都先读取当前值,然后+1,再更新。这个过程是读-改-写的过程,如果不能保证这个过程是原子性的,那么就会出现线程安问题。
那么如何才能保证多个操作的原子性呢?最简单的方法就是使用 synchronized 关键字进行同步。还可以用CAS操作。从Java 1.5开始,JDK的并发包里也提供了一些类来支持原子操作。
synchronized 是独占锁,没有获取内部锁的线程会被阻塞掉,大大降级了并发性。
===================================================================================
在Java中, 锁在并发处理中占据了一席之地,但是使用锁有有个不好的地方,就是当线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。
Java 提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是 volatile 只能保 共享变量可见性,不能解决读-改-写等的原子性问题。
CAS即 Compre and Swap ,其是 JDK 提供的非阻塞原子性操作,它通过硬件保证了比较-更新
操作的原子性。JDK 里面的 Unsafe 类提供了一系列的compareAndSwap *方法,以 compareAndSwapLong 方法为例,看一下什么是CAS操作。
- boolean compareAndSwapLong(Object obj,long valueOffset,long expect, long update ): CAS 有四个操作数,分别为对象内存位置、 对象中 变量的偏移量、变量预期值和新的值 。其操作含义是:只有当对象 obj 中内存偏移量为 valueOffset 的变量预期值为 expect 的时候,才会将ecpect更新为update。 这是处理器提供的一个原子性指令。
CAS有个经典的ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化,则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它 的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
=========================================================================
乐观锁和悲观锁是在数据库中引入的名词,但是在并发包锁里面引入了类似的思想。
悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。悲观锁的实现往往依靠数据库提供的锁机制,即在数据 ,在对数据记录操作前给记录排它锁。如果获取锁失败, 则说明数据正在被其它线程修改,当前线程则等待或者抛出异常。 如果获取锁成功,则对记录进行操作 ,然后提交事务后释放排它锁。
乐观锁相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而在进行数据提交更新时,才会正式对数据冲 与否进行检测 。具体来说,根据 update 返回的行数让用户决定如何去做 。
根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。
而非公平锁是在运行时闯入,也就是先来不一定先得。
ReentrantLock 提供了公平锁和非公平锁的实现:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
2021年Java中高级面试必备知识点总结
在这个部分总结了2019年到目前为止Java常见面试问题,取其面试核心编写成这份文档笔记,从中分析面试官的心理,摸清面试官的“套路”,可以说搞定90%以上的Java中高级面试没一点难度。
本节总结的内容涵盖了:消息队列、Redis缓存、分库分表、读写分离、设计高并发系统、分布式系统、高可用系统、SpringCloud微服务架构等一系列互联网主流高级技术的知识点。
目录:
(上述只是一个整体目录大纲,每个点里面都有如下所示的详细内容,从面试问题——分析面试官心理——剖析面试题——完美解答的一个过程)
部分内容:
对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。
不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
712166456900)]
[外链图片转存中…(img-cbDHqK5V-1712166456900)]
对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。
不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算