Java多线程编程七(单例模式)

饿汉模式(立即加载)

“饿汉模式”也就是立即加载,在使用类时对应的对象已经创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MySingleton  {
private static MySingleton mySingleton = new MySingleton();

private MySingleton() {
}

public static MySingleton getInstance() {
return mySingleton;
}

public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + " " + MySingleton.getInstance().hashCode());
}
}).start();
}
}
}

运行结果为:

1
2
3
Thread-0  321580105
Thread-1 321580105
Thread-2 321580105

由结果可知,三个线程中的hashCode一样,则证明所打印的是同一个对象,即立即加载饿汉式的单例模式。

懒汉模式(延迟加载)

延迟加载和立即加载相对,也就是在使用时才创建,通常的实现是在get()方法中进行new实例化。

懒汉单例简单使用

修改上面的单例代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MySingleton  {

private static MySingleton mySingleton;

private MySingleton() {
}

public static MySingleton getInstance() {
if (mySingleton == null) {
mySingleton = new MySingleton();
}
return mySingleton;
}
}

其他不变,运行结果为:

1
2
3
Thread-0  500866883
Thread-1 500866883
Thread-2 500866883

由结果可知,三个线程中的hashCode一样,则证明所打印的是同一个对象,即立即延迟懒汉式的单例模式。

懒汉单例的缺点

虽然上述代码实现了单例模式,但是在多线程的环境下,“懒汉式”的单例就有一定几率出现错误,不能保持单例的状态。举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MySingleton {

private static MySingleton mySingleton;

private MySingleton() {
}

public static MySingleton getInstance() {
try {
if (mySingleton == null) {
Thread.sleep(3000);
mySingleton = new MySingleton();
System.out.println("创建单例对象 " + mySingleton);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return mySingleton;
}

public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+" "+MySingleton.getInstance().hashCode());
}
}).start();
}
}
}

运行结果如下:

1
2
3
4
5
6
创建单例对象 cn.appblog.thread.MySingleton@182acfc9
Thread-0 405658889
创建单例对象 cn.appblog.thread.MySingleton@182acfc9
Thread-1 405658889
创建单例对象 cn.appblog.thread.MySingleton@9c3310a5
Thread-2 158381581

由结果可知,不再是单例。由于多个线程操作一个变量,导致其不确定性,所以我们需要对其进行同步处理,可以在getInstance()方法前加上synchronized关键字,如:

运行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
public synchronized static MySingleton getInstance() {
try {
if (mySingleton == null) {
Thread.sleep(3000);
mySingleton = new MySingleton();
System.out.println("创建单例对象 " + mySingleton);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return mySingleton;
}

运行结果为:

1
2
3
4
创建单例对象 cn.appblog.thread.MySingleton@619d7c8
Thread-0 102356936
Thread-1 102356936
Thread-2 102356936

由于在同一时间只能有一个线程访问,只有当获取锁的线程将其释放其他线程才能访问getInstance()同步方法,也就成功避免了非线程安全问题。

我们还可以利用同步代码块来解决这个问题,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static MySingleton getInstance() {
try {
synchronized (MySingleton.class) {
if (mySingleton == null) {
Thread.sleep(3000);
mySingleton = new MySingleton();
System.out.println("创建单例对象 " + mySingleton);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return mySingleton;
}

或者使用ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MySingleton {

private static MySingleton mySingleton;

private static ReentrantLock lock = new ReentrantLock();

private MySingleton() {
}

public static MySingleton getInstance() {
try {
lock.lock();
if (mySingleton == null) {
Thread.sleep(3000);
mySingleton = new MySingleton();
System.out.println("创建单例对象 " + mySingleton);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return mySingleton;
}
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MySingleton {

private static MySingleton mySingleton;

private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

private MySingleton() {
}

public static MySingleton getInstance() {
try {
lock.writeLock().lock();
if (mySingleton == null) {
Thread.sleep(3000);
mySingleton = new MySingleton();
System.out.println("创建单例对象 " + mySingleton);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
return mySingleton;
}
}

上述方法都可以解决这个非线程安全问题,但是其执行效率低下。同步代码块可以同步和异步混合分别执行来提高线程的执行效率,我们只关注关于修改共享变量的那些逻辑代码即可,如果按照这种思想来修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MySingleton {

private static MySingleton mySingleton;

private MySingleton() {
}

public static MySingleton getInstance() {
try {
if (mySingleton == null) {
synchronized (MySingleton.class) {
Thread.sleep(3000);
mySingleton = new MySingleton();
System.out.println("创建单例对象 " + mySingleton);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return mySingleton;
}
}

运行结果如下:

1
2
3
4
5
6
创建单例对象 cn.appblog.thread.MySingleton@108c737f
Thread-0 277650063
创建单例对象 cn.appblog.thread.MySingleton@db6222b
Thread-1 230058813
创建单例对象 cn.appblog.thread.MySingleton@1dda492b
Thread-2 500844802

由此来看,虽然程序的运行效率提高了,但是最核心的多线程安全问题并未得到解决,因此我们采用DCL双检查锁机制(在同步代码块里再进行一次判空,这样在下个线程获取锁来执行时就会判断当前是否已经创建,可以避免再次重新创建实例)如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MySingleton {

private static MySingleton mySingleton;

private MySingleton() {
}

public static MySingleton getInstance() {
try {
if (mySingleton == null) {
Thread.sleep(3000);
synchronized (MySingleton.class) {
if (mySingleton == null) {
mySingleton = new MySingleton();
System.out.println("创建单例对象 " + mySingleton);
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return mySingleton;
}
}

运行结果:

1
2
3
4
创建单例对象 cn.appblog.thread.MySingleton@8f62e66
Thread-0 150351562
Thread-1 150351562
Thread-2 150351562

由结果可知,成功解决非线程安全问题和效率低下的问题。

静态内置类模式

采用静态内部类的静态对象来实现单例

1
2
3
4
5
6
7
8
9
10
11
12
public class MySingleton {

private static class MySingletonHelper {
private static MySingleton mySingleton = new MySingleton();
}
private MySingleton() {
}

public static MySingleton getInstance() {
return MySingletonHelper.mySingleton;
}
}

同样的main方法运行结果为:

1
2
3
Thread-0  1603718986
Thread-1 1603718986
Thread-2 1603718986

序列化和反序列化的单例实现

如果遇到了序列化对象,使用默认的方式运行得到的结果还是多例的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MySingleton implements Serializable {
private static class MySingletonHelper {
private static MySingleton mySingleton = new MySingleton();
}
private MySingleton() {
}
public static MySingleton getInstance() {
return MySingletonHelper.mySingleton;
}

public static void main(String[] args) {
try {
MySingleton instance = MySingleton.getInstance();
FileOutputStream fileOutputStream = new FileOutputStream(new File("mySingleton.txt"));
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance);
objectOutputStream.close();
fileOutputStream.close();
System.out.println(instance.hashCode());
} catch (IOException e) {
e.printStackTrace();
}
try {
FileInputStream fileInputStream = new FileInputStream(new File("mySingleton.txt"));
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
MySingleton mySingleton = (MySingleton) objectInputStream.readObject();
objectInputStream.close();
fileInputStream.close();
System.out.println(mySingleton.hashCode());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

运行结果为:

1
2
1670675563
189568618

出现多例情况(具体原因将会在后续文章中进行剖析,这里就直接给出解决方案)

由于序列化/反序列化会破坏单例模式,所以需要在单例模式中加上如下方法,返回我们的单例变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MySingleton implements Serializable {
private MySingleton() {
}
private static class MySingletonHelper {
private static MySingleton mySingleton = new MySingleton();
}
public static MySingleton getInstance() {
return MySingletonHelper.mySingleton;
}

protected Object readResolve() {
System.out.println("调用 readResolve 方法!");
return MySingletonHelp.mySingleton;
}
}

就解决我们单例被破坏的问题。

使用static代码块实现单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MySingleton {
private static MySingleton mySingleton = null;
static {
mySingleton = new MySingleton();
}
private MySingleton() {
}
public static MySingleton getInstance() {
return mySingleton;
}

public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+" "+MySingleton.getInstance().hashCode());
}
}).start();
}
}

}

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :