设计模式
设计模式(Design Patterns),是指在软件设计中,被反复使用的一种代码设计经验
所谓的设计模式其实就是计算机前辈们根据一些常见的需求场景, 整理出来的一些对应的解决办法, 类似于棋谱, 只要照着这些设计模式写,不至于很好,但是也不至于写的很差
设计模式有很多, 本文主要是关于单例模式 和 工厂模式
单例模式
单例模式 : 只能创建一个实例(instance)的设计模式, 只能创建一个实例这是由需求决定的
之前学的类与对象,一个类可以创建多个对象, 举个例子: 假设车是一个类, 那么小轿车可以是一个对象, 火车可以是一个对象…
之前学习的JDBC编程创建的DataSource类也是只会创建出一个实例
单例模式,本质上就是借助编程语言自身的语法特性, 强行限制某一个类,只能创建一个实例
在Java中, 存在一个天然的只能有一个实例的东西,那就是static
static 成员/属性 就变成了类成员/类属性,此时就只有一份了
更加具体地说, 类创建的对象就是类对象 , 类对象是由JVM加载.class文件来的, 而JVM加载.class文件只会加载一次, 也就是会有一个类对象, 类对象里面的static修饰的成员/属性也就只有一份了
饿汉模式
要是在main中有创建出一个实例,就不是单例模式了, 所以要想办法禁止在类外面创建新的实例
package Threading;
class Singleton{
private static Singleton instance = new Singleton();//进行实例化,此时的实例是私有的&&是属于类的,只有一份
public static Singleton getInstance() {
return instance;
}
private Singleton(){ //私有的构造方法
}
}
public class Demo20 {
public static void main(String[] args) {
//要想禁止在内外面创建新的实例,只要将类的构造方法设为私有的就行了
Singleton instance = Singleton.getInstance();//这样子才能得到instance
}
}
以上的代码 在Singleton类中在类加载阶段就创建了实例 , 这就是单例模式中的饿汉模式
懒汉模式
在单例模式中还有一个方式—懒汉模式
package Threading;
class SingletonLazy{
private static SingletonLazy instance = null;//先不实例化
public static SingletonLazy getInstance () {
if(instance == null){ //首次调用getSingleLazy时,才会实例化
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){ //私有的构造方法禁止类外创建出新的实例
}
}
public class Demo21 {
public static void main(String[] args) {
}
}
要是没有调用过getSingleLazy 就不会创建出实例, 或者就算是调用了getSingleLazy,在程序刚刚开始启动的时候, 系统资源比较紧张,创建实例的时机靠后,也能提高效率
以上两种代码哪一种是线程安全的?哪一种是线程不安全的?
所谓的线程安不安全就是说调用getInstance的时候, 会不会有问题
饿汉 只涉及到了读取操作
懒汉 既有读取操作,又有修改操作, 要是多个线程同时修改同一个变量就会出现线程不安全
要想解决懒汉模式线程不安全的情况,就要加锁
package Threading;
class SingletonLazy{
private static SingletonLazy instance = null;//先不实例化
public static SingletonLazy getInstance () {
synchronized (SingletonLazy.class) { //锁对象是类对象
if (instance == null) { //首次调用getSingleLazy时,才会实例化
instance = new SingletonLazy();
}
return instance;
}
}
private SingletonLazy(){
}
}
public class Demo21 {
public static void main(String[] args) {
}
}
加上锁就能够保证线程安全,但是这样写的话, 每次都要加锁, 加锁的开销是比较大的(加锁会涉及到用户态–>内核态之间的切换,这样的成本是比较高的),也就是说,加锁虽然能保证线程安全,但是每次都要加锁资源开销就会比较大
package Threading;
class SingletonLazy {
private static SingletonLazy instance = null;//先不实例化
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) { //锁对象是类对象
if (instance == null) { //首次调用getSingleLazy时,才会实例化
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo21 {
public static void main(String[] args) {
}
}
此时就可以在前面再加一个 if 判断,来确定是不是要加锁
这两个if 是不一样的含义
这两个if中间隔着一个加锁操作,加锁就意味着锁竞争, 锁竞争就意味着可能会导致阻塞,一旦阻塞,就不知道什么时候能唤醒,所以两个if的结果可能是完全不一样的, 第一个if成立,不一定第二个if能成立
第一个if : 判定是否要加锁
第二个if : 判定是否要创建线程
public static SingletonLazy getInstance() {
if (instance != null) {
return instance;
}
synchronized (SingletonLazy.class) { //锁对象是类对象
if (instance == null) { //首次调用getSingleLazy时,才会实例化
instance = new SingletonLazy();
}
}
return instance;
}
这样写也是可以的
但是,上面的代码还是有问题的, 在new 对象的时候, new 操作也是分为三个步骤的
new 对象的三个操作:
- 申请内存, 得到内存地址
- 调用构造方法, 来初始化实例
- 把内存的首地址赋值给instance 引用
2 3 两个步骤可能会调换顺序,单线程的时候没事,多线程的时候,要是先执行3 再执行2 , 得到的就是对象只有内存, 内存上的数据无效, getInstance 就会认为对象非空, 直接就返回了,这就是指令重排序问题
要想要禁止指令重排序, 可以使用volatile
其实,多线程的时候, 有的线程在读,有的线程在修改,此时也是有可能出现内存可见性问题的,也是要使用volatile
package Threading;
class SingletonLazy {
private volatile static SingletonLazy instance = null;//先不实例化
public static SingletonLazy getInstance() {
if(instance == null){
synchronized (SingletonLazy.class) { //锁对象是类对象
if (instance == null) { //首次调用getSingleLazy时,才会实例化
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo21 {
public static void main(String[] args) {
}
}
以上的代码就是一个完整的单例模式的懒汉实现(十分重要)
主要的要点有三个:
- synchronized 加在哪里
- 为什么要加两个if , 两个if 的含义
- 为什么要加volatile
线程安全版本的单例模式一定要会写!!!