牛客网Java错题总结(1)

目录

一、java并发

二、字符串与数组的长度

三、Java的变量命名

四、CMS垃圾回收

五、单例设计模式

六、Hashtable 和 HashMap 的区别


一、java并发

解析:

  • A、CopyOnWriteArrayList适用于写少读多的并发场景
  • B、ReadWriteLock即为读写锁,他要求写与写之间互斥,读与写之间互斥,读与读之间可以并发执行。在读多写少的情况下可以提高效率
  • C、ConcurrentHashMap是同步的HashMap,读操作不加锁,写操作加锁
  • D、volatile只保证多线程操作的可见性,不保证原子性。自增操作不是原子性操作(包括读取变量、变量+1、写入内存三步),在执行时可能会有安全问题。
    • 概念
      • volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略
    • 特性
      • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
      • 禁止进行指令重排序。(实现有序性)
      • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。关于volatile 原子性可以理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
    • 参考网址:《volatile 关键字,你真的理解吗?》https://zhuanlan.zhihu.com/p/138819184、《浅谈volatile在i++情况下失效》https://blog.csdn.net/liuzhixiong_521/article/details/85246543

二、字符串与数组的长度

解析:

  1. String获取长度用的是length()方法,而数组类型我们直接用属性length获取长度,所以String[]数组类型我们应该用length获取长度;
  2. 总结来说,因为原字符串不包含分隔符,所以直接返回原字符串,分割出来只有一个空的字符串数组,所以结果是1。(注意,虽然原字符串为空,存到字符串数组为空,但是这个空字符串也会算一个元素)

三、Java的变量命名

解析:

Java标识符由数字,字母和下划线(_),美元符号($)或人民币符号(¥)组成。

在Java中是区分大小写的,而且还要求首位不能是数字,可以是下划线。最重要的是,Java关键字不能当作Java标识符。

四、CMS垃圾回收

解析:

CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器,对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。

CMS的基础算法是:标记—清除。它的过程可以分为以下6个步骤:

  • 初始标记(STW initial mark)
    • 在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了
  • 并发标记(Concurrent marking)
    • 这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿
  • 并发预清理(Concurrent precleaning)
    • 并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。
  • 重新标记(STW remark)
    • 这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"根对象"开始向下追溯,并处理对象关联。
  • 并发清理(Concurrent sweeping)
    • 清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行
  • 并发重置(Concurrent reset)
    • 这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。

五、单例设计模式

解析:

  • 单例设计模式定义
    • 确保一个类只有一个实例,并提供该实例的全局访问点。
    • 这样做的好处是:有些实例,全局只需要一个就够了,使用单例模式就可以避免一个全局使用的类,频繁的创建与销毁,耗费系统资源。
  • 单例模式的设计要素
    • 创建一个私有构造函数 (确保只能单例类自己创建实例)
    • 创建一个私有静态变量 (确保只有一个实例)
    • 创建一个公有静态函数 (给使用者提供调用方法)

单例设计模式的实现与各自优缺点:

1、懒汉式(线程不安全)

public class Singleton { 
    private static Singleton uniqueInstance;
    private Singleton() {}
    public static Singleton getUniqueInstance() { 
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        } return uniqueInstance;
    }
}
  • 说明:先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。
  • 优点:延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。
  • 缺点:线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;
  • 解决方案:在getUniqueInstance()方法上添加锁:private static synchronized Singleton getUinqueInstance(),避免多个线程同时获取实例

2、饿汉式(线程安全)

public class Singleton {
    private static Singleton uniqueInstance = new Singleton();
    private Singleton() {}
    public static Singleton getUniqueInstance() {
        return uniqueInstance;
    }
}
  • 说明:先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样),然后当需要使用的时候,直接调方法就可以使用了。
  • 优点:提前实例化好了一个实例,避免了线程不安全问题的出现。
  • 缺点:直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费

3、双重检查锁实现(线程安全)

public class Singleton {
    private volatile static Singleton uniqueInstance; 
    private Singleton() {}
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
  • 说明:双重检查锁相当于是改进了线程安全的懒汉式。
    • 线程安全的懒汉式的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。
    • 为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量 ?
    • uniqueInstance = new Singleton(); 这段代码执行时分为三步:
      • 1.为 uniqueInstance 分配内存空间
      • 2.初始化 uniqueInstance
      • 3.将 uniqueInstance 指向分配的内存地址
    • 正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。
    • 单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。
    • 例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。
    • 解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。
  • 优点:延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。
  • 缺点:volatile 关键字,对性能也有一些影响。

4、静态内部类实现(线程安全)

public class Singleton { 
    private Singleton() {} 
    private static class SingletonHolder { 
        private static final Singleton INSTANCE = new Singleton();
    } 
    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE; 
    }
}
  • 说明:
    • 首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。
    • 当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE;  
    • 触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。
  • 优点:延迟实例化,节约了资源;且线程安全;性能也提高了。

5、枚举类实现(线程安全)

public enum Singleton {
    INSTANCE; //添加自己需要的操作 public void doSomeThing() {}
}
  • 说明:默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。
  • 优点:写法简单,线程安全,天然防止反射和反序列化调用。

防止反序列化

序列化:把java对象转换为字节序列的过程;

反序列化:通过这些字节序列在内存中新建java对象的过程;

也就是说,反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。

我们要防止反序列化,避免得到多个实例。

其中枚举类天然防止反序列化。

其他单例模式可以通过重写 readResolve() 方法,从而防止反序列化,使实例唯一,重写 readResolve() :

private Object readResolve() throws ObjectStreamException{ 
    return singleton;
}

六、Hashtable 和 HashMap 的区别

解析:

  • A、×,Hashtable 是一个哈希表,该类继承自Dictionary类,实现了 Map 接口
  • B、√,HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。该类继承AbstractMap,实现Map接口
  • C、√,Hashtable 线程安全的,而 HashMap 是线程不安全的。Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。
  • D、√,讲的没错,但是不符合题意
  • E、√,语句挑不出毛病。

Hashtable 和 HashMap 的区别:

  • 1、继承的父类不同
    • Hashtable继承自Dictionary类
    • HashMap继承自AbstractMap类
    • 但二者都实现了Map接口
  • 2、线程的安全性
    • Hashtable是同步(方法中使用了Synchronize)的,是线程安全的
    • HashMap是未同步(方法中缺省Synchronize)的,是线程不安全的
  • 3、是否有contains方法
    • Hashtable有一个contains(Object value)方法,功能和containsValue(Object value)功能一样
    • HashMap把Hashtable的contains方法去掉了,改成containsValue(Object value)和containsKey(Object key)
  • 4、是否允许null值
    • Hashtable中,key和value都不允许出现null值
    • HashMap允许null值(key和value都可以),因为在HashMap中null可以作为健,而它对应的值可以有多个null
  • 5、遍历方式
    • HashTable使用Enumeration
    • HashMap使用Iterator
  • 6、Hash值
    • Hashtable直接使用对象的hashCode

      //hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值
      int hash = key.hashCode();
      int index = (hash & 0x7FFFFFFF) % tab.length;

    • HashMap要重新计算key值和hash值

      int hash = hash(k);
      int i = indexFor(hash, table.length);

      static int hash(Object x) {
        int h = x.hashCode();

        h += ~(h << 9);
        h ^= (h >>> 14);
        h += (h << 4);
        h ^= (h >>> 10);
        return h;
      }
      static int indexFor(int h, int length) {
        return h & (length-1);
      }

  • 7、数组初始化和扩容
    • Hashtable中hash数组默认大小是11,增加的方式是2*n+1(n为当前数组的大小)
    • HashMap中hash数组的默认大小是16,增加的方式是2*n(n为当前数组的大小)

猜你喜欢

转载自blog.csdn.net/weixin_39478524/article/details/114942159