目录
一.上节内容
1.什么是线程安全
2.线程不安全的原因
3.JMM(Java内存模型)
4.synchronized锁
5.锁对象
6.volatile关键字
7.wait()和notify()
8.Java中线程安全的类
二.单例模式
1.什么是单例
2.怎么设计一个单例
1.口头约定
2.使用编程语言的特性
三.饿汉模式
四.懒汉模式
1.单线程下的懒汉模式
2.多线程下的懒汉模式
3.分析线程不安全的原因
4.加锁解决这个问题
5.双重检查锁
6.加volatile优化(单例懒汉模式最终版)
一.上节内容
上节内容指路:Java之线程安全
1.什么是线程安全
在多线程环境下程序运行的结果与单线程环境下程序运行的结果不一样(不达预期)
2.线程不安全的原因
1.多个变量对一个共享变量做修改
2.线程是抢占式执行的,CPU的调度是随机的
3.原子性:代码没有一次性的执行完毕
4.内存可见性:线程1修改了共享变量的值,线程2不能及时获取到最新的值
5.有序性:由于指令重排序,指令无法按照书写的顺序进行执行.
3.JMM(Java内存模型)
JMM可以解决原子性,内存可见性,有序性的问题
1.主内存,进程启动时在主内存中申请内存资源,也就是内存条,用来保存所有的变量
2.工作内存,对应CPU中的缓存,每个线程都有自己的工作内存,工作内存互不影响,线程之间起到了内存隔离的效果
3.JMM规定,一个线程更新共享变量值的时候,必须先从主内存中加载到变量值到对应线程的工作内存中,修改完成后再将工作内存中的值刷新到主内存中.
4.synchronized锁
1.加锁之后可以将多线程(并发执行)变成单线程(串行执行). ---解决了原子性
2.由于是单线程执行,线程2读到的值一定是线程1修改过的值 ---解决了内存可见性
3.不能解决有序性
synchronized的使用
1.可以修饰普通的方法 ---锁对象是new出来的对象(当前对象,this)
2.可以修饰静态的方法 ---锁对象是类对象,类名.class
3.可以修饰代码块 ---锁对象是指定的
5.锁对象
1.任何对象都可以充当锁对象,类对象,普通对象
2.锁对象的对象头中的markword区域记录当前争抢到锁的线程信息
3.多线程环境下判断是否发生锁竞争,只需要判断是否争抢的是一把锁.
6.volatile关键字
1.可以解决内存可见性 MESI缓存一致性协议和内存屏障
2.可以解决有序性
3.不能解决原子性
4.只能修饰变量
原子性 | 可见性 | 有序性 | |
synchronized | Y | Y | N |
volatile | N | Y | Y |
7.wait()和notify()
1.wait()是让线程进入休眠状态,notify()是唤醒等待的线程,都是Object中的类
2.wait()和notify()都会释放锁资源,wait()和notify()要对于同一个锁搭配使用.
面试题:说一下wait()和sleep()的区别
1.本质上都是让线程等待,但是两个方法没什么关系
2.wait()是Object类中定义的方法,sleep()是Thread类中定义的方法
3.wait()必须与synchronized搭配使用,调用之后会释放锁.sleep()只是让线程进入堵塞等待,和锁没有什么区别
8.Java中线程安全的类
线程安全的类:Vector (不推荐使用) Stack HashTable (不推荐使用) ConcurrentHashMap
StringBuffer
线程不安全的类:ArrayList LinkedList HashMap TreeMap HashSet TreeSet StringBuilder
二.单例模式
1.什么是单例
单例模式是一种设计模式(设计模式:就是在特定的场景下,解决问题最优的方式,类似于棋谱),单例:顾名思义,全局只有一个实例对象
2.怎么设计一个单例
1.口头约定
对外提供一个方法,规定使用者只能通过这个方法来获取这个类的实例对象(不靠谱,有些人就是new出来对象,不采用你这个方法获取对象)
2.使用编程语言的特性
Java中哪些对象时全局唯一的?
.class对象,比如Object.class 类对象
使用static修饰的属性,所有的实例对象访问的都是同一个类属性.
因此我们可以通过类对象和static配合的方式实现单例
实现单例有两种模式,饿汉模式和懒汉模式
三.饿汉模式
类的加载就完成初始化
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
优点:书写简单,不容易出错
四.懒汉模式
1.单线程下的懒汉模式
避免程序启动的时候浪费过多的系统资源,当程序使用这个对象的时候再对这个对象进行初始化
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
//在获取单例对象的时候,判断是否已经被创建,没有创建则创建
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
其实也不难写出,只需要在将instance初始化为null,当需要获取实例(getInstance)的时候,判断instance是否为null,为null就创建对象,否则直接返回.
这样我们就实现了懒汉模式的单例吗?当然以上的程序在单线程的情况下是正确的,但是多线程的情况下会出现一些问题.
2.多线程下的懒汉模式
模拟多线程的情况下通过以上程序获取实例对象:
public class Demo02_SingletonLazy {
public static void main(String[] args) {
//创建是个线程
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(() -> {
//获取实例对象并打印
SingletonLazy instance = SingletonLazy.getInstance();
System.out.println(instance);
});
thread.start();
}
}
}
打印的结果:
由打印的结果可以看出,发现获取到的对象不一定都是同一个对象,不符合我们的预期,发生了线程不安全的现象
3.分析线程不安全的原因
刚开始的时候instance=null;
可以观察出来,仅有两个线程的时候,就有可能有两个不同的对象.
其实我们仔细思考可以想出具有不同的对象只会发生没有对象的初期,后期对象不为空的时候,多个线程再进来,对象也不会发生改变了,但我们要求的是单例,所以我们要完全满足单例模式!
为了解决这个问题,我们可以给这个加锁
4.加锁解决这个问题
我们在if循环外面加上锁
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
synchronized (SingletonLazy.class) {
//在获取单例对象的时候,判断是否已经被创建,没有创建则创建
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
}
打印结果:
我们考虑以下,是否能将synchronized包裹的代码块发到if语句的下面
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
//在获取单例对象的时候,判断是否已经被创建,没有创建则创建
if (instance == null) {
synchronized (SingletonLazy.class) {
instance = new SingletonLazy();
}
}
return instance;
}
}
打印的结果:
是不可以的!Load和CMP操作没有原子执行,线程1加载到instace=null,线程2加载到的instace=null,所以线程1和线程2都是new出来一个新的SingletonLazy对象,所以要扩大加锁的范围
进行修改之后看起来就是正确的了,但是不高效.
5.双重检查锁
前面我们已经分析了,线程不安全的线程只存在于前期,也就是instace=null进入到方法的时期,instance!=null的时候是不会发生线程不安全的现象的,但是我们之前写的代码,不论在什么时期都要参与锁竞争,而锁竞争是十分耗费系统资源.
用户态:Java层面,JVM中执行的代码
内核态:执行的是CPU指令,加入synchronized后参与锁竞争就从用户态到内核态,而内核态执行效率没有用户态执行效率高.
来举一个现实中的例子:拿批改作业为例,同学们可以自己批改作业(用户态),也可以上交给老师进行批改(内核态),自己批改效率是很高的,但是交给老师,老师可能要备课,也要批改别人的作业,所以完成你作业的批改是十分不高效的.
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
if (instance == null) {
//在获取单例对象的时候,判断是否已经被创建,没有创建则创建
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
这种用双重if判断的方式叫做双重检查锁,两层if语句判断的目的不一样,外层if是判断是否为空,避免参与锁竞争,内层if判断是否为空,为instance进行赋值.
初始状态instance=null,外层if判断为true,进入,此时三个线程参与锁竞争,线程1拿到了锁,线程2和线程3处在堵塞状态,线程1内层if判断为true,执行instance的赋值操作,之后释放锁资源,返回赋值之后的对象,线程2拿到了锁资源,内层if判断为false,释放锁资源,返回instance,线程3拿到锁资源后,和线程2一样.
之后instance!=null,外层if判断为false,直接返回instance,不要参与锁竞争,提高了效率.
现在已经做到了确保原子性,内存可见性,但是还没有确保有序性,下面我们做进一步的优化
6.加volatile优化(单例懒汉模式最终版)
给共享变量instance加volatile关键字,可以避免指令重排序问题.
public class SingletonLazy {
private static volatile SingletonLazy instance = null;
private SingletonLazy() {
}
public static SingletonLazy getInstance() {
if (instance == null) {
//在获取单例对象的时候,判断是否已经被创建,没有创建则创建
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
懒汉模式的优点:节约资源.