今天我的一个开发项目可能需要用到单例来实现一些功能,因此在网上各大博客看到许多的关于java多线程的单例模式,虽然网上有许多这方面的博客,但是这是我通过学习和自己实验理解之后总结出来的,不一定适合所有人。
单例的几种写法
1、懒汉式(线程不安全)[不可用]
实验代码
package test;
public class Test {
public static void main(String[] args) {
A obj1 = new A();
B obj2 = new B();
obj1.start();
obj2.start();
}
}
class T{
public int b ;
private T(){
b = 1;
}
private static T obj;
public static T getinstance(){
if (obj==null){
obj = new T();
}
return obj;
}
}
class A extends Thread{
@Override
public void run(){
T obj1= T.getinstance();
for(int i=0;i<10;i++){
System.out.println("A"+obj1.b);
obj1.b++;
}
}
}
class B extends Thread{
@Override
public void run(){
T obj2 = T.getinstance();
for(int i=0;i<10;i++){
System.out.println("B"+obj2.b);
obj2.b++;
}
}
}
实验结果:
run:
B1
A1
B2
A2
B3
A3
A4
A5
B4
B5
A6
B6
A7
B7
B8
A8
B9
A9
B10
A10
由以上结果显示,T类创建了两个完全不相关的对象。这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (obj== null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。很明显A、B就是这种情况,产生了两个不相关的实例。所以在多线程环境下一定不可使用这种方式。
这里我要说明一下Lazy Loading的含义:从字面解释就是懒惰加载,其实这个解释并不准确,应该是延迟加载,就是不用类的实例不用在初始化装载,只有到使用的时候才加载。
2、懒汉式(线程安全,同步方法)[不推荐用]
这种方法就是在getinstance方法添加同步锁
实验代码:
package test;
public class Test {
public static void main(String[] args) {
A obj1=new A();
B obj2 = new B();
obj1.start();
obj2.start();
}
}
class T{
public int b ;
private T(){
b = 1;
}
private static T obj;
public static synchronized T getinstance(){
if (obj==null){
obj = new T();
}
return obj;
}
}
class A extends Thread{
@Override
public void run(){
T obj1= T.getinstance();
for(int i=0;i<10;i++){
System.out.println("A"+obj1.b);
obj1.b++;
}
}
}
class B extends Thread{
@Override
public void run(){
T obj2 = T.getinstance();
for(int i=0;i<10;i++){
System.out.println("B"+obj2.b);
obj2.b++;
}
}
}
实验结果:
run:
A1
B1
A2
B3
A4
B5
A6
B7
B9
A8
B10
A11
B12
A13
B14
A15
B16
A17
B18
A19
可见这种也能实现多线程的单例安全,但是就是效率太低了,每个线程在想获得类的实例时候,执行getinstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。这是只有两个线程竞争,如果在分布式中可能有多个线程竞争效率会更低。
3、懒汉式(同步代码块)[视情况而定,但如果可用也会降低效率同样不推荐使用]
实验代码:
package test;
public class Test {
static void main(String[] args) {
A obj1=new A();
B obj2 = new B();
obj1.start();
obj2.start();
}
}
class T{
public int b ;
private T(){
b = 1;
}
private static T obj;
public static T getinstance(){
if (obj==null){ //synchronized(T.class){
// if (obj==null){
synchronized(T.class){ // obj = new T();
obj = new T(); // }
} //}
}
return obj;
}
}
class A extends Thread{
@Override
public void run(){
T obj1= T.getinstance();
for(int i=0;i<10;i++){
System.out.println("A"+obj1.b);
obj1.b++;
}
}
}
class B extends Thread{
@Override
public void run(){
T obj2 = T.getinstance();
for(int i=0;i<10;i++){
System.out.println("B"+obj2.b);
obj2.b++;
}
}
}
结果:
run:
B1
A1
B2
A2
B3
A3
B4
B5
A4
A5
A6
A7
B6
B7
B8
A8
B9
A9
B10
A10
同一一样。但是有趣的是,作者如果把整个if语句同步(像注释中一样的写法),能够保证单例安全。因此在这个例子中关键还是A、B两个线程竞争if判断语句。同时这个写法也会降低效率。我看到网上的博客还有双重检查通过两次检查,其中第二次检查同步来保证线程安全。但是我觉得没有我注释中写的效率高,只进行了一次检查。
4、饿汉式(静态常量)[可用]
实验代码:
package test;
public class Test {
public static void main(String[] args) {
A obj1=new A();
B obj2 = new B();
obj1.start();
obj2.start();
}
}
class T{
public int b ;
private T(){
b = 1;
}
private static final T obj = new T();
public static T getinstance(){
return obj;
}
}
class A extends Thread{
@Override
public void run(){
T obj1= T.getinstance();
for(int i=0;i<10;i++){
System.out.println("A"+obj1.b);
obj1.b++;
}
}
}
class B extends Thread{
@Override
public void run(){
T obj2 = T.getinstance();
for(int i=0;i<10;i++){
System.out.println("B"+obj2.b);
obj2.b++;
}
}
}
结果
run:
A1
A2
A3
A4
A5
A6
A7
A8
A9
A10
B1
B12
B13
B14
B15
B16
B17
B18
B19
B20
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
5、静态内部类[推荐用]
实验代码:
package test;
public class Test {
public static void main(String[] args) {
A obj1=new A();
B obj2 = new B();
obj1.start();
obj2.start();
}
}
class T{
public int b ;
private T(){
b = 1;
}
private static class T1{
private static final T obj = new T();
}
public static T getinstance(){
return T1.obj;
}
}
class A extends Thread{
@Override
public void run(){
T obj1= T.getinstance();
for(int i=0;i<10;i++){
System.out.println("A"+obj1.b);
obj1.b++;
}
}
}
class B extends Thread{
@Override
public void run(){
T obj2 = T.getinstance();
for(int i=0;i<10;i++){
System.out.println("B"+obj2.b);
obj2.b++;
}
}
}
结果:
run:
A1
A2
B1
A3
B4
A5
B6
A7
B8
B10
B11
B12
A9
A14
A15
B13
A16
B17
A18
B19
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
以上就是我结合网上其他博主的博客,加上自己的实验理解总结的关于java单例在多线程的实现和安全。