谈谈Serializable、transient、volatile、final在多线程编程中的应用

一个实现Serializable的类的对象才可以序列化,实现序列号的对象可以以数据流的形式传输Serializable接口中没有任何的方法。当一个类声明要实现Serializable接口时,只是表明该类参加序列化协议。

java.io包中,提供了ObjectInputStream和ObjectOutputStream将数据流功能扩展至可读写对象 。在ObjectInputStream中用readObject()方法可以直接读取一个对象,ObjectOutputStream中用writeObject()方法可以直接将对象保存到输出流中。

使用对象:属性

对象类:

public class Student implements Serializable {
	private static final long serialVersionUID  = 1L;
	int							id;			// 学号
	String						name;			// 姓名
	int							age;			// 年龄
	String						department;	// 系别
	public Student(int id, String name, 
int age, String department) {
		this.id = id;
		this.name = name;
		this.age = age;
		this.department = department;
	}
}

测试类:

static void testSerializable() throws Exception {
		//	实例化对象
		Student stu = new Student(10086,
 "gshen", 20, "计算机科学与技术");
		//通过对象输出流将对象状态保存下来
		FileOutputStream fo = new 		
								FileOutputStream("data.ser");
		ObjectOutputStream so = new ObjectOutputStream(fo);
		try {
			so.writeObject(stu);
			so.close();
		} catch (IOException e) {
			System.out.println(e);
		}
		
		//通过对象输入流将对象状态恢复
		stu = null;
		FileInputStream fi = new FileInputStream("data.ser");
		ObjectInputStream si = new ObjectInputStream(fi);
		try {
			stu = (Student) si.readObject();
			si.close();
		} catch (IOException e)
		{
			System.out.println(e);
		}
		System.out.println("Student Info:");
		System.out.println("ID:" + stu.id + "\n" 
						+ "Name:" + stu.name + "\n" 
						+ "Age:" + stu.age + "\n" 
						+ "Dep:" + stu.department );
	}

序列化只能保存对象的非静态成员变量不能保存任何的成员方法和静态成员变量而且序列化保存的只是变量的值,对于变量的任何修饰符都不能保存。


transient关键字 

      一、对于某些类型的对象,其状态是瞬时的,这样的对象是无法保存其状态的。例如一个Thread对象或一个FileInputStream对象 ,对于这些字段,我们必须用transient关键字标明否则编译器将报措。 
     二、另外 ,串行化可能涉及将对象存放到磁盘上或在网络上发送数据,这时候就会产生安全问题。因为数据位于Java运行环境之外,不在Java安全机制的控制之中。对于这些需要保密的字段,不应保存在永久介质中 ,或者不应简单地不加处理地保存下来 ,为了保证安全性。应该在这些字段前加上transient关键字。

transient例子:

/**
 * 变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
 */
public class LoggingInfo implements java.io.Serializable {
	private static final long	serialVersionUID	= 1L;
	private String				    uid;
	private transient String	pwd; 	

	LoggingInfo(String user, String password) {
		uid = user;
		pwd = password;
	}

	public String toString() {
		String password = null;
		if ( pwd == null ) {
			password = "NOT SET";
		} else {
			password = pwd;
		}
		return "logon info: \n   " + 
			   "user: " + uid + 
			   "\n   password: " + password;
	}

	public static void main(String[] args) {
		LoggingInfo logInfo = new LoggingInfo("MIKE", "MECHANICS");
		System.out.println( logInfo.toString() );
		try {
			ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(
					"logInfo.out"));
			o.writeObject( logInfo );
			o.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		try {
			ObjectInputStream in = new ObjectInputStream(new FileInputStream(
					"logInfo.out"));
			LoggingInfo logInfo1 = (LoggingInfo) in.readObject();
			System.out.println(logInfo1.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

输出:

login info:

   user: MIKE

   password: MECHANICS

login info:

   user: MIKE

   password: NOT SET

序列化ID的问题:

A 和 B 端都有一个类文件,功能代码完全一致。也都实现了Serializable 接口。如果序列化ID不同,他们无法相互序列化和反序列化。

如果没有特殊需求一般使用默认的1L:

private staticfinal long serialVersionUID = 1L

; m� a>P�pan class=apple-style-span>二、另外 ,串行化可能涉及将对象存放到磁盘上或在网络上发送数据,这时候就会产生安全问题。因为数据位于Java运行环境之外,不在Java安全机制的控制之中。 对于这些需要保密的字段,不应保存在永久介质中 ,或者不应简单地不加处理地保存下来 ,为了保证安全性。应该在这些字段前加上transient关键字。

 

volatile

使用对象:属性

 

先了解下synchronized

所有加上synchronized和 块语句,在多线程访问的时候,同一时刻只能有一个线程能够用synchronized修饰的方法或者代码块。

volatile介绍:

volatile用在多线程,同步变量。线程为了提高效率,将某成员变量(A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行AB的同步。因此存在AB不一致的情况。volatile就是用来避免这种情况的。

在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 

 

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的值,即被volatile修饰的成员变量在线程调用是要求程序从内存读出它最新的值。

 

为什么说异步线程可以成员变量,从java内存的观点来看,都是线程创建的副本。

int	i1;
	int geti1() {
		return i1;
	}

	volatile int i2;
	int geti2() {
		return i2;
	}

	int	i3;
	synchronized int geti3() {
		return i3;
	}

得到存储在当前线程中i1的数值。多个线程有多个i1变量拷贝,而且这些i1之间可以互不相同。换句话说,另一个线程可能已经改变了它线程内的i1值,而这个值可以和当前线程中的i1值不相同。事实上,Java有个思想叫“主”内存区域,这里存放了变量目前的“准确值”。每个线程可以有它自己的变量拷贝,而这个变量拷贝值可以和“主”内存区域里存放的不同。因此实际上存在一种可能:“主”内存区域里的i1值是1,线程1里的i1值是2,线程2里的i1值是3——这在线程1和线程2都改变了它们各自的i1值,而且这个改变还没来得及传递给“主”内存区域或其他线程时就会发生。 
  而 geti2()得到的是“主”内存区域的i2数值。用volatile修饰后的变量不允许有不同于“主”内存区域的变量拷贝。换句话说,一个变量经 volatile修饰后在所有线程中必须是同步的;任何线程中改变了它的值,所有其他线程立即获取到了相同的值。理所当然的,volatile修饰的变量存取时比一般变量消耗的资源要多一点,因为线程有它自己的变量拷贝更为高效。 
  既然volatile关键字已经实现了线程间数据同步,又要 synchronized干什么呢?它们之间有两点不同。首先,synchronized获得并释放监视器——如果两个线程使用了同一个对象锁,监视器能强制保证代码块同时只被一个线程所执行——这是众所周知的事实。但是,synchronized也同步内存:事实上,synchronized在“ 主”内存区域同步整个线程的内存。因此,执行geti3()方法做了如下几步: 
1. 线程请求获得监视this对象的对象锁(假设未被锁,否则线程等待直到锁释放) 
2. 线程内存的数据被消除,从“主”内存区域中读入
3. 代码块被执行 
4. 对于变量的任何改变现在可以安全地写到“主”内存区域中(不过geti3()方法不会改变变量值) 
5. 线程释放监视this对象的对象锁 
  因此volatile只是在线程内存和“主”内存间同步某个变量的值,而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。

 

volatile关键字用于声明简单类型变量,如int、float、 boolean等数据类型。如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的。但这有一定的限制。例如,下面的例子中的n就不是原子级别的:

public class JoinThread extends Thread {
	public static volatile int	n	= 0;

	public void run() {
		for (int i = 0; i < 10; i++)
			try {
				n = n + 1;
				sleep(3); // 为了使运行结果更随机,延迟3毫秒
			} catch (Exception e) {
				e.printStackTrace();
			}
	}

	public static void main(String[] args) throws Exception {

		Thread threads[] = new Thread[10];
		for (int i = 0; i < threads.length; i++)
			// 建立10个线程
			threads[i] = new JoinThread();
		for (int i = 0; i < threads.length; i++)
			// 运行刚才建立的10个线程
			threads[i].start();
		for (int i = 0; i < threads.length; i++)
			// 10个线程都执行完后继续
			threads[i].join();
		System.out.println(" n= " + JoinThread.n);
	}
} 

如果对n的操作是原子级别的,最后输出的结果应该为n=100,而在执行上面代码时,有时侯输出的n都小于100,这说明n=n+1不是原子级别的操作。原因是声明为volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作: 
n  =  n  +   1 ; 
n ++ ; 

 

如果要想使这种情况变成原子操作,需要使用synchronized关键字,如上的代码可以改成如下的形式:

public class JoinThread extends Thread {
    public static int n   = 0;
 
    public static synchronized void inc() {
       n++;
    }
 
    public void run() {
       for (int i = 0; i < 10; i++)
           try {
              inc();// n = n + 1 改成了 inc();
              sleep(3);// 为了使运行结果更随机,延迟3毫秒
           } catch (Exception e) {
              e.printStackTrace();
           }
    }
 
    public static void main(String[] args) throws Exception {
       Threadthreads[] = new Thread[10];
       for (int i = 0; i < threads.length; i++)
           // 建立10个线程
           threads[i]= new JoinThread();
       for (int i = 0; i < threads.length; i++)
           // 运行刚才建立的10个线程
           threads[i].start();
       for (int i = 0; i < threads.length; i++)
           // 10个线程都执行完后继续
           threads[i].join();
       System.out.println(" n=" + JoinThread.n);
    }
}


 上面的代码将n=n+1改成了inc(),其中inc方法使用了synchronized关键字进行方法同步。因此,在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,当变量的值由自身的上一个决定时,如n=n+1n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile 


终于ok了


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sg_0504

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值