static
字面上,意思是静态的,一旦被static修饰,说明被修饰的对象在一定范围内是共享的,这时候需要注意并发读写的问题。
static 修饰类成员
static 修饰类成员时,如何保证线程安全是我们常常需要考虑的。当多个线程同时对共享变量进行读写时,很有可能会出现并发问题,如我们定义了:public static List<String> list = new ArrayList();
这样的共享变量。这个 list 如果同时被多个线程访问的话,就有线程安全的问题,这时候一般有两个解决办法:
- 把线程不安全的 ArrayList 换成 线程安全的 CopyOnWriteArrayList;
- 每次访问时,手动加锁。
static 修饰方法
当 static 修饰方法时,代表该方法和当前类是无关的,任意类都可以直接访问(如果权限是 public 的话)。
有一点需要注意的是,该方法内部只能调用同样被 static 修饰的方法,不能调用普通方法。
我们常用的 util 类里面的各种方法,我们比较喜欢用 static 修饰方法,好处就是调用特别方便,无需每次new出一个对象。
static 方法内部的变量在执行时是没有线程安全问题的。方法执行时,数据运行在栈里面,栈的数据每个线程都是隔离开的,所以不会有线程安全的问题。
static 修饰方法块
当 static 修饰方法块时,我们叫做静态代码块
,静态代码块常常用于在类启动之前,初始化一些值。
final
final 的意思是不变的、最终的,一般来说用于以下三种场景:
- 被 final 修饰的类,表明该类是无法继承的;
- 被 final 修饰的方法,表明该方法是无法覆写的;
- 被 final 修饰的变量,说明该变量在声明的时候,就必须初始化完成,而且以后也不能修改其
内存地址
。
注意第三点,无法修改其内存地址,并没有说无法修改其值。因为对于 List、Map 这些集合类或传址的对象来说,被 final 修饰后,是可以修改其内部值的,但却无法修改其初始化时的内存地址。
try、catch、finally
这三个关键字常用于捕捉异常,try 用来确定代码执行的范围,catch 捕捉可能会发生的异常,finally 用来执行一定要执行的代码块
,除了这些,我们还需要清楚,每个地方如果发生异常会怎么样,我们举一个例子来演示一下:
public static void testError () {
try {
System.out.println("try code");
if (true) {
throw new RuntimeException("try exception");
}
} catch (Exception e) {
System.out.println("catch code");
if (true) {
throw new RuntimeException("catch exception");
}
} finally {
System.out.println("finally code");
}
}
运行结果
可以看到两点:
- finally 先执行后,再抛出 catch 的异常;
- 最终捕获的异常是 catch 的异常,try 抛出来的异常已经被 catch 吃掉了。所以当我们遇见 catch 也有可能会抛出异常时,我们可以先打印出 try 的异常,这样 try 的异常在日志中就会有所体现。
default
default 关键字被很多源码使用,它是在jdk8时,被引入的。default 关键字用在接口的方法上,意思是对于该方法,实现类是无需强制实现的,但自己必须有默认实现。
volatile
volatile在牛津词典中,释义为易变的;无定性的;无常性的;不稳定的。
在java中,它常用来修饰某个共享变量,意思是当共享变量的值被修改后,会及时通知到其它线程上,其它线程就能知道当前共享变量的值已经被修改了。个人理解它的语义:是在声明时就告诉jvm,这个变量是容易改变的,需要时刻注意它的状态。
背景知识:在多核 CPU 下,为了提高效率,线程在拿值时,是直接和 CPU 缓存打交道的,而不是内存。主要是因为 CPU 缓存执行速度更快。比如线程要拿值 C,会直接从 CPU 缓存中拿, CPU 缓存中没有,就会从内存中拿,所以线程读的操作永远都是拿 CPU 缓存的值。
这时候会产生一个问题,CPU 缓存中的值和内存中的值可能并不是时刻都同步,导致线程计算的值可能不是最新的,共享变量的值有可能已经被其它线程所修改了,但此时修改是机器内存的值,CPU 缓存的值还是老的,导致计算会出现数据不一致的问题。
这时候有个机制,就是内存会主动通知 CPU 缓存。当前共享变量的值已经失效了,你需要重新来获取一份,CPU 缓存就会重新从内存中拿取一份最新的值。
volatile 关键字就会触发这种机制,加了 volatile 关键字的变量,就会被识别成共享变量,内存中值被修改后,会通知到各个 CPU 缓存,使 CPU 缓存中的值也对应被修改,从而保证线程从 CPU 缓存中拿取出来的值是最新的。
从图中我们可以看到,线程 1 和线程 2 一开始都读取了 C 值,CPU 1 和 CPU 2 缓存中也都有了 C 值,然后线程 1 把 C 值修改了,这时候内存的值和 CPU 2 缓存中的 C 值就不等了,内存这时发现 C 值被 volatile 关键字修饰,发现其是共享变量,就会使 CPU 2 缓存中的 C 值状态置为无效,CPU 2 会从内存中重新拉取最新的值,这时候线程 2 再来读取 C 值时,读取的已经是内存中最新的值了。
面试题
为什么使用了volatile变量i,而i++还是无法保证线程安全
答:volatile 可以保证可见性,但无法保证原子性,i++已经不是原子操作了,所以即使使用 volatile 修饰,也无法保证其线程安全,比较好的做法可以参考 AtomicInteger,值使用 volatile 修饰,保证多核下的可见性,数据修改使用 unsafe 方法,保证原子性。