java 关键字总结


Java有50个关键字,它们是:

abstract      do     implements      private       throw      Boolean     double     import      protected      
throws        break     else    instanceof     public    transient     byte      extends    int      return
true      case       false     interface     short     try    catch    final      long      static      void
char     finally       native    super       volatile       class    float       new        switch      while
continue    for       null        synchronized     const    default       if        package      this       goto

接下来对其中常用的几个关键字进行概括。


public private protected

public,protected,private是Java里用来定义成员的访问权限的,另外还有一种是“default”,也就是在成员前不加任何权限修饰符。
这四个修饰词de访问权限见下:

-- 类内部 package内 子类 其他
public 允许 允许 允许 允许
protected 允许 允许 允许 不允许
default 允许 允许 不允许 不允许
private 允许 不允许 不允许 不允许

比如:用protected修饰的成员(变量或方法),在类内部可以调用,同一个package下的其他类也可以调用,子类里也可以调用,其他地方则不可以调用,也就是说在其他。

在java中,除了这四种修饰词外,还有其他如abstract、static、final等11个修饰词。

  • public

使用对象:类、接口、成员
介绍:无论它所处在的包定义在哪,该类(接口、成员)都是可访问的

  • private

使用对象:成员
介绍:成员只可以在定义它的类中被访问

  • static

使用对象:类、方法、字段、初始化函数
介绍:成名为static的内部类是一个顶级类,它和包含类的成员是不相关的。静态方法是类方法,是被指向到所属的类而不是类的实例。静态字段是类字段,无论该字段所在的类创建了多少实例,该字段只存在一个实例被指向到所属的类而不是类的实例。初始化函数是在装载类时执行的,而不是在创建实例时执行的。

  • final

使用对象:类、方法、字段、变量
介绍:被定义成final的类不允许出现子类,不能被覆盖(不应用于动态查询),字段值不允许被修改。

  • abstract

使用对象:类、接口、方法
介绍:类中包括没有实现的方法,不能被实例化。如果是一个abstract方法,则方法体为空,该方法的实现在子类中被定义,并且包含一个abstract方法的类必须是一个abstract类

  • protected

使用对象:成员
介绍:成员只能在定义它的包中被访问,如果在其他包中被访问,则实现这个方法的类必须是该成员所属类的子类。

  • native

使用对象:成员
介绍:与操作平台相关,定义时并不定义其方法,方法的实现被一个外部的库实现。native可以与所有其它的java标识符连用,但是abstract除外。

public native int hashCode(); 
  • strictfp

使用对象:类、方法
介绍:strictfp修饰的类中所有的方法都隐藏了strictfp修饰词,方法执行的所有浮点计算遵守IEEE 754标准,所有取值包括中间的结果都必须表示为float或double类型,而不能利用由本地平台浮点格式或硬件提供的额外精度或表示范围。

  • synchronized

使用对象:方法
介绍:对于一个静态的方法,在执行之前jvm把它所在的类锁定;对于一个非静态类的方法,执行前把某个特定对象实例锁定。

  • volatile
    使用对象:字段

介绍:因为异步线程可以访问字段,所以有些优化操作是一定不能作用在字段上的。volatile有时可以代替synchronized。

  • transient
    使用对象:字段

介绍:字段不是对象持久状态的一部分,不应该把字段和对象一起串起。


volatile

先补充一下概念:Java 内存模型中的可见性、原子性和有序性。

可见性:

  可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

  可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

  在 Java 中 volatile、synchronized 和 final 实现可见性。

原子性:

  原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

  在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。

有序性:

  Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。
volatile强制要求了所有线程在使用变量的时候要去公共内存堆中获取值, 不可以偷懒使用自己的.
volatile绝对不保证原子性, 原子性只能用Synchronized同步修饰符实现.
下面看一个例子:

public class Counter {
 
    public static int count = 0;
 
    public static void inc() {
 
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
 
        count++;
    }
 
    public static void main(String[] args) {
 
        //同时启动1000个线程,去进行i++计算,看看实际结果
 
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        }
 
        //这里每次运行的值都有可能不同,可能为1000
        System.out.println("运行结果:Counter.count=" + Counter.count);
    }
}

运行结果:Counter.count=995
实际运算结果每次可能都不一样,本机的结果为:运行结果:Counter.count=995,可以看出,在多线程的环境下,Counter.count并没有期望结果是1000。
很多人以为,这个是多线程并发问题,只需要在变量count之前加上volatile就可以避免这个问题,那我们在修改代码看看,看看结果是不是符合我们的期望

public class Counter {
 
    public volatile static int count = 0;
 
    public static void inc() {
 
        //这里延迟1毫秒,使得结果明显
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
 
        count++;
    }
 
    public static void main(String[] args) {
 
        //同时启动1000个线程,去进行i++计算,看看实际结果
 
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Counter.inc();
                }
            }).start();
        }
 
        //这里每次运行的值都有可能不同,可能为1000
        System.out.println("运行结果:Counter.count=" + Counter.count);
    }
}

运行结果:Counter.count=992

运行结果还是没有我们期望的1000,下面我们分析一下原因:

在 java 垃圾回收整理一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

image
read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容

其中use and assign 可以多次出现
但是这一些操作并不是原子性,也就是在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。

对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的

例如假如线程1,线程2 在进行read,load操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6,线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6,导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。


测试volatile、AtomicInteger

这是美团一面面试官的一个问题:i++;在多线程环境下是否存在问题?如果存在,那怎么解决?。。。大部分人会说加锁或者synchronized同步方法。那有没有更好的方法?
示例代码:

public class IncrementTestDemo {

    public static int count = 0;
    public static Counter counter = new Counter();
    public static AtomicInteger atomicInteger = new AtomicInteger(0);
    volatile public static int countVolatile = 0;
    
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        count++;
                        counter.increment();
                        atomicInteger.getAndIncrement();
                        countVolatile++;
                    }
                }
            }.start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("static count: " + count);
        System.out.println("Counter: " + counter.getValue());
        System.out.println("AtomicInteger: " + atomicInteger.intValue());
        System.out.println("countVolatile: " + countVolatile);
    }
    
}

class Counter {
    private int value;

    public synchronized int getValue() {
        return value;
    }

    public synchronized int increment() {
        return ++value;
    }

    public synchronized int decrement() {
        return --value;
    }
}

输出结果为:  
static count: 9952
Counter: 10000
AtomicInteger: 10000
countVolatile: 9979

通过上面的例子说明,要解决自增操作在多线程环境下线程不安全的问题,可以选择使用Java提供的原子类,或者使用synchronized同步方法。
而通过Volatile关键字,并不能解决非原子操作的线程安全性。

结论分析:虽然递增操作++i是一种紧凑的语法,使其看上去只是一个操作,但这个操作并非原子的,因而它并不会作为一个不可分割的操作来执行。实际上,它包含了三个独立的操作:读取count的值,将值加1,然后将计算结果写入count。这是一个“读取 - 修改 - 写入”的操作序列,并且其结果状态依赖于之前的状态。

使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。

由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
参考文献:Java中Volatile关键字详解
参考文献:Java自增原子性问题(测试Volatile、AtomicInteger)


transient

Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想   
用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。   
transient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。 

注意static变量也是可以串行化的

下面使用实例可以看出效果:

public class Login implements java.io.Serializable   
{   
    private Date now = new Date();   
    private String uid;   
    private transient String pwd;   
      
    LoggingInfo(String username, String password)   
    {   
        uid = username;   
        pwd = password;   
    }   
    public String toString()   
    {   
        String password=null;   
        if(pwd == null)   
        {   
        password = "NOT SET";   
        }   
        else  
        {   
            password = pwd;   
        }   
        return "logon info: \n   " + "username: " + uid +   
            "\n   login date : " + now.toString() +   
            "\n   password: " + password;   
    }   
}   

现在我们创建一个这个类的实例,并且串行化(serialize)它 ,然后将这个串行化对象写如磁盘。

Login login = new Login("yy", "123456");   
System.out.println(login.toString());   
try  
{   
   ObjectOutputStream o = new ObjectOutputStream(   
                new FileOutputStream("login.out"));   
   o.writeObject(login);   
   o.close();   
}   
catch(Exception e) {//deal with exception}   
  
To read the object back, we can write   
  
try  
{   
   ObjectInputStream in =new ObjectInputStream(   
                new FileInputStream("logInfo.out"));   
   LoggingInfo logInfo = (LoggingInfo)in.readObject();   
   System.out.println(logInfo.toString());   
}   
catch(Exception e) {//deal with exception}   

如果我们运行这段代码,我们会注意到从磁盘中读回(read——back (de-serializing))的对象打印password为"NOT SET"。这是当我们定义pwd域为transient时,所期望的正确结果。
现在,让我们来看一下粗心对待transient域可能引起的潜在问题。假设我们修改了类定义,提供给transient域一个默认值,
代码如下:

public class Login implements java.io.Serializable   
{   
    private Date now = new Date();   
    private String uid;   
    private transient String pwd;   
      
    Login()   
    {   
        uid = "guest";   
        pwd = "guest";   
    }   
    public String toString()   
    {   
        //same as above   
     }   
}   

现在,如果我们串行化Login的一个实例,将它写入磁盘,并且再将它从磁盘中读出,我们仍然看到读回的对象打印password 为 "NOT SET"。当从磁盘中读出某个类的实例时,实际上并不会执行这个类的构造函数, 而是载入了一个该类对象的持久化状态,并将这个状态赋值给该类的另一个对象。

参考文献:Java transient关键字


static

Java语言的关键字,用来定义一个变量为类变量。类只维护一个类变量的拷贝,不管该类当前有多少个实例。"static" 同样能够用来定义一个方法为类方法。类方法通过类名调用而不是特定的实例,并且只能操作类变量。

static这块面试经常问的则是:Java的初始化块、静态初始化块、构造函数的执行顺序及用途
执行顺序:
写一个简单的demo来实验:

class A {
    static {
        System.out.println("Static init A.");
    }

    {
        System.out.println("Instance init A.");
    }

    A() {
        System.out.println("Constructor A.");
    }
}

class B extends A {
    static {
        System.out.println("Static init B.");
    }

    {
        System.out.println("Instance init B.");
    }

    B() {
        System.out.println("Constructor B.");
    }
}

class C extends B {

    static {
        System.out.println("Static init C.");
    }

    {
        System.out.println("Instance init C.");
    }

    C() {
        System.out.println("Constructor C.");
    }
}

public class Main {

    static {
        System.out.println("Static init Main.");
    }

    {
        System.out.println("Instance init Main.");
    }

    public Main() {
        System.out.println("Constructor Main.");
    }

    public static void main(String[] args) {
        C c = new C();
        //B b = new B();
    }

当然这里不使用内部类,因为==内部类不能使用静态的定义==;而用静态内部类就失去了一般性。
执行main方法,结果为:

Static init Main.
Static init A.
Static init B.
Static init C.
Instance init A.
Constructor A.
Instance init B.
Constructor B.
Instance init C.
Constructor C.

由以上结果我们可以发现:
先执行了Main类的静态块,接下来分别是A、B、C类的静态块,然后是A、B、C的初始化块和构造函数。其中,Main类的构造函数没有执行。
所有的静态初始化块都优先执行,其次才是非静态的初始化块和构造函数,它们的执行顺序是:

  1. 父类的静态初始化块
  2. 子类的静态初始化块
  3. 父类的初始化块
  4. 父类的构造函数
  5. 子类的初始化块
  6. 子类的构造函数

总结

  • 静态初始化块的优先级最高,也就是最先执行,并且仅在类第一次被加载时执行
  • 非静态初始化块和构造函数后执行,并且在每次生成对象时执行一次
  • 非静态初始化块的代码会在类构造函数之前执行。因此若要使用,应当养成把初始化块写在构造 函数之前的习惯,便于调试;
  • 静态初始化块既可以用于初始化静态成员变量,也可以执行初始化代码;
  • 非静态初始化块可以针对多个重载构造函数进行代码复用。

拓展

在spring中,如果在某一个类中使用初始化块或者静态块的话,要注意一点:不能再静态块或者初始化块中使用其他注入容器的bean或者带有@Value注解的变量值,因为该静态块或者初始化块会在spring容器初始化bean之前就执行,这样的话,在块中拿到的值则为null。但是如果只是要执行一下其他的操作(没有引用其他注入容器的bean或者带有@Value注解的变量值)时,则可以代替@PostConstruct或者implement InitializingBean 类。


synchronized

synchronized,Java同步关键字。用来标记方法或者代码块是同步的。Java同步块用来避免线程竞争。同步块在Java中是同步在某个对象上。所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。
有四种不同的同步块:

  • 实例方法
  • 静态方法
  • 实例方法中的同步块
  • 静态方法中的同步块

上述同步块都同步在不同对象上。实际需要那种同步块视具体情况而定。

1.实例方法同步

下面是一个同步的实例方法:

public synchronized void add(int value){
    this.count += value;
}

在方法声明synchronized关键字,告诉Java该方法是同步的。
Java实例方法同步是同步在拥有该方法的对象上。只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线程一次可以在一个实例同步块中执行操作。一个实例一个线程。

2.静态方法同步

Java静态方法同步如下示例:

public static synchronized void add(int value){
    this.count += value;
}

同样,这里synchronized 关键字告诉Java这个方法是同步的。

静态方法的同步是指同步在该方法所在的类对象上。因为在Java虚拟机中一个类只能对应一个类对象,而静态方法是类对象所持有的,所以同时只允许一个线程执行同一个类中的静态同步方法。

3.实例方法中的同步块

在非同步的Java方法中的同步块的例子如下所示:

public void add(int value){
    synchronized(this){
       this.count += value;
    }
}

注意Java同步块构造器用括号将对象括起来。在上例中,使用了“this”,即为调用add方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。上述代码使用监视器对象同步,同步实例方法使用调用方法本身的实例作为监视器对象。

一次只有一个线程能够在同步于同一个监视器对象的Java方法内执行。

下面两个例子都同步他们所调用的实例对象上,因此他们在同步的执行效果上是等效的。

public class MyClass {
    public synchronized void log1(String msg1, String msg2){
        log.writeln(msg1);
        log.writeln(msg2);
    }
    public void log2(String msg1, String msg2){
        synchronized(this){
            log.writeln(msg1);
            log.writeln(msg2);
       }
    }
}

在上例中,每次只有一个线程能够在两个同步块中任意一个方法内执行。

如果第二个同步块不是同步在this实例对象上,那么两个方法可以被线程同时执行。

4.静态方法中的同步块

public class MyClass {
    public static synchronized void log1(String msg1, String msg2){
        log.writeln(msg1);
        log.writeln(msg2);
    }
    public static void log2(String msg1, String msg2){
        synchronized(MyClass.class){
            log.writeln(msg1);
            log.writeln(msg2);
       }
    }
}

这两个方法不允许同时被线程访问。
如果第二个同步块不是同步在MyClass.class这个对象上。那么这两个方法可以同时被线程访问。

关于synchronized的用法,很多人用它的时候都会理解偏差。我们来看一个例子,下面这个例子也是很经典的。

public class Demo {
    public void synchronize A() {
        //do something...
    }
    public void synchronized B() {
        //do something...
    }
    public void C() {
        synchronized(this){
            //do something
        }
    }
}

很多人认为在多线程情况下,线程执行A方法和B方法时可以同时进行的,其实是错的。如果是不同的实例,当然不会有影响,但是那样synchronized就会失去意义。还有一种情况下,在使用spring进行web开发时,ApplicationContext容器默认所有的bean都是单例的,所以在这种情况下,同一时间,只能有一个线程进入A方法或者B方法。这样的话,在A方法和B方法上分别加synchronized就失去了高并发的意义。C方法意义和A、B方法是一样的,都是使用当前实例作为对象锁。所以我们尽量避免这样使用。可以参考下面的做法。

public class Demo {
    private byte[] lock1 = new byte[0];//java中生成一个0长度的byte数组比生成一个普通的Object容易;
    private byte[] lock2 = new byte[0];
    public void synchronize A() {
        //do something...
    }
    public void B() {
        synchronized(lock) {
            //do something...    
        }
        
    }
    public void C() {
        synchronized(lock2){
            //do something
        }
    }
}

除此之外,我们还要注意死锁的情况。在JAVA环境下 ReentrantLock和synchronized都是可重入锁,一定情况下避免了死锁。详情请参考可重入锁



丶木叶
112 声望5 粉丝

我始终相信比你优秀的人过着比你更努力的生活。