什么是 Java Marker Interface(标记接口)
先看看什么是标记接口?标记接口有时也叫标签接口(Tag interface),即接口不包含任何方法。在 Java 里很容易找到标记接口的例子,比如 JDK 里的 Serializable 接口就是一个标记接口。
首先明确一点,Marker Interface(标记接口)决不是 Java 这门编程语言特有的,而是计算机科学中一种通用的设计理念。
我们看 Wikipedia 里对标记接口的定义。
“The tag/ marker interface pattern is a design pattern in computer science, used with languages that provide run-time type information about objects. It provides a means to associate metadata with a class where the language does not have explicit support for such metadata.“
我试了下 Google Translate 翻译上面这段话,翻得很差劲,所以我来解释一下。
标记接口是计算机科学中的一种设计思路。编程语言本身不支持为类维护元数据。而标记接口则弥补了这个功能上的缺失——一个类实现某个没有任何方法的标记接口,实际上标记接口从某种意义上说就成为了这个类的元数据之一。运行时,通过编程语言的反射机制,我们就可以在代码里拿到这种元数据。
以 Serializable 接口为例。一个类实现了这个接口,说明它可以被序列化。因此,我们实际上通过 Serializable 这个接口,给该类标记了“可被序列化”的元数据,打上了“可被序列化”的标签。这也是标记/标签接口名字的由来。
下面的代码是我从 JDK 源代码中摘出来的:
Java 里的序列化,字符串,数组,枚举类和普通类是分别进行的。如果当前待序列化的变量既不是字符串,也不是数组和枚举类,那么就检测该类是否实现了 Serializable 的接口,大家注意下图第 1177 行就执行了这种检测。如果没有实现 Serializable 接口,就会抛出异常 NotSerializableException。
大家也许会问,在 Spring 里满天飞的注解(Annotation)不是最好的用来维护元数据的方式么?确实,Annotation 能声明在 Java 包、类、字段、方法、局部变量、方法参数等的前面用于维护元数据的目的,既灵活又方便。然而这么好的东西,只有在 JDK1.5 之后才能用。JDK1.5 之前维护元数据的重任就落在标记接口上了。
大家看另一个标记接口,Cloneable。下图第 51 行清晰标注了该接口从 JDK1.0 起就有了。
JDK 源代码里的 Clone 方法的注释也清晰注明了,如果一个类没有实现 Cloneable 接口,在执行 clone 方法时会抛出 CloneNotSupportedException 异常。
相信大多数 Java 程序员都学习过 volatile 这个关键字的用法。百度百科上对 volatile 的定义:
volatile 是一个类型修饰符(type specifier),被设计用来修饰被不同线程访问和修改的变量。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
可能有很多刚学 Java 的朋友们看了上面这段非常笼统的描述后仍然觉得云里雾里的。
下面我们就用一个具体的例子来学习 volatile 的用法。
看这个例子:
这段代码在主线程的第二行定义了一个布尔变量 stop, 然后主线程启动一个新线程,在线程里不停得增加计数器 i 的值,直到主线程的布尔变量 stop 被主线程置为 true 才结束循环。
主线程用 Thread.sleep 停顿 1 秒后将布尔值 stop 置为 true。
因此,我们期望的结果是,上述 Java 代码执行 1 秒钟后停止,并且打印出 1 秒钟内计数器 i 的实际值。
然而,执行这个 Java 应用后,你发现它进入了死循环,在任务管理器里发现这个 Java 程序 CPU 占用率飙升。
原因是什么呢?让我们温习下计算机专业课操作系统中讲过的内存模型的知识。
以 Java 内存模型为例,Java 内存模型分为主内存(main memory)和工作内存(work memory)。主内存内的变量由所有线程共享,每个线程拥有自己的工作内存,里面的变量包含了线程局部变量。主内存中的变量如果被线程使用到,则线程的工作内存会维护一份主内存变量的副本拷贝。
线程对变量的所有读写操作必须在工作内存中进行,不能直接操作主内存中的变量。不同线程之间也无法直接访问对方的工作内存。线程间变量的传递需通过主内存来完成。线程、主内存、工作内存三者之间的交互关系如下图:
如果线程在自己的执行代码里修改了定义在主线程(主内存)中的变量,修改直接发生在线程的工作内存里,然后在某个时刻(Java 程序员无法控制这个时刻,而是由 JVM 调度的),这个修改从工作内存写回到主内存。
回到我们的例子。尽管主线程修改了 stop 变量,但是仅仅修改了主内存中的值,而操作计数器的线程的工作内存里的 stop 变量还是旧的值,始终为 false。因此这个线程陷入了死循环。
知道了原理,解决方案就很简单了。在 stop 变量前加上关键字 volatile 进行修饰,这样在计数器线程里每次读取 stop 的值时,volatile 会强制该线程从主内存读取,而不是从当前线程的工作内存读取。这样就避免了死循环。下图显示 1 秒钟之后,计数器执行了 14 亿次。
要获取更多 Jerry 的原创技术文章,请关注公众号"汪子熙"。
版权声明: 本文为 InfoQ 作者【Jerry Wang】的原创文章。
原文链接:【http://xie.infoq.cn/article/1d3f9c74e69d1e39388844476】。文章转载请联系作者。
评论