写点什么

什么是 Java Marker Interface(标记接口)

作者:Jerry Wang
  • 2021 年 12 月 13 日
  • 本文字数:2470 字

    阅读完需:约 8 分钟

什么是 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 源代码中摘出来的:


if (obj instanceof String) {  writeString((String) obj, unshared);} else if (cl.isArray()) {  writeArray(obj, desc, unshared);} else if (obj instanceof Enum) {  writeEnum((Enum) obj, desc, unshared);} else if (obj instanceof Serializable) {  writeOrdinaryObject(obj, desc, unshared);} else {  if (extendedDebugInfo) {    throw new NotSerializableException(cl.getName() + " "    + debugInfoStack.toString());  } else {    throw new NotSerializableException(cl.getName());  }}
复制代码


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 的用法。


看这个例子:


public class ThreadVerify {  public static Boolean stop = false;  public static void main(String args[]) throws InterruptedException {    Thread testThread = new Thread(){      @Override      public void run(){        int i = 1;        while(!stop){          //System.out.println("in thread: " + Thread.currentThread() + " i: " + i);          i++;        }        System.out.println("Thread stop i="+ i);      }    }    ;    testThread.start();    Thread.sleep(1000);    stop = true;    System.out.println("now, in main thread stop is: " + stop);    testThread.join();  }}
复制代码


这段代码在主线程的第二行定义了一个布尔变量 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 的原创技术文章,请关注公众号"汪子熙"。


发布于: 1 小时前阅读数: 7
用户头像

Jerry Wang

关注

个人微信公众号:汪子熙 2017.12.03 加入

SAP成都研究院开发专家,SAP社区导师,SAP中国技术大使。

评论

发布
暂无评论
什么是 Java Marker Interface(标记接口)