先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)

正文

System.out.println(“hello”);

}

}

public class Test2 {

public static void main(String[] args) {

// 需要注意的是 这里 我们实例化是 Thread 类本身,

// 只不过构造方法里给指定了 MyRunnable 实例

Thread t = new Thread(new MyRunnable());

t.start();

}

}

写法 3 和 写法 4 :就是上面两种写法的翻版 - 使用了匿名内部类。

知识点:内部类

写法3

创建了一个匿名内部类,继承自 Thread 类。

同时重写 run 方法 和 new 了

同时再 new 了 个 匿名内部类的实例。

【也就是 t 所指向的实例】

public class Test3 {

public static void main(String[] args) {

Thread t = new Thread(){

@Override

public void run() {

System.out.println(“hello thread”);

}

};

t.start();// 此处仍然是调用 start 来开启线程

}

}

写法4

这一次,我们是针对 Runnable 接口 创建的 匿名内部类(这个匿名内部类实现了Runnable 接口)。

同时 将创建出的实例 作为 参数 传给了 Thread 的构造方法

public class Test4 {

public static void main(String[] args) {

Thread t = new Thread(new Runnable() {

@Override

public void run() {

System.out.println(“hello thread”);

}

});

t.start();

}

}

小结

通过上面的四种写法,我们认识 Thread 方法 和 Runnable 方法。

那么这两种方法,哪一个更好?

通常 认为 Runnable 这种方法更好一点!

它能够做到让 线程 和 线程 执行的任务,更好的进行解耦(解除耦合)。

我们写代码一般希望:高内聚(同一类功能的代码放在一起),低耦合(不同的功能模块之间,没有关联关系)。

其实,我们在使用 Runable 方式 来创建线程的时候,就把当前的线程要执行的任务 与 整个线程的概念给分开了。

换句话说: Runnable 只是单纯的描述了一个任务,至于这个任务是要通过一个进程来执行,还是线程来执行,还是线程池来执行,还是协程来执行,都无所谓!

Runnable 本身并不关心,Runnable 里面的代码也不关心。

就好像通缉令悬赏一个罪犯,是谁抓住的不重要,重要的是内容是否 被 完成 / 执行。

写法五

相当于 第 4 种写法的延伸 》》 进一步简化 - lambda表达式

说白了:使用 lambda 表达式 代替 Runnable。

public class Test5 {

public static void main(String[] args) {

//() 表示无参数的run 方法(Runnable 的 run 方法)

// -> 表示 这是一个lambda 表达式

//{ lambda 表达式里面 具体内容 }

Thread t = new Thread(()-> {

System.out.println(“hello thread”);

});

t.start();

}

}

多线程的优势

=====================================================================

多线程能够提高任务完成的效率。

为了证明 多线程的完成任务的效率。

我们下面一起来实践一下

假设:

现有两个整数变量,分别要对这辆变量,进行自增 10 亿次。

分别使用一个线程 和 两个线程。

我们通过这种方式来体现多线程的效率

总程序

public class Test6 {

private static final long count =10_0000_0000;

public static void serial(){

// 记录程序自增开始的时间

long begin = System.currentTimeMillis();

long a = 0;

for(long i = 0;i < count;i++){

a++;

}

long b = 0;

for (long i = 0; i < count; i++) {

b++;

}

// 记录自增程序结束时间。

long end = System.currentTimeMillis();

System.out.println(“花费的时间:”+ (end - begin) + “ms”);

}

public static void concurrency() throws InterruptedException {

long begin = System.currentTimeMillis();

Thread t1 = new Thread(()->{

long a = 0;

for (long i = 0; i < count; i++) {

a++;

}

});

t1.start();

Thread t2 = new Thread(()->{

long b = 0;

for(long i = 0;i < count;i++){

b++;

}

});

t2.start();

// join 效果 就是等待线程结束。

t1.join();// 让 main线程 等待 t1 线程执行结束

t2.join();// 让 main线程 等待 t2 线程执行结束

long end = System.currentTimeMillis();

System.out.println(“花费时间:” + (end - begin));

}

public static void main(String[] args) throws InterruptedException {

serial();

concurrency();

}

}

很明显 多线程 比 单线程 效率 大概高出了 3 分之 1.【数据量更大的话,效果更明显】

多线程 与 单线程 的效率差距 还是特别明显的!

但是! t1 和 t2 在底层中,是 并发执行, 还是并行执行。是不确定的!

多线程在真正并行执行的时候,效率才会有显著的提升!

另外,多线程在数据量庞大的情况下,效率的提升才是最明显!反而数据量很少的情况下,效率还会有所降低。因为创建线程也是有开销的。

所以讲到这,大家一定要明白一件事。

就是 多线程,它不是万能良药,不是说使用了多线程,代码的执行效率就一定能提高!还需要看使用场景!!!!

多线程特适合那种 CPU 密集型的程序:程序需要进行大量的计算,使用多线程就可以更充分的CPU的多核资源。

换句话来说,

我们使用多线程来提升程序的效率的前提是:这个任务是由CPU来完成的,并且我们需要进行大量的计算,让计算机的所有核心都工作起来。

Thread 类常见方法

===========================================================================

Thread 的常见构造方法

z只讲解一些主要的方法

| 方法 | 说明 |

| — | — |

| Thread() | 创建线程对象 |

| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |

| Thread(String name) | 创建线程对象,并命名 |

| Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |

| 【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |

Thread(String name) - 创建线程对象,并命名

这个构造方法是给线程(thread 对象)起一个名字。

需要注意的是:起一个什么样子的名字,不影响线程的本身的执行。

取得名字要贴合使用场景,不能瞎取名字。

因为乱取线程名字,会影响到 程序员 对 代码 的 后续调试。

因为程序员在调试的时候,可以借助一些工具看到每个线程以及名字,很容易在调式中对线程做出区分。

Thread 的几个常见属性

| 属性 | 获取方法 |

| — | — |

| ID(身份标识) | getId() |

| 名称(就是我们上面构成方法给新城指定的名字) | getName() |

| 状态(线程程的状态) | getState() |

| 优先级 | (线程的优先级) getPriority() |

| 是否后台线程 | isDaemon() |

| 是否存活 | isAlive() |

| 是否被中断 | isInterrupted() |

是否后台线程 - isDaemon()

如果线程是后台线程,就不影响进程退出;

如果线程是前台线程,就会影响到进程退出。

【我们刚才在程序中创建的t1 和 t2 就是前台线程】

即使main 方法执行完,进程也不能退出,得等 t1 和 t2 都执行完。

整个程序才能退出!!!!

如果 t1 和 t2 是 后台线程,此时如果main执行完毕,整个进程就直接退出,

t1 和 t2 就被强行终止。

是否存活 - isAlive()

判断 操作系统中对应的编程是否正常运行。

Thread t 对象的生命周期 和 内核中对应的线程,生命周期并不完全一至。

因为创建出 t 对象之后,在调用 start之前,系统中是是没有对应线程的。

进一步来说,在 run 方法执行完了之后,系统中的线程就销毁了,但是 t 这个 对象 可能 还存在。

所以我们就可以通过 isAlive 来 判断 当前系统的线程的运行情况。

如果 调用 start 之后,run执行完之前,isAlive 就返回 true

入股 调用 start 之前,或者run执行完之后,isAlive 就返回 false

Thread 中的一些重要方法

start 方法 - 启动线程

start 决定了系统中是不是真的创建出线程。

经典面试题: start 与 run方法 的区别

start 操作就是在创建新的线程,run 就是一个普通方法 描述一个任务的内容。

中断一个线程

一般通过 Tread 来创建的线程,想让一个线程停下来的关键,就是要让线程对应 run 方法 执行完。【这是中断线程的关键】

还有一个特殊的,就是main这个线程。

对于 main线程 来说,必须要等到 main 方法执行完,线程才能结束。

让线程结束有以下几个方法:

1、可以手动的设置一个标志位(自己创建一个变量,boolean 和 int 类型都行),来控制线程是否要执行的结果。

结论:在其它线程中控制某个标志位,就能影响对应线程的运行的状态(提前中止该线程的运行)。

另外,此处因为多个线程共用一个虚拟地址空间!

因此,main 线程 修改的 isQuit 和 t 线程判定 的 isQuit 是同一个值。

但是,如果是在进程的那种情况下,在不同的虚拟地址的情况下,这种写法就会失效。

2、使用 Thread 中内置的一个标志位来进行判断来进行判定(比第一种方法更好)

上面那种写法其实还存在着一个问题:标志位的写法,还不够严谨,存在某些问题。

这样写,只能保证 在上面的程序中运行,可能在其他程序中就没有效果了。

这时候,我们就需要使用 第二种方法:使用 Thread 中内置的一个标志位来进行判断来进行判定:

1、Thread.interrupted(); 【这是一个静态方法】

2、Thread currentThread().isInterrupted() 【这是一个实例方法,其中 currentThread 能够获取当前线程的实例】

推荐使用第二种方法!!

public class Test10 {

public static void main(String[] args) {

Thread t = new Thread(()->{

// Thread.currentThread() 获取目前所在线程 t

// isInterrupted() 判断 线程 t 是否中断

// 中断返回 true,再根据 !取反,为 false,跳出循环,从而结束 run任务,致使线程t中断结束执行

// 执行中返回 false,,再根据 !取反,为 true,执行 run 的 输出语句。

while(!Thread.currentThread().isInterrupted()){

System.out.println(“hello thread”);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

});

t.start();

try {

Thread.sleep(5000);// 在main线程中,5s之后,执行下面 代码t.interrupt()

} catch (InterruptedException e) {

e.printStackTrace();

}

// 在主线程中,调用 interrupt 方块,来中断这个线程

// t.interrupt 的 意思是: t线程被中断

t.interrupt();

}

}

但是呢!运行的结果 与我们想象的不同!

期望:在 5s之后,线程 t 被中断

实际:5s之后,编译器报出一个异常,线程 t 继续执行,线程t还没有终止。

也就是说: t.interrupt() 不仅仅是针对 while循环的条件(标记位) 进行操作,它还可能触发一个异常。

需要注意的是:在使用 interrupt 方法的时候,我们的interrupt可能会有两种情况,而这两种情况都是需要考虑到的。

如果我们线程中没有什么代码导致线程进入阻塞状态的操作,直接一个循环判断就是够了。

如果有,我们就需要借助 catch代码块(处理异常),在里面进行添加相应的操作。

Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.:

public boolean isInterrupted() :判断对象关联的线程的标志位是否设置,调用后不清除标志位.【第三个方法,这个实际开发中 常用的 写法】

【我们上面while循环调用 interrupted方法是第三个,线程 t. interrupt 是 第一个】

毕竟一个代码中的线程有很多个,随时哪个线程都可能会终止

Thread.interrupted() 这个方法判定的标志位置是 Thread 的 static 成员。

又因为 一个程序中只有一个标志位,很显然这么多的线程,一个标志位怎么够用。

Thread.currentThread().isinterrupted() 这个方法判定的标志位 是 Thread 的 普通成员,每个示例都有自己的标志位。【一般无脑用这个方法即可】

线程等待

===================================================================

前面说到:多个线程之间,调度的顺序是不确定的。(顺序取决于系统)

但是这样的不确定性 并不好。有的时候,我们是需要让线程有明确顺序的。

换个说法:

线程之间的执行是按照调度器来安排的,这个过程可以视为是“无序,随机”。

这样不太好,有时候,我们需要能够控制线程之间的顺序。

线程等待就是其中一种,控制线程执行顺序的手段

此处的线程等待,主要是控制线程结束的先后顺序。

其实 join 也是其中的一种

调用 join 的时候,哪个线程调用的 join,那个线程就会阻塞等待。

等到对应的线程执行完毕为止(对应线程的 run 执行完)

获取当前线程引用

=======================================================================

| 方法 | 说明 |

| — | — |

| public static Thread currentThread(); | 返回当前线程对象的引用(Thread 实例的引用) |

哪个线程调用的这个currentThread,就获取到哪个线程的实例。

线程休眠

===================================================================

就是 sleep,前面也用了很多。

这里,我们进一步的解析它。

所谓的 线程休眠的具体作用是什么?

回顾

进程是通过 PCB 来描述。

进程是通过 双向链表来组织的。

前面的说法是针对只有一个线程的进程 的 这种情况。

但如果是一个进程有多个线程,此时每个线程都有一个PCB。

也就是过更为准确的说:一个进程 对应的就是 这一组PCB了。

然后 PCB 上有一个字段 tgroupld,这个 id 其实就相当于 进程的 id,同一个进程中的若干个线程的 tgroupld 是相同。

那么 PCB - process control block 进程控制块 和 线程有什么关系?

其实在linux系统中 内核是不区分进程和线程。

只是程序员在写应用程序代码的时候,弄出来的词。

实际上 linux 内黑 指认 PCB!!!

在内核里 linux 把 线程 称为 轻量级进程。

线程的状态

====================================================================

主要就介绍两个状态:

1、就绪

2、阻塞

前面所讲的进程状态,其实都是指的是系统按照“什么样子的态度”来调度这个进程

但是!这种说法并不是很严谨!

上面的说法时针对一个进程中只有一个线程的情况。

更常见的情况:一个进程包含多个线程。

所谓的状态其实是绑定在线程上。【前面讲的例子都在透露这个信息】

与线程休眠想表达出的意思一样:Linux 中认为 PCB 和 线程一一对应,一个进程对应一组PCB。

状态本来就是 PCB 的 一个属性,现在正好每个线程都有着各自的PCB,也就说每个线程都有属于自己的状态。

因此,我们系统在调用线程的时候,就可以根据每个线程不同的状态,来确定 是否调度该线程。

并且我们还可以通过状态来更好的区分线程。

上面说的 “就绪” 和 “阻塞” 都是针对系统层面上的线程的状态【PCB的状态】。

在Java中,尤其是在 thread 类中,又对线程的状态进行了进一步的细化。

1、NEW: 安排了工作, 还未开始行动

把 Thread 对象创建好了,但是没有调用start方法。

2、TERMINATED: 工作完成了.

操作系统中的线程已经执行完毕,销毁了。

但是 Thread 对象还在,此时获取的状态就是 terminated。

小结

3、RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.

RUNNABLE 就是 我们常说的就绪状态。

处于这个状态的线程,就是在 就绪队列中。

随时可以被被调度到 CPU 上。

对 就绪状态 的线程,有两种情况:

1、正在被执行

2、还没有执行,但是随时可以调度它。

如果代码中没有进行 sleep,也没有进行其他的可能导致线程阻塞的操作。代码大概率都是出于 RUNNABLE 状态

4、TIMED_WAITING: 这几个都表示排队等着其他事情

代码中调用了 sleep、join(超时时间),就会进入到 TIMED_WAITING。

它的意思就是当前的线程在一定的时间内,是阻塞状态。

5、BLOCKED: 这几个都表示排队等着其他事情

当前线程在等待 锁,导致到了阻塞(阻塞状态之一)

一般是在我们使用 synchronized 来去加锁的时候,可能会触发这种状态。

6、WAITING: 这几个都表示排队等着其他事情

当前线程在等待 唤醒,导致到了阻塞(阻塞状态之一)

一般是在我们使用 wait 来等待唤醒的时候,可能会触发这种状态。

注意

BLOCKED 和 WAITING 这两个状态,由于还没有介绍加锁和等待,所以暂时不演示,后面讲到 加锁 和 等待的时候,会拿出说的。

但是从整体上来看,大家要明确: 上面的 三个状态 TIMED_WAITING、BLOCKED、WAITING,它们都是阻塞状态。

本来在系统里阻塞状态只有一种,但在Java又进行了进一步的细分:根据不同的原因,分成了不同的状态。

这个时候,其实就有利于程序员理解代码是怎么执行的。

这么说吧,在日常开发过程中经常会遇到一种情况:程序“卡死”了.

其实程序“卡死” 的真实原因:一些关键的线程阻塞了。

所以当我们程序看到 成宿 卡死的情况,肯定就需要去分析 程序 卡死的原因是什么。

在分析卡死原因的时候,

第一步就可以先来看看当前程序中 各种关键线程的状态是什么?

一看状态就知道大概是什么原因 导致程序卡死。

如果是 TIMED_WAITING,那么就去看代码中 sleep 是不是有问题。

如果是 BLOCKED,那么就去看 代码 是不是 哪里锁住了,没解开。

如果是 WAITING,说明 哪里的代码 处于 睡眠状态,却没有程序去唤醒。

因此,我们可以通过这样的一个不同的状态,就可以进一步的细分出当前的程序是为什么卡死!

所以,这也是我们学习 线程状态 的 重要意义。

当前学的很多东西,总得来说分为两方面:

1、敲代码(外功)

2、原理(内功)

原理解释了代码为什么要这么去写。

也解释了 当前这个 bug 是怎么造成的。

线程状态和状态转移的意义

这个图看不懂,就看下面的 简化图

线程状态转换简略图

上图中 最关键的主线任务:NEW -> RUNNABLE -> TERMINATED。

其他的都是支线任务。

毕竟我们创建出线程 就是为了让它执行一些任务的。

所以 这个 RUNNABLE 一定是主线任务。

然后,在 RUNNABLE 过程中,可能我们需要执行一些特殊代码 导致 线程切换成其它的阻塞状态。

使用不同代码,就会进入不同的阻塞状态。

所以这六种状态是需要我们重点掌握的!

线程安全问题 - 重中之重

============================================================================

整个多线程中最重要,也是最复杂的问题。

换个话来说:未来面试官问及多线程的问题,那么一定会问到 线程安全!!

因为在日常开发中,如果用到多线程编程,也一定会涉及到线程安全问题。

因为 线程安全问题不太好理解!

所以,大佬们才尝试开发出更多的编程模型来处理并发编程的任务。

像多线程 和 多进程 ,这都是属于比较基本的并发模型。

除此之外,还有一些其它的:

1、actor 模型

2、Go 语言 中 csp 模型

3、JS - Java Script 中 是 通过 定时器 加上 回调 的模型

4、async 模型

5、await 模型

在Java中,多线程 处理 并发编程 是 主要的处理方式。

线程安全问题 又称 竞态条件 问题。

我们可以这么去认为:操作系统调度线程的时候,是随机的(抢占式执行)。

正是因为这样的随机性,就可能导致程序在执行的时候出现一些bug。

如果因为这样的调度随机性 引入了 bug,就认为代码是线程不安全的!

如果是因为这样的调度随机性,也没有带来bug,就认为代码是线程安全的!!

总得来说:衡量线程安全的关键,就是有没有bug。有bug 就是不安全的,无bug就是安全的.【在于代码本身的安全性】

跟我们平时谈到的“安全”是不一样。平时提到的 “安全”,主要是指 黑客 是不是会入侵你的计算机,破坏你的系统。

一个线程不安全的典型案例

使用两个线程,对同一个整形变量 进行自增操作。

【每个线程对这个变量进行自增五万次,看最终结果】

class Counter{

// 这个变量 就是 两个线程要去自增的变量

public int count;

public void increase(){

count++;

}

}

public class Test15 {

private static Counter counter = new Counter();

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(()->{

for (int i = 0; i < 5_0000; i++) {

counter.increase();

}

});

Thread t2 = new Thread(()->{

for (int i = 0; i < 5_0000; i++) {

counter.increase();

}

});

t1.start();

t2.start();

// 为了保证得到的count的结果,是两个线程执行完毕后的结果

// 我们需要使用 join 来等待 线程 执行结束

// 这样在 main 线程中,打印的 count 的 结果,才是两个线程对 count 的 自增最终结果

// 因为 三个线程之间关系 为 并发关系。

// 如果不使用 join, main下城压根就不会等 t1 和 t2 自增完,直接输出count。

// 使用 join 之后,只有 t1 和 t2 线程都结束了之后,main线程才能结束。

t1.join();// 先执行 t1.join,然后等待 t1 结束

t2.join();// 与 t1.join同理,再等待 t2 结束。

// 这两个join,谁在前,谁在后,都无所谓!

//由于 这个线程调度 是随机的,我们也不能确定 是 t1 先结束,还是t2先结束。

// 就算 是 t2 先结束,t2.join也要等t1结束之后,t2.join才能返回。

// 当这两个线程结束后,main线程才会执行sout语句。

// 在 main 线程中 打印一下两个线程自增完成之后,得到的count的结果

System.out.println(counter.count);

}

}

当两个线程之间 同时 对 同一个变量(与其变量的静态属性没有关系),进行并发自增操作的时候,就会出现一些情况。

这是因为 t1 和 t2 线程同时 对 count 进行加 1,照理说count应该被加上2了,但是实际上 count 值 只加了一个1。

【简单来说:两次 自增,count只加了1】

解析

如何解决线程安全问题?

加锁 - synchronized

我们 通过 关键字 synchronized, 我们确实解决了代码的线程安全。

代码执行的结果也达到了我们的期望值。

现在我们回过头来看,什么情况线程会不安全?

标题也可以理解为:什么样子的代码会产生中线程不安全问题呢?

首先,我们要知道:不是所有的多线程代码都要加锁。

如果每个多线程的代码都进行加锁操作(synchronized),那么多线程的并发能力就跟没有一样。还不如直接写一个单线程代码的好。

产生线程不安全的原因如下:

1、线程是抢占式执行,线程间的调度充满着随机性。【造成线程不安全的直接原因,而且是无法解决的问题】

正是因为“抢占式执行” 才导致了 线程安全问题的出现。

假设,我们线程之间,不是 “抢占式执行”的,而是其它的调度方式。

那么,很可能就没有线程安全的问题了。

另外,拓展一下:

其实在操作系统中,不仅仅有 抢占式执行,还有 协商式执行的。

协商式:在调度线程的时候,让多个线程之间进行商量,看让那个线程来执行。

然后,在调度这个线程之前,先去让 正在 CPU 上执行的线程先去完成一些基本的工作,把该做的事做完了,再去调度。

但是目前大多数电脑的操作系统的执行方式都是 抢占式执行。

2、多个线程对同一个变量进行修改操作。

如果是 多个线程针对不同的变量进行修改操作,线程安全没有问题。

如果是多个线程针对同一个变量进行读取操作,线程安全也没有问题。

解决办法

放在代码中,我们可以通过调整 代码结构,来使不同的线程操作不同变量。

但是还是需要根据场景来判断使用,因为有些场景是不适合使用的。

3、针对变量的操作不是原子的

在讲数据库MySQL的事务的时候,我们讲到过 将 几个操作打包成一个整体,要么就全部执行,要么就一个都不执行。

我们多线程中 “原子性” 也是一样的。

针对有些操作:

1、比如 读取变量的值,这个读操作只是对应一条 机器指令(这也是为什么说多个线程针对同一个变量读是安全的),此时这样的操作本身就可以是视为是原子的。

2、通过加锁操作,也就是把好几个指令给打包成一个原子的了。

这种方法是最长使用的一个操作。

通过 加锁操作,把这里的多个操作打包成一个原子的操作。

如果操作是原子的,那么线程安全就没有问题了。

4、内存可见性,也会影响到线程安全。

t1 这个大胆的操作 是 客观存在的!

是 Java编译器 进行代码优化 产生的结果。

现代编译器都会有这样优化功能,并且不止是 Java,还有C++、Python、各种主流的语里面都充满了 各种非常神奇的 “编译器优化操作”.

存在这样的优化机制,是因为 编译器不信任程序员。

换个说法:编译器假设这个程序员非常的垃圾,写的代码惨不忍睹。

编译器 就会 对 程序员 写的代码做出一些调整,在保证原有逻辑不变的前提下,提升程序执行的效率。【而且是大大提升!!!】

【实现编译器的程序员都是 大佬中的大佬】

但是! “编译器优化操作”在保证原有逻辑不变的前提下,提升程序执行的效率。

这个保证原有逻辑不变的前提下,大部分情况都是可以的。

但是在多线程中,是可能翻车的。

因为多线程代码中。它是一个并发执行,执行的时候充满着不确定性。

编译器在编译阶段,是很难预知到 执行 行为的!!

进行的优化,可能就会发生误判!!!

代码示例:针对 内存可见性 的线程安全问题

import java.util.Scanner;

public class Test16 {

private static int isQuit = 0;

public static void main(String[] args) {

Thread t1 = new Thread(()->{

while(0 == isQuit){

}

System.out.println(“循环结束!t1 线程执行结束”);

});

t1.start();

Scanner sc = new Scanner(System.in);

System.out.println(“请输入一个isQuit 的值:”);

// 预期理想情况:

// 只要用户一输入数据,就会把 isQuit 的 值 进行修改

// 从而 不满足上面 t1 线程中 while 的循环条件。

// 导致 线程 t1 中断,结束运行。

isQuit = sc.nextInt();

System.out.println(“main 线程执行完毕!”);

}

}

造成 线程安全问题之一 的 内存可见性 原因是:

t1 线程中 run 任务,正在不断的从内存读取数据。【我电脑的主频是2.8GHz,也就是说: 1s 可以执行 28 亿条 指令】

而这种操作,我们知道是非常低效的!

所以 t1 线程 做出那个大胆的想法(假设isQuit值不会再被改变),不再从内存中读取数据了。

改从 寄存器中读取数据,效率大大提升【寄存器的读取速度 大约是 内存的读取的 万倍左右】

所以 在 我们输入 1 来改变 isQuit 值的时候,此时 编译器已经是处在读取 寄存器中数据了,所以我们对 isQuit 值 的 改动,它是感知不到的内存中 isQuit变化!!!

从而 线程 t run 方法 的 循环条件 一直都是满足条件的,所以 run 继续执行,t1线程也就没有被中断。

解决办法

1、加锁操作:使用 synchronized 关键字

synchronized 并不能保证内存可见性。

而是通过给循环里面进行加锁操作,导致循环的速度降低了。

因此没有处罚相关的优化操作,算是一种技巧吧。

如果,我们将 synchronized 加锁操作 放到 while外面,将 while循环包裹在内。

此时,再执行这个程序,就会发现:另一个线程修改了 flag, if 语句的条件判断也是感知不到的。

2、volatile 关键字 - 必须会读会写这个关键字

volatile 和 原子性无关,但是能够保证内存可见性。

换个说法:禁止编译器对其做出上述的优化操作,编译器每一次进行 判定相等的时候,都会重新从内存中读取 isQuit 的 值。

内存可见性 是属于编译器优化范围中的一个典型案例。

编译器优化机制,本身就是一个玄学问题,对于普通程序员来说:什么时候不优化,什么时候优化都是一个问题!

再将上面的那个列子改动一下

1、没有 volatile 关键字 来 修饰 isQuit

2、run方法中 while循环中添加了操作。

简单来说:就是像这种类型的代码,在循环中加上 sleep,编译器的优化机制就消失了。 也就没有内存可见性问题了、

其实还有一个最可能 不进行优化的原因:是因为 加上sleep(1000)之后,1s读一次,对于编译器来说,调用频率就不是很高了。所以也就没必要进行优化了。

5、指令重排序

指令重排序 也是编译器优化中的一种操作。

小结

上述介绍的五种情况,都是线程不安全的原因!

这几个原因都必须掌握!!

因为这些都是日常代码密切相关的!!!

重点解析 - synchronized 关键字 - 监视器锁 monitor lock

==========================================================================================================

synchronized 的 使用方式

使用 synchronized 的时候,本质上是在针对某个“对象” 进行加锁。

什么意思呢?

这就要涉及到synchronized背后的一些故事。

其实我们的 synchronized 相当于是 针对 对象里的一个 对象头中的一个属性进行了一个设置。

1、直接修饰 普通方法。

2、修饰一个代码块

需要显示指定针对哪个对象进行加锁。

Java中的任意对象都可以作为锁对象

3、修饰一个静态方法

相当于针对当前类的类对象进行加锁。

例如

Counter.class (反射)

可参考这篇反射文章

另外,思考一个问题:静态方法,有this吗?【没有】

更具体的来说:

所谓的“静态方法”,更严谨的叫法,应该叫做“类方法”。【通过类来调用】

普通方法,更严谨的叫法,应该叫做“实例方法”。【通过new实例化,才能访问】

回过头来,既然静态方法是没有this的,那么synchronized修饰一个静态方法,就是在针对类对象加锁。

小拓展:类对象是什么?

类对象就是,就是我们在运行程序的时候的 .class 文件 被加载到 JVM 内存中的模样。

就好比一个人,他很多“面”, 面对不同的人或事物,他的表现都不一样。

对于 .class文件 来说,它也具有很多面。

它原本存放在磁盘当中文件,但是当我们运行程序的时候,它就会加载到 内存中,而内存中的模样 就像 这样的 类名 .class 的类对象。

这样的类对象里面就包含这 .class文件的一切信息。

后续我们运行代码,访问这里面的属性 都和这里的.class文件 密不可分,甚至说我们创建实例的时候,也跟它有着密切的联系。

正是因为有了 类对象中的这些信息,进一步的,我们才能有 反射机制。

而反射机制都是来自于 .class 赋予的“力量”。

也就是说 你想要反射,就必须先要有类对象,才能进行下一步操作。

【反射中获取类对象 :”Class c = Class.forName(“文件名”)】

标题提到 synchronized 又称 监视器锁 monitor lock。

对于这个 monitor lock,大家要有所印象!

有些时候,代码中的异常信息,可能会提到 monitor 这个词。

刷新内存

synchronized 的工作过程

1、 获得互斥锁

2、 从主内存拷贝变量的最新副本到工作的内存

3、 执行代码

4、 将更改后的共享变量的值刷新到主内存

5、 释放互斥锁

这也是为什么前面 synchronized 可以解决 内存可见性的线程问题。

可重入

直观来说:同一个线程针对同一个锁,连续加锁两次。

如果出现了死锁,就是不可重入,如果不会死锁,就是可重入。

死锁 其实非常容易理解,直白来说就是卡bug。

举个例子:

假设,路人甲 在 某一个厂里工作。

有一天他工作回来,准备回宿舍睡觉。

但是!走到宿舍门口后,被保安拦下来了。

他说:“由于疫情原因,我需要检查一下你的厂牌,否则不能进!”

路人甲,摸了摸自己的口袋,发现没有,与此同时他想起来今天没有带,厂牌还在宿舍里。

于是说:“能不能先让我进去,我进寝室里拿厂牌给你,我可以把手机压在这里。”

解雇这保安是一个死脑子,说:“没有厂牌是吧,那不能进!”

路人甲就急了,你不让我进去,我怎么拿厂牌给你看,而且我还把手机在这里!

而这保安就是死活不听,非要看到厂牌,否则不让进。

此时这两个人,再怎么争执,也没有用。两个人就在这里卡bug。

这种情况就是 “死锁”。

放在实际情况中,这种代码还是很有可能会写出来的。如果没有进行特殊处理,这种代码也确实非常容易死锁。

如果代码真的死锁了,岂不是实程序员的 bug 就太多太多了。

实现 JVM 的大佬们显然也注意到了这一点,就把 synchronized 实现成了可重入锁。

对于 可重入锁来说:上图中连续加锁的操作,不会导致死锁。

可重入锁内部,会记录当前的所被哪个线程占用,同时也会记录一个“加锁次数”。

假设: 线程 a 针对锁,第一次加锁的时候,肯定是能加锁成功。

此时锁内部记录了,当前占用锁的线程是 a,同时加锁次数为 1。

后续再对 a 进行 加锁,此时就不是真的加锁,而是单纯的把 加锁次数进行自增。

【后续的加锁操作 是没有实质的影响的】

最后解锁的时候,每解一次锁,就是将 加锁次数进行 - 1。

当 加锁次数 减到 0 的 时候,就是真的解锁了。

可重入锁的意义就是降低了程序员的负担。

降低了使用成本,提高了开发效率。

但是也需要付出代价,代码需要更多的内存开销,来维护锁 属于哪个线程,并且加减计数。

也就是说: 降低了使用成本,提高了代码的开发效率的同时,也降低了代码的运行效率。

小拓展:开发效率 和 运行效率 谁更重要?【两者是不能共存的,需要作出取舍】

这就涉及到 程序员的 “核心价值观”。

程序员也是要追求幸福感的!

程序员的幸福感,具体体现在 2 个 方面:

1、挣得多【月入2w 肯定比月入 4、5千要幸福的多】

2、加班少【朝九晚五,甚是美哉】

结合上面的 开发效率 和 运行效率,那个更能提升我们的幸福感?显然是 开发效率,但是执行效率就低了。

理由:

拿重入锁来举例,如果我们使用的是不可重入锁,此时的开发效率就低了(一不小心,代码就死锁了,线上程序出现了问题,就意味着要加班修bug,同时bug比较严重,年终奖可能就要没了),但是运行的效率提高。

从另一方面来说:程序员的主要工作就是 驱使机器做事。我们能让机器多做一点事,就让机器多做一点,让人少做一点点,都是好的!!

不光是 程序员 举的开发效率高更好。

同时公司(组本家) 也觉得开发效率更好,在互联网圈子中,人力成本是非常高的!!

拓展 :死锁的其他场景

1、一个线程。一把锁

就是我们上面讲的那个。

两个线程,两把锁

N个线程了,M把锁。

这个情况更复杂,我们来使用一个教科书上的经典案例。

解决方法

但是在实际开发中,很少出现这种一个线程需要锁里再套锁的情况

如果不嵌套使用锁,也就没那么容易死锁了。

如果我们的使用场景,不得不进行嵌套的时候,大家一定要记得,一定要约定好加锁的顺序。

所有的线程都按照 a->b->c 这样的顺序进行加锁。

千万别有的线程 a->b->c,有的线程 c->b->a,这样就很容易出现循环等待。

小结:死锁的四个必要条件

1、互斥使用:一个所被一个线程占用了之后,其它线程占用不了。【锁的本质:保证原子性】

2、不可抢占:一个锁被一个线程占用了之后,其它的线程不能把这个锁给抢走。

3、请求和保持:当一个线程占据了多把锁之后,除非显式的释放锁,否则这些锁始终都是被该线程持有的。

4、环路等待:等待关系形成了一个环 。【A 等 B,B 等C,C 又等 A】

前三条 都是属于 锁本身的特点。

实际开发中要想避免死锁,关键点还是从 4 个条件出发进行切入。

如何避免出现环路等待?

只要约定好,针对多把锁加锁的时候,有固定的顺序即可。

所有的线程都遵守同样的规则顺序,就不会出现环路等待。

Java标准库中的线程安全类

=============================================================================

Java有很多线程的类,有些是线程安全的,有些是不安全。

在多线程环境下,如果使用线程不全的类,就徐亚谨慎。

线程不安全的类

ArrayList

LinkedList

HashMap

TreeMap

HashSet

TreeSet

StringBuilder

线程安全的类

Vector (不推荐使用)

HashTable (不推荐使用)

ConcurrentHashMap

StringBuffer

String

前四个类是线程安全的,是因为在一些关键方法上都有 synchronized 修饰 / 加锁。

有了这个操作,就可以保证在多线程环境下,修改同一个对象,就没有大问题。

Vector 对标的是 ArrayList 。

HashTable 和 ConcurrentHashMap对标的是 HashMap。

【更推荐使用 ConcurrentHashMap,因为HashTabe 存在一些性能上的问题】

StringBuffer 和 StringBuilder 功能上都一样的。只是StringBuffer 加上了 synchronized,适用于多线程,而StringBuilder没有synchronized的,适用于单线程。

至于第五个 String 虽然也是线程安全的,但是与前4个类不同,它有些特殊。

它没有 synchronized。

String 是 不可变对象,故无法在多个线程中同时改同一个String。

哪怕是在单线程中也无法改 String、【要想改变只能创建一个新的,来代替旧的】

拓展:

不可变对象 和 常量 / final 之间没有必然联系。

不可变对象 之所以不可变,是因为对象中没有提供 public 的 修改属性的操作。

而且你可以自己去翻一下 String 底层的方法,就可以发现没有一个可以修改字符串里面内容的 public 方法。

正因为不可变对象有这样的特性,有的编程语言,就天然把所有的对象来设计成不可变。然后,这样的语言就更方便处理并发问题。

比如: Erlang,这个语言就是如此,里面就没有变量这个东西,都是属于不可变的量。

volatile 关键字

===========================================================================

volatile 能保证你内存可见性

volatile 修饰的变量,能够保证“内存可见性”。

换个说法:禁止编译器优化,保证内存可见性。

前面也讲到了,之所以存在 “内存可见性”问题,是因为我们的计算机的硬件所决定的。

volatile 关键字 只能保证“内存可见性”,但不能保证原子性。

volatile 只是处理 一个线程度,一个线程写的情况。

synchronized 都能处理。

volatile 和 synchronized 的区别 - 面试会问到。

这两个本来就没有什么联系。

只是在 Java 中 恰好都是关键字。

其他语言,加锁不是关键字,C++中的锁就是一个单独的普通类而已

拓展:

synchronized 不能 无脑使用,凡事都是有代价的。

代价就是一旦使用synchronized 很容易使线程阻塞,一旦线程阻塞(放弃CPU),下次回到CPU的时间就不可控了。【可能就是一辈子,因为 可能有几个线程在卡bug】

如果调度不回来,自然对应的任务执行时间也就是拖慢了。

用一句话来说 synchronized:一旦使用了 synchronized,这个代码大概率就和“高性能无缘了。

开发效率高,固然好,但有些时候还是需要考虑执行效率的。

volatile 就不会引起线程阻塞。

wait 和 notify :等待 和 通知

=====================================================================================

wait 和 notify 为了处理线程调度随机性的问题。

还是那句话,多线程的调度,因为它的随机性,就导致代码谁先执行,谁后执行,存在太多的变数。

而我们程序员是不喜欢随机性,我们喜欢确定的东西。

需要能够让线程彼此之间,有一个固定的顺序。

举个例子:打篮球

篮球里面有一个典型的操作:传球,上篮。

那么我们肯定得先传球,再上篮。需要两个队员的相互配合。

两个队员也就是两个线程。

如果是先做个上篮动作,但是球没到,也就上了个寂寞。

一般最稳的方法,都是先传球再上篮。

像这样的顺序,在我们实际开发中也是非常需要的。

因此我们就需要有手段去控制!

前面讲到的 join 也是一种控制顺序的方式,但是join更倾向于控制线程结束。因此 join 是有使用的局限性。

就不像 wait 和 notify 用起来更加合适。

wait 和 notify 都是 Object 对象的方法。

调用 wait 方法的线程,就会陷入阻塞,阻塞到有其它线程通过 notify 来通知。

目前,我们只是使用了 wait 方法,

接下来 我们来实践一下: notify 和 wait 的组合使用

wait 使线程处于阻塞状态,notify 来唤醒 调用wait 方法陷入睡眠的线程。

public class Test18 {

// 为了两个线程能够顺利交互,我们创建一个锁对象

private static Object locker = new Object();

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(()->{

//进行 wait

synchronized (locker){

System.out.println(“wait 之前”);

try {

locker.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“wait 之后”);

}

});

t1.start();

// 为了大家能更清楚的观察到现象,我这里使用 sleep 延迟3s

Thread.sleep(3000);

Thread t2 = new Thread(()->{

// 进行 notify

synchronized (locker){

System.out.println(“notify 之前”);

locker.notify();

System.out.println(“notify 之后”);

}

});

t2.start();

}

}

notifyAll

前面说的 wait 和 notify 都是针对同一个对象来操作。

例如:

现在有一个对象 o,被 10个线程调用了 o.wait。

此时 10 个 线程都是阻塞状态。

如果调用了 o.notify,就会把 10个线程中的一个给唤醒。【随机唤醒:不确定下一个被唤醒的线程是哪一个】

被唤醒的线程就会继续往下执行。其他线程仍处于阻塞状态。

如果调用的 o.notifyAll,就会把所有的线程全部唤醒。

wait 在被唤醒之后,会重新尝试获取到锁,这个过程就会发生竞争。

也就是说:唤醒所有的线程,都去抢锁。

抢到的,才可以继续往下执行。

没抢到的线程,继续等待,等待下一次的 notifyAll。

其实不难发现:与其notifyAll一次唤醒全部线程,还不如notify一个接着一个人唤醒。

因为 一次唤醒全部线程会造成竞争,不缺定下一个占用锁的是哪一个线程8。

而 一次唤醒一个,既能保证执行顺序,又可以不发生竞争。

对于日常使用来说:notify 更常用!

小结

=================================================================

目前为止,我们讲了

1、线程的基本概念

2、Thread 类

2.1、创建线程

2.2、中断线程

2.3、等待线程

2.4、获取线程实例

2.5、线程休眠

3、线程状态

4、线程安全问题(最重要)

5、内存可见性

6、wait / notify

掌握这些,就可以完成一些多线程的开发任务了。

关于多线程的案例

=======================================================================

1、实现一个线程安全版本的单例模式

线程安全,我们已经知道了。

但是 单例模式又是一个新的东西。

单例模式:设计模式之一。

设计模式:可以理解为“棋谱”,就是说:设计模式就是一些固定的代码套路。

当下棋,下到一种程度。都会去学习棋谱,也就是学习下棋的招数 和 应对之法。

代码中,有很多经典场景。

经典场景中,也就有一些经典的应对手段。

一些大佬们就把这些常见的应对手段,给收集整理起来,起了个名字,就叫“设计模式”。

不要想得太nb,就是收集资料整理得出的结果,俗称套路。

这些套路就可以让程序员(哪怕是新手),按照套路去写,就不至于把代码写的太差。(就像做PPT一样,下一个模板,自己去填充)

从客观角度出发,可能很多的程序水平一般。

因此,如何才能保证 即使程序员水平一般,也能代码写好?

那就需要通过 设计模式 来去做出规范。

另外,关于“设计模式”的书,有不少。

我推荐是不着急去学习更多关于“设计模式” 的东西。

我们当前需要解决的问题是从无到有。

直白点说 就是 从不会写,到能写出来。

这是因为 “设计模式” 这个东西,它从 有 到 优。

就是说:我们本来就会写,只是写得不是很好。现在就可以通过设计模式,来进行进一步的优化代码。

所以话说回来,我们目前的重点:是从无到有,从不会写到会写。

不过也别着急,等我们工作了,有了一定工作经验,这些东西你都会遇到的。

而且只要代码敲得多了,问题也就不存在了。

虽然 “设计模式” 不着急学。

但是!我们不能完全不会!

至少校招中,有两种“设计模式”是常提问的。

1、单例模式

2、工厂模式

这个我后面都会讲,这里先关注于 “单例模式”。

单例模式

要求我们代码中的某个类,只能有一个实例,不能有多个实例。

实例就是对象。

就是说某个类只能new 一个对象,不能new多个对象。

这种单例模式,在实际开发中是非常常见的,也是非常有用的。

开发中的很多“概念”,天然就是单例的。

比如说:我前面写的MySQL 的 JDBC编程 里面,有一个DataSource(数据源)类,像数据源这样的对象就应该是单例的。

毕竟作为一个程序,数据源应该只有一个。

有一个源就可以了,我们只要描述这些数据只来自于 一个 数据源, 就行了。

像这种,就应该像是一个单例。

我在讲 JDBC 并没讲那么多,现在我来说一下:在真实情况下,像这种数据库的数据源都会被设计成单例模式的。

大部分跟数据有关的东西,服务器里面只存一份。那么,就都可以使用“单例模式”来进行表示。

单例模式的两种典型实现

单例模式中有两个典型实现:

1、饿汉模式

2、懒汉模式

我们来通过一个生活上的例子来给大家讲讲什么是饿汉模式,什么是懒汉模式。

洗碗,这件事不陌生把?

第一种情况:

假设我们中午吃饭的时候,一家人用了4个碗。然后吃完之后,马上就把碗给洗了。

这种情况,就是饿汉模式。

注意!饿汉模式的 “饿” 指的是着急的意思,不是肚子饿。

第二种情况

中午吃饭的时候,一家人用了4个碗。然后吃完之后,碗先放着,不着急洗。

等待晚上吃饭的时候,发现只需要2个碗。

那么就将 4个没洗的碗 中,洗出2个碗,拿来用。吃完之后,碗先放着,不着急洗。

如果下一顿只用一个玩,就洗出1个碗。

简单来说:就是用多少,拿多少。少的不够,多的不要。

这就是懒汉模式

懒汉模式不推荐现实生活中使用,挺砸吧的。。

但是在计算机中,普遍认为 懒汉模式 比 饿汉模式好。

主要因为 懒汉模式 的效率更高

也很好理解:洗 2 个 碗,肯定比洗4个碗轻松。

所以用几个洗几个。

根据需要,进行操作。

“懒” 这个字一般 在计算机中,是一个褒义词。

1、饿汉模式

饿汉的单例模式,是比较着急的去进行创建实例的。

//饿汉模式

class Singleton{

// 1、使用 static 创建一个实例,并且立即进行实例化,

private static Singleton instance = new Singleton();

// 2、为了防止程序员在其他地方不小心new这个 Singleton,需要把这个类的构造方法设置为 private

private Singleton(){};

//3、提供一个方法,让外面能够拿到唯一的实例。

public static Singleton getInstance(){

return instance;

}

}

2、懒汉模式

懒汉的单例模式,是不太着急的去进行创建实例的,只有在用的时候,才真正创建实例。

懒汉模式的代码 和 饿汉模式的代码非常相似。

//单例模式 - 懒汉模式

class Singleton2{

//1、现在就不是立即初始化实例

private static Singleton2 instance;// 默认值:Null

//2、把构造方法设为 private

private Singleton2(){};

//3、提供一个公开的方法,来获取这个 单例模式的唯一实例

public static Singleton2 getInstance(){

// 只有当我们真正用到这个实例的时候,才会真正去创建这个实例

if(instance == null){

instance = new Singleton2();

}

return instance;

}

}

public class Test20 {

public static void main(String[] args) {

Singleton2 instance = Singleton2.getInstance();

}

}

饿汉模式 和 懒汉模式 的唯一区别就在于 创建实例的时机不一样。

饿汉模式 是 类加载时,创建。

懒汉模式 是 首次使用时,创建。

所以懒汉模式就更懒一些,不用的时候,不创建;等到用用的时候,再去创建。

这样做的目的,就是节省资源。

如果像 饿汉模式一样,一开始就实例化对象。

此时这个对象就存储在堆上。【这是需要耗费资源】

我们也不确定 这个 对象 什么时候会被用到。

那么,我们一直不调用,这资源还是一直挂在那里。

这就不就是浪费嘛!

如果像 懒汉模式一样,到了真正用到的时候,才会去实例化唯一的对象。

拓展:进一步帮助你们理解 饿汉 和 懒汉模式

其实在计算机很多其它场景中,也会涉及这情况。

一个典型的案例:

notepad 这样的程序(记事本软件),在打开大文件的时候是很慢的。

假如,你要打开一个 1G 大小的文件,此时 notepad 就会尝试把这 1 G 的 所有内容都读到内存中。

将 1G 的数据量 存入 内存,显然是非常慢的。

不管你要不要,全部都给你。

这就是 饿汉模式。

问题也随之而来:这些数据,我们真的能全部用得到吗?显示是不太可能的。

因此就会浪费很多资源.

像一些其他的程序,在打开大文件的时候就有优化。

假设也是打开 1G的文件,但是只先加载这一个屏幕中能显示出来的部分。

看到哪,加载到哪里。这样不会用空间上的浪费。

这就是 懒汉模式。

回过头,以上这些只是作为铺垫,真正要解决的问题是 实现一个线程安全的单例模式

接下来,我们来观察一下,上面讲到的两种单例模式谁是线程安全的。

现在,我们再来针对这里的懒汉模式代码,使它线程安全,

说到让一个代码线程安全,我们自然而然的就想到加锁!

但是问题就在于:在哪个地方加锁合适呢?

其实也很好观察,将 if 语句的执行操作 给 加锁,使其两个操作为原子性。

直白来说: 就是 if 语句 打包成“一个整体”,就跟前面分析 count++ 一样。

一致性执行完。

加锁范围 一定要包含 if 语句!!!

要不然没有效果,就像下面这样!

本来我们是想将 读 和 写 操作,打包成一个整体,

但是现在只是 针对写操作进行加锁,这时候就跟没加锁 一样,是没有区别的。

请大家注意!并不是代码中有 synchronized,一定就是线程安全的。

这需要看 synchronized 加的位置,也要正确。

所以 synchronized 写的位置。不能随便。

回过头来,我们再来看一下 synchronized 锁的对象写我们应该些什么。

虽然我们确实通过上述加锁操作,解决了 if 语句 的原子性问题。

但是!这样的程序,还存在这几个问题!

1、代码执行效率问题

再举一个形象的例子

2、指令重排序

虽然其他线程再调用 单例线程的时候,也是加了 synchronized 的。

减缓了循环速度,从而保证了 内存可见性。

但是!还有一个问题,来看下面。

此时,我们才完成一个线程安全的单例模式 - 懒汉模式

1、正确的位置加锁

2、双重if判定

3、volatile关键字

//单例模式 - 懒汉模式

class Singleton2{

//1、就不是立即初始化实例

private static volatile Singleton2 instance;// 默认值:Null

//2、把构造方法设为 private

private Singleton2(){};

//3、提供一个公开的方法,来获取这个 单例模式的唯一实例

public static Singleton2 getInstance(){

if(instance == null){

// 只有当我们真正用到这个实例的时候,才会真正去创建这个实例

synchronized(Singleton2.class){

if(instance == null){

最后

金三银四马上就到了,希望大家能好好学习一下这些技术点

学习视频:

大厂面试真题:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

决的问题是 实现一个线程安全的单例模式

接下来,我们来观察一下,上面讲到的两种单例模式谁是线程安全的。

现在,我们再来针对这里的懒汉模式代码,使它线程安全,

说到让一个代码线程安全,我们自然而然的就想到加锁!

但是问题就在于:在哪个地方加锁合适呢?

其实也很好观察,将 if 语句的执行操作 给 加锁,使其两个操作为原子性。

直白来说: 就是 if 语句 打包成“一个整体”,就跟前面分析 count++ 一样。

一致性执行完。

加锁范围 一定要包含 if 语句!!!

要不然没有效果,就像下面这样!

本来我们是想将 读 和 写 操作,打包成一个整体,

但是现在只是 针对写操作进行加锁,这时候就跟没加锁 一样,是没有区别的。

请大家注意!并不是代码中有 synchronized,一定就是线程安全的。

这需要看 synchronized 加的位置,也要正确。

所以 synchronized 写的位置。不能随便。

回过头来,我们再来看一下 synchronized 锁的对象写我们应该些什么。

虽然我们确实通过上述加锁操作,解决了 if 语句 的原子性问题。

但是!这样的程序,还存在这几个问题!

1、代码执行效率问题

再举一个形象的例子

2、指令重排序

虽然其他线程再调用 单例线程的时候,也是加了 synchronized 的。

减缓了循环速度,从而保证了 内存可见性。

但是!还有一个问题,来看下面。

此时,我们才完成一个线程安全的单例模式 - 懒汉模式

1、正确的位置加锁

2、双重if判定

3、volatile关键字

//单例模式 - 懒汉模式

class Singleton2{

//1、就不是立即初始化实例

private static volatile Singleton2 instance;// 默认值:Null

//2、把构造方法设为 private

private Singleton2(){};

//3、提供一个公开的方法,来获取这个 单例模式的唯一实例

public static Singleton2 getInstance(){

if(instance == null){

// 只有当我们真正用到这个实例的时候,才会真正去创建这个实例

synchronized(Singleton2.class){

if(instance == null){

最后

金三银四马上就到了,希望大家能好好学习一下这些技术点

学习视频:

[外链图片转存中…(img-KzRopZK5-1713696331674)]

大厂面试真题:

[外链图片转存中…(img-mNoU9WDT-1713696331674)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java) [外链图片转存中…(img-JretmB3q-1713696331675)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

参考阅读

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。