Java随笔(四)

与之前写的Java随笔一样,此篇博客主要是用来记录我之前存放在本地的Word文档中的一些Java的自身理解,由于水平有限所以可能只适合自己加深理解与记忆。


目录

 

单例模式Java实现

普通单例模式

饿汉单例模式

懒汉单例模式

饿汉单例模式和懒汉单例模式比较

延迟加载单例模式

原型模式Java实现

 浅复制

深度复制

多线程相关

未加同步导致问题演示

Synchronized修饰方法解决上述问题

使用volatile解决上述问题


单例模式Java实现

单例模式,实现单例的基本原理就是将构造方法隐藏,提供一个公有方法给用户调用。

普通单例模式

最简单的不考虑多线程的情况,可以写成如下代码:

public class MySingle{
    private static  MySingle mySingle = null;
    private MySingle(){
    }
    public static MySingle getInstance(){
        if(mySingle == null){
            mySingle = new MySingle();
        }
        return mySingle;
    }
}

这个例子在单线程的环境可以完美运行,但是多线程环境就会出问题,可以使用饿汉单例模式或者懒汉单例模式解决。

饿汉单例模式

public class MySingle{
    private static MySingle mySingle = new MySingle();
    private MySingle(){
    }
    public static MySingle getSinstance(){
        return mySingle;
    }
}

上面就是饿汉单例模式,但这种方式的一个弊端是,在类加载的时候就会创建MySingle的对象,然而可能这个对象根本就不会用到,浪费了内存。

如果不想浪费内存,那么我们可以使用懒汉单例模式。

懒汉单例模式

public class LazySingle {
	private volatile static LazySingle lazySingle = null;

	private LazySingle() {
	}

	public static LazySingle getInstance() {
		// 第一次判空
		if (lazySingle == null) {
			// 锁定代码
			synchronized (LazySingle.class) {
				// 第二次判空
				if (lazySingle == null) {
					lazySingle = new LazySingle();
				}
			}
		}
		return lazySingle;
	}
}

这里需要注意,变量lazySingle被 volatile 修饰!

另外注意这里synchronized后面还有一次判空,这是因为在多线程环境中,可能有好多线程在第一次判空后 切换到别的线程,而一旦锁定代码的线程放开锁定,其他线程会再次锁定,再次生成新对象,所以要在锁定代码里面再判断一次是不是没生成单例对象。

饿汉单例模式和懒汉单例模式比较

饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。

懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

延迟加载单例模式

在懒汉单例模式 由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。因此设计了使用nitialization Demand Holder (IoDH)技术来进行延迟加载的单例模式。

class MySingle {
	private MySingle() {
	}

	private static class HolderClass {
		private final static MySingle mySingle = new MySingle();
	}

	public static MySingle getInstance() {
		return HolderClass.mySingle;
	}
}

上面利用了类在被主动引用才会进行初始化,因此只有在第一次调用getInstance方法的时候才会触发HolderClass的初始化,从而初始化MySingle对象,之后调用getInstance都会得到之前初始化的MySingle对象,从而实现了单例模式。

这里在加载MySingle类的时候没有直接建立MySingle对象,因此加载速度比饿汉模式要快,而第一次调用getInstance方法的时候会初始化内部类HolderClass从而创建了MySingle的对象,没有懒汉模式的二次验证以及volatile关键字,所以运行性能损失比懒汉模式要小。这个单例模式 拥有饿汉单例模式和懒汉单例模式的优点。

原型模式Java实现

原型模式,实际上可以看成让我们能够很轻松的根据一个对象快速创建一个类似的对象。

首先提一下clone这个方法,在Java中,可以让一个类通过实现Cloneable接口,然后调用类对象的clone方法对对象进行复制,但是这种复制实质是一种浅复制。

下面就是使用浅复制实现的原型模式

 浅复制

public class WeeklyLog implements Cloneable{
	class Child{
		private String childName;
		//省略get/set方法
	}
	private String username;
	private String date;
	private String content;
	private Child child;
	//省略get/set方法
	public WeeklyLog clone(){
		Object obj = null;
		try{
			obj = super.clone();
			return (WeeklyLog)obj;
		}catch(CloneNotSupportedException e){
			System.out.println("不支持复制");
			return null;
		}
	}
	public static void main(String[] args) {
		WeeklyLog log_previous = new WeeklyLog();
		log_previous.setUsername("UserName");
		log_previous.setDate("第23周");
		log_previous.setContent("工作总结");
		Child child = log_previous.new Child();
		child.setChildName("childdd");
		log_previous.setChild(child);
		WeeklyLog log_new;
		log_new = log_previous.clone(); //调用克隆方法创建克隆对象
		log_new.setDate("第24周");
		System.out.println(log_previous == log_new);
		log_previous.getChild().setChildName("改变了");
		System.out.println(log_new.getChild().getChildName());
            System.out.println(log_previous.getChild() == log_new.getChild());
		System.out.println(log_previous.getUsername() == log_new.getUsername());
	}
}

log_previous == log_new输出  false,log_previous.getChild() == log_new.getChild()输出  true,log_previous.getUsername() == log_new.getUsername()输出  true,由此我们可以知道,在默认的clone方法中,对于对象实质是复制了对象引用的地址。这就会出现我们修改复制体里面的对象的时候,原来的对象里面对应的对象也会发生改变,一般来说,这并不是我们想要的!因此可以使用自己写一个方法来实现真正的复制。

深度复制

public class WeeklyLogDeepClone implements Serializable{
	class Child implements Serializable{
		private String childName;
//省略get/set方法
	}
	private String username;
	private String date;
	private String content;
	private Child child;
//省略get/set方法
	public WeeklyLogDeepClone deepClone() throws Exception{
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(bos);
		oos.writeObject(this);
		ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
		ObjectInputStream ois = new ObjectInputStream(bis);
		return (WeeklyLogDeepClone) ois.readObject();
	}
	public static void main(String[] args) throws Exception {
		WeeklyLogDeepClone log_previous = new WeeklyLogDeepClone();
		log_previous.setUsername("UserName");
		log_previous.setDate("第23周");
		log_previous.setContent("工作总结");
		Child child = log_previous.new Child();
		child.setChildName("childdd");
		log_previous.setChild(child);
		WeeklyLogDeepClone log_new;
		log_new = log_previous.deepClone(); //深度克隆
		log_new.setDate("第24周");
		log_previous.getChild().setChildName("改变了");
		System.out.println(log_previous.getChild() == log_new.getChild());
		System.out.println(log_previous.getUsername() == log_new.getUsername());
	}
}

上面我们可以看到我们其实使用序列化保证了对象的深度克隆,首先将对象序列化写入到输出流中,再从输出流中读出对象,我们就得到了一个跟原来对象看起来一模一样的克隆体了,而且他们内部的对象也不一样了!(这是必然的,因为内部的对象也被序列化到流里面了)这里需要注意,所有要序列化的对象都要实现Serializable 接口。

多线程相关

在多线程中,如果要使用一个线程通过改变变量来使另外一个线程结束的时候(因为Threadstop方法不推荐使用,所以建议做法是:第一个线程轮询一个boolean域,这个域一开始是false,但是可以通过第二个线程设置这个boolean域为true,从而让第一个线程在boolean域变为true之后结束),一定要注意同步。为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的

未加同步导致问题演示

如下面代码就是没添加同步导致结果和预期不一致的情况:

import java.util.concurrent.TimeUnit;

public class StopThread {
	private static boolean stopRequested;
	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(new Runnable() {
			public void run() {
				int i = 0;
				while (!stopRequested) {
					i++;
				}
			}
		});
		backgroundThread.start();
		TimeUnit.SECONDS.sleep(1);
		stopRequested = true;
	}
}

在预期中,应该主线程运行了1秒后,之后将stopRequested设置为true,后台线程关闭,然后程序结束。然而,这个程序实际不会停止

问题就在于,由于没有同步,因此不能保证后台线程何时“看到”主线程对stopRequested的值所做的改变,没有同步,虚拟机将代码while(done) i++;转化为

if(done){

        while(true)

        i++;

}

Synchronized修饰方法解决上述问题

import java.util.concurrent.TimeUnit;

public class StopThread {
	private static boolean stopRequested;

	private static synchronized void requestStop() {
		stopRequested = true;
	}

	private static synchronized boolean stopRequested() {
		return stopRequested;
	}

	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(new Runnable() {
			public void run() {
				int i = 0;
				while (!stopRequested()) {
					i++;
				}
			}
		});
		backgroundThread.start();
		TimeUnit.SECONDS.sleep(1);
		requestStop();
	}
}

注意写方法和读方法都被同步了 !只同步一个是不行的!

使用volatile解决上述问题

import java.util.concurrent.TimeUnit;

public class StopThread {
	private static volatile boolean stopRequested;

	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(new Runnable() {
			public void run() {
				int i = 0;
				while (!stopRequested) {
					i++;
				}
			}
		});
		backgroundThread.start();
		TimeUnit.SECONDS.sleep(1);
		stopRequested = true;
	}
}

虽然volatile不执行互斥访问,但是它可以保证每个线程在读取该域的时候看到的是最新值,阻止了之前产生问题的编译器的优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值