由两个bug引发的对Java类加载时机的思考

有一个单例类是这么写的:

public class BluetoothManager {

    private static BluetoothManager sInstance;

    public static BluetoothManager getInstance() {
        if (sInstance == null) {
            synchronized (BluetoothManager.class) {
                if (sInstance == null) {
                    sInstance = new BluetoothManager();
                }
            }
        }
        return sInstance;
    }

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    private final LeScanCallback mLeScanCallback = new LeScanCallback() {
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {

        }
    };
}

这个类在实际运行的过程中偶然会崩溃,原因在于调用getInstance可能在主线程,也可能在子线程,第一次调用getInstance会触发BluetoothManager单例对象的创建,由于mHandler是对象内部的成员变量,所以会跟着一起初始化,由于没有传入Looper,所以会默认传入当前调用者所在线程的Looper,倘若该线程没有Looper就会抛出异常。所以这个问题会出现在首次调用getInstance在没有Looper的子线程中,解决的办法是构造Handler时要传入一个有效的Looper。

第二个问题是LeScanCallback,这个类是用于蓝牙BLE扫描的回调,需要在API 18及以上使用,否则会崩溃。在API 18以下即便不扫描,没有用到这个mLeScanCallback,只要有任何操作触发了mLeScanCallback的初始化,都会导致崩溃。

那什么情况下会触发BluetoothManager的这个内部成员变量的初始化呢?由于不是静态的,所以只有创建BluetoothManager对象的时候会初始化mLeScanCallback。倘若mLeScanCallback是静态的,则只要BluetoothManager类初始化时就会被初始化。

接下来总结一下Java的类加载,先看看类初始化的时机:

  1. 使用new关键字实例化对象的时候,引用一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)或调用静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

注意上面第一条,被final修饰的静态变量,如果是类似于String这种基本类型会被放入常量池,引用这种字段是和类没关系的,所以不会触发类的初始化。

顺便提一下,判断一个类是否被初始化可以考虑的办法是在类的全局static代码块中打日志。

当一个类初始化时,先调用类的static代码块,然后初始化类中的静态成员(不包括静态内部类),如果是创建类对象,则接下来还要初始化类中的非静态成员,最后才会调用类的构造函数。

来看看如下的单例模式:

public class BluetoothManager {

    private BluetoothManager() {

    }

    private static class BluetoothManagerHolder {
        private static BluetoothManager instance = new BluetoothManager();
    }

    public static BluetoothManager getInstance() {
        return BluetoothManagerHolder.instance;
    }
}

当BluetoothManager类初始化时不会触发BluetoothManagerHolder的初始化,只有调用getInstance时引用到了BluetoothManagerHolder的内部静态成员变量时才会触发BluetoothManagerHolder的初始化,这样可以达到延迟加载的效果。假如有多个线程同时调用getInstance,由于instance是在BluetoothManagerHolder类初始化时跟着初始化的,所以是由Java虚拟机保证线程安全的,无需额外的同步,这种单例模式值得推荐。

时间: 2024-08-30 01:36:51

由两个bug引发的对Java类加载时机的思考的相关文章

从一道面试题来认识java类加载时机与过程

说明:本文的内容是看了<深入理解Java虚拟机:JVM高级特性与最佳实践>后为加印象和理解,便记录了重要的内容. 1  开门见山 以前曾经看到过一个java的面试题,当时觉得此题很简单,可是自己把代码运行起来,可是结果并不是自己想象的那样.题目如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class SingleTon {     private static SingleTon singleTon = new S

从一道面试题来认识java类加载时机与过程【转】

说明:本文的内容是看了<深入理解Java虚拟机:JVM高级特性与最佳实践>后为加印象和理解,便记录了重要的内容. 1  开门见山 以前曾经看到过一个java的面试题,当时觉得此题很简单,可是自己把代码运行起来,可是结果并不是自己想象的那样.题目如下: class SingleTon { private static SingleTon singleTon = new SingleTon(); public static int count1; public static int count2

java类加载时机与过程

1  开门见山 以前曾经看到过一个java的面试题,当时觉得此题很简单,可是自己把代码运行起来,可是结果并不是自己想象的那样.题目如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class SingleTon {     private static SingleTon singleTon = new SingleTon();     public static int count1;     public static

计算两个集合的交集数字(java)

循环判断2个数组 将相同的公共元素复制到新数组中即可 1 2 3 import java.util.Arrays; 4 5 public class count_same_number { 6 7 public static int[] join(int[] a,int[] b) 8 { 9 int count=0; 10 int new_target[]=new int[Math.max(a.length, b.length)];//新数组 11 int index=0; 12 for(int

android4.4的两个bug

1.Android4.4在解析jpg头的时候使用的库不支持多线程,造成拷贝大量jpg文件的时候出错. step to reproduce. a. Connect PC & DUT via USB cable. b. Settings -> Storage, check with MTP mode. c.copy jpg files to the device.Total size is about 100M. Actual result :Sometimes, the copy will f

两种常用的全排列算法(java)

问题:给出一个字符串,输出所有可能的排列. 全排列有多种算法,此处仅介绍常用的两种:字典序法和递归法. 1.字典序法: 如何计算字符串的下一个排列了?来考虑"926520"这个字符串,我们从后向前找第一双相邻的递增数字,"20"."52"都是非递增的,"26 "即满足要求,称前一个数字2为替换数,替换数的下标称为替换点,再从后面找一个比替换数大的最小数(这个数必然存在),0.2都不行,5可以,将5和2交换得到"956

thrift的bug引发对Go异常处理机制的思考

Go语言没有异常处理机制,大部分情况下只能用panic和recover.在这种机制下,函数体中的被调函数中出现的错误是无法处理的,只能静候崩溃. 今天我发现thrift在生成的RPC代码有空指针BUG导致程序崩溃,这个有BUG的函数如果被有try/catch的语言调用,至少是可以保证程序不崩溃的.但是在Go的机制下,我们只能放弃了RPC的某些功能. 由此,我认为GO的异常处理机制需要完善. thrift的bug引发对Go异常处理机制的思考

两道面试题,带你透彻解析Java类加载机制

在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa { static { System.out.println("爷爷在静态代码块"); } } class Father extends Grandpa { static { System.out.println("爸爸在静态代码块"); } public static int factor = 25; public Father() { System.ou

用两道面试题带你详细了解 Java 类加载机制

在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa{static{System.out.println("爷爷在静态代码块");}} class Father extends Grandpa{static{System.out.println("爸爸在静态代码块");}public static int factor = 25;public Father(){System.out.println("