目录
前言
尽力的把涉及到的知识点全部写上,一两篇博客不太能总结的完,之后肯定还是要针对部分知识点详细说明的。
一丶什么是线程
前面对于类的加载过程和一个进程程该具有的属性也进行了说明。
进程是一个具有一定独立功能的程序在某个数据集合上的一次运行,他是操作系统分配资源的基本单位,也是基本的执行单位。
那么什么是线程呢?
线程是进程的一个实体,是CPU进行资源调度和分配的基本单位,它是一个比进程更小的能独立运行的一个基本单位。
那么线程和进程的区别是什么呢?
1.进程包含线程,也就是说一个进程可以包含多个线程,多个线程可以并发运行。
2.进程的状态改变会耗费很多的资源,所以线程的效率更高。(当然线程状态改变消耗资源也不少,但是相对于进程来说很少了)
3.线程基本上不含有系统资源,只有一些在程序运行中不可缺少的资源,比如程序计数器丶寄存器和栈等等。
4.线程可以和其他线程共享进程的全部资源
5.线程可以创建和撤销另外一个线程。
这里针对以上的第4点,需要再说明一下
一个进程占用的是独立的虚拟地址空间,而进程内的所有线程都是可以共享当前进程的内存的。
一个进程如果要访问另外一个进程,那么就需要使用通信的方式(这里有哪些通信方式上一篇博客也说了),这种方式代价比较大,但是同一个进程的线程,就可以直接使用共享变量。
一个进程如果挂了,那么不会影响其他的进程,但是线程如果挂了,就可能会影响到整个进程。比如说一个线程申请了太多的资源,那么就会严重影响到当前进程中的其他线程。
<1>关于进程状态
这就是进程的五个状态,当然这里目前我们使用五态模型,还有三态模型和七态模型,这里我们暂时不讨论。
创建状态:这里指的是进程在创建的时候,需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源的对应分配(比如说文件,I/O设备,内存等)。当申请的资源无法满足,也就是说它不能够被调用的时候,这个时候就是创建状态。
就绪状态:当我们在创建状态申请的资源被满足的时候,这个时候就是就绪态。也就是说这个状态就是除了CPU以外的资源全部获得,是一个在等待CPU的状态。
执行态:我们就绪态得到CPU了,就会进入执行状态
阻塞态:正在执行的进程因为某些原因(I/O请求,申请缓存区失败,)而停止运行,这个时候就会进程阻塞状态,在自身的请求被满足的时候就会重新进入就绪状态,然后等待分配CPU资源。
终止状态:进程结束,或者说进程出现了错误(不是异常!!),再或者说被系统终止,无法执行。
当然这里漏了一个点,就是执行态–>就绪态的转变
这里是因为CPU分配的时间片用完了,自身的资源是足够用的。或者说有更高优先级的进程出现了。
另外为了满足用户观察需要,还有挂起和激活两种操作。挂起后进程处于静止状态不再被系统调用,对于操作是激活操作。
在JAVA中,线程以轻量进程的方式实现,它也就具有进程的特征。它也需要系统CPU来执行
1.并发:一个CPU以时间片轮转的方式,依次执行多个程序
2.并行:多个CPU在同一时间片,同时执行多个线程。
<2>创建线程的方式
一个JAVA程序要是运行,就必须先有一个main方法的入口,先执行main方法(这里也会创建一个main进程)。
1>继承Thread类
首先继承Thread类来创建一个线程类(静态内部类)
private static class MyThread extends Thread{
@Override
public void run() {//run方法内,描述了线程要执行的任务
System.out.println("my thread run");
}
}
然后通过父类引用指向子类对象
public static void main(String[] args) {
Thread t = new myThread();
t.start();
}
接着一个线程就成功创建成功啦
2>实现Runnable接口
先实现Runnable接口
private static class myRunnable implements Runnable{
@Override
public void run() {
System.out.println("my thread run");
}
}
然后把对应的对象传入Thread的构造方法当中,当做target参数。
public static void main(String[] args) {
Thread t = new Thread(new myRunnable());
t.start();
}
然后线程就创建成功了。
3>其他方式
匿 名 内 部 类 − − 继 承 T h r e a d 类 \color{red}{匿名内部类--继承Thread类} 匿名内部类−−继承Thread类
这里本质还是继承Thread类
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("匿名内部类创建的线程正在运行");
}
};
t.start();
}
对应运行如下:
匿 名 内 部 类 − − 继 承 R u n n a b l e 接 口 \color{red}{匿名内部类--继承Runnable接口} 匿名内部类−−继承Runnable接口
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类创建的线程正在运行");
}
});
t.start();
}
运行如下:
l a m b d a 表 达 式 创 建 R u n n a b l e 子 类 对 象 \color{red}{lambda表达式创建Runnable子类对象} lambda表达式创建Runnable子类对象
public static void main(String[] args) {
Thread t = new Thread(() -> System.out.println("使用匿名内部类创建Thread子类对象"));
t.start();
}
运行如下:
其实还有一种创建方式是实现Callable接口。但是这种方式我们先不介绍,后面在说。
<3>多线程的优势
多线程的优势主要体现之一就是可以提升程序的运行效率。
这里我们使用一个程序来说明。
不 使 用 多 线 程 \color{red}{不使用多线程} 不使用多线程
这里我们记录一下程序执行的时间就好
long start = System.currentTimeMillis();
long end = System.currentTimeMillis();
System.out.printf("线程执行时间:%s",start - end);
然后执行什么操作呢?
public static void main(String[] args) {
//执行两次操作,每次循环加加十亿次
int a = 10_0000_0000;
//记录执行的时间
//返回这行代码执行的时候,从1970-01-01经过的毫秒数
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < 10_0000_1000; j++) {
//这里不需要任何代码,j已经++十亿次了
}
}
long end = System.currentTimeMillis();
System.out.printf("执行时间:%s",end-start);
}
接下里我们执行代码,看下运行时间:
使 用 多 线 程 \color{red}{使用多线程} 使用多线程
public static void main(String[] args) {
int num = 10_0000_0000;
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10_0000_0000; j++) {
}
}
});
}
while(Thread.activeCount() > 1){//activeCount返回活跃线程数
Thread.yield();//main就让步(从运行态转为就绪态)
}
long end = System.currentTimeMillis();
System.out.printf("线程执行时间:%s",end - start);
}
所以综上所述:
多线程能提高程序运行效率
当然这不是绝对的,有多个耗时任务在执行的时候,使用多线程当然可以节约时间。如果很简单的操作,那就没必要了,还浪费资源。
<4>多线程并发特性
我们这里还是用一段代码来进行解释:
public static void main(String[] args) {
//设计两个线程,每个循环10次,每次打印一个语句
for (int i = 0; i < 2; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
//Thread.currentThread()返回这行代码运行时候所在的线程引用
//Thread.getName();返回线程的名称
try {
String name = Thread.currentThread().getName();
for (int i = 0; i < 10; i++) {
Thread.sleep(10);
System.out.printf("线程:%s,执行第%s次\n", name, i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t" + i);//创建线程时候的第二个参数,表示线程名称
t.start();
}
}
这里执行完main线程之后,就会直接执行t.start(),不会等待就继续往下执行。
二丶Thread常用API
Thread有静态方法,也有实例方法。
<1>关于run()和start()
我们在上面通过run()方法成功的创建出来了一个线程,但是创建出来意味着线程成功运行了吗?
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
}
}
}, "t1线程");
//main线程执行完start就结束了:t1运行,main结束
t1.start();
首先我们先来看一下成功运行的例子,这里我们创建一个线程并且通过start成功运行该线程,接着通过jconsole
命令在控制台打开对应的线程查看窗口
这里可以看到,对应的线程已经成功运行,但是如果我们通过run()方法来运行一个线程呢?
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
}
}
}, "t1线程");
//main线程调用了thread对象的run方法(和普通方法调用没有啥区别):
// main线程运行,t1是没有创建的
t1.run();
}
这里我们再次查看
可以看到,main线程虽然调用了thread对象的run方法,但是在main线程运行结束之后,t1线程并没有成功创建。
<2>常用API以及守护线程
这里常用API如下
void run():定义线程需要执行的任务代码
void start():创建线程并且申请系统调度(转换为就绪态),如果调度到了,就进入运行态
,执行run方法的任务代码
static Thread currentThread():返回这行代码执行时的当前线程引用对象
String getName():返回线程名称
static void sleep(long millis):让当前线程休眠mills毫秒
static void yield():让当前线程让步(从运行态到就绪态)
关于以上这些方法,就不写例子一一演示。
接下来关于守护线程。那么什么是守护线程呢?
守护线程
讲解这个问题之前,要知道一个前置问题,那就是JVM在什么情况下可以正常退出呢?
在JDK官方文档是这样子说的
The Java Virtual Machine exits when the only threads running are all daemon threads.
也就是说,当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。
感觉有点麻烦?那么我们用代码来演示
public static void main(String[] args) {
Thread t =new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
System.out.println("正在运行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
System.out.println("主线程即将退出");
}
我们可以运行这段代码,看一下,JVM能不能正常退出呢?
可以看到因为有一个非守护线程一直在后台运行,那么JVM无法正常退出,那么如果说我们这个线程是一个守护线程呢?
public static void main(String[] args) {
Thread t =new Thread(new Runnable() {
@Override
public void run() {
while(true){
try {
Thread.sleep(1000);
System.out.println("正在运行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
//t线程作为守护线程,整个java进程,就没有处于活跃的非守护线程了
t.setDaemon(true);
t.start();
System.out.println("主线程即将退出");
}
运行如下
可以看到,当主线程退出时候,JVM也会随之退出,守护线程也会被回收,就算里面有一个死循环也不会碍事。
那么回到上面的问题,什么是守护线程?
守护线程就是有着自动结束自己生命周期特性的线程,而非守护线程不具备这个特性。
<3>中断一个线程
中断线程的方式主要有以下几种方式
1>自定义标志位
这里也就是自己设置一个标志位,用来表示是否是否需要被中断,然后每一次在线程执行的时候进行判断。
//自定义一个标志位
private static boolean isStop = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
//没有被中断就一直执行
while(!isStop){
//休眠一秒钟,打印一下
//但是如果说线程处于休眠/阻塞状态,就不会被中断
Thread.sleep(1000);
System.out.println("t run");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//让t线程运行三秒之后,再中断
Thread.sleep(3000);
isStop = true;
}
这里我们自己设置一个中断标志isStop,主线程睡眠三秒之后,再中断t线程。
但是这里是有一个问题的,如果说我们的t线程当前在休眠/阻塞状态,那么就不会被中断。
2>API
为了解决上述的问题,那么我们使用以下的三个API来解决
public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,
否则设置标志位
public static booleaninterrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位
对应使用如下:
//Thread对象中,内置了一个中断标志位(类似我们自定义的)
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
//当前线程,没有被中断,就一直执行。Thread.currentThread()表示获取当线程的对象
while (!Thread.currentThread().isInterrupted()){
Thread.sleep(10000);
System.out.println("t run");
}
//线程.sleep时,被中断,会抛出一个InterruptedException异常
} catch (InterruptedException e) {
//是否要中断,是有线程自己的代码决定
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
//main线程中,中断t线程
t.interrupt();
可以看到,当前代码和我们自己设置标志位时,唯一的不同之处就在于,我们把t线程的休眠时间改为了10000毫秒,如果是自己设置标志位,那么当主线程休眠时间过后去中断t线程之后,t线程就算处于休眠状态也会被中断。
那么问题又来了,如果说此时我说我不想要中断,或者说是忽略中断,那我怎么做呢?
//Thread对象中,内置了一个中断标志位(类似我们自定义的)
//t线程中断标志位=false
Thread t = new Thread(new Runnable() {
@Override
public void run() {
//当前线程,没有被中断,就一直执行
while (!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(100000);//休眠100秒,但休眠到3秒的时候,被中断
System.out.println("t run");
//3秒后,抛了一个异常,捕获到进入catch语句:此时会重置中断标志位=false
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
Thread.sleep(3000);
//main线程中,中断t线程: t线程中断标志位=true
t.interrupt();
那么来解释一下,为什么是忽略中断?
主要就在于这一行代码
catch (InterruptedException e) {
e.printStackTrace();
}
在这里抛异常了之后,是会重置中断标志位的,也就是会重置标志位为false。
3>join
join方法的含义是:中断当前正在执行的线程,然后去执行调用了join方法的线程,执行完调用join方法的线程之后,再沿着当前正在执行的线程往下执行。
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
//循环三次每次休眠一秒钟并且打印
for (int i = 0; i < 3; i++) {
System.out.println("t run" + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//t线程加入到当前线程,表示当前线程等待t线程执行完之后再执行后后边的。
t.join();
System.out.println("main run");
}
}
这里对应的运行结果如下:
4>关于线程状态的转换
这张图算是很详细的讲解了我们线程转换的方式以及状态,部分方法是我们上面提到的,还有部分是我们后边再讲的。
这里主要还是讲解一下部分状态。
1.new:安排了工作但是还没有启动
2.runnable:可工作的,又可以分为正在工作中和即将开始工作。
3.blocked:这几个都表示排队等着其他事情
4.waiting:这几个都表示排队等着其他事情
5.timed_waiting:这几个都表示排队等着其他事情
6.terminated:工作完成了
三丶关于线程安全问题
<1>产生线程不安全的原因
线程不安全是因为什么原因呢?
那么首先我还是想用一段代码先来进行演示
//多个线程使用同样的共享变量
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//把num循环++ 10000次
for (int i = 0; i < 10000; i++) {
num++;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//把num循环-- 10000次
for (int i = 0; i < 10000; i++) {
num--;
}
}
});
//++一万次
t1.start();
//--一万次
t2.start();
//让t1,t2线程执行完,按逻辑来说,num应该就是0
t1.join();
t2.join();
//打印的num值,不是预期的0,而是-10000到10000之间的随机数
System.out.println(num);
}
这里我们可以看到,我们定义了一个共享变量num,然后定义了一个t1线程对num进行++操作,定义了一个t2线程对num进行–操作。按道理来说,最终的结果应该就是0,但是事实上是0嘛?
当然,我们每一次运行程序的结果都是不相同的,这只是其中一次的运行结果。虽然结果不相同,但是每一次的运行结果都应该是在-10000到+10000之间。
那么回到刚开始的问题,为什么会产生线程不安全的原因?
在讲解之前,这里引入一下线程对num变量的操作流程:
<1>把内存的数据读取到cpu寄存器中
<2>接着对寄存器中的内容进行指令操作,比如++,–。然后把结果放在寄存器中
<3>把寄存器中的数据写入内存中。
1>原子性
首先什么是原子性呢?
如果说一组操作是不可拆分的最小执行单位,就可以表示这组操作是原子性的。但是如果说一个线程对某个共享变量的多次操作中存在其他并发并行线程对同一个共享变量的操作,那么就说不具备原子性。
在这里,对于上述线程对num的操作就是穿插执行,也就是说,在线程t1对num变量进行++操作的时候,t2线程正在从内存中杜村
2>线程的抢占式执行
线程执行具有随机性,由操作系统内核实现
3>可见性
我们上面两个线程同时操作一个内存,两个都在写,但是一个写线程读取到的是修改前的数据,也有可能读到修改后的数据,这是很不确定的。
内存可见性的本质是编辑器优化导致了一个写线程修改的数据没有及时的写入内存当中,这样另外一个线程就读取不到最新的数据。
所以如果要解决这个问题,那么就禁止编辑器优化好了,虽然慢一些,但是准确率高了。
4>有序性
为了提高程序的执行效率,调整了执行的顺序(调整的目的是为了提高效率,不改变逻辑)。这里如果是单线程,那么不会出现问题,但是如果是多线程,那么就会出现问题。
<2>解决线程不安全问题
1>解决方式一:使用synchronized关键字
第一种方法就是对某一段代码使用synchronized关键字。那么现在就对这个关键字的用法进行具体的解析。
修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method() {
//业务代码
}
修饰静态方法
这就是给当前类加锁,作用于所有类的对象。进入同步代码前要获得当前class的锁。因为静态成员不属于任何一个实例对象,是类成员。所以如果一个线程A调用一个实例对象的非静态方法synchronized方法,而线程B调用这个实例对象所属的类的静态synchronized方法,这是允许的,不会发生互斥现象。
synchronized void staic method() {
//业务代码
}
修饰代码块
指定加锁对象,对给定对象/类加锁,synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
synchronized(this) {
//业务代码
}
所以综上所述,我们使用synchronized给对应代码块加锁。
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//把num循环++ 10000次
for(int i=0; i<10000; i++){
//也可以写一个synchronized同步方法,调用该方法
synchronized (线程安全问题_synchronized.class) {
num++;
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//把num循环-- 10000次
for(int i=0; i<10000; i++){
synchronized (线程安全问题_synchronized.class) {
num--;
}
}
}
});
//++一万次
t1.start();
//--一万次
t2.start();
//让t1,t2线程执行完,按逻辑来说,num应该就是0
t1.join();
t2.join();
//打印的num值,不是预期的0,而是-10000到10000之间的随机数
System.out.println(num);
}
最后运行结果如下
原理
synchronized 通过当前线程持有对象锁,从而拥有访问权限,而其他没有持有当前对象锁的线程无法拥有访问权限,保证在同一时刻,只有一个线程可以执行某个方法或者某个代码块,从而保证线程安全。
synchronized 同步语句块的实现是显式同步的,通过 monitorenter 和 monitorexit 指令实现,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置,当执行 monitorenter 指令时,当前线程将尝试获取 objectref(即对象锁)所对应的 monitor 的持有权:
1.当对象锁的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。
2.如果当前线程已经拥有对象锁的 monitor 的持有权,那它可以重入这个 monitor,重入时计数器的值也会加1。
3.若其他线程已经拥有对象锁的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit 指令被执行,执行线程将释放 monitor 并设置计数器值为0,其他线程将有机会持有 monitor。
synchronizde特性
1. 互 斥 \color{red}{1.互斥} 1.互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到
同一个对象 synchronized 就会阻塞等待
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
2. 刷 新 内 存 \color{red}{2.刷新内存} 2.刷新内存
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
3. 可 重 入 \color{red}{3.可重入} 3.可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
2>解决方式二:使用volatile
使用volatile是用来修饰一个关键字的,它的作用就是保证线程的可见性和有序性。
但是这里不保证原子性。什么意思呢?
就是在我们上面的《产生线程不安全的原因》这一部分,就算我们给变量num加上volatile关键字,他也是不安全的,最后输出的值也不会是0。
一般来说,volatile使用场景如下:
1.读操作:读操作本身是原子性,所以使用volatile肯定是线程安全的
2.写操作:赋值操作是一个常量值,也可以保证线程安全。
所以之前在我们中断线程时自己设置标志位应该加上volatile才是线程安全的。
四丶多线程案例–单例模式
我们在使用多线程的时候,需要考虑什么呢?
1.提高效率:多线程作用就是充分利用cpu资源,提高任务的执行效率
2.线程安全:线程安全,多线程程序的底线。
也就是说,设计多线程程序代码的时候,在满足多线程安全的前提下,我们提高任务的效率就好。
所以我们一般的设计方式就是以下几种
1.加锁细粒度化(加锁的代码少一点,让部分代码可以并发执行)
2.没有共享变量操作的代码,没有多线程安全问题
3.共享变量的读,使用volatile关键字。
4.共享变量的写,加锁就好
那么这里我们介绍一种设计模式–单例模式,什么是单例模式呢?
单例模式能保证某个类在程序当中只存在唯一一份实例,而不会创建出多个实例。
具体实施的话,这里介绍两种方式。
<1>饿汉式
饿汉式就是在类记载的时候,直接就创建实例对象。
public class 单例模式_饿汉式 {
private static 单例模式_饿汉式 instance = new 单例模式_饿汉式();
public static 单例模式_饿汉式 getInstance(){
return instance;
}
}
写法就是静态变量 = new 对象
,多线程的话也会满足线程安全,这里JVM内部使用了加锁来保证,即使多个线程调用静态方法,静态变量也只会在类加载的时候执行并且只会执行一次。
<2>懒汉式–单线程
下面这种写法只用于单线程,不能用于多线程,如果是多线程,那么会出现线程安全问题。
public class 单例模式_懒汉式 {
private static 单例模式_懒汉式 instance = null;
public static 单例模式_懒汉式 getInstance(){
if(instance == null) instance = new 单例模式_懒汉式();
return instance;
}
}
<3>懒汉式–多线程
这里这种写法适用于多线程,但是效率很低
public class 单例模式_懒汉式_多线程效率低 {
private static 单例模式_懒汉式_多线程效率低 instance = null;
public synchronized static 单例模式_懒汉式_多线程效率低 getInstance(){
if(instance == null) instance = new 单例模式_懒汉式_多线程效率低();
return instance;
}
}
这里针对于上部分就是在这里加了一个锁,也就是说,多个线程同时调用这个方法的时候,只能有一个线程进入这个方法。
但是为什么说效率低呢?
1.在instance == null的时候,如果多个线程调用getInstance()方法,需要保证线程安全,也就是说需要保证一个线程去写,其他线程只能读
2.在instance已经赋值给一个创建好的对象以后,可能还会有多个线程调用getInstance()
那么我们怎么解决上述问题呢?
<3>懒汉式–双重检验锁
为了解决上述问题,那么针对第一种情况
加锁
为了针对第二种情况
使用volatile关键字
因为volatile它并不会加锁
public class 单例模式_懒汉式_双重校验锁 {
private static volatile 单例模式_懒汉式_双重校验锁 instance = null;
public static 单例模式_懒汉式_双重校验锁 getInstance(){
//第1个情况,instance=null, 多个线程会进入
if(instance == null) {//保证第2个情况,已经实例化对象,不需要再次竞争锁
//第1个情况,多个线程进入到这行代码,一个线程竞争成功,执行同步代码,其他线程竞争失败阻塞
synchronized (单例模式_懒汉式_双重校验锁.class){
if(instance == null) {
instance = new 单例模式_懒汉式_双重校验锁();
}
}
}
return instance;
}
}
这里面主要的问题就是,里面的if语句判断是否有存在的必要呢?
要回答这个问题,我们就要理解这个if判定做了什么以及单例模式的设计目的。
加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候。 因此后续使用的时候, 不必再进行加锁了。外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.。同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile 。当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作。当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例
所以为了满足单例模式,防止其他线程创建多个实例对象,就需要两个if来进行判断。