单例模式(创建型)
饿汉式
饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。
该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。
package com.zbz.设计模式;
public class Hungry {
/*
* 缺点:直接创建一个静态的对象,可能造成内容消耗过大
* */
private byte[] b1=new byte[1024*1024];
private byte[] b2=new byte[1024*1024];
private byte[] b3=new byte[1024*1024];
private byte[] b4=new byte[1024*1024];
//私有构造类,阻止实例的生成
private Hungry(){
}
public static final Hungry hungry=new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
解析:
私有构造器
所谓私有构造器,就是用private关键字声明的构造器。
其访问权限是private,它只能被类自身所访问,而无法在类的外部调用,故而可以阻止对象的生成。
所以,如果一个类只有一个私有构造器,而没有任何公有构造器,是无法生成任何对象的。
那么无法生成对象的带有私有构造器的类究竟有什么作用呢?
这样的类在实际应用中最常用的是作为工具类,如字符串的验证、枚举转换之类的,通常只做成静态接口被外部调用即可。
静态方法是用static关键字声明的方法,可以用类来直接调用而无需用从类中实例化出来的具体对象来调用,因此这样的方法也被称为类方法。static方法只能访问类中的static字段和其他的static方法,这是因为非static成员必须通过对象的引用来访问。
1、我们要知道静态属性是在初始化对象(new Hungry())第一时间初始化的:
2、对象的内存布局
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充数据。
3、new Hungry()背后操作系统做的事
指令1:分配内存空间
指令2:执行构造方法,初始化对象
指令3:把这个对象指向这个空间
但是cpu允许 132这样的执行顺序,所以多线程情况下:
A线程执行了132 B线程来的时候对象正在初始化中,此时对象是不为空的,所以就会被拿去用。
并发编程volatile
Java内存结构
Java的内存结构就是之前在学习Java虚拟机时的内存区域的划分。
绿色代表线程私有的内存区域,紫色的代表所有线程共享的内存区域。
Java内存结构在这里就不展开了,在JVM那里在详细说,这里提起是要说明哪些区域是线程私有的,哪些是线程共享的,还有就是和JMM区分开来。
JMM
JMM 是 JVM 中定义的一种并发编程的底层模型机制。
JMM,全称为Java Memory Model
,Java内存模型。不要和Java内存结构搞混了。
Java内存模型是一组规范或者说是规则,每个线程执行都要遵循这个规范,是用来解决在线程的通信问题的。
JMM是一种规范,是一个抽象的概念,并不真实存在,内存结构才是真实存在的。
在讲解JMM之前先要理解两个概念,主内存和工作内存。
主内存
主内存是Java运行时在计算机存储数据的地方,是所有线程共享的,同时多个线程对这个主内存进行修改,就会出现很多的问题,这就是并发操作的问题,需要我们去解决。
工作内存
每个线程都有一个存储数据的地方,用来存储线程需要操作的数据,为什么要这样呢?
因为线程是不能直接对主内存中的数据进行修改的,只能修改线程工作内存中的数据,所以线程修改主内存中的数据时就会将主内存中的数据保存在自己的工作内存,然后在进行操作。
这样就会存在一个问题,每个线程都会对自己的工作内存进行操作,所以每一个线程都无法得知其他线程工作内存中的数据是怎么样的,这就是一个可见性的问题。
JMM的抽象结构
JMM 的规定:
- 所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
以下是JMM对主内存数据操作时会执行的八个操作。(按顺序)
1、lock
将主内存中的数据变量标识为线程独占状态,即对该变量进行加锁操作,其他线程不能对其操作。2、read
读取主内存中需要修改的变量,即上个经过加锁操作的变量。3、load
将读取到的数据变量载入到线程的工作内存之中。
4、use
把工作内存中的变量传输给执行引擎,即对该变量进行操作。
5、assign
执行引擎对变量进行操作之后,将得到的变量的值放回工作内存。6、store
将线程工作内存中的变量存储好。7、write
将上述存储的变量写入主内存,实现刷新主内存中的值。8、unlock
将该变量的锁释放,使其能让其他线程进行操作。
JMM的三个特性
Java内存模型就是为了解决对共享数据中的可见性,原子性和有序性问题的一组规则。
即JMM的存在就是为了 保证这三个特性,现在具体来看看这三个特性。
可见性
可见性刚刚也讲工作内存的时候也是有提到的,这个其实很好理解,每个线程中的工作内存经过修改写回主内存之后,其他线程都可以看见主内存中的值发生变化,从而解决一些缓存不一致的情况。
原子性
原子性表示一个操作在执行中是不可以被中断的,有点类似事务的原子性,要么成功完成,要么直接失败。
有序性
有序性表示JMM会保证操作是有序执行的。或许有人会感到疑惑,难道程序不都是有序执行的吗?
这就要说到处理器的指令重排了,这涉及到了一些汇编的知识,所以不怎么展开了,大概了解一下。
为了提高CPU的使用率,在程序编译执行的时候,处理器会将指令进行重排优化,一般分为以下三个。
- 编译器优化的重排
- 指令并行的重排
- 内存系统的重排
指令重排使得语句不一定是按从上到下执行的,可能会是乱序执行的,有些语句是存在数据依赖性的才会保持前后顺序。
为什么单线程的时候没有感觉呢?
这是因为指令重排不会干扰到单线程执行的结果的,但是在多线程中乱序执行就会出现一些问题,导致得到的结果不一样。
多线程下的变量不可见现象
JMM 的规定可能会导致线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。
内存可见性
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。
package com.zbz.设计模式;
public class visibility {
public static void main(String[] args) {
MyThread th=new MyThread();
th.start();
while (true){
if(th.isFlag()){//th.start子线程启动1s后才设置flag=true
//主线程一直处于while死循环,但是并不能进入if
System.out.println("进入while---");
}
}
}
}
class MyThread extends Thread{
private boolean flag=false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
//模拟在flag=true执行前执行其他代码花费的时间
try {
Thread.sleep(1000);//1s
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=true;
System.out.println("flag="+flag);
}
}
可见性问题的解决方案
我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。
这里有两种方案:加锁 和 使用 volatile 关键字。
加锁synchronized:
package com.zbz.设计模式;
public class visibility {
public static void main(String[] args) {
MyThread th=new MyThread();
th.start();
while (true){
//**这里大家应该有个疑问是,为什么加锁后就保证了变量的内存可见性了?**
synchronized(th) {//加锁
if (th.isFlag()) {//th.start子线程启动1s后才设置flag=true
System.out.println("进入while---");
}
}
}
}
}
class MyThread extends Thread{
private boolean flag=false;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
//模拟在flag=true执行前执行其他代码花费的时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=true;
System.out.println("flag="+flag);
}
}
这里大家应该有个疑问是,为什么加锁后就保证了变量的内存可见性了?
因为当一个线程进入 synchronized 代码块后,
线程获取到锁,
会清空本地内存,
然后从主内存中拷贝共享变量的最新值到本地内存作为副本,
执行代码,又将修改后的副本值刷新到主内存中,
最后线程释放锁。
这里除了 synchronized外,其它锁也能保证变量的内存可见性。
使用 volatile 关键字
使用 volatile 修饰共享变量后,每个线程要操作变量时
会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。
package com.zbz.设计模式;
public class visibility {
public static void main(String[] args) {
MyThread th=new MyThread();
th.start();
while (true){
if (th.isFlag()) {//th.start子线程启动1s后才设置flag=true
//主线程一直处于while死循环,但是并不能进入if
System.out.println("进入while---");
}
}
}
}
class MyThread extends Thread{
private volatile boolean flag=false; //volatile关键字
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
//模拟在flag=true执行前执行其他代码花费的时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=true;
System.out.println("flag="+flag);
}
}
总线嗅探机制
为了提高处理速度,CPU 不直接和内存进行通信,而是在 CPU 与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了 CPU 运算速度和内存读取速度不一致问题。
由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而嗅探是实现缓存一致性的常见机制。
处理器内存模型
注意,缓存的一致性问题,不是多处理器导致,而是多缓存导致的。
嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
注意:基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起总线风暴。所以,volatile 的使用要适合具体场景。
可见性问题小结
上面的例子中,我们看到,使用 volatile 和 synchronized 锁都可以保证共享变量的可见性。相比 synchronized 而言,volatile 可以看作是一个轻量级锁,所以使用 volatile 的成本更低,因为它不会引起线程上下文的切换和调度。但 volatile 无法像 synchronized 一样保证操作的原子性。
volatile原子性问题
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
要解决线程不安全的这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。
特别说明:
对任意单个使用 volatile 修饰的变量的读 / 写是具有原子性,但类似于 flag = !flag
这种复合操作不具有原子性。简单地说就是,单纯的赋值操作是原子性的。
package com.zbz.设计模式;
//创建100个线程,每个线程将共享数据count累加10000次 count最终结果应该为100000
public class Atomicity {
public static void main(String[] args) {
Runnable target=new MyThread0();
for (int i=1;i<=100;++i){
new Thread(target,"第"+i+"个线程").start();
}
}
}
class MyThread0 implements Runnable{
private int count=0; //共享变量
@Override
public void run() {
for(int i=1;i<=1000;i++){
++count;
System.out.println(Thread.currentThread().getName()+"--->count: "+count);
}
}
}
结果是并不能够加到100000
浅析:线程A和线程B都拿到了count,A拿到count时的值100,B同时也拿到count的值也为100,并“同时修改”count的值为101,并同时覆盖进主内存
加上volatile
结果一样,所以在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
解决办法
要解决线程不安全的这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。
package com.zbz.设计模式;
//创建100个线程,每个线程将共享数据count累加10000次 count最终结果应该为1000000次
public class Atomicity {
public static void main(String[] args) {
Runnable target=new MyThread0();
for (int i=1;i<=100;++i){
new Thread(target,"第"+i+"个线程").start();
}
}
}
class MyThread0 implements Runnable{
private int count=0;
@Override
public void run() {
synchronized (MyThread0.class) { //加锁
for (int i = 1; i <= 1000; i++) {
++count;
System.out.println(Thread.currentThread().getName() + "--->count: " + count);
}
}
}
}
volatile禁止指令重排序
指令重排序
为了提高性能,在遵守 as-if-serial
语义(即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守。)的情况下,编译器和处理器常常会对指令做重排序。
一般重排序可以分为如下三种类型:
- 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
从 Java 源代码到最终执行的指令序列,会分别经历下面三种重排序:
重排序顺序
为了更好地理解重排序,请看下面的部分示例代码:
int a = 0;
// 线程 A
a = 1; // 1
flag = true; // 2
// 线程 B
if (flag) { // 3
int i = a; // 4
}
单看上面的程序好像没有问题,最后 i 的值是 1。
为了提高性能,编译器和处理器常常会在不改变数据依赖的情况下对指令做重排序。
假设线程 A 在执行时被重排序成:先执行代码 2,再执行代码 1;
而线程 B :在线程 A 执行完代码 2 后,读取了 flag 变量。
由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,那么 i 最后的值是 0,导致执行结果不正确。
**那么如何程序执行结果正确呢?**这里仍然可以使用 volatile 关键字。
这个例子中, 使用 volatile 不仅保证了变量的内存可见性,还禁止了指令的重排序,即保证了 volatile 修饰的变量编译后的顺序与程序的执行顺序一样。那么使用 volatile 修饰 flag 变量后,在线程 A 中,保证了代码 1 的执行顺序一定在代码 2 之前。
volatile 是如何禁止指令重排序的呢?
内存屏障指令
编译器不会对 volatile 读与 volatile 读后面的任意内存操作重排序;编译器不会对 volatile 写与 volatile 写前面的任意内存操作重排序。
内存屏障
是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。
JMM 把内存屏障指令分为下列四类:
StoreLoad 屏障是一个全能型的屏障,它同时具有其他三个屏障的效果。所以执行该屏障开销会很大,因为它使处理器要把缓存中的数据全部刷新到内存中。
下面我们来看看 volatile 读 / 写时是如何插入内存屏障的,见下图:
从上图,我们可以知道 volatile 读 / 写插入内存屏障规则:
- 在每个 volatile 读操作的后面插入 LoadLoad 屏障和 LoadStore 屏障。
- 在每个 volatile 写操作的前后分别插入一个 StoreStore 屏障和一个 StoreLoad 屏障。
happens-before
什么是happens-before
happens-before不会影响指令重排,是在排好序的前提下保证可见性
happens-before规则
**程序次序规则:**在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变。
**管程锁定规则:**就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
**volatile变量规则:**就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
**线程启动规则:**在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。(注意:但是线程A之后对共享变量的修改结果对线程B不一定可见)
**线程终止规则:**在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。也称线程join()规则。
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
**传递性规则:**这个简单的,就是happens-before原则具有传递性,即hb(A, B) , hb(B, C),那么hb(A, C)。
**对象终结规则:**这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
volatile写读建立的happens-before
问题:
package com.zbz.设计模式;
public class HappensBefore {
private int a=1;
private int b=2;
public void write(){
a=3;
b=a;
}
public void read(){
System.out.println("b="+b+",a="+a);
}
public static void main(String[] args) {
HappensBefore hb=new HappensBefore();
new Thread(new Runnable() {//线程1
@Override
public void run() {
hb.write();//写
}
}).start();
new Thread(new Runnable() {//线程2
@Override
public void run() {
hb.read();//读
}
}).start();
}
}
package com.zbz.设计模式;
public class HappensBefore {
private int a=1;
private volatile int b=2; //加上 volatile关键字
public void write(){
a=3;
b=a;
}
public void read(){
System.out.println("b="+b+",a="+a);
}
public static void main(String[] args) {
HappensBefore hb=new HappensBefore();
while(true){
new Thread(new Runnable() {
@Override
public void run() {
hb.write();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
hb.read();
}
}).start();
}
}
}
b被写入a=3,b之前的操作全部可见:a=3是可见的。
懒汉式
package com.zbz.设计模式;
//懒汉式单例设计模式
public class LazyMan {
//private 避免类在外部被实例化
private LazyMan(){
}
private static LazyMan lazyMan;
private static LazyMan getInstance(){
if(lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
LazyMan instance =LazyMan.getInstance();
LazyMan instance0=LazyMan.getInstance();
System.out.println(instance);
System.out.println(instance0);
}
}
结果:
com.zbz.设计模式.LazyMan@10f87f48
com.zbz.设计模式.LazyMan@10f87f48
反射破坏单例式
单例模式的终结者:
declaredConstructor.setAccessible(true);//设置在使用构造器的时候不执行权限检查
package com.zbz.设计模式;
import java.lang.reflect.Constructor;
//懒汉式单例设计模式
public class LazyMan {
//private 避免类在外部被实例化
private LazyMan(){
}
//懒汉式单例,只有在调用getInstance时才会实例化一个单例对象
//但是啊但是,反射很强大!!!
private static volatile LazyMan lazyMan;
private static LazyMan getInstance(){
if(lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
LazyMan instance =LazyMan.getInstance();
//LazyMan instance0=LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor=LazyMan.class.getDeclaredConstructor(null);//无参构造
declaredConstructor.setAccessible(true);//设置在使用构造器的时候不执行权限检查
LazyMan instance1=declaredConstructor.newInstance();//用无参构造来创建一个对象实例
//由于没有了权限检查,所以在Lazyman类外面也可以创建对象了,然后执行方法
//观察控制台,私有构造器又被调用了一次,单例模式被攻陷了,执行方法成功
System.out.println(instance);
System.out.println(instance1);
}
}
结果为:
com.zbz.设计模式.LazyMan@10f87f48
com.zbz.设计模式.LazyMan@b4c966a
哈希值不一样,证明是两个不同的实例,所以单例模式被破坏
属性lazyman加volatile关键字的原因:
上述代码中lazyMan=new LazyMan();并不是原子操作 ,JVM会分解成以下几个命令执行:
1给对象分配空间
2初始化对象
3将初始化对象和内存地址建立关联
按照上面的分解顺序(1->2->3)执行不存在任何问题,但是由于JVM编译优化的存在,可能导致2和3步骤颠倒,即按1->3->2顺序执行(这就是指令重排序)。按照1->3->2顺序执行,在多线程环境中执行getInstance就有可能出现lazyMan已经和初始对象内存建立关联,但是对象还没有初始化完成的情况,即执行if (lazyMan == null)的时候lazyMan != null 直接返回没有初始化完成的lazyMan,导致再使用lazyMan实例的时候报错。
volatile关键字是可以解决指令重排序问题的一种方式
也可以使用锁 synchronzied:
volatile可以理解为是轻量级锁
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
复习Java构造器
构造器特性:
1.如果我们的类当中没有定义任何构造器,系统会给我们默认提供一个无参的构造器。
2.如果我们的类当中定义了构造器,那么系统就不会再给我们提供默认的无参构造器。
==作用:==构建创造一个对象。同时可以给我们的属性做一个初始化操作。
下面主要学习构造器和方法的区别:
1、功能和作用的不同
构造器是为了创建一个类的实例。用来创建一个对象,同时可以给属性做初始化。这个过程也可以在创建一个对象的时候用到:Platypus p1 = new Platypus();
相反,方法的作用是仅仅是功能函数,为了执行java代码。
2、修饰符,返回值和命名的不同
构造器和方法在下面三个方便的区别:修饰符,返回值,命名。
和方法一样,构造器可以有任何访问的修饰:
public, protected, private或者没有修饰(通常被package 和 friendly调用).
不同于方法的是,构造器不能有以下非访问性质的修饰: abstract, final, native, static, 或者 synchronized。
3、返回类型
方法必须要有返回值,能返回任何类型的值或者无返回值(void),构造器没有返回值,也不需要void。
4、命名
构造器使用和类相同的名字,而方法则不同。按照习惯,方法通常用小写字母开始,而构造器通常用大写字母开始。
构造器通常是一个名词,因为它和类名相同;而方法通常更接近动词,因为它说明一个操作。
5、调用:
构造方法:只有在对象创建的时候才会去调用,而且只会调用一次。
一般方法:在对象创建之后才可以调用,并且可以调用多次。
6、"this"的用法
构造器和方法使用关键字this有很大的区别。方法引用this指向正在执行方法的类的实例。静态方法不能使用this关键字,因为静态方法不属于类的实例,所以this也就没有什么东西去指向。构造器的this指向同一个类中,不同参数列表的另外一个构造器
单例模式被破坏解决方法1:
package com.zbz.设计模式;
import java.lang.reflect.Constructor;
//懒汉式单例设计模式
public class LazyMan {
private static int count=0; //通过static count变量控制
//private 避免类在外部被实例化
private LazyMan(){
System.out.println("构造器被调用了"+(++count)+"次");
if(count>1){
throw new RuntimeException("单例构造器不能重复使用");
}
}
private static volatile LazyMan lazyMan;
private static LazyMan getInstance(){
if(lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
LazyMan instance =LazyMan.getInstance();
//LazyMan instance0=LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor=LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance1=declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
}
但是还是可以通过反射进行破坏:
package com.zbz.设计模式;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
//懒汉式单例设计模式
public class LazyMan {
private static int count=0;
//private 避免类在外部被实例化
private LazyMan(){
System.out.println("构造器被调用了"+(++count)+"次");
if(count>1){
throw new RuntimeException("单例构造器不能重复使用");
}
}
private static volatile LazyMan lazyMan;
private static LazyMan getInstance(){
if(lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) throws Exception {
//LazyMan instance =LazyMan.getInstance();
//LazyMan instance0=LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor=LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//反射:构造器实例化instance2
LazyMan instance2=declaredConstructor.newInstance();
//获取字段
Field count=LazyMan.class.getDeclaredField("count");
//破坏 变量count的private属性
count.setAccessible(true);
//实例化instance2后给instance2的static count重新复制为0
count.set(instance2,0);
//通过反射构造器实例化对象,此时count已经被重置为1
LazyMan instance1=declaredConstructor.newInstance();
System.out.println(instance2);
System.out.println(instance1);
}
}
java中,类刚被加载时,所有类的信息都放在方法区,包括static
静态属性和非静态属性的区别:
1、在内存中存放位置不同 所有带static修饰符的属性或者方法都存放在内存中的方法区 而非静态属性存放在内存中的堆区
2、出现时机不同 静态属性或方法在没创建对象之前就已经存在 而非静态属性在创建对象之后才存在
3、静态属性是在某个类中的所有对象是共享的
4、生命周期不同 静态属性在类消失之后才销毁 而非晶态属性在对象销毁之后才销毁
5、用法:
a.静态属性可以直接通过类名直接访问 非静态属性不能通过类直接访问只能通过对象访问
b.二者的相同点是都可以在创建完对象后使用下面看个静态属性是在某个类中的所有对象是共享的例子 最后结果都是20
静态内部类实现
代码
1.非静态内部类中不允许定义静态成员
2.外部类的静态成员不可以直接使用非静态内部类
3.静态内部类,不能访问外部类的实例成员,只能访问外部类的类成员
package com.zbz.设计模式;
//静态内部类实现单例模式
public class Holder {
//单例模式:构造器私有
private Holder(){
}
//外部类的静态成员不可以直接使用非静态内部类
public static Holder getInstance(){
return InnerClass.HOLDER;
}
//静态内部类
public static class InnerClass{
private static final Holder HOLDER=new Holder();
}
}
但还是可以被反射破坏
枚举实现
package com.zbz.设计模式;
//枚举实现单例模式可以不被反射破坏
//枚举本质上也是一个类
public enum EnumSingle {
INSTANCE;//实例
public static EnumSingle getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
EnumSingle ins1=EnumSingle.INSTANCE;
EnumSingle ins2=EnumSingle.getInstance();
System.out.println(ins1);
System.out.println(ins2);
}
}
输出:
1、我们试图通过反射破坏:
package com.zbz.设计模式;
import java.lang.reflect.Constructor;
//枚举实现单例模式可以不被反射破坏
//枚举本质上也是类
public enum EnumSingle {
INSTANCE;//实例
public static EnumSingle getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws Exception {
EnumSingle ins1=EnumSingle.INSTANCE;
//通过反射 实例化对象
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor();//无参构造
declaredConstructor.setAccessible(true);
EnumSingle ins2 = declaredConstructor.newInstance();
System.out.println(ins1);
System.out.println(ins2);
}
}
2、我们通过jdk自带的反编译发现存在无参构造:
3、于是我们产生了疑问?不能通过无参构造实例化对象呢?
于是我们用专业一点的反编译工具 jad.exe:
我们要把jad.exe文件放在.class文件同级目录下
4、于是我们发现源码中只有有参构造:
于是我们使用反射再次破坏:
package com.zbz.设计模式;
import java.lang.reflect.Constructor;
//枚举实现单例模式可以不被反射破坏
//枚举本质上也是类
public enum EnumSingle {
INSTANCE;//实例
public static EnumSingle getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws Exception {
EnumSingle ins1=EnumSingle.INSTANCE;
//通过反射 实例化对象
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);//有参构造
declaredConstructor.setAccessible(true);
EnumSingle ins2 = declaredConstructor.newInstance();
System.out.println(ins1);
System.out.println(ins2);
}
}
5、证明反射不能破坏枚举实现单例模式
最后:总结及其应用场景
实现单例模式的思路是
一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名 称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们 还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
需要注意的地方:
单例模式在多线程的 应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例, 这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
优点:
1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
3.提供了对唯一实例的受控访问。
4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
5.允许可变数目的实例。
6.避免对共享资源的多重占用。
缺点:
1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
使用注意事项
1.使用时不能用反射模式创建单例,否则会实例化一个新的对象
2.使用懒单例模式时注意线程安全问题
3.饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)
适用场景
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
1.需要频繁实例化然后销毁的对象。
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
3.有状态的工具类对象。
4.频繁访问数据库或文件的对象。
以下都是单例模式的经典使用场景:
1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
2.控制资源的情况下,方便资源之间的互相通信。如线程池等。
应用场景举例:
1.外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~ 3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.
实现单利模式的原则和过程:
1.单例模式:确保一个类只有一个实例,自行实例化并向系统提供这个实例
2.单例模式分类:饿单例模式(类加载时实例化一个对象给自己的引用),懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)(java中饿单例模式性能优于懒单例模式,c++中一般使用懒单例模式)
3.单例模式要素:
a.私有构造方法
b.私有静态引用指向自己实例
c.以自己实例为返回值的公有静态方法