public class Lazy { private static boolean initial = false; static { Thread t = new Thread(new Runnable() { public void run() { System.out.println("befor...");//此句会输出 /* * 由于使用Lazy.initial静态成员,又因为Lazy还未 初 * 始化完成,所以该线程会在这里等待主线程初始化完成 */ initial = true; System.out.println("after...");//此句不会输出 } }); t.start(); try { t.join();// 主线程等待t线程结束 } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { System.out.println(initial); } }
看看上面变态的程序,一个静态变量的初始化由静态块里的线程来初始化,最后的结果怎样?
当一个线程访问一个类的某个成员的时候,它会去检查这个类是否已经被初始化,在这一过程中会有以下四种情况:
1、 这个类尚未被初始化
2、 这个类正在被当前线程初始化:这是对初始化的递归请求,会直接忽略掉(另,请参考《构造器中静态常量的引用问题》一节)
3、 这个类正在被其他线程而不是当前线程初始化:需等待其他线程初始化完成再使用类的Class对象,而不会两个线程都会去初始化一遍(如果这样,那不类会初始化两遍,这显示不合理)
4、 这个类已经被初始化
当主线程调用Lazy.main,它会检查Lazy类是否已经被初始化。此时它并没有被初始化(情况1),所以主线程会记录下当前正在进行的初始化,并开始对这个类进行初始化。这个过程是:主线程会将initial的值设为false,然后在静态块中创建并启动一个初始化initial的线程t,该线程的run方法会将initial设为true,然后主线程会等待t线程执行完毕,此时,问题就来了。
由于t线程将Lazy.initial设为true之前,它也会去检查Lazy类是否已经被初始化。这时,这个类正在被另外一个线程(mian线程)进行初始化(情况3)。在这种情况下,当前线程,也就是t线程,会等待Class对象直到初始化完成,可惜的是,那个正在进行初始化工作的main线程,也正在等待t线程的运行结束。因为这两个线程现在正相互等待,形成了死锁。
修正这个程序的方法就是让主线程在等待线程前就完成初始化操作:
public class Lazy { private static boolean initial = false; static Thread t = new Thread(new Runnable() { public void run() { initial = true; } }); static { t.start(); } public static void main(String[] args) { // 让Lazy类初始化完成后再调用join方法 try { t.join();// 主线程等待t线程结束 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(initial); } }
虽然修正了该程序挂起问题,但如果还有另一线程要访问Lazy的initial时,则还是很有可能不等initial最后赋值就被使用了。
总之,在类的初始化期间等待某个线程很可能会造成死锁,要让类初始化的动作序列尽可能地简单。
一般地,要想实例化一个内部类,如类Inner1,需要提供一个外围类的实例给构造器。一般情况下,它是隐式地传递给内部类的构造器,但是它也是可以以 expression.super(args) 的方式即通过调用超类的构造器显式的传递。
public class Outer { class Inner1 extends Outer{ Inner1(){ super(); } } class Inner2 extends Inner1{ Inner2(){ Outer.this.super(); } Inner2(Outer outer){ outer.super(); } } }
class WithInner { class Inner {} } class InheritInner extends WithInner.Inner { // ! InheritInner() {} // 不能编译 /* * 这里的super指InheritInner类的父类WithInner.Inner的默认构造函数,而不是 * WithInner的父类构造函数,这种特殊的语法只在继承一个非静态内部类时才用到, * 表示继承非静态内部类时,外围对象一定要存在,并且只能在 第一行调用,而且一 * 定要调用一下。为什么不能直接使用 super()或不直接写出呢?最主要原因就是每个 * 非静态的内部类都会与一个外围类实例对应,这个外围类实例是运行时传到内 * 部类里去的,所以在内部类里可以直接使用那个对象(比如Outer.this),但这里 * 是在外部内外 ,使用时还是需要存在外围类实例对象,所以这里就显示的通过构造 * 器传递进来,并且在外围对象上显示的调用一下内部类的构造器,这样就确保了在 * 继承至一个类部类的情况下 ,外围对象一类会存在的约束。 */ InheritInner(WithInner wi) { wi.super(); } public static void main(String[] args) { WithInner wi = new WithInner(); InheritInner ii = new InheritInner(wi); } }
class Super implements Serializable{ // HashSet要放置在父类中会百分百机率出现 // 放置到子类中就不一定会出现问题了 final Set set = new HashSet(); } class Sub extends Super { private int id; public Sub(int id) { this.id = id; set.add(this); } public int hashCode() { return id; } public boolean equals(Object o) { return (o instanceof Sub) && (id == ((Sub) o).id); } } public class SerialKiller { public static void main(String[] args) throws Exception { Sub sb = new Sub(888); System.out.println(sb.set.contains(sb));// true ByteArrayOutputStream bos = new ByteArrayOutputStream(); new ObjectOutputStream(bos).writeObject(sb); ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray()); sb = (Sub) new ObjectInputStream(bin).readObject(); System.out.println(sb.set.contains(sb));// false } }
Hash一类集合都实现了序列化的writeObject()与readObject()方法。这里错误原因是由HashSet的readObject方法引起的。在某些情况下,这个方法会间接地调用某个未初始化对象的被覆写的方法。为了组装正在反序列化的HashSet,HashSet.readObject调用了HashMap.put方法,而put方法会去调用键的hashCode方法。由于整个对象图正在被反序列
化,并没有什么可以保证每个键在它的hashCode方法被调用时已经被完全初始化了,因为HashSet是在父类中定义的,而在序列化HashSet时子类还没有开始初始化(这里应该是序列化)子类,所以这就造成了在父类中调用还没有初始完成(此时id为0)的被子类覆写的hashCode方法,导致该对象重新放入hash表格的位置与反序列化前不一样了。hashCode返回了错误的值,相应的键值对条目将会放入错误的单元格中,当id被初始化为888时,一切都太迟了。
这个程序的说明,包含了HashMap的readObject方法的序列化系统总体上违背了不能从类的构造器或伪构造器(如序列化的readObject)中调用可覆写方法的规则。
如果一个HashSet、Hashtable或HashMap被序列化,那么请确认它们的内容没有直接或间接地引用它们自身,即正在被序列化的对象。
另外,在readObject或readResolve方法中,请避免直接或间接地在正在进行反序列化的对象上调用任何方法,因为正在反序列化的对象处于不稳定状态。
public class Twisted { private final String name; Twisted(String name) { this.name = name; } // 私有的不能被继承,但能被內部类直接访问 private String name() { return name; } private void reproduce() { new Twisted("reproduce") { void printName() { // name()为外部类的,因为没有被继承过来 System.out.println(name());// main } }.printName(); } public static void main(String[] args) { new Twisted("main").reproduce(); } }
在顶层的类型中,即本例中的Twisted类,所有的本地的、内部的、嵌套的长匿名的类都可以毫无限制地访问彼此的成员。
另一个原因是私有的不能被继承。
第一个PrintWords代表客户端,第二个Words代表一个类库:
class PrintWords { public static void main(String[] args) { System.out//引用常量变量 .println(Words.FIRST + " " + Words.SECOND + " " + Words.THIRD); } } class Words { // 常量变量 public static final String FIRST = "the"; // 非常量变量 public static final String SECOND = null; // 常量变量 public static final String THIRD = "set"; }
现在假设你像下面这样改变了那个库类并且重新编译了这个类,但并不重新编译客户端的程序PrintWords:
class Words { public static final String FIRST = "physics"; public static final String SECOND = "chemistry"; public static final String THIRD = "biology"; }
此时,端的程序会打印出什么呢?结果是 the chemistry set,不是the null set,也不是physics chemistry biology,为什么?原因就是 null 不是一个编译期常量表达式,而其他两个都是。
对于常量变量(如上面Words类中的FIRST、THIRD)的引用(如在PrintWords类中对Words.FIRST、Words.THIRD的引用)会在编译期被转换为它们所表示的常量的值(即PrintWords类中的Words.FIRST、Words.THIRD引用会替换成"the"与"set")。
一个常量变量(如上面Words类中的FIRST、THIRD)的定义是,一个在编译期被常量表达式(即编译期常量表达式)初
始化的final的原生类型或String类型的变量。
那什么是“编译期常量表达式”?精确定义在[JLS 15.28]中可以找到,这样要说的是null不是一个编译期常量表达式。
由于常量变量会编译进客户端,API的设计者在设计一个常量域之前应该仔细考虑一下是否应该定义成常量变量。
如果你使用了一个非常量的表达式去初始化一个域,甚至是一个final或,那么这个域就不是一个常量。下面你可以通过将一个常量表达式传给一个方法使用得它变成一个非常量:
class Words { // 以下都成非常量变量 public static final String FIRST = ident("the"); public static final String SECOND = ident(null); public static final String THIRD = ident("set"); private static String ident(String s) { return s; } }
总之,常量变量将会被编译进那些引用它们的类中。一个常量变量就是任何常量表达式初始化的原生类型或字符串变量。且null不是一个常量表达式。
class Shuffle { private static Random rd = new Random(); public static void shuffle(Object[] a) { for (int i = 0; i < a.length; i++) { swap(a, i, rd.nextInt(a.length)); } } public static void swap(Object[] a, int i, int j) { Object tmp = a[i]; a[i] = a[j]; a[j] = tmp; } public static void main(String[] args) { Map map = new TreeMap(); for (int i = 0; i < 9; i++) { map.put(i, 0); } // 测试数组上的每个位置放置的元素是否等概率 for (int i = 0; i < 10000; i++) { Integer[] intArr = new Integer[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }; shuffle(intArr); for (int j = 0; j < 9; j++) { map.put(j,(Integer)map.get(j)+intArr[j]); } } System.out.println(map); for (int i = 0; i < 9; i++) { map.put(i,(Integer) map.get(i)/10000f); } System.out.println(map); } }
上面的算法不是很等概率的让某个元素打乱到其位置,程序运行了多次,大致的结果为:
{0=36031, 1=38094, 2=39347, 3=40264, 4=41374, 5=41648, 6=41780, 7=41188, 8=40274}
{0=3.6031, 1=3.8094, 2=3.9347, 3=4.0264, 4=4.1374, 5=4.1648, 6=4.178, 7=4.1188, 8=4.0274}
如果某个位置上等概率出现这9个值的话,则平均值会趋近于4,但测试的结果表明:开始的时候比较低,然后增长超过了平均值,最后又降下来了。
如果改用下面算法:
public static void shuffle(Object[] a) { for (int i = 0; i < a.length; i++) { swap(a, i, i + rd.nextInt(a.length - i)); } }
多次测试的结果大致如下:
{0=40207, 1=40398, 2=40179, 3=39766, 4=39735, 5=39710, 6=40074, 7=39871, 8=40060}
{0=4.0207, 1=4.0398, 2=4.0179, 3=3.9766, 4=3.9735, 5=3.971, 6=4.0074, 7=3.9871, 8=4.006}
所以修改后的算法是合理的。
另一种打乱集合的方式是通过Api中的Collections工具类:
public static void shuffle(Object[] a) { Collections.shuffle(Arrays.asList(a)); }
其实算法与上面的基本相似,当然我们使用API中提供的会更好,会在效率上获得最大的受益。