Java中的关键字
transient
在关于 java 的集合类的学习中,我们发现 ArrayList 类和 Vector 类都是使用数组实 现的,但是在定义数组 elementData 这个属性时稍有不同,那就是 ArrayList 使用 transient 关键字
private transient Object[] elementData; protected Object[] elementData;
那么,首先我们来看一下 transient 关键字的作用是什么。
Transient
Java 语言的关键字,变量修饰符,如果用 transient 声明一个实例变量,当对象存储 时,它的值不需要维持。这里的对象存储是指,Java 的 serialization 提供的一种持久化对 象实例的机制。当一个对象被序列化的时候,transient 型变量的值不包括在序列化的表示 中,然而非 transient 型的变量是被包括进去的。使用情况是:当持久化对象时,可能有一 个特殊的对象数据成员,我们不想用 serialization 机制来保存它。为了在一个特定对象的 一个域上关闭 serialization,可以在这个域前加上关键字 transient。
简单点说,就是被 transient 修饰的成员变量,在序列化的时候其值会被忽略,在被反 序列化后, transient 变量的值被设为初始值, 如 int 型的是 0,对象型的是 null。
instanceof
instanceof 是 Java 的一个二元操作符,类似于 ==,>,< 等操作符。
instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类 的实例,返回 boolean 的数据类型。
以下实例创建 displayObjectClass() 方法来演示 Java instanceof 关键字用法:
public static void displayObjectClass(Object o){ if (o instanceof Vector) System.out.println("对象是 java.util.Vector 类的实例"); else if (o instanceof ArrayList) System.out.println("对象是 java.util.ArrayList 类的实例"); Else System.out.println("对象是 " + o.getClass() + " 类的实例"); }
volatile
在再有人问你 Java 内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java 语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相 关的关键字,比如 synchronized、volatile、final、concurren 包等。在前一篇文章中, 我们也介绍了 synchronized 的用法及原理。本文,来分析一下另外一个关键字—— volatile。
本文就围绕 volatile 展开,主要介绍 volatile 的用法、volatile 的原理,以及 volatile 是如何提供可见性和有序性保障的等。
volatile 这个关键字,不仅仅在 Java 语言中有,在很多语言中都有的,而且其用法和 语义也都是不尽相同的。尤其在 C 语言、C++以及 Java 中,都有 volatile 关键字。都可 以用来声明变量或者对象。下面简单来介绍一下 Java 语言中的 volatile 关键字。
volatile 的用法
volatile 通常被比喻成"轻量级的 synchronized",也是 Java 并发编程中比较重要的 一个关键字。和 synchronized 不同,volatile 是一个变量修饰符,只能用来修饰变量。无 法修饰方法及代码块等。
volatile 的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用 volatile 修饰就可以了。
public class Singleton{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
如以上代码,是一个比较典型的使用双重锁校验的形式实现单例的,其中使用 volatile 关键字修饰可能被多个线程同时访问到的 singleton。
volatile 的原理
在再有人问你 Java 内存模型是什么,就把这篇文章发给他中我们曾经介绍过,为了提 高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓 存,就存在缓存数据不一致问题。
但是,对于 volatile 变量,当对 volatile 变量进行写操作的时候,JVM 会向处理器发 送一条 lock 前缀的指令,将这个缓存中的变量回写到系统主存中。
但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题, 所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。
缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不 是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设 置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数 据读到处理器缓存里。
所以,如果一个变量被 volatile 所修饰的话,在每次数据变化之后,其值都会被强制刷 入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载 到自己的缓存中。这就保证了一个 volatile 在并发编程中,其值在多个缓存中是可见的。
volatile 与可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能 够立即看得到修改的值。
我们在再有人问你 Java 内存模型是什么,就把这篇文章发给他中分析过:Java 内存 模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存 中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内 存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量, 线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现 线程 1 改了某个变量的值,但是线程 2 不可见的情况。
前面的关于 volatile 的原理中介绍过了,Java 中的 volatile 关键字提供了一个功能, 那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前 都从主内存刷新。因此,可以使用 volatile 来保证多线程操作时变量的可见性。
volatile 与有序性
有序性即程序执行的顺序按照代码的先后顺序执行。
我们在再有人问你 Java 内存模型是什么,就把这篇文章发给他中分析过:除了引入了 时间片以外,由于处理器优化和指令重排等,CPU 还可能对输入代码进行乱序执行,比如 load->add->save 有可能被优化成 load->save->add 。这就是可能存在有序性问题。
而 volatile 除了可以保证数据的可见性之外,还有一个强大的功能,那就是他可以禁止 指令重排优化等。
普通的变量仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确 的结果,而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。
volatile 可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。 这就保证了有序性。被 volati le 修饰的变量的操作,会严格按照代码顺序执行, load->add->save 的执行顺序就是:load、add、save。
volatile 与原子性
原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
我们在 Java 的并发编程中的多线程问题到底是怎么回事儿?中分析过:线程是 CPU 调度的基本单位。CPU 有时间片的概念,会根据不同的调度算法进行线程调度。当一个线 程获得时间片之后开始执行,在时间片耗尽之后,就会失去 CPU 使用权。所以在多线程场 景下,由于时间片在线程间轮换,就会发生原子性问题。
在上一篇文章中,我们介绍 synchronized 的时候,提到过,为了保证原子性,需要 通过字节码指令 monitorenter 和 monitorexit,但是 volatile 和这两个指令之间是没有任何关系的。
所以,volatile 是不能保证原子性的。
在以下两个场景中可以使用 volatile 来代替 synchronized:
1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值。
2、变量不需要与其他状态变量共同参与不变约束。
除以上场景外,都需要使用其他方式来保证原子性,如 synchronized 或者 concurrent 包。
public class Test{ public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start();} while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
我们来看一下 volatile 和原子性的例子:
以上代码比较简单,就是创建 10 个线程,然后分别执行 1000 次 i++操作。正常情况 下,程序的输出结果应该是 10000,但是,多次执行的结果都小于 10000。这其实就是 volatile 无法满足原子性的原因。
为什么会出现这种情况呢,那就是因为虽然 volatile 可以保证 inc 在多个线程之间的可 见性。但是无法 inc++的原子性。
总结与思考
我们介绍过了 volatile 关键字和 synchronized 关键字。现在我们知道, synchronized 可以保证原子性、有序性和可见性。而 volatile 却只能保证有序性和可见性。
那么,我们再来看一下双重校验锁实现的单例,已经使用了 synchronized,为什么还 需要 volatile?
public class Singleton{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
答案,我们在下一篇文章:既生 synchronized,何生 volatile 中介绍。
synchronized
在再有人问你 Java 内存模型是什么,就把这篇文章发给他。中我们曾经介绍过, Java 语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发 处理相关的关键字,比如 synchronized、volatile、final、concurren 包等。
在《深入理解 Java 虚拟机》中,有这样一段话:
synchronized 关键字在需要原子性、可见性和有序性这三种特性的时候都可以作为其 中 一 种 解 决 方 案 , 看 起 来 是“ 万 能 ” 的 。 的 确 , 大 部 分 并 发 控 制 操 作 都 能 使 用 synchronized 来完成。
海明威在他的《午后之死》说过的:“冰山运动之雄伟壮观,是因为他只有八分之一在 水面上。”对于程序员来说,synchronized 只是个关键字而已,用起来很简单。之所以我 们可以在处理多线程问题时可以不用考虑太多,就是因为这个关键字帮我们屏蔽了很多细 节。
那么,本文就围绕 synchronized 展开,主要介绍 synchronized 的用法、 synchronized 的原理,以及 synchronized 是如何提供原子性、可见性和有序性保障的等。
synchronized 的用法
synchronized 是 Java 提供的一个并发控制的关键字。主要有两种用法,分别是同步 方法和同步代码块。也就是说,synchronized 既可以修饰方法也可以修饰代码块。
/ ** * @author Hollis 18/08/04. */ public class SynchronizedDemo { //同步方法 public synchronized void doSth(){ System.out.println("Hello World"); } //同步代码块 public void doSth1(){ synchronized (SynchronizedDemo.class){ System.out.println("Hello World"); } } }
被 synchronized 修饰的代码块及方法,在同一时间,只能被单个线程访问。
synchronized 的实现原理
synchronized,是 Java 中用于解决并发情况下数据同步访问的一个很重要的关键字。 当我们想要保证一个共享资源在同一时间只会被一个线程访问到时,我们可以在代码中使用 synchronized 关键字对类或者对象加锁。
在深入理解多线程(一)——Synchronized 的实现原理中我曾经介绍过其实现原理, 为了保证知识的完整性,这里再简单介绍一下,详细的内容请去原文阅读。
我们对上面的代码进行反编译,可以得到如下代码:
public synchronized void doSth(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/ io/PrintStream; 3: ldc #3 // String Hello World 5: invokevirtual #4 // Method java/io/PrintStream.print ln:(Ljava/lang/String;)V 8: return public void doSth1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: ldc #5 // class com/hollis/SynchronizedTest 2: dup 3: astore_1 4: monitorenter 5: getstatic #2 // Field java/lang/System.out:Ljava/ io/PrintStream; 8: ldc #3 // String Hello World 10: invokevirtual #4 // Method java/io/PrintStream.print ln:(Ljava/lang/String;)V 13: aload_1 14: monitorexit 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return
通过反编译后代码可以看出:对于同步方法,JVM 采用 ACC_SYNCHRONIZED 标 记符来实现同步。 对于同步代码块。JVM 采用 monitorenter、monitorexit 两个指令来 实现同步。
在 The Java® Virtual Machine Specification 中有关于同步方法和同步代码块的 实现原理的介绍,我翻译成中文如下:
方法级的同步是隐式的。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标 志。当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果 有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时 如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果 在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法 外面之前监视器锁会被自动释放。
同步代码块使用 monitorenter 和 monitorexit 两个指令实现。可以把执行 monitorenter 指令理解为加锁,执行 monitorexit 理解为释放锁。 每个对象维护着一个记 录着被锁次数的计数器。未被锁定的对象的该计数器为 0,当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计 数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当 计数器为 0 的时候。锁将被释放,其他线程便可以获得锁。
无论是 ACC_SYNCHRONIZED 还是 monitorenter、monitorexit 都是基于 Monitor 实现的,在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由 ObjectMonitor 实现。
ObjectMonitor 类中提供了几个方法,如 enter、exit、wait、notify、notifyAll 等。 sychronized 加锁的时候,会调用 objectMonitor 的 enter 方法,解锁的时候会调用 exit 方法。(关于 Monitor 详见深入理解多线程(四)—— Moniter 的实现原理)
synchronized 与原子性
原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
我们在 Java 的并发编程中的多线程问题到底是怎么回事儿?中分析过:线程是 CPU 调度的基本单位。CPU 有时间片的概念,会根据不同的调度算法进行线程调度。当一个线 程获得时间片之后开始执行,在时间片耗尽之后,就会失去 CPU 使用权。所以在多线程场 景下,由于时间片在线程间轮换,就会发生原子性问题。
在 Java 中,为了保证原子性,提供了两个高级的字节码指令 monitorenter 和 monitorexit。前面中,介绍过,这两个字节码指令,在 Java 中对应的关键字就是 synchronized。
通过 monitorenter 和 monitorexit 指令,可以保证被 synchronized 修饰的代码在同 一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在 Java 中 可以使用 synchronized 来保证方法和代码块内的操作是原子性的。
线程 1 在执行 monitorenter 指令的时候,会对 Monitor 进行加锁,加锁后其他线程无 法获得锁,除非线程 1 主动解锁。即使在执行过程中,由于某种原因,比如 CPU 时间片用 完,线程 1 放弃了 CPU,但是,他并没有进行解锁。而由于 synchronized 的锁是可重入 的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。 这就保证了原子性。
synchronized 与可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能 够立即看得到修改的值。
我们在再有人问你 Java 内存模型是什么,就把这篇文章发给他。中分析过:Java 内 存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内 存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作 内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变 量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能 出现线程 1 改了某个变量的值,但是线程 2 不可见的情况。
前面我们介绍过,被 synchronized 修饰的代码,在开始执行时会加锁,执行完成后 会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此 变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。
所以,synchronized 关键字锁住的对象,其值是具有可见性的。
synchronized 与有序性
有序性即程序执行的顺序按照代码的先后顺序执行。
我们在再有人问你 Java 内存模型是什么,就把这篇文章发给他。中分析过:除了引入 了时间片以外,由于处理器优化和指令重排等,CPU 还可能对输入代码进行乱序执行,比 如 load->add->save 有可能被优化成 load->save->add 。这就是可能存在有序性问题。
这里需要注意的是,synchronized 是无法禁止指令重排和处理器优化的。也就是说, synchronized 无法避免上述提到的问题。
那么,为什么还说 synchronized 也提供了有序性保证呢?
这就要再把有序性的概念扩展一下了。Java 程序中天然的有序性可以总结为一句话: 如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有 操作都是无序的。
以上这句话也是《深入理解 Java 虚拟机》中的原句,但是怎么理解呢?周志明并没有 详细的解释。这里我简单扩展一下,这其实和 as-if-serial 语义有关。
as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单 线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守 as-if-serial 语义。
这里不对 as-if-serial 语义详细展开了,简单说就是,as-if-serial 语义保证了单线 程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以 认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种 重排的干扰。
所以呢,由于 synchronized 修饰的代码,同一时间只能被同一线程访问。那么也就 是单线程执行的。所以,可以保证其有序性。
synchronized 与锁优化
前面介绍了 synchronized 的用法、原理以及对并发编程的作用。是一个很好用的关 键字。
synchronized 其实是借助 Monitor 实现的,在加锁时会调用 objectMonitor 的 enter 方法,解锁的时候会调用 exit 方法。事实上,只有在 JDK1.6 之前,synchronized 的实现才会直接调用 ObjectMonitor 的 enter 和 exit,这种锁被称之为重量级锁。
所以,在 JDK1.6 中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除, 适应性自旋锁,锁粗化(自旋锁在 1.4 就有,只不过默认的是关闭的,jdk1.6 是默认开启的), 这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题。
关于自旋锁、锁粗化和锁消除可以参考:
好啦,关于 synchronized 关键字,我们介绍了其用法、原理、以及如何保证的原子 性、顺序性和可见性,同时也扩展的留下了锁优化相关的资料及思考。后面我们会继续介绍 volatile 关键字以及他和 synchronized 的区别等。
final
final 是 Java 中的一个关键字,它所表示的是“这部分是无法修改的”。
使用 final 可以定义 :变量、方法、类。
final 变量
如果将变量设置为 final,则不能更改 final 变量的值(它将是常量)。
class Test{ final String name = "Hollis"; }
一旦 final 变量被定义之后,是无法进行修改的。
final 方法
如果任何方法声明为 final,则不能覆盖它。
class Parent{ final void name() { System.out.println("Hollis"); } }
当我们定义以上类的子类的时候,无法覆盖其 name 方法,会编译失败。
final 类
如果把任何一个类声明为 final,则不能继承它。
final class Parent { }
以上类不能被继承!
static
static 表示“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态 static 代码块。
静态变量
我们用 static 表示变量的级别,一个类中的静态变量,不属于类的对象或者实例。因 为静态变量与所有的对象实例共享,因此他们不具线程安全性。
通常,静态变量常用 final 关键来修饰,表示通用资源或可以被所有的对象所使用。如 果静态变量未被私有化,可以用“类名.变量名”的方式来使用。
//static variable example private static int count; public static String str;
静态方法
与静态变量一样,静态方法是属于类而不是实例。
一个静态方法只能使用静态变量和调用静态方法。通常静态方法通常用于想给其他的类 使用而不需要创建实例。例如:Collections class(类集合)。
Java 的包装类和实用类包含许多静态方法。main()方法就是 Java 程序入口点,是静 态方法。
//static method example public static void setCount(int count) { if(count > 0) StaticExample.count = count; } //static util method public static int addInts(int i, int...js){ int sum=i; for(int x : js) sum+=x; return sum; }
从 Java8 以上版本开始也可以有接口类型的静态方法了。
静态代码块
Java 的静态块是一组指令在类装载的时候在内存中由 Java ClassLoader 执行。
静态块常用于初始化类的静态变量。大多时候还用于在类装载时候创建静态资源。
Java 不允许在静态块中使用非静态变量。一个类中可以有多个静态块,尽管这似乎没 有什么用。静态块只在类装载入内存时,执行一次。
Static{ //can be used to initialize resources when class is loaded System.out.println("StaticExample static block"); //can access only static variables and methods str="Test"; setCount(2); }
静态类
Java 可以嵌套使用静态类,但是静态类不能用于嵌套的顶层。
静态嵌套类的使用与其他顶层类一样,嵌套只是为了便于项目打包。
原文地址:https://zhuanlan.zhihu.com/p/26819685
const
const 是 Java 预留关键字,用于后期扩展用,用法跟 final 相似,不常用。