Java

Netty面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

曾经使用 Netty 写过一个 RPC 框架,对 Netty 有一点点研究。这篇回答可以带你入门 Netty,即使你没有网络编程基础应该也能看懂。

老套路,学习某一门技术或者框架的时候,第一步当然是要了解下面这几样东西。

  1. 是什么?
  2. 有哪些特点?
  3. 有哪些应用场景?
  4. 有哪些成功使用的案例?
  5. …..

为了让你更好地了解 Netty 以及它诞生的原因,先从传统的网络编程说起吧!

还是要从 BIO 说起

传统的阻塞式通信流程

早期的 Java 网络相关的 API(java.net包) 使用 Socket(套接字)进行网络通信,不过只支持阻塞函数使用。

要通过互联网进行通信,至少需要一对套接字

  1. 运行于服务器端的 Server Socket。
  2. 运行于客户机端的 Client Socket

Socket 网络通信过程如下图所示:

Socket 网络通信过程简单来说分为下面 4 步:

  1. 建立服务端并且监听客户端请求
  2. 客户端请求,服务端和客户端建立连接
  3. 两端之间可以传递数据
  4. 关闭资源

对应到服务端和客户端的话,是下面这样的。

服务器端

  1. 创建 ServerSocket 对象并且绑定地址(ip)和端口号(port): server.bind(new InetSocketAddress(host, port))
  2. 通过 accept()方法监听客户端请求
  3. 连接建立后,通过输入流读取客户端发送的请求信息
  4. 通过输出流向客户端发送响应信息
  5. 关闭相关资源

客户端:

  1. 创建Socket 对象并且连接指定的服务器的地址(ip)和端口号(port):socket.connect(inetSocketAddress)
  2. 连接建立后,通过输出流向服务器端发送请求信息
  3. 通过输入流获取服务器响应的信息
  4. 关闭相关资源

一个简单的 demo

为了便于理解,我写了一个简单的代码帮助各位小伙伴理解。

服务端:

public class HelloServer {
    private static final Logger logger = LoggerFactory.getLogger(HelloServer.class);

    public void start(int port) {
        //1.创建 ServerSocket 对象并且绑定一个端口
        try (ServerSocket server = new ServerSocket(port);) {
            Socket socket;
            //2.通过 accept()方法监听客户端请求, 这个方法会一直阻塞到有一个连接建立
            while ((socket = server.accept()) != null) {
                logger.info("client connected");
                try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                     ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
                   //3.通过输入流读取客户端发送的请求信息
                    Message message = (Message) objectInputStream.readObject();
                    logger.info("server receive message:" + message.getContent());
                    message.setContent("new content");
                    //4.通过输出流向客户端发送响应信息
                    objectOutputStream.writeObject(message);
                    objectOutputStream.flush();
                } catch (IOException | ClassNotFoundException e) {
                    logger.error("occur exception:", e);
                }
            }
        } catch (IOException e) {
            logger.error("occur IOException:", e);
        }
    }

    public static void main(String[] args) {
        HelloServer helloServer = new HelloServer();
        helloServer.start(6666);
    }
}

ServerSocketaccept() 方法是阻塞方法,也就是说 ServerSocket 在调用 accept()等待客户端的连接请求时会阻塞,直到收到客户端发送的连接请求才会继续往下执行代码,因此我们需要要为每个 Socket 连接开启一个线程(可以通过线程池来做)。

上述服务端的代码只是为了演示,并没有考虑多个客户端连接并发的情况。

客户端:

/**
 * @author shuang.kou
 * @createTime 2020年05月11日 16:56:00
 */
public class HelloClient {

    private static final Logger logger = LoggerFactory.getLogger(HelloClient.class);

    public Object send(Message message, String host, int port) {
        //1. 创建Socket对象并且指定服务器的地址和端口号
        try (Socket socket = new Socket(host, port)) {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            //2.通过输出流向服务器端发送请求信息
            objectOutputStream.writeObject(message);
            //3.通过输入流获取服务器响应的信息
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            return objectInputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            logger.error("occur exception:", e);
        }
        return null;
    }

    public static void main(String[] args) {
        HelloClient helloClient = new HelloClient();
        helloClient.send(new Message("content from client"), "127.0.0.1", 6666);
        System.out.println("client receive message:" + message.getContent());
    }
}

发送的消息实体类

/**
 * @author shuang.kou
 * @createTime 2020年05月11日 17:02:00
 */
@Data
@AllArgsConstructor
public class Message implements Serializable {

    private String content;
}

首先运行服务端,然后再运行客户端,控制台输出如下:

查看更多

Shiro面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

一、Shiro框架介绍
是一个轻量级的安全框架,主要提供了 授权、认证、加密、会话管理这几个功能。

二、shiro安全数据源有哪些
1.数据库
2.静态ini文件
3.session

三、Shiro运行流程
比如一个登陆流程:
1、首先调用Subject.login(token)进行登录,他会委托给SecurityManager
2、SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;
3、Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有就返回认证失败,有的话就继续执行操作。

四、Shiro 的优点
1、简单的身份认证, 支持多种数据源;非常简单的加密 API;
2、对角色的简单的授权, 支持细粒度的授权(方法级);
3、支持一级缓存,以提升应用程序的性能;

4、内置的基于 POJO 企业会话管理, 适用于 Web 以及非 Web 的环境;
5、不跟任何的框架或者容器捆绑, 可以独立运行。

五、比较 SpringSecurity 和 Shiro
1、相比 Spring Security, Shiro 在保持强大功能的同时, 使用简单性和灵活性;
2、SpringSecurity: 即使是一个一个简单的请求,最少得经过它的 8 个Filter;
3、SpringSecurity 必须在 Spring 的环境下使用;

4、初学 Spring Security, 曲线还是较大, 需要深入学习其源码和框架, 配置起来也较费力。

六、简述 Shiro 的3个核心组件
1.Subject
正与系统进行交互的人, 或某一个第三方服务
所有 Subject 实例都被绑定到一个SecurityManager 上。

2.SecurityManager
Shiro 架构的心脏, 用来协调内部各安全组件, 管理内部组件实例, 并通过它来提供安全管理的各种服务。
当 Shiro 与一个 Subject 进行交互时, 实质上是幕后的 SecurityManager 处理所有繁重的 Subject 安全操作。

3.Realms
本质上是一个特定安全的 DAO. 当配置 Shiro 时, 必须指定至少一个 Realm 用来进行身份验证和授权。
Shiro 提供了多种可用的 Realms 来获取安全相关的数据. 例如关系数据库(JDBC), INI 及属性文件等。
可以定义自己 Realm 实现来代表自定义的数据源

七、Shiro认证过程
1、应用程序代码调用 Subject.login 方法,传递创建好的包含终端用户的 Principals(身份)和 Credentials(凭证)的 AuthenticationToken 实例;
2、Subject 实例委托应用程序的 SecurityManager 通过调用securityManager.login(token) 开始真正的验证;
Subject 实例(通常为 DelegatingSubject或它的子类)
3、SubjectManager 接收 token,调用内部的 Authenticator 实例调用 authenticator.authenticate(token).Authenticator 通常是一个 ModularRealmAuthenticator 实例, 支持在身份验证中协调一个或多个Realm 实例;
4、如果应用程序中配置了一个以上的 Realm, ModularRealmAuthenticator 实例将利用配置好的AuthenticationStrategy 来启动 Multi-Realm 认证尝试。在Realms 被身份验证调用之前、调用期间、调用之后,AuthenticationStrategy 被调用使其能够对每个Realm 的结果作出反应。(AuthenticationStrategy都会被调用,对每个Realm 的结果作出反应);
5、每个配置的 Realm 用来帮助看它是否支持提交的 AuthenticationToken. 如果支持, 那么支持 Realm 的 getAuthenticationInfo 方法将会伴随着提交的 token 被调用. getAuthenticationInfo 方法有效地代表一个特定 Realm 的单一的身份验证尝试

八、Shiro授权过程
1、应用程序或框架代码调用任何 Subject 的hasRole*, checkRole*, isPermitted*,或者checkPermission*方法的变体, 传递任何所需的权限;
2、Subject 的实例 调用securityManager 的对应的方法。
Subject 实例(通常为 DelegatingSubject或它的子类)
3、SecurityManager 调用 org.apache.shiro.authz.Authorizer 接口的对应方法.默认情况下,authorizer 实例是一个 ModularRealmAuthorizer 实例, 它支持协调任何授权操作过程中的一个或多个Realm 实例;
4、每个配置好的 Realm 被检查是否实现了相同的 Authorizer 接口. 如果是, Realm 各自的 hasRole*, checkRole*,isPermitted*,或 checkPermission* 方法将被调用。

九、Shiro 如何自实现认证

查看更多

23种设计模式面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

1.单例模式(Singleton Pattern)

定义:Ensure a class has only one instance, and provide a global point of access to it. (确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实

例。)

通用代码:(是线程安全的)

public class Singleton {
    private static final Singleton singleton = new Singleton();

    //限制产生多个对象
    private Singleton() {
    }

    //通过该方法获得实例对象
    public static Singleton getSingleton() {
        return singleton;
    }

    //类中其他方法,尽量是 static
    public static void doSomething() {
    }
}

使用场景:

●  要求生成唯一序列号的环境;

● 在整个项目中需要一个共享访问点或共享数据,例如一个 Web 页面上的计数

器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确 保是线程安全的;

● 创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源;

●  需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式 (当然,也可以直接声明为 static 的方式)。

线程不安全实例:

public class Singleton {
    private static Singleton singleton = null;

    //限制产生多个对象
    private Singleton() {
    }

    //通过该方法获得实例对象
    public static Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();

        }
        return singleton;
    }
}

解决办法:

在 getSingleton 方法前加 synchronized 关键字,也可以在 getSingleton 方法内增 加 synchronized 来实现。 最优的办法是如通用代码那样写。

2.工厂模式

定义:Define an interface for creating an object,but let subclasses decide which   class to instantiate.Factory Method lets a class defer instantiation to subclasses.   (定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类 的实例化延迟到其子类。)

Product 为抽象产品类负责定义产品的共性,实现对事物最抽象的定义;

Creator 为抽象创建类,也就是抽象工厂,具体如何创建产品类是由具体的实现工 厂 ConcreteCreator 完成的。

具体工厂类代码:

public class ConcreteCreator extends Creator {
    public <T extends Product> T createProduct(Class<T> c) {
        Product product = null;
        try {
            product =
                    (Product) Class.forName(c.getName()).newInstance();
        } catch (Exception e) {
            //异常处理
        }
        return (T) product;
    }
}

简单工厂模式:

一个模块仅需要一个工厂类,没有必要把它产生出来,使用静态的方法

多个工厂类:

每个人种(具体的产品类)都对应了一个创建者,每个创建者独立负责创建对应的 产品对象,非常符合单一职责原则

代替单例模式:

单例模式的核心要求就是在内存中只有一个对象,通过工厂方法模式也可以只在内 存中生产一个对象

延迟初始化:

ProductFactory 负责产品类对象的创建工作,并且通过 prMap 变量产生一个缓 存,对需要再次被重用的对象保留

使用场景: jdbc 连接数据库,硬件访问,降低对象的产生和销毁

3.抽象工厂模式(Abstract Factory Pattern)

定义:Provide an interface for creating families of related or dependent objects     without specifying their concrete classes. (为创建一组相关或相互依赖的对象提供 一个接口,而且无须指定它们的具体类。)

抽象工厂模式通用类图:

抽象工厂模式通用源码类图:

抽象工厂类代码:

public abstract class AbstractCreator {
    //创建 A 产品家族
    public abstract AbstractProductA createProductA();

    //创建 B 产品家族
    public abstract AbstractProductB createProductB();
}

使用场景:

一个对象族(或是一组没有任何关系的对象)都有相同的约束。

涉及不同操作系统的时候,都可以考虑使用抽象工厂模式

4.模板方法模式(Template Method Pattern

定义:Define the skeleton of an algorithm in an operation,deferring some steps to subclasses.Template Method lets subclasses redefine certain steps of an

algorithm without changing the algorithm\’s structure. (定义一个操作中的算法的框 架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义 该算法的某些特定步骤。)

AbstractClass 叫做抽象模板,它的方法分为两类:

● 基本方法

基本方法也叫做基本操作,是由子类实现的方法,并且在模板方法被调用。

● 模板方法

可以有一个或几个, 一般是一个具体方法,也就是一个框架,实现对基本方法的调 度,完成固定的逻辑。

注意:   为了防止恶意的操作, 一般模板方法都加上 final 关键字,不允许被覆

写。

具体模板:ConcreteClass1 和 ConcreteClass2 属于具体模板,实现父类所定义的 一个或多个抽象方法,也就是父类定义的基本方法在子类中得以实现

使用场景:

●  多个子类有公有的方法,并且逻辑基本相同时。

● 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由 各个子类实现。

● 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然 后通过钩子函数(见“模板方法模式的扩展” )约束其行为。

5.建造者模式(Builder Pattern)

查看更多

Java JVM面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

JDK、JRE、JVM关系?

  • Jdk (Java Development Kit)  : java语言的软件开发包。包括Java运行时环境Jre。
  • Jre   (Java Runtime Environment) :Java运行时环境,包括Jvm。
  • Jvm (Java Virtual Machine) :一种用于计算机设备的规范,Java语言在不同平台上运行时不需要重新编译。  Java语言使用Java虚拟机屏蔽了与具体平台相关的信 息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台 上不加修改地运行,Jdk包括Jre,Jre包括jvm。

启动程序如何查看加载了哪些类,以及加载顺序?

  • java -XX:+TraceClassLoading  具体类
  • Java -verbose 具体类

class字节码文件10个主要组成部分?

  •    MagicNumber
  •    Version
  •    Constant_pool
  •    Access_flag
  •    This_class
  •    Super_class
  •    Interfaces
  •    Fields
  •    Methods
  •    Attributes

画一下jvm内存结构图?

程序计数器

属于线程私有内存。占用一块非常小的空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的指令的字节码,分支、 循环、跳转、异常处理、线程恢复等基础功能都依赖这个计数器来完成。

Java虚拟机栈

属于线程私有内存。它的生命周期与线程相同,虚拟机栈描述的是Java方法执行内存模型;每个方法被 执行的时候都会同时创建一个栈板用于存储局部变量表、操作栈、动态链接、方法出口信息等。每一个 方法被调用直至执行完成的过程,就对应着一个栈顺再虚拟机中从入栈到出栈的过程。

本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,只不过虚拟机栈对虚拟机执行Java方法服务,而本 地栈是为虚拟机使用到Native方法服务。

Java堆

是Java虚拟机所管理的内存中最大的一块。 Java堆事被所有线程共享的一块内存区域,在虚拟机启动时 创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Tips:但随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标亮替换优化技术将会导师一些 微妙的变化发生,所有的对象都分配在堆上就不那么绝对了。

方法区

是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译 后的代码等数据。

运行时常量池?

是方法区的一部分, Class文件中除了有类的版本、字段、方法、接口等描述信息,还有一项是常量池  (Constant PoolTable)用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放

道方法区的运行时常量池中。

什么时候抛出StackOverflowError?

如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError。

Java7和Java8在内存模型上有什么区别?

Java8取消了永久代,用元空间  (Metaspace)代替了,元空间是存在本地内存(Native memory)中。

程序员最关注的两个内存区域?

堆(Heap)和栈  (Stack) ,一般大家会把Java内存分为堆内存和栈内存,这是一种比较粗糙的划分方式, 但实际上Java内存区域是很复杂的。

直接内存是什么?

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内 存也频繁被实用,也有OutOfMemoryError异常的出现的可能。

Tips:JDK1.4中加入了NIO  (new input/output)类,引入了一种基于通道(Channe)与缓冲区  (Buffer)的I/O方式,也可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer的对象作为这块内存的引用进行操作。

除 了 哪 个 区 域 外 , 虚 拟 机 内 存 其 他 运 行 时 区 域 都 会 发 生 OutOfMemoryError?

程序计数器。

什么情况下会出现堆内存溢出?

堆内存存储对象实例。我们只要不断地创建对象。并保证gc roots到对象之间有可达路径来避免垃圾回 收机制清除这些对象。就会在对象数量到达最大。堆容量限制后,产生内存溢出异常。

如何实现一个堆内存溢出?

public class Cat {
    public static void main(String[] args) {
        List list = new ArrayList();
        while (true) {
            list.add(new Cat());
        }
    }
}

空间什么情况下会抛出OutOfMemoryError?

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError。

如何实现StrackOverflowError?

    public static void main(String[] args) {
        eat();
    }

    public static void eat() {
        eat();
    }

如何设置直接内存容量?

通过 -XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值一样。

Java堆内存组成?

堆大小 = 新生代 + 老年代。如果是Java8则没有Permanent Generation。

其中新生代(Young) 被分为 Eden和S0(from)和S1(to)。

Edem : from : to默认比例是?

Edem : from : to = 8 : 1 : 1

此比例可以通过 –XX:SurvivorRatio 来设定

垃圾标记阶段?

在GC执行垃圾回收之前,为了区分对象存活与否,当对象被标记为死亡时,  GC才回执行垃圾回收,这 个过程就是垃圾标记阶段。

引用计数法?

比如对象a,只要任何一个对象引用了a,则a的引用计数器就加1,当引用失效时,引用计数器就减1,当计 数器为0时,就可以对其回收。

但是无法解决循环引用的问题。

根搜索算法?

跟搜索算法是以跟为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达(使用根 搜索算法后,内存中 的存活对象都会被根对象集合直接或间接连接着),如果目标对象不可达,就意味   着该对象己经死亡,便可以在 instanceOopDesc的 Mark World 中将其标记为垃圾对象。

在根搜索算法中,   只有能够被根对象集合直接或者间接连接的对象才是存活对象。

JVM中三种常见的垃圾收集算法?

  • 标记-清除算法(Mark_Sweep)
  • 复制算法(Copying)
  • 标记-压缩算法(Mark-Compact)

标记-清除算法?

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有的被标记对象。

缺点:

  • 标记和清除的效率都不高。
  • 空间问题,清除后产生大量不连续的内存随便。如果有大对象会出现空间不够的现象从而不得不提前 触发另一次垃圾收集动作。

复制算法?

他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存 活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点: 解决了内存碎片问题。

缺点:将原来的内存缩小为原来的一半,存活对象越多效率越低。

标记-整理算法?

先标记出要被回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。解 决了复制算法和标记清理算法的问题。

分代收集算法?

当前商业虚拟机的垃圾收集都采用“分代手机算法” ,其实就根据对象存活周期的不同将内存划分为几 块, 一般是新老年代。根据各个年代的特点采用最适当的收集算法。

垃圾收集器?

如果说垃圾收集算法是方法论,那么垃圾收集器就是具体实现。连线代表可以搭配使用。

Stop The World?

进行垃圾收集时,必须暂停其他所有工作线程,  Sun将这种事情叫做”Stop The World”。

Serial收集器?

单线程收集器,单线程的含义在于它会 stop the world。垃圾回收时需要stop the world ,直到它收集结 束。所以这种收集器体验比较差。

PartNew收集器?

Serial收集器的多线程版本,除了使用采用并行收回的方式回收内存外,其他行为几乎和Serial没区别。

可以通过选项“-XX:+UseParNewGC”手动指定使用 ParNew收集器执行内存回收任务。

Parallel Scavenge?

是一个新生代收集器,也是复制算法的收集器,同时也是多线程并行收集器,与PartNew 不同是,它重 点关注的是程序达到一个可控制的吞吐量(Thoughput ,CPU 用于运行用户代码 的时间/CPU 总消耗时  间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)), 高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。

他可以通过2个参数精确的控制吞吐量,更高效的利用cpu。

分别是: -XX:MaxCcPauseMillis 和 -XX:GCTimeRatio

Parallel Old收集器?

Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。  JDK 1.6中才开始提供。

CMS 收集器?

Concurrent Mar Sweep 收集器是一种以获取最短回收停顿时间为目标的收集器。重视服务的响应速 度,希望系统停顿时间最短。采用标记-清除的算法来进行垃圾回收。

CMS垃圾回收的步骤?

1.  初始标记 (stop the world)

2.  并发标记

3.  重新标记 (stop the world)

4.  并发清除

  • 初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
  • 并发标记就是进行Gc Roots Tracing的过程。
  • 重新标记则是为了修正并发标记期间,因用户程序继续运行而导致的标记产生变动的那一部分对象的 标记记录,这个阶段停顿时间一般比初始标记时间长,但是远比并发标记时间短。
  • 整个过程中并发标记时间最长,但此时可以和用户线程一起工作。

CMS收集器优点?缺点?

优点:

并发收集、低停顿

缺点:

  • 对cpu资源非常敏感。
  • 无法处理浮动垃圾。
  • 内存碎片问题。

G1收集器?

Garbage First 收集器是当前收集器技术发展的最前沿成果。jdk 1.6_update14中提供了 g1收集器。

G1收集器是基于标记-整理算法的收集器,它避免了内存碎片的问题。

可以非常精确控制停顿时间,既能让使用者明确指定一个长度为 M毫秒的时间片段内,消耗在垃圾收集 上的时间不多超过N毫秒,这几乎已经是实时java(rtsj)的垃圾收集器特征了。

G1收集器是如何改进收集方式的?

极力避免全区域垃圾收集,之前的收集器进行收集的范围都是整个新生代或者老年代。而g1将整个Java 堆(包括新生代、老年代)划分为多个大小固定的独立区域,并且跟踪这些区域垃圾堆积程度,维护一 个优先级李彪,每次根据允许的收集时间,优先回收垃圾最多的区域。从而获得更高的效率。

虚拟机进程状况工具?

jps (Jvm process status tool ),他的功能与ps类似。

可以列出正在运行的虚拟机进程,并显示执行主类(Main Class,main()函数所在的类)的名称,以及浙 西进程的本地虚拟机的唯一ID。

语法 : jps [options] [hostid]

  • -q 主输出lvmid,省略主类的名称
  • -m 输出虚拟机进程启动时传递给主类main()函数的参数
  • -l    输出主类全名,如果进程执行是Jar包,输出Jar路径
  • -v 输出虚拟机进程启动时JVM参数

虚拟机统计信息工具?

jstat(JVM Statistics Montoring Tool)是用于监视虚拟机各种运行状态信息命令行工机具。他可以显示本 地或远程虚拟机进程中的类装载、内存、垃圾收集、  jit编译等运行数据。

jstat [option vmid [interval[s|ms] [count]] ]

interval 查询间隔

count 查询次数,如果不用这两个参数,就默认查询一次。

option代表用户希望查询的虚拟机信息,主要分3类:

  • 类装载
  • 垃圾收集
  • 运行期编译状况

jstat 工具主要选项?

  • -class    监视类装载、卸载数量、总空间及类装载锁消耗的时间
  • -gc         监视Java堆状况,包括Eden区, 2个survivor区、老年代
  • -gccapacity 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用的最大和最小空间
  • -gcutil  监视内容与-gc基本相同,主要关注已经使用空间站空间百分比
  • -gccause 与-gcutil 功能一样,但是会额外输出导致上一次GC产生的原因
  • -gcnew  监视新生代的GC的状况
  • -gcnewcapacity 监视内容与-gcnew基本相同,输出主要关注使用到的最大最小空间
  • -gcold  监视老年代的GC情况
  • -gcoldcapacity  监控内容与-gcold基本相同,主要关注使用到的最大最小空间
  • -compiler 输出jit 编译器编译过的方法、耗时等信息

配置信息工具?

jinfo(Configuration Info for Java)的作用是实时地查看和调整虚拟机的各项参数。

使用jps 命令的 -v 参数可以查看虚拟机启动时显示指定的参数列表。

jinfo 语法: jinfo [option] pid

内存映像工具?

jmap(Memory Map for Java) 命令用于生成堆转储快照  (一般称为heapdump或dump文件)。

语法 :jmap [option] vmid

它还可以查询finalize执行队列,  Java堆和永久代的详细信息,如果空间使用率、当前用的是哪种收集器 等。

   -dump 生成Java堆转储快照,其中live自参数说明是否只dump出存活对象

   -finalizerinfo 显示在F -Queue 中等待Finalizer线程执行finalize方法的对象。只在Linux/Solaris平台 下有效

   -heap 显示Java堆详细信息,如使用哪种回收器、参数配置、分代状况。

   -histo 显示堆中对象统计信息、包括类、实例数量和合计容量。

   -F 当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照。

虚拟机堆转存储快照分析工具?

jhat ( JVM Heap Analysis Tool) 用来分析jmap生成的堆转储快照。

堆栈跟踪工具?

jstack(Stack Trace for Java) 命令用于生成虚拟机当前时刻的线程快照  (一般称为thread dump 或

javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的 主要目的是定位线程出现长时间停顿的原因。

jstack [option] vmid

  • -F  当正常输出的请求不被响应时,强制输出线程堆栈
  • -l  除堆栈外,显示关于锁的附加信息
  • -m 如果调用本地方法的花,可以显示C/C++ 的堆栈

除了命令行,还有什么可视化工具?

JConsole 和 VisualVM,这两个工具是JDK的正式成员。

类加载过程?

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

查看更多

Java多线程面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

什么是进程?

进程是系统中正在运行的一个程序,程序一旦运行就是进程。

进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。 一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要 使用进程间通信,比如管道,文件,套接字等。

什么是线程?

是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。  一条线程 指的是进程中一个单一顺序的控制流,  一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程的实现方式?

  1. 继承Thread类
  2. 实现Runnable接口
  3. 使用Callable和Future

Thread 类中的start() 和 run() 方法有什么区别?

1.start  样)方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接 继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程,   这时此线程是处于就绪状态,并没有运行。   然后通过此Thread类调用方法run()来完成其运行操作的,   这里方法run()称为线程体,它 包含了要执行的这个线程的内容,   Run方法运行结束,   此线程终止。然后CPU再调度其它线程。

2.run  样)方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继 续执行下面的代码;   程序中只有主线程——这一个线程,   其程序执行路径还是只有一条,  这样就没有 达到写线程的目的。

线程NEW状态

new创建一个Thread对象时,并没处于执行状态,因为没有调用start方法启动改线程,那么此时的状态 就是新建状态。

线程RUNNABLE状态

线程对象通过start方法进入runnable状态,启动的线程不一定会立即得到执行,线程的运行与否要看 cpu的调度,我们把这个中间状态叫可执行状态样RUNNABLE)。

线程的RUNNING状态

一旦cpu通过轮询货其他方式从任务可以执行队列中选中了线程,此时它才能真正的执行自己的逻辑代 码。

线程的BLOCKED状态

  • 线程正在等待获取锁。
  • 进入BLOCKED状态,比如调用了sleep,或者wait方法
  • 进行某个阻塞的io操作,比如因网络数据的读写进入BLOCKED状态
  • 获取某个锁资源,从而加入到该锁的阻塞队列中而进入BLOCKED状态

线程的TERMINATED状态

TERMINATED是一个线程的最终状态,在该状态下线程不会再切换到其他任何状态了,代表整个生命周期都结束了。

下面几种情况会进入TERMINATED状态:

  •   线程运行正常结束,结束生命周期
  •   线程运行出错意外结束
  •    JVM Crash 导致所有的线程都结束

线程状态转化图

i——————与System.out.println()的异常

示例代码:

public class XkThread extends Thread {
    private int i = 5;
    @Override
    public void run() {
        System.out.println("i=" + (i—————— ) + "  threadName=" + Thread.currentThread().getName());
    }
    public static void main(String[] args) {
        XkThread xk = new XkThread();
        Thread t1 = new Thread(xk);
        Thread t2 = new Thread(xk);
        Thread t3 = new Thread(xk);
        Thread t4 = new Thread(xk);
        Thread t5 = new Thread(xk);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

结果:

i = 5 threadName = Thread - 1
i = 2 threadName = Thread - 5
i = 5 threadName = Thread - 2
i = 4 threadName = Thread - 3
i = 3 threadName = Thread - 4

虽然println()方法在内部是同步的,但i——————的操作却是在进入println()之前发生的,所以有发生非线程安全的概率。

println()源码:

    public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

如何知道代码段被哪个线程调用?

System.out .println(Thread .currentThread ().getName());

线程活动状态?

public class XKThread extends Thread {
    @Override
    public void run() {
        System.out.println("run  run  run  is  " + this.isAlive());
    }
    public static void main(String[] args) {
        XKThread xk = new XKThread();
        System.out.println("begin  ———  " + xk.isAlive());
        xk.start();
        System.out.println("end  —————  " + xk.isAlive());

    }
}

sleep()方法

方法sleep()的作用是在指定的毫秒数内让当前的“正在执行的线程”休眠(暂停执行)。

如何优雅的设置睡眠时间?

jdk1.5 后,引入了一个枚举TimeUnit,对sleep方法提供了很好的封装。比如要表达2小时22分55秒899毫秒。

Thread.sleep(8575899L);
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);

可以看到表达的含义更清晰,更优雅。

停止线程

  • run方法执行完成,自然终止。
  • stop()方法, suspend()以及resume()都是过期作废方法,使用它们结果不可预期。
  • 大多数停止一个线程的操作使用Thread.interrupt()等于说给线程打一个停止的标记, 此方法不会去终止 一个正在运行的线程,需要加入一个判断才能可以完成线程的停止。

interrupted 和 isInterrupted

interrupted :  判断当前线程是否已经中断,会清除状态。

isInterrupted :判断线程是否已经中断,不会清除状态。

yield

放弃当前cpu资源,将它让给其他的任务占用cpu执行时间。但放弃的时间不确定,有可能刚刚放弃,马 上又获得cpu时间片。

测试代码:(cpu独占时间片)

public class XKThread extends Thread {
    @Override
    public void run() {
        long beginTime = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 50000000; i++) {
            count = count + (i + 1);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("用时  =  " + (endTime - beginTime) + "  毫秒!  ");
    }

    public static void main(String[] args) {
        XKThread xkThread = new XKThread();
        xkThread.start();
    }
}

结果:

用时  =  20  毫秒!

加入yield,再来测试。  (cpu让给其他资源导致速度变慢)

public class XKThread extends Thread {

    @Override
    public void run() {
        long beginTime = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 50000000; i++) {
            Thread.yield();
            count = count + (i + 1);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("用时  =  " + (endTime - beginTime) + "  毫秒!  ");
    }

    public static void main(String[] args) {
        XKThread xkThread = new XKThread();
        xkThread.start();
    }
}

结果:

用时  =  38424  毫秒!

19.线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到cpu资源比较多,也就是cpu有限执行优先 级较高的线程对象中的任务,但是不能保证一定优先级高,就先执行。

Java的优先级分为1 ~10个等级,数字越大优先级越高,默认优先级大小为5。超出范围则抛出: java.lang.IllegalArgumentException。

优先级继承特性

线程的优先级具有继承性,比如a线程启动b线程, b线程与a优先级是一样的。

谁跑的更快?

设置优先级高低两个线程,累加数字,看谁跑的快,上代码。

public class Run extends Thread {

    public static void main(String[] args) {
        try {
            ThreadLow low = new ThreadLow();
            low.setPriority(2);
            low.start();

            ThreadHigh high = new ThreadHigh();
            high.setPriority(8);
            high.start();

            Thread.sleep(2000);
            low.stop();
            high.stop();
            System.out.println("low   =  " + low.getCount());
            System.out.println("high  =  " + high.getCount());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadHigh extends Thread {
    private int count = 0;
    
    public int getCount() {
        return count;
    }


    @Override
    public void run() {
        while (true) {
            count++;
        }
    }
}

class ThreadLow extends Thread {
    private int count = 0;

    public int getCount() {
        return count;
    }
    
    @Override
    public void run() {
        while (true) {
            count++;
        }
    }
}

结果:

low    =  1193854568
high  =  1204372373

线程种类

Java线程有两种, 一种是用户线程, 一种是守护线程。

守护线程的特点

守护线程是一个比较特殊的线程,主要被用做程序中后台调度以及支持性工作。当Java虚拟机中不存在 非守护线程时,守护线程才会随着JVM一同结束工作。

Java中典型的守护线程

GC  (垃圾回收器)

如何设置守护线程

Thread.setDaemon(true)

PS:Daemon属性需要再启动线程之前设置,不能再启动后设置。

Java虚拟机退出时Daemon线程中的finally块一定会执行?

Java虚拟机退出时Daemon线程中的finally块并不一定会执行。

代码示例:

public class XKDaemon {
    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner(), "xkDaemonRunner");
        thread.setDaemon(true);
        thread.start();
    }

    static class DaemonRunner implements Runnable {

        @Override
        public void run() {
            try {
                SleepUtils.sleep(10);
            } finally {
                System.out.println("Java小咖秀  daemonThread  finally  run  …");
            }

        }
    }
}

结果:

没有任何的输出,说明没有执行finally。

设置线程上下文类加载器

获取线程上下文类加载器

public ClassLoader getContextClassLoader();

设置线程类加载器(可以打破Java类加载器的父类委托机制)

public void setContextClassLoader(ClassLoader cl);

join

join是指把指定的线程加入到当前线程,比如join某个线程a,会让当前线程b进入等待,直到a的生命周期结 束,此期间b线程是处于blocked状态。

什么是synchronized?

synchronized关键字可以时间一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象是对多 个线程可见的,那么对该对想的所有读写都将通过同步的方式来进行。

synchronized包括哪两个jvm重要的指令?

monitor enter 和 monitor exit

synchronized关键字用法?

可以用于对代码块或方法的修饰

synchronized锁的是什么?

普通同步方法 —————> 锁的是当前实力对象。

静态同步方法—————> 锁的是当前类的Class对象。

同步方法快 —————> 锁的是synchonized括号里配置的对象。

Java对象头

synchronized用的锁是存在Java对象头里的。对象如果是数组类型,虚拟机用3个字宽(Word)存储对象 头,如果对象是非数组类型,用2字宽存储对象头。

Tips:32位虚拟机中一个字宽等于4字节。

Java对象头长度

Java对象头的存储结构

32位JVM的Mark Word 默认存储结构

Mark Word 的状态变化

Mark Word 存储的数据会随着锁标志为的变化而变化。

64位虚拟机下,  Mark Word是64bit大小的

锁的升降级规则

Java SE 1.6 为了提高锁的性能。引入了“偏向锁”和轻量级锁“。

Java SE 1.6 中锁有4种状态。级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

锁只能升级不能降级。

偏向锁

大多数情况,锁不仅不存在多线程竞争,而且总由同一线程多次获得。当一个线程访问同步块并获取锁 时,会在对象头和栈顺中记录存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行 cas操作来加锁和解锁,只需测试一下对象头 Mark Word里是否存储着指向当前线程的偏向锁。如果测试成 功,表示线程已经获得了锁,如果失败,则需要测试下Mark Word中偏向锁的标示是否已经设置成1  (表示当前时偏向锁),如果没有设置,则使用cas竞争锁,如果设置了,则尝试使用cas将对象头的偏向 锁只想当前线程。

关闭偏向锁延迟

java6和7中默认启用,但是会在程序启动几秒后才激活,如果需要关闭延迟,-XX:BiasedLockingStartupDelay=0。

如何关闭偏向锁

JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

Tips:如果你可以确定程序的所有锁通常情况处于竞态,则可以选择关闭。

轻量级锁

线程在执行同步块,  jvm会现在当前线程的栈顺中创建用于储存锁记录的空间。并将对象头中的Mark    Word复制到锁记录中。然后线程尝试使用cas将对象头中的Mark Word替换为之乡锁记录的指针。如果 成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量锁的解锁

轻量锁解锁时,会使原子操作cas将 displaced Mark Word 替换回对象头,如果成功则表示没有竞争发 生,如果失败,表示存在竞争,此时锁就会膨胀为重量级锁。

锁的优缺点对比

什么是原子操作

不可被中断的一个或一系列操作

Java如何实现原子操作

Java中通过锁和循环cas的方式来实现原子操作,  JVM的CAS操作利用了处理器提供的CMPXCHG指令 来实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

CAS实现原子操作的3大问题

ABA问题,循环时间长消耗资源大,只能保证一个共享变量的原子操作

什么是ABA问题

问题:

因为cas需要在操作值的时候,检查值有没有变化,如果没有变化则更新,如果一个值原来是A,变成了B, 又变成了A,那么使用cas进行检测时会发现发的值没有发生变化,其实是变过的。

解决:

添加版本号,每次更新的时候追加版本号,  A-B-A —> 1A-2B-3A。

从jdk1.5开始,Atomic包提供了一个类AtomicStampedReference来解决ABA的问题。

CAS循环时间长占用资源大问题

如果jvm能支持处理器提供的pause指令,那么效率会有一定的提升。

一、它可以延迟流水线执行指令(de-pipeline),使cpu不会消耗过多的执行资源,延迟的时间取决于具体 实现的版本,有些处理器延迟时间是0。

二、它可以避免在退出循环的时候因内存顺序冲突而引起的cpu流水线被清空,从而提高cpu执行效率。

CAS只能保证一个共享变量原子操作

一、对多个共享变量操作时,可以用锁。

二、可以把多个共享变量合并成一个共享变量来操作。比如,x=1,k=a,合并xk=1a,然后用cas操作xk。

Tips:java 1.5开始,jdk提供了AtomicReference类来保证饮用对象之间的原子性,就可以把多个变量放在 一个对象来进行cas操作。

volatile关键字

volatile 是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性“。

Java语言规范第3版对volatile定义如下,  Java允许线程访问共享变量,为了保证共享变量能准确和一致 的更新,线程应该确保排它锁单独获得这个变量。如果一个字段被声明为volatile,Java线程内存模型所 有线程看到这个变量的值是一致的。

等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。

wait

方法wait()的作用是使当前执行代码的线程进行等待,  wait()是Object类通用的方法,该方法用来将当前 线程置入“预执行队列” 中,并在 wait()所在的代码处停止执行,直到接到通知或中断为止。

在调用wait之前线程需要获得该对象的对象级别的锁。代码体现上,即只能是同步方法或同步代码块 内。调用wait()后当前线程释放锁。

notify

notify()也是Object类的通用方法,也要在同步方法或同步代码块内调用,该方法用来通知哪些可能灯光 该对象的对象锁的其他线程,如果有多个线程等待,则随机挑选出其中一个呈wait状态的线程,对其发 出 通知 notify,并让它等待获取该对象的对象锁。

notify/notifyAll

notify等于说将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部 移动到同步队列中。

等待/通知经典范式

    synchronized(obj){
        while (条件不满足) {
            obj.wait();
        }
        执行对应逻辑
    }

通知

    synchronized(obj){
        改变条件
        obj.notifyAll();
    }

ThreadLocal

主要解决每一个线程想绑定自己的值,存放线程的私有数据。

ThreadLocal使用

获取当前的线程的值通过get(),设置set(T) 方式来设置值。

public class XKThreadLocal {

    public static ThreadLocal threadLocal = new ThreadLocal();

    public static void main(String[] args) {
        if (threadLocal.get() == null) {
            System.out.println("未设置过值");
            threadLocal.set("Java小咖秀 ");
        }
        System.out.println(threadLocal.get());
    }
}

输出:

未设置过值Java小咖秀

Tips:默认值为null

解决get()返回null问题

通过继承重写initialValue()方法即可。

代码实现:

public class ThreadLocalExt extends ThreadLocal {
    static ThreadLocalExt threadLocalExt = new ThreadLocalExt();

    @Override
    protected Object initialValue() {
        return "Java小咖秀";
    }

    public static void main(String[] args) {
        System.out.println(threadLocalExt.get());
    }
}

输出结果:

Java小咖秀

Lock接口

锁可以防止多个线程同时共享资源。Java5前程序是靠synchronized实现锁功能。Java5之后,并发包新 增Lock接口来实现锁功能。

Lock接口提供 synchronized不具备的主要特性

重入锁 ReentrantLock

支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时 的公平和非公平性选择。

重进入是什么意思?

重进入是指任意线程在获取到锁之后能够再次获锁而不被锁阻塞。

该特性主要解决以下两个问题:

一、锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。

二、所得最终释放。线程重复n次是获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。

ReentrantLock默认锁?

默认非公平锁

代码为证:

public class ThreadLocalExt extends ThreadLocal {
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)  //  overflow
                throw new Error("Maximum  lock  count  exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

公平锁和非公平锁的区别

公平性与否针对获取锁来说的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺 序,也就是FIFO。

读写锁

读写锁允许同一时刻多个读线程访问,但是写线程和其他写线程均被阻 塞。读写锁维护一个读锁一个写锁,读写分离,并发性得到了提升。

Java中提供读写锁的实现类是ReentrantReadWriteLock。

LockSupport工具

定义了一组公共静态方法,提供了最基本的线程阻塞和唤醒功能。

Condition接口

提供了类似Object监视器方法,与 Lock配合使用实现等待/通知模式。

Condition使用

代码示例:

public class XKCondition {
    Lock lock = new ReentrantLock();
    Condition cd = lock.newCondition();

    public void await() throws InterruptedException {
        lock.lock();
        try {
            cd.await();//相当于Object  方法中的wait()
        } finally {
            lock.unlock();
        }
    }

    public void signal() {
        lock.lock();
        try {
            cd.signal();  //相当于Object  方法中的notify()
        } finally {
            lock.unlock();
        }
    }
}

ArrayBlockingQueue?

一个由数据支持的有界阻塞队列,此队列FIFO原则对元素进行排序。队列头部在队列中存在的时间最 长,队列尾部存在时间最短。

PriorityBlockingQueue?

一个支持优先级排序的无界阻塞队列,但它不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞 数据的消费者。

DelayQueue?

是一个支持延时获取元素的使用优先级队列的实现的无界阻塞队列。队列中的元素必须实现Delayed接 口和 Comparable接口,在创建元素时可以指定多久才能从队列中获取当前元素。

Java并发容器,你知道几个?

ConcurrentHashMap 、CopyOnWriteArrayList 、CopyOnWriteArraySet 、ConcurrentLinkedQueue、ConcurrentLinkedDeque 、ConcurrentSkipListMap 、ConcurrentSkipListSet 、ArrayBlockingQueue、LinkedBlockingQueue 、LinkedBlockingDeque 、PriorityBlockingQueue 、SynchronousQueue、LinkedTransferQueue 、DelayQueue

ConcurrentHashMap

并发安全版HashMap,java7中采用分段锁技术来提高并发效率,默认分16段。Java8放弃了分段锁,采用 CAS,同时当哈希冲突时,当链表的长度到8时,会转化成红黑树。(如需了解细节,见jdk中代码)

ConcurrentLinkedQueue

基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时 候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用cas算法来实   现。(如需了解细节,见jdk中代码)

什么是阻塞队列?

阻塞队列是一个支持两个附加操作的队列,这两个附加操作支持阻塞的插入和移除方法。

1、支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。

2、支持阻塞的移除方法:当队列空时,获取元素的线程会等待队列变为非空。

阻塞队列常用的应用场景?

常用于生产者和消费者场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程。阻 塞队列正好是生产者存放、消费者来获取的容器。

Java里的阻塞的队列

  • ArrayBlockingQueue:数组结构组成的&有界阻塞队列
  • LinkedBlockingQueue:链表结构组成的&有界阻塞队列
  • PriorityBlockingQueue: 支持优先级排序&无界阻塞队列
  • DelayQueue:优先级队列实现&无界阻塞队列
  • SynchronousQueue:不存储元素&阻塞队列
  • LinkedTransferQueue:链表结构组成&无界阻塞队列
  • LinkedBlockingDeque:链表结构组成&双向阻塞队列

Fork/Join

java7提供的一个用于并行执行任务的框架,把一个大任务分割成若干个小任务,最终汇总每个小任务结 果的后得到大任务结果的框架。

工作窃取算法

是指某个线程从其他队列里窃取任务来执行。当大任务被分割成小任务时,有的线程可能提前完成任务,此时闲着不如去帮其他没完成工作线程。此时可以去其他队列窃取任务,为了减少竞争,通常使用 双端队列,被窃取的线程从头部拿,窃取的线程从尾部拿任务执行。

工作窃取算法的有缺点

优点:充分利用线程进行并行计算,减少了线程间的竞争。

缺点:有些情况下还是存在竞争,比如双端队列中只有一个任务。这样就消耗了更多资源。

Java中原子操作更新基本类型,Atomic包提供了哪几个类?

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整形
  • AtomicLong:原子更新长整形

Java中原子操作更新数组,Atomic包提供了哪几个类?

  • AtomicIntegerArray: 原子更新整形数据里的元素
  • AtomicLongArray: 原子更新长整形数组里的元素
  • AtomicReferenceArray: 原子更新饮用类型数组里的元素
  • AtomicIntegerArray: 主要提供原子方式更新数组里的整形

Java中原子操作更新引用类型,Atomic包提供了哪几个类?

如果原子需要更新多个变量,就需要用引用类型了。

  • AtomicReference :  原子更新引用类型。
  • AtomicReferenceFieldUpdater: 原子更新引用类型里的字段。
  • AtomicMarkableReference: 原子更新带有标记位的引用类型。标记位用boolean类型表示,构造方法时 AtomicMarkableReference(V initialRef,boolean initialMark)

Java中原子操作更新字段类, Atomic包提供了哪几个类?

  • AtomiceIntegerFieldUpdater:   原子更新整形字段的更新器
  • AtomiceLongFieldUpdater:       原子更新长整形字段的更新器
  • AtomiceStampedFieldUpdater:   原子更新带有版本号的引用类型,将整数值

JDK并发包中提供了哪几个比较常见的处理并发的工具类?

  • 提供并发控制手段: CountDownLatch 、CyclicBarrier 、Semaphore
  • 线程间数据交换:     Exchanger

CountDownLatch

允许一个或多个线程等待其他线程完成操作。CountDownLatch的构造函数接受一个int类型的参数作为计数器,你想等待n个点完成,就传入n。

两个重要的方法:

  • countDown() : 调用时,  n会减1。
  • await() : 调用会阻塞当前线程,直到n变成0。
  • await(longtime,TimeUnit unit) : 等待特定时间后,就不会继续阻塞当前线程。

tips:计数器必须大于等于0,当为0时, await就不会阻塞当前线程。不提供重新初始化或修改内部计数器的值的功能。

CyclicBarrier

可循环使用的屏障。让一组线程到达一个屏障样也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开 门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier默认构造放时CyclicBarrier(intparities) ,其参数表示屏障拦截的线程数量,每个线程调用 await方法告诉CyclicBarrier我已经到达屏障,然后当前线程被阻塞。

CountDownLatch与CyclicBarrier区别

CountDownLatch:

计数器:计数器只能使用一次。

等待:  一个线程或多个等待另外n个线程完成之后才能执行。

CyclicBarrier

计数器:计数器可以重置样通过reset()方法)。

等待:  n个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

Semaphore

用来控制同时访问资源的线程数量,通过协调各个线程,来保证合理的公共资源的访问。

应用场景:流量控制,特别是公共资源有限的应用场景,比如数据链接,限流等。

Exchanger

Exchanger是一个用于线程间协作的工具类,它提供一个同步点,在这个同步点上,两个线程可以交换  彼此的数据。比如第一个线程执行exchange()方法,它会一直等待第二个线程也执行exchange,当两个 线程都到同步点,就可以交换数据了。

一般来说为了避免一直等待的情况,可以使用exchange(Vx,longtimeout,TimeUnit unit),设置最大等待时间。

Exchanger可以用于遗传算法。

为什么使用线程池

几乎所有需要异步或者并发执行任务的程序都可以使用线程池。合理使用会给我们带来以下好处。

  • 降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
  • 提高响应速度:   当任务到达时,任务不需要等到线程创建就可以立即执行。
  • 提供线程可以管理性:   可以通过设置合理分配、调优、监控。

线程池工作流程

  1. 判断核心线程池里的线程是否都有在执行任务,否->创建一个新工作线程来执行任务。是->走下个流程。
  2. 判断工作队列是否已满,否->新任务存储在这个工作队列里,是->走下个流程。
  3. 判断线程池里的线程是否都在工作状态,否->创建一个新的工作线程来执行任务,是->走下个流程。
  4. 按照设置的策略来处理无法执行的任务。

创建线程池参数有哪些,作用?

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
}

1.corePoolSize:核心线程池大小,当提交一个任务时,线程池会创建一个线程来执行任务,即使其他空 闲的核心线程能够执行新任务也会创建,等待需要执行的任务数大于线程核心大小就不会继续创建。

2.maximumPoolSize:线程池最大数,允许创建的最大线程数,如果队列满了,并且已经创建的线程数小 于最大线程数,则会创建新的线程执行任务。如果是无界队列,这个参数基本没用。

3.keepAliveTime: 线程保持活动时间,线程池工作线程空闲后,保持存活的时间,所以如果任务很多, 并且每个任务执行时间较短,可以调大时间,提高线程利用率。

4.unit: 线程保持活动时间单位,天  (DAYS)、小时(HOURS)、分钟(MINUTES、毫秒 MILLISECONDS)、微秒(MICROSECONDS)、纳秒(NANOSECONDS)

5.workQueue: 任务队列,保存等待执行的任务的阻塞队列。

一般来说可以选择如下阻塞队列:

  • ArrayBlockingQueue:基于数组的有界阻塞队列。
  • LinkedBlockingQueue:基于链表的阻塞队列。
  • SynchronizedQueue:一个不存储元素的阻塞队列。
  • PriorityBlockingQueue:一个具有优先级的阻塞队列。

6.threadFactory:设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名 字。

7.  handler: 饱和策略也叫拒绝策略。当队列和线程池都满了,即达到饱和状态。所以需要采取策略来处

理新的任务。默认策略是AbortPolicy。

  • AbortPolicy:直接抛出异常。
  • CallerRunsPolicy: 调用者所在的线程来运行任务。
  • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
  • DiscardPolicy:不处理,直接丢掉。
  • 当然可以根据自己的应用场景,实现RejectedExecutionHandler接口自定义策略。

向线程池提交任务

可以使用execute()和submit() 两种方式提交任务。

  • execute():无返回值,所以无法判断任务是否被执行成功。
  • submit():用于提交需要有返回值的任务。线程池返回一个future类型的对象,通过这个future对象可以  判断任务是否执行成功,并且可以通过future的get()来获取返回值,  get()方法会阻塞当前线程知道任务 完成。 get(longtimeout,TimeUnit unit)可以设置超市时间。

关闭线程池

  • 可以通过shutdown()或shutdownNow()来关闭线程池。它们的原理是遍历线程池中的工作线程,然后 逐个调用线程的interrupt来中断线程,所以无法响应终端的任务可以能永远无法停止。
  • shutdownNow首先将线程池状态设置成STOP,然后尝试停止所有的正在执行或者暂停的线程,并返回等 待执行任务的列表。
  • shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。
  • 只要调用两者之一,isShutdown就会返回true,当所有任务都已关闭,  isTerminaed就会返回true。
  • 一般来说调用shutdown方法来关闭线程池,如果任务不一定要执行完,可以直接调用shutdownNow方法。

线程池如何合理设置

配置线程池可以从以下几个方面考虑。

  •   任务是cpu密集型、  IO密集型或者混合型
  •   任务优先级,高中低。
  •   任务时间执行长短。
  •   任务依赖性:是否依赖其他系统资源。

cpu密集型可以配置可能小的线程,比如 n + 1个线程。

io密集型可以配置较多的线程,如 2n个线程。

混合型可以拆成io密集型任务和cpu密集型任务,

Executor

从JDK5开始,把工作单元和执行机制分开。工作单元包括Runnable和Callable,而执行机制由Executor框 架提供。

Executor框架的主要成员

  • ThreadPoolExecutor  :可以通过工厂类Executors来创建。
  • 可以创建3种类型的ThreadPoolExecutor :SingleThreadExecutor 、FixedThreadPool、 CachedThreadPool。
  • ScheduledThreadPoolExecutor :可以通过工厂类Executors来创建。
  • 可以创建2中类型的ScheduledThreadPoolExecutor :ScheduledThreadPoolExecutor、 SingleThreadScheduledExecutor
  • Future接口:Future和实现Future接口的FutureTask类来表示异步计算的结果。
  • Runnable和Callable:它们的接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor 执行。 Runnable不能返回结果,  Callable可以返回结果。

FixedThreadPool

可重用固定线程数的线程池。

查看源码:

    public static return new ExecutorService newFixedThreadPool(int nThreads) {
        ThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    }

corePoolSize 和maxPoolSize都被设置成我们设置的nThreads。

当线程池中的线程数大于corePoolSize,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止,如果设为0,表示多余的空闲线程会立即终止。

工作流程:

  1. 当前线程少于corePoolSize,创建新线程执行任务。
  2. 当前运行线程等于corePoolSize,将任务加入LinkedBlockingQueue。
  3. 线程执行完1中的任务,会循环反复从LinkedBlockingQueue获取任务来执行。

LinkedBlockingQueue作为线程池工作队列(默认容量Integer.MAX_VALUE)。因此可能会造成如下影响。

  1. 当线程数等于corePoolSize时,新任务将在队列中等待,因为线程池中的线程不会超过corePoolSize。
  2. maxnumPoolSize等于说是一个无效参数。
  3. keepAliveTime等于说也是一个无效参数。
  4. 运行中的FixedThreadPool(未执行shundown或shundownNow))则不会调用拒绝策略。
  5. 由于任务可以不停的加到队列,当任务越来越多时很容易造成OOM。

SingleThreadExecutor

是使用单个worker线程的Executor。

查看源码:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>()));
    }

corePoolSize和maxnumPoolSize被设置为1。其他参数和FixedThreadPool相同。

执行流程以及造成的影响同FixedThreadPool.

CachedThreadPool

根据需要创建新线程的线程池。

查看源码:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
    }

corePoolSize设置为0 ,maxmumPoolSize为Integer.MAX_VALUE 。keepAliveTime为60秒。

工作流程:

  1. 首先执行SynchronousQueue.offer (Runnable task)。如果当前maximumPool 中有空闲线程正在执行SynchronousQueue.poll(keepAliveTIme,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空    闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute方 法执行完成;否则执行下面   的步骤2。
  2. 当初始maximumPool为空或者maximumPool中当前没有空闲线程时,将没有线程执行SynchronousQueue.poll (keepAliveTime ,TimeUnit.NANOSECONDS)。这种情况下,步骤 1将失 败。此时CachedThreadPool会创建一个新线程执行任务,  execute()方法执行完成。
  3. 在步骤2中新创建的线程将任务执行完后,会执行SynchronousQueue.poll (keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如 果60秒钟内主线程提交了一个新任务(主线程执行步骤1),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止, 因此长时间保持空闲的 CachedThreadPool不会使用任何资源。一般来说它适合处理时间短、大量的任务。

查看更多

Java 异常/反射面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

error和exception有什么区别?

error表示系统级的错误,是java运行环境内部错误或者硬件问题,不能指望程序来处理这样的问题,除 了退出运行外别无选择,它是Java虚拟机抛出的。

exception 表示程序需要捕捉、需要处理的异常,是由与程序设计的不完善而出现的问题,程序必须处 理的问题。

说出5个常见的RuntimeException?

(1)Java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对 象。

(2)Java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字 符。

(3)Java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。

(4)Java.lang.IllegalArgumentException 方法传递参数错误。

(5)Java.lang.ClassCastException 数据类型转换异常。

throw和throws的区别?

throw:

(1)throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。

(2)throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行 throw一定是抛出了某种异常。

throws:

(1)@throws 语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。

(2)throws 主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。

(3)throws 表示出现异常的一种可能性,并不一定会发生这种异常。

Java中异常分类

按照异常处理时机分为:编译时异常(受控异常(CheckedException))和运行时异常(非受控异常(UnCheckedException))

如何自定义异常

继承Exception是检查性异常,继承RuntimeException是非检查性异常,  一般要复写两个构造方法,用 throw抛出新异常

如果同时有很多异常抛出,那可能就是异常链,就是一个异常引发另一个异常,另一个异常引发更多异 常, 一般我们会找它的原始异常来解决问题,一般会在开头或结尾,异常可通过initCause串起来,可以通过自定义异常

Java中异常处理

首先处理异常主要有两种方式:一种try catch ,一种是throws。

1.  try catch:

  • try{} 中放入可能发生异常的代码。  catch{}中放入对捕获到异常之后的处理。

2.throw throws:

  • throw是语句抛出异常,出现于函数内部,用来抛出一个具体异常实例,  throw被执行后面的语句不 起作用,直接转入异常处理阶段。
  • throws是函数方法抛出异常,  一般写在方法的头部,抛出异常,给方法的调用者进行解决。

什么是Java反射机制?

Java的反射  (reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一 个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种 动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。

举例什么地方用到反射机制?

1. JDBC中,利用反射动态加载了数据库驱动程序。

2.  Web服务器中利用反射调用了Sevlet的服务方法。

3.  Eclispe等开发工具利用反射动态刨析对象的类型与结构,动态提示对象的属性和方法。 4.  很多框架都用到反射机制,注入属性,调用方法,如Spring。

java反射机制的作用

  •   在运行时判定任意一个对象所属的类
  •   在运行时构造任意一个类的对象;
  •   在运行时判定任意一个类所具有的成员变量和方法;
  •   在运行时调用任意一个对象的方法;
  •   生成动态代理;

Java反射机制类

java. lang.Class;  //类
java.lang. reflect .Constructor;//构造方法
java.lang. reflect . Field;  //类的成员变量
java.lang. reflect .Method;//类的方法
java.lang. reflect .Modifier;//访问权限

反射机制优缺点?

优点:运行期类型的判断,动态加载类,提高代码灵活度。

缺点:性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很 多。

利用反射创建对象?

1.通过一个全限类名创建一个对象

  • Class.forName(“全限类名”); 例如: com.mysql.jdbc.Driver Driver类已经被加载到 jvm中,并且完成了 类的初始化工作就行了
  • 类名.class; 获取Class<? > clz 对象
  • 对象.getClass();

2.获取构造器对象,通过构造器new出一个对象

  • Clazz.getConstructor([String.class]);
  • Con.newInstance([参数]);

3.通过class对象创建一个实例对象(就相当与new类名()无参构造器)

  • Cls.newInstance();

查看更多

Java IO面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

什么是IO流?

它是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流都包括了。输入流从文件中读取 数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。

java中有几种类型的流?

按照单位大小:字符流、字节流。按照流的方向:输出流、输入流。

字节流和字符流哪个好?怎么选择?

1.  缓大多数情况下使用字节流会更好,因为字节流是字符流的包装,而大多数时候 IO 操作都是直接操 作磁盘文件,所以这些流在传输时都是以字节的方式进行的(图片等都是按字节存储的)

2.  如果对于操作需要通过 IO 在内存中频繁处理字符串的情况使用字符流会好些,因为字符流具备缓冲区,提高了性能

读取数据量大的文件时,速度会很慢,如何选择流?

字节流时,选择BufferedInputStream和BufferedOutputStream。

字符流时,选择BufferedReader 和 BufferedWriter

IO模型有几种?

阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。

阻塞IO  (blocking IO)

应用程序调用一个IO函数,导致应用程序阻塞,如果数据已经准备好,从内核拷贝到用户空间,否则一 直等待下去。  一个典型的读操作流程大致如下图,当用户进程调用recvfrom这个系统调用时,  kernel就   开始了IO的第一个阶段:准备数据,就是数据被拷贝到内核缓冲区中的一个过程(很多网络IO数据不会 那么快到达,如没收一个完整的UDP包),等数据到操作系统内核缓冲区了,就到了第二阶段:将数据  从内核缓冲区拷贝到用户内存,然后kernel返回结果,用户进程才会解除block状态,重新运行起来。

blocking IO 的特点就是在IO执行的两个阶段用户进程都会block住;

非阻塞I/O(nonblocking IO)

非阻塞I/O模型,我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不  要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果  没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,  会大量的占用CPU 的时间

当用户进程发出read操作时,如果kernel中数据还没准备好,那么并不会block用户进程,而是立即返 回error ,用户进程判断结果是error ,就知道数据还没准备好,用户可以再次发read,直到kernel中数据 准备好,并且用户再一次发read操作,产生system call,那么kernel 马上将数据拷贝到用户内存,然后  返回;所以nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

阻塞IO一个线程只能处理一个IO流事件,要想同时处理多个IO流事件要么多线程要么多进程,这样 做效率显然不会高,而非阻塞IO可以一个线程处理多个流事件,只要不停地询所有流事件即可,当然这  个方式也不好,当大多数流没数据时,也是会大量浪费CPU资源;为了避免CPU空转,引进代理(select  和poll,两种方式相差不大),代理可以观察多个流I/O事件,空闲时会把当前线程阻塞掉,当有一个或   多个I/O事件时,就从阻塞态醒过来,把所有IO流都轮询一遍,于是没有IO事件我们的程序就阻塞在select方法处,即便这样依然存在问题,我们从select出只是知道有IO事件发生,却不知道是哪几个流, 还是只能轮询所有流,  epoll这样的代理就可以把哪个流发生怎样的IO事件通知我们;

I/O多路复用模型(IO multiplexing)

I/O多路复用就在于单个进程可以同时处理多个网络连接IO,基本原理就是select ,poll ,epoll这些个函   数会不断轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,这三个functon会阻  塞进程,但和IO阻塞不同,这些函数可以同时阻塞多个IO操作,而且可以同时对多个读操作,写操作IO 进行检验,直到有数据到达,才真正调用IO操作函数,调用过程如下图;  所以IO多路复用的特点是通过 一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中任意一个进入就   绪状态,  select函数就可以返回。

IO多路复用的优势在于并发数比较高的IO操作情况,可以同时处理多个连接,和bloking IO一样 socket是被阻塞的,只不过在多路复用中socket是被select阻塞,而在阻塞IO中是被socket IO给阻塞。

信号驱动I/O模型

可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们,通过sigaction系统调用安装一个信号处  理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取 时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据

报,并通知主循环数据已经准备好待处理。特点:等待数据报到达期间进程不被阻塞。主循环可以继续 执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取

异步 I/O(asynchronous IO)

异步IO告知内核启动某个操作,并让内核在整个操作(包括将内核数据复制到我们自己的缓冲区)完成后  通知我们,调用aio_read  (Posix异步I/O函数以aiolio开头)函数,给内核传递描述字、缓冲区指针、 缓冲区大小(与read相同的3个参数)、文件偏移以及通知的方式,然后系统立即返回。我们的进程不   阻塞于等待I/0操作的完成。当内核将数据拷贝到缓冲区后,再通知应用程序。

用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到 一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,  kernel  会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,  kernel会给用户进程发送一 个signal,告诉它read操作完成了

NIO与IO 的区别?

NIO即New IO,这个库是在JDK1.4中才引入的。  NIO和IO有相同的作用和目的,但实现方式不同,

NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO ,一套是针对标准 输入输出NIO,另一套就是网络编程NIO。

NIO和IO适用场景

NIO是为弥补传统IO的不足而诞生的,但是尺有所短寸有所长,  NIO也有缺点,因为NIO是面向缓冲 区的操作,每一次的数据处理都是对缓冲区进行的,那么就会有一个问题,在数据处理之前必须要判断 缓冲区的数据是否完整或者已经读取完毕,如果没有,假设数据只读取了一部分,那么对不完整的数据 处理没有任何意义。所以每次数据处理之前都要检测缓冲区数据。

那么NIO和IO各适用的场景是什么呢?

如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器, 这时候用NIO处理数据可能是个很好的选择。而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的IO更合适。使用哪种处 理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。

NIO核心组件

channel 、buffer 、selector

什么是channel

一个Channel  (通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道 是Java NIO提供的一座桥梁,用于我们的程序和操作系统底层I/O服务进行交互。

通道是一种很基本很抽象的描述,和不同的I/O服务交互,执行不同的I/O操作,实现不一样,因此具 体的有FileChannel 、SocketChannel等。

通道使用起来跟Stream比较像,可以读取数据到Buffer中,也可以把Buffer中的数据写入通道。

当然,也有区别,主要体现在如下两点:

  一个通道,既可以读又可以写,而一个Stream是单向的(所以分 InputStream 和 OutputStream)   通道有非阻塞I/O模式

Java NIO中最常用的通道实现?

   FileChannel:读写文件

   DatagramChannel: UDP协议网络通信

   SocketChannel : TCP协议网络通信

   ServerSocketChannel:监听TCP连接

Buffer是什么?

NIO中所使用的缓冲区不是一个简单的byte数组,而是封装过的Buffer类,通过它提供的API,我们可以 灵活的操纵数据。

与Java基本类型相对应,  NIO提供了多种 Buffer 类型,如ByteBuffer 、CharBuffer 、IntBuffer等,区别 就是读写缓冲区时的单位长度不一样(以对应类型的变量为单位进行读写)。

核心Buffer实现有哪些?

核心的buffer实现有这些: ByteBuffer 、CharBuffer 、DoubleBuffer 、FloatBuffer 、IntBuffer、

LongBuffer 、ShortBuffer,涵盖了所有的基本数据类型(4类8种,除了Boolean)。也有其他的buffer如 MappedByteBuffer。

buffer读写数据基本操作

1)、将数据写入buffer

2)、调用buffer.flip()

3)、将数据从buffer中读取出来

4)、调用buffer.clear()或者buffer.compact()

在写buffer的时候,  buffer会跟踪写入了多少数据,需要读buffer的时候,需要调用flip()来将buffer从写 模式切换成读模式,读模式中只能读取写入的数据,而非整个buffer。

当数据都读完了,你需要清空buffer以供下次使用,可以有2种方法来操作:调用clear() 或者 调用 compact()。

区别: clear方法清空整个buffer ,compact方法只清除你已经读取的数据,未读取的数据会被移到 buffer的开头,此时写入数据会从当前数据的末尾开始。

    // 创建⼀个容量为48的ByteBuffer
    ByteBufferbuf=ByteBuffer.allocate(48);// 从channel中读(取数据然后写)⼊buffer
    intbytesRead=inChannel.read(buf);
    // 下⾯是读取buffer
    while(bytesRead!=-1){
        buf.flip();  // 转换buffer为读模式
        System.out.print((char) buf.get()); // ⼀次读取⼀个byte
        buf.clear();  //清空buffer准备下⼀次写⼊
    }

Selector是什么?

Selector  (选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册 到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。

通道可以监听那几个事件?

通道有如下4个事件可供我们监听:

  • Accept:有可以接受的连接
  • Connect:连接成功
  • Read:有数据可读
  • Write:可以写入数据了

为什么要用Selector?

如果用阻塞I/O,需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。 Selector 的出现解决了这施起的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用盲目的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select() 方法处被挂起,从而让出了CPU资源。

Selector处理多Channel 图文说明

要使用一个Selector,你要先注册这个Selector的Channels。然后你调用Selector的select()方法。这个方   法会阻塞,直到它注册的Channels当中有一个准备好了的事件发生了。当select()方法返回的时候,线程 可以处理这些事件,如新的连接的到来,数据收到了等。

查看更多

java集合面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

Java集合框架的基础接口有哪些?

  • Collection为集合层级的根接口。  一个集合代表一组对象,这些对象即为它的元素。  Java平台不提供这 个接口任何直接的实现。
  • Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副 牌。
  • List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。  List更像长度动态变换 的数组。
  • Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。
  • 一些其它的接口有Queue 、Dequeue 、SortedSet 、SortedMap和ListIterator。

2.Collection Collections 有什么区别?

  • Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的 子类,比如 List 、Set 等。
  • Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序 方法:Collections. sort(list)。

List、 Set、Map是否继承自Collection接口?

List 、Set 是, Map 不是。 Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素 且不允许有重复元素(数学中的集合也是如此), List是线性结构的容器,适用于按数值索引访问元素  的情形。

Collections.sort排序内部原理

在Java 6中Arrays.sort()和Collections.sort()使用的是MergeSort,而在Java 7中,内部实现换成了 TimSort,其对对象间比较的实现要求更加严格。

List、 Set、Map 之间的区别是什么?

List 、Set 、Map 的区别主要体现在两个方面:元素是否有序、是否允许元素重复。

三者之间的区别,如下表:

HashMap 和 Hashtable 有什么区别?

(1)HashMap允许key和value为null,而HashTable不允许。

(2)HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境, HashTable适合多线程 环境。

(3)在Java1.4中引入了LinkedHashMap , HashMap的一个子类,假如你想要遍历顺序,你很容易从 HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的。

(4)HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key的Enumeration 进行遍历,它不支持fail-fast。

(5)HashTable被认为是个遗留的类,如果你寻求在选代的时候修改Map,你应该使用 CocurrentHashMap。

如何决定使用 HashMap 还是 TreeMap?

对于在 Map 中插入、删除、定位一个元素这类操作, HashMap 是最好的选择,因为相对而言HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择。

说一下 HashMap 的实现原理?

HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储, get(key)来获取。当传入 key 时,HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出 的 hash 值相同时,我们称之为 hash 冲突, HashMap 的做法是用链表和红黑树存储相同 hash 值的value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。

说一下 HashSet 的实现原理?

HashSet 是基于 HashMap 实现的,  HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实 现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,  HashSet 不允许重复的值。

ArrayList 和 LinkedList 的区别是什么?

  • 数据结构实现:  ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
  • 随机访问效率:  ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据 存储方式,所以需要移动指针从前往后依次查找。
  • 增加和删除效率:在非首尾的增加和删除操作,  LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时, 更推荐使用 LinkedList。

为何Map接口不继承Collection接口?

尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此,  Map继承 Collection毫无意义,反之亦然。

如果Map继承Collection接口,那么元素去哪儿? Map包含key-value对,它提供抽取key或value列表集 合的方法,但是它不适合“一组对象”规范。

ArrayList和Vector有何异同点?

ArrayList和Vector在很多时候都很类似。

(1)两者都是基于索引的,内部由一个数组支持。

(2)两者维护插入的顺序,我们可以根据插入顺序来获取元素。

(3)ArrayList和Vector的选代器实现都是fail-fast的。

(4)ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。

以下是ArrayList和Vector的不同点。

(1)Vector是同步的,而ArrayList不是。然而,如果你寻求在选代的时候对列表进行改变,你应该使 用CopyOnWriteArrayList。

(2)ArrayList比Vector快,它因为有同步,不会过载。

(3)ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。

Array 和 ArrayList 有何区别?

  • Array 可以存储基本数据类型和对象,  ArrayList 只能存储对象。
  • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
  • Array 内置方法没有 ArrayList 多,比如 addAll 、removeAll 、iteration 等方法只有 ArrayList 有。

在 Queue 中 poll()和 remove()有什么区别?

相同点知都是返回第一个元素,并在队列中删除返回的对象。不同点知如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。

代码示例知

Queue<String> queue = new LinkedList<String>();
queue. offer("string"); // add
System. out. println(queue. poll());
System. out. println(queue. remove());
System. out. println(queue. size());

15.LinkedHashMap有什么特点?

LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

16.HashMap的底层实现原理?(高频问题)

从结构实现来讲,  HashMap是数组+链表+红黑树  (JDK1.8增加了红黑树部分)实现的,如下如所示。

这里需要讲明白两个问题:数据底层具体存储的是什么?这样的存储方式有什么优点呢?

(1) 从源码可知,  HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是 一个Node的数组。我们来看Node[JDK1.8]是何物。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; //⽤来定位数组索引位置
    final K key;
    V value;
    Node<K,V> next; //链表的下⼀个node
    Node(int hash, K key, V value, Node<K,V> next) { … }
    public final K getKey(){ … }
    public final V getValue() { … }
    public final String toString() { … }
    public final int hashCode() { … }
    public final V setValue(V newValue) { … }
    public final boolean equals(Object o) { … }
}

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个 黑色圆点就是一个Node对象。

(2) HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决   问题, Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元 素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。例如程序 执行下面代码:

map .put("美团" ,"小美 " );

系统将调用”美团”这个key的hashCode()方法得到其hashCode 值(该方法适用于每个Java对象),然后  再通过Hash算法的后两步运算(高位运算和取模运算,下文有介绍)来定位该键值对的存储位置,有时 两个key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结果越分散均匀,  Hash碰撞   的概率就越小,  map的存取效率就会越高。

如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash   算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈  希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。那么通过什么方式来控制map使得   Hash碰撞的概率又小,哈希桶数组  (Node[] table)占用空间又少呢?答案就是好的Hash算法和扩容机 制。

在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码 可知,构造函数就是对下面几个字段进行初始化,源码如下:

 int threshold; // 所能容纳的key-value对极限
 final float loadFactor; // 负载因⼦
 int modCount; 
 int size;

首先, Node[] table的初始化长度length(默认值是16) ,Load factor为负载因子(默认值是0.75),

threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。 也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

结合负载因子的定义公式可知,  threshold就是在此Load factor和length(数组长度)对应下允许的最大元 素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因 子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存 空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

size这个字段其实很好理解,就是HashMap中实际存在的键值对数量。注意和table的长度length、容纳 最大键值对数量threshold的区别。而modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于选代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值 对,但是某个key对应的value值被覆盖不属于结构变化。

在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设 计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,具体证明可以参 考http://blog.csdn.net/liuqiyao_01/article/details/14475159 ,Hashtable初始化桶大小为11,就是桶  大小设计为素数的应用  (Hashtable扩容后不能保证还是素数)。  HashMap采用这种非常规设计,主要  是为了在取模和扩容时做优化,同时为了减少冲突,  HashMap定位哈希桶索引位置时,也加入了高位   参与运算的过程。

这里存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,  一旦出 现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删 改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

功能实现-方法

HashMap的内部功能实现很多,本文主要从根据key获取哈希桶数组索引位置、  put方法的详细执行、 扩容过程三个具有代表性的点深入展开讲解。

1.确定哈希桶数组索引位置

不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的   数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽   量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道 对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。  HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现(方法一+方法二):

⽅法⼀:
static final int hash(Object key){ //jdk1.8 & jdk1.7
    int h;
    // h = key.hashCode() 为第⼀步 取hashCode值
    // h ^ (h >>> 16) 为第⼆步 ⾼位参与运算
    return(key==null)?0:(h=key.hashCode())^(h>>>16);
}
        ⽅法⼆:
static int indexFor(int h,int length){ 
     // jdk1.7的源码,jdk1.8没有这个⽅法,但是实现原理⼀样的
     return h&(length-1); //第三步 取模运算
}

这里的Hash算法本质上就是三步:  keyhashCode值、高位运算、取模运算

对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值   总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比 较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用方法二来计算该对象   应该保存在table数组的哪个索引处。

这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总 是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,  h& (length-1)运算等价于对    length取模,也就是h%length,但是&比%具有更高的效率。

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:  (h =k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比 较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

下面举例说明下,  n为table的长度。

2.HashMap的put方法

HashMap的put方法执行过程可以通过下图来理解,自己有兴趣可以去对比源码更清楚地研究学习。

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如 果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是 hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对, 否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作, 否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

JDK1.8HashMap的put方法源码如下:

public V put(K key, V value) {
        // 对key的hashCode()做hash
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
        // 步骤①:tab为空则创建
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 步骤②:计算index,并对null做处理
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K, V> e;
            K k;
            // 步骤③:节点key存在,直接覆盖value
            if (p.hash == hash &&
                    19 ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            // 步骤④:判断该链为红⿊树
             else if (p instanceof TreeNode)
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key,
                        value);
                // 步骤⑤:该链为链表
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //链表⻓度⼤于8转换为红⿊树进⾏处理
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 
                            treeifyBin(tab, hash);
                        break;
                    }
                    // key已经存在直接覆盖value
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null &&
                                    key.equals(k)))) break;
                    p = e;
                }
            }

            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 步骤⑥:超过最⼤容量 就扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

3.扩容机制

扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法 装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自 动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更 多的水,就得换大水桶。

我们分析下resize的源码,鉴于JDK1.8融入了红黑树,较复杂,为了便于理解我们仍然使用JDK1.7的代 码,好理解一些,本质上区别不大,具体区别后文再说。

    void resize(int newCapacity) { //传⼊新的容量
        Entry[] oldTable = table; //引⽤扩容前的Entry数组
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组⼤⼩如果已经达到最⼤
            (2 ^ 30) 了
                    threshold = Integer.MAX_VALUE; //修改阈值为int的最⼤值(2^31-1),这样以后就不会扩容了
            return;
        }

        Entry[] newTable = new Entry[newCapacity]; //初始化⼀个新的Entry数组
        transfer(newTable); //!!将数据转移到新的Entry数组⾥
        table = newTable; //HashMap的table属性引⽤新的Entry数组
        threshold = (int) (newCapacity * loadFactor);//修改阈值
    }

这里就是使用一个容量更大的数组来代替已有的容量小的数组,  transfer()方法将原有Entry数组的元素 拷贝到新的Entry数组里。

    void transfer(Entry[] newTable) {
        Entry[] src = table; //src引⽤了旧的Entry数组
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
            Entry<K, V> e = src[j]; //取得旧Entry数组的每个元素
            if (e != null) {
                src[j] = null;//释放旧Entry数组的对象引⽤(for循环后,旧的Entry数组不再引⽤任何对象)
                do {
                    Entry<K, V> next = e.next;
                    int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
                    e.next = newTable[i]; //标记[1]
                    newTable[i] = e; //将元素放在数组上
                    e = next; //访问下⼀个Entry链上的元素
                } while (e != null);
            }
        }
    }

newTable[i]的引用赋给了e.next,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链 表的头部位置;这样先放在一个索引上的元素终会被放到Entry链的尾部(如果发生了hash冲突的话),这一点和Jdk1.8有区别,下文详解。在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有 可能被放到了新数组的不同位置上。

下面举个例子说明下扩容过程。假设了我们的hash算法就是简单的用key mod一下表的大小(也就是数 组的长度)。其中的哈希桶数组table的size=2, 所以key = 3 、7 、5 ,put顺序依次为 5 、7 、3。在mod   2以后都冲突在table[1]这里了。这里假设负载因子 loadFactor=1,即当键值对的实际大小size 大于 table 的实际大小时进行扩容。接下来的三个步骤是哈希桶数组 resize成4,然后所有的Node重新rehash的过   程。

下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幕的扩展(指长度扩为原来  2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幕的位置。看下图可以明白这句话 的意思,  n为table的长度,图  (a)表示扩容前的key1和key2两种key确定索引位置的示例,图  (b)表    示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就 会发生这样的变化:

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看 看下图为16扩充为32的resize示意图:

这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以 认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,  JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新 表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,  JDK1.8不会倒置。有兴趣的同学可 以研究下JDK1.8的resize源码,写的很赞,如下:

final Node<K, V>[] resize() {
        Node<K, V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 超过最⼤值就不再扩充了,就只好随你碰撞去吧
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 没超过最⼤值,就扩充为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        } else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else { // zero initial threshold signifies using
            defaults
                    newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 计算新的resize上限
        if (newThr == 0) {

            float ft = (float) newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft <
                    (float) MAXIMUM_CAPACITY ?
                    (int) ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            // 把每个bucket都移动到新的buckets中
            for (int j = 0; j < oldCap; ++j) {
                Node<K, V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                    else { // 链表优化重hash的代码块
                        Node<K, V> loHead = null, loTail = null;
                        Node<K, V> hiHead = null, hiTail = null;
                        Node<K, V> next;
                        do {
                            next = e.next;
                            // 原索引
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 原索引+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 原索引放到bucket⾥
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 原索引+oldCap放到bucket⾥
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

17.HashMap并发安全的问题

并发的多线程使用场景中使用HashMap可能造成死循环。代码例子如下(便于理解,仍然使用JDK1.7的 环境):

public class HashMapInfiniteLoop {
    private static HashMap<Integer, String> map = new HashMap<Integer, String>
            (2,0.75f);

    public static void main(String[] args) {
        map.put(5, "C");
        new Thread("Thread1") {
            public void run() {
                map.put(7, "B");
                System.out.println(map);
            }
        }.start();
        new Thread("Thread2") {
            public void run() {
                map.put(3, "A); 
                        System.out.println(map);
            }
        }.start();
    }
}

其中, map初始化为一个长度为2的数组,  loadFactor=0.75 ,threshold=2*0.75=1,也就是说当put第二 个key的时候,  map就需要进行resize。

通过设置断点让线程1和线程2同时debug到transfer方法(3.3小节代码块)的首行。注意此时两个线程已经 成功添加数据。放开thread1的断点至transfer方法的“Entry next = e.next;” 这一行;然后放开线程2的的 断点,让线程2进行resize。结果如下图。

注意, Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的 链表。

线程一被调度回来执行,先是执行 newTalbe[i] = e, 然后是e = next,导致了e指向了key(7),而下一次 循环的next = e.next导致了next指向了key(3)。

e.next = newTable[i] 导致 key(3).next 指向了 key(7)。注意:此时的key(7).next 已经指向了key(3), 环 形链表就这样出现了。

于是,当我们用线程一调用map.get(11)时,悲剧就出现了——Infinite Loop。

JDK1.8与JDK1.7的性能对比

HashMap中,如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话, getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中, 或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn) 。 鉴于JDK1.8做了多方面的优化,总体性能优于 JDK1.7,下面我们从两个方面用例子证明这一点。

Hash较均匀的情况为了便于测试,我们先写一个类Key,如下:

class Key implements Comparable<Key> {
    private final int value;

    Key(int value) {
        this.value = value;
    }

    @Override
    public int compareTo(Key o) {
        return Integer.compare(this.value, o.value);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass())
            return false;
        Key key = (Key) o;
        return value == key.value;
    }

    @Override
    public int hashCode() {
        return value;
    }
}

这个类复写了equals方法,并且提供了相当好的hashCode函数,任何一个值的hashCode都不会相同,  因为直接使用value当做hashcode。为了避免频繁的GC,我将不变的Key实例缓存了起来,而不是一遍 一遍的创建它们。代码如下:

public class Keys {
    public static final int MAX_KEY = 10_000_000;
    private static final Key[] KEYS_CACHE = new Key[MAX_KEY];

    static {
        for (int i = 0; i < MAX_KEY; ++i) {
            KEYS_CACHE[i] = new Key(i);
        }
    }

    public static Key of(int value) {
        return KEYS_CACHE[value];
    }
}

现在开始我们的试验,测试需要做的仅仅是,创建不同size的HashMap  (1、10 、100 、 …… 10000000),屏蔽了扩容的情况,代码如下:

 static void test(int mapSize) {
        HashMap<Key, Integer> map = new HashMap<Key, Integer>(mapSize);
        for (int i = 0; i < mapSize; ++i) {
            map.put(Keys.of(i), i);
        }
        long beginTime = System.nanoTime(); //获取纳秒
        for (int i = 0; i < mapSize; i++) {
            map.get(Keys.of(i));
        }
        long endTime = System.nanoTime();
        System.out.println(endTime - beginTime);
    }

    public static void main(String[] args) {
        for (int i = 10; i <= 1000 0000;
        i *= 10){
            test(i);
        }
    }

在测试中会查找不同的值,然后度量花费的时间,为了计算getKey的平均时间,我们遍历所有的get方 法,计算总的时间,除以key的数量,计算一个平均值,主要用来比较,绝对值可能会受很多环境因素 的影响。结果如下:

通过观测测试结果可知,  JDK1.8的性能要高于JDK1.7 15%以上,在某些size的区域上,甚至高于100%。 由于Hash算法较均匀,  JDK1.8引入的红黑树效果不明显,下面我们看看Hash不均匀的的情况。

Hash极不均匀的情况假设我们又一个非常差的Key,它们所有的实例都返回相同的hashCode值。这是使用HashMap最坏的 情况。代码修改如下:

class Key implements Comparable<Key> {
    //…
    @Override
    public int hashCode() {
        return 1;
    }
}

仍然执行main方法,得出的结果如下表所示:

从表中结果中可知,随着size的变大,  JDK1.7的花费时间是增长的趋势,而JDK1.8是明显的降低趋势, 并且呈现对数增长稳定。当一个链表太长的时候,  HashMap会动态的将它替换成一个红黑树,这话的  话会将时间复杂度从O(n)降为O(logn) 。hash算法均匀和不均匀所花费的时间明显也不相同,这两种情 况的相对比较,可以说明一个好的hash算法的重要性。

测试环境:处理器为2.2 GHz Intel Core i7,内存为16 GB 1600 MHz DDR3 ,SSD硬盘,使用默认的 JVM参数,运行在64位的OS X 10.10.1上。

HashMap操作注意事项以及优化?

(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的 时候给一个大致的数值,避免map进行频繁的扩容。

(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。

(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用

ConcurrentHashMap。

(4) JDK1.8引入红黑树大程度优化了HashMap的性能。

(5) 还没升级JDK1.8的,现在开始升级吧。  HashMap的性能提升仅仅是JDK1.8的冰山一角。

查看更多

Java基础面试题

《林老师带你学编程》知识星球是由多个工作10年以上的一线大厂开发人员联合创建,希望通过我们的分享,帮助大家少走弯路,可以在技术的领域不断突破和发展。

🔥 具体的加入方式:

Java语言的三大特性

1.  封装:

首先,属性可用来描述同一类事物的特征,方法可描述一类事物可做的操作。封装就是把属于同一类 事物的共性(包括属性与方法)归到一个类中,以方便使用。

  • 概念:封装也称为信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构 成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留 一些对外接口使之与外部发生联系。系统的其他部分只有通过包裹在数据外面的被授权的操作来与这 个抽象数据类型交流与交互。也就是说,用户无需知道对象内部方法的实现细节,但可以根据对象提 供的外部接口(对象名和参数)访问该对象。
  • 好处: (1)实现了专业的分工。将能实现某一特定功能的代码封装成一个独立的实体后,各程序员可 以在需要的时候调用,从而实现了专业的分工。  (2)隐藏信息,实现细节。通过控制访问权限可以将可 以将不想让客户端程序员看到的信息隐藏起来,如某客户的银行的密码需要保密,只能对该客户开发 权限。

2.  继承:

就是个性对共性的属性与方法的接受,并加入个性特有的属性与方法

  • 概念: 一个类继承另一个类,则称继承的类为子类,被继承的类为父类。
  • 目的:实现代码的复用。
  • 理解:子类与父类的关系并不是日常生活中的父子关系,子类与父类而是一种特殊化与一般化的关 系,是is-a的关系,子类是父类更加详细的分类。如class dog extends animal,就可以理解为dog is a   animal.注意设计继承的时候,若要让某个类能继承,父类需适当开放访问权限,遵循里氏代换原则,即向修改关闭对扩展开放,也就是开- 闭原则。
  • 结果:继承后子类自动拥有了父类的属性和方法,但特别注意的是,父类的私有属性和构造方法并 不能被继承。另外子类可以写自己特有的属性和方法,目的是实现功能的扩展,子类也可以复写父类的方法即方法 的重写。

3.  多态:

多态的概念发展出来,是以封装和继承为基础的。

多态就是在抽象的层面上实施一个统一的行为,到个体(具体)的层面上时,这个统一的行为会因为 个体(具体)的形态特征而实施自己的特征行为。(针对一个抽象的事,对于内部个体又能找到其自 身的行为去执行。)

  1. 概念:相同的事物,调用其相同的方法,参数也相同时,但表现的行为却不同。
  2. 理解:子类以父类的身份出现,但做事情时还是以自己的方法实现。子类以父类的身份出现需要向 上转型(upcast),其中向上转型是由JVM自动实现的,是安全的,但向下转型(downcast)是不安全的,需要强制转换。子类以父类的身份出现时自己特有的属性和方法将不能使用。

Java语言主要特性

  1. Java语言是易学的。Java语言的语法与C语言和C++语言很接近,使得大多数程序员很容易学习和使 用Java。
  2. Java语言是强制面向对象的。Java语言提供类、接口和继承等原语,为了简单起见,只支持类之间的 单继承,但支持接口之间的多继承,并支持类与接口之间的实现机制(关键字为implements)。
  3. Java语言是分布式的 。Java语言支持Internet应用的开发,在基本的Java应用编程接口中有一个网络 应用编程接口  (java net),它提供了用于网络应用编程的类库,包括URL 、URLConnection、Socket 、ServerSocket等。 Java的RMI  (远程方法激活)机制也是开发分布式应用的重要手段。
  4. Java语言是健壮的。Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保 证。对指针的丢弃是Java的明智选择。
  5. Java语言是安全的。Java通常被用在网络环境中,为此,  Java提供了一个安全机制以防恶意代码的攻 击。如:安全防范机制(类ClassLoader),如分配不同的名字空间以防替代本地的同名类、字节代  码检查。
  6. Java语言是体系结构中立的。Java程序(后缀为java的文件)在Java平台上被编译为体系结构中立的 字节码格式(后缀为class的文件),然后可以在实现这个Java平台的任何系统中运行。
  7. Java语言是解释型的。如前所述,  Java程序在Java平台上被编译为字节码格式,然后可以在实现这个 Java平台的任何系统的解释器中运行。    (一次编译,到处运行)。
  8. Java是性能略高的。与那些解释型的高级脚本语言相比,  Java的性能还是较优的。
  9. Java语言是原生支持多线程的。在Java语言中,线程是一种特殊的对象,它必须由Thread类或其子 (孙)类来创建。

JDK 和 JRE 有什么区别

  •  JDK:Java Development Kit 的简称,  Java 开发工具包,提供了 Java 的开发环境和运行环境。
  • .JRE:Java Runtime Environment 的简称,  Java 运行环境,为 Java 的运行提供了所需环境。

具体来说 JDK 其实包含了 JRE,同时还包含了编译 Java 源码的编译器 Javac,还包含了很多 Java 程序 调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要安装 JDK。

Java基本数据类型及其封装类

Tips:boolean类型占了单独使用是4个字节,在数组中又是1个字节

  • 基本类型所占的存储空间是不变的。这种不变性也是Java具有可移植性的原因之一。 基本类型放在栈中,直接存储值。
  • 所有数值类型都有正负号,没有无符号的数值类型。

为什么需要封装类?

因为泛型类包括预定义的集合,使用的参数都是对象类型,无法直接使用基本数据类型,所以Java又提 供了这些基本类型的封装类。

基本类型和对应的封装类由于本质的不同。具有一些区别:

  1. 基本类型只能按值传递,而封装类按引用传递。
  2. 基本类型会在栈中创建,而对于对象类型,对象在堆中创建,对象的引用在栈中创建,基本类型由于 在栈中,效率会比较高,但是可能存在内存泄漏的问题。

如果main方法被声明为private会怎样?

能正常编译,但运行的时候会提示”main方法不是public的” 。在idea中如果不用public修饰,则会自动 去掉可运行的按钮。

说明一下public static void main(String args[])这段声明里每个关键字的 作用

public: main方法是Java程序运行时调用的第一个方法,因此它必须对Java环境可见。所以可见性设置为 pulic.

static: Java平台调用这个方法时不会创建这个类的一个实例,因此这个方法必须声明为static。

void: main方法没有返回值。

String是命令行传进参数的类型,  args是指命令行传进的字符串数组。

==与equals的区别

==比较两个对象在内存里是不是同一个对象,就是说在内存里的存储位置一致。两个String对象存储的 值是一样的,但有可能在内存里存储在不同的地方 .

==比较的是引用而equals方法比较的是内容。  public boolean equals(Object obj) 这个方法是由Object对  象提供的,可以由子类进行重写。默认的实现只有当对象和自身进行比较时才会返回true,这个时候和== 是等价的。  String, BitSet, Date, 和File都对equals方法进行了重写,对两个String对象 而言,值相等意味 着它们包含同样的字符序列。对于基本类型的包装类来说,值相等意味着对应的基本类型的值一样。

public class EqualsTest {
    public static void main(String[] args) {
        String s1 = “abc”;
        String s2 = s1;
        String s5 = “abc”;
        String s3 = new String(”abc”);
        String s4 = new String(”abc”);
        System.out.println(” == comparison : ”+(s1 == s5));
        System.out.println(” == comparison : ”+(s1 == s2));
        System.out.println(”Using equals method : ”+s1.equals(s2));
        System.out.println(” == comparison : ”+s3 == s4);
        System.out.println(”Using equals method : ”+s3.equals(s4));
    }
}

结果:

== comparison : true
== comparison : true
Using equals method : true
false
Using equals method :true

Object有哪些公用方法

Object是所有类的父类,任何类都默认继承Object

  • clone  保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出 CloneNotSupportedException异常。
  • equals  在Object中与==是一样的,子类一般需要重写该方法。
  • hashCode  该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有 哈希功能的Collection中用到。
  • getClass  final方法,获得运行时类型
  • wait   使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。   wait()  方法一直等待,直到获得锁或者被中断。   
  • wait(longtimeout) 设定一个超时间隔,如果在规定时间内没 有获得锁就返回。

调用wait方法后当前线程进入睡眠状态,直到以下事件发生

  1. 其他线程调用了该对象的notify方法。
  2. 其他线程调用了该对象的notifyAll方法。
  3. 其他线程调用了interrupt中断该线程。
  4. 时间间隔到了。
  5. 此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
  • notify 唤醒在该对象上等待的某个线程。
  • notifyAll 唤醒在该对象上等待的所有线程。
  • toString 转换成字符串,  一般子类都有重写,否则打印句柄。

为什么Java里没有全局变量?

全局变量是全局可见的,  Java不支持全局可见的变量,因为:全局变量破坏了引用透明性原则。全局变量导致了命名空间的冲突。

while循环和do循环有什么不同?

while结构在循环的开始判断下一个选代是否应该继续。  do/while结构在循环的结尾来判断是否将继续 下一轮选代。  do结构至少会执行一次循环体。

char型变量中能不能存储一个中文汉字?为什么?

可以。 Java默认Unicode编码。 Unicode码占16位。  char两个字节刚好16位。

public、protected、default、private的作用域分别是什么?

Tips:不写默认default

float f=3.4;是否正确?

不正确。  3.4是双精度数,将双精度型  (double)赋值给浮点型(float)属于下转型(down-casting,也 称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F。

short s1 = 1; s1 = s1 + 1;有错吗?  short s1 = 1; s1 += 1;有错吗?

对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋 值给short型。而short s1 = 1; s1 += 1;+=操作符会进行隐式自动类型转换,是 Java 语言规定的运算符;Java编译器会对它进行特殊处理,因此可以正确编译。因为s1+= 1;相当于s1 = (short)(s1 + 1)。

&和&&的区别?

1.  & :(1)按位与;  (2)逻辑与。

  • 按位与: 0 & 1 = 0 ; 0 & 0 = 0; 1 & 1 = 1
  • 逻辑与: a == b &  b ==c    (即使a==b已经是 false了,程序还会继续判断b是否等于c)

2.&&: 短路与

  • a== b && b== c   (当a==b 为false则不会继续判断b是否等与c)
  • 比如判断某对象中的属性是否等于某值,则必须用&&,否则会出现空指针问题。

IntegerCache是什么?

public class IntegerTest {
    public static void main(String[] args) {
           Integer a = 100, b = 100 ,c = 129,d = 129;
           System.out.println(a==b);
           System.out.println(c==d);
   }
}

结果:

true 
false

小朋友,你是否有很多问号?

来解释一下:

/*Cache to support the object identity semantics of autoboxing for
        values between
        *-128and 127(inclusive)as required by JLS.
        *
        *The cache is initialized on first usage.The size of the cache
        *may be controlled by the{@code -XX:AutoBoxCacheMax=<size>}option.
        *During VM initialization,java.lang.Integer.IntegerCache.high property
        *may be set and saved in the private system properties in the
        *sun.misc.VM class.
        */

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
            } catch (NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore

            }
        }
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for (int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {
    }
}
public static Integer valueOf(int i) {
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high){
        return IntegerCache.cache[i + (-IntegerCache.low)];
    }
    return new Integer(i);
}

通过源码,我们可以看出, -128~127之间做了缓存。考虑到高频数值的复用场景,这样做还是很合理 的,合理优化。最大边界可以通过-XX:AutoBoxCacheMax进行配置。

Locale类是什么?

Locale类用来根据语言环境来动态调整程序的输出。

Java中final、finally、finalize 的区别与用法

1. final

  • final是一个修饰符也是一个关键字。
  • 被final修饰的类无法被继承
  • 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是它指向的对象的内 容是可变的
  • 被final修饰的方法将无法被重写,但允许重载

注意:类的private方法会隐式地被指定为final方法。

2. finally

finally是一个关键字。

  •    finally在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出或者捕获,  finally块 都会执行,通常用于释放资源。
  •    finally块正常情况下一定会被执行。但是有至少两个极端情况:
    • 如果对应的try块没有执行,则这个try块的finally块并不会被执行
    • 如果在try块中jvm关机,例如system.exit(n),则finally块也不会执行(都拔电源了,怎么执行)
  •    finally块中如果有return语句,则会覆盖try或者catch中的return语句,导致二者无法return,所以强 烈建议finally块中不要存在return关键字

3. finalize

finalize()是Object类的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前都会调用该方法

finalize()方法是存在很多问题的:

  • java语言规范并不保证finalize方法会被及时地执行,更根本不会保证它们一定会被执行
  • finalize()方法可能带来性能问题,因为JVM通常在单独的低优先级线程中完成finalize的执行
  • finalize()方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的    finalize方法最多由GC执行一次(但可以手动调用对象的finalize方法)

hashCode()和equals()的区别

下边从两个角度介绍了他们的区别:  一个是性能,  一个是可靠性。他们之间的主要区别也基本体现在这里。

1.equals()既然已经能实现对比的功能了,为什么还要hashCode()呢?

因为重写的equals  ()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对 比,则只要生成一个hash值进行比较就可以了,效率很高。

2.hashCode()既然效率这么高为什么还要equals()呢?

因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式。

可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出 (PS:以下两条结论是重点,很多人面试的时候都说不出来):

  • equals()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。
  • hashCode()相等的两个对象他们的equals()不一定相等,也就是hashCode()不是绝对可靠的。

扩展

1.阿里巴巴开发规范明确规定:

  • 只要重写 equals,就必须重写 hashCode;
  • 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这 两个方法;
  • 如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals ;
  • String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用;

2.什么时候需要重写?

一般的地方不需要重载hashCode,只有当类需要放在HashTable 、HashMap 、HashSet等等hash结构的,集合时才会重载hashCode。

3.那么为什么要重载hashCode呢?

如果你重写了equals,比如说是基于对象的内容实现的,而保留hashCode的实现不变,那么很可能某两 个对象明明是“相等” ,而hashCode却不一样。这样,当你用其中的一个作为键保存到hashMap 、hasoTable或hashSet中,再以“相等的”找另一个作为 键值去查找他们的时候,则根本找不到。

4.为什么equals()相等, hashCode就一定要相等,而hashCode相等,却不要求equals相等?

因为是按照hashCode来访问小内存块,所以hashCode必须相等。HashMap获取一个对象是比较key的hashCode相等和equals为true。之所以hashCode相等,却可以equal不等,就比如ObjectA和ObjectB他们都有属性name ,那么hashCode都以name计算,所以hashCode一样,但是两个对象属于不同类型,所以equals为false。

为什么需要hashCode?

通过hashCode可以很快的查到小内存块。通过hashCode比较比equals方法快,当get时先比较hashCode,如果hashCode不同,直接返回false。

深拷贝和浅拷贝的区别是什么?

浅拷贝

(1)、定义

被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。  ”里面的对象“会在原来的 对象和它的副本之间共享。简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象

浅拷贝实例

public class ShallowCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        Teacher teacher = new Teacher();
        teacher.setName("riemann");
        teacher.setAge(27);
        Student2 student1 = new Student2();
        student1.setName("edgar");
        student1.setAge(18);
        student1.setTeacher(teacher);
        Student2 student2 = (Student2) student1.clone();
        System.out.println("拷⻉后");
        System.out.println(student2.getName());
        System.out.println(student2.getAge());
        System.out.println(student2.getTeacher().getName());
        System.out.println(student2.getTeacher().getAge());
        System.out.println("修改⽼师的信息后——————");
        // 修改⽼师的信息
        teacher.setName("Games");
        System.out.println(student1.getTeacher().getName());
        System.out.println(student2.getTeacher().getName());
    }
}

class Teacher implements Cloneable {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

class Student2 implements Cloneable {
    private String name;
    private int age;
    private Teacher teacher;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Teacher getTeacher() {
        return teacher;
    }

    public void setTeacher(Teacher teacher) {
        this.teacher = teacher;
    }

    public Object clone() throws CloneNotSupportedException {
        Object object = super.clone();
        return object;
    }
}

输出结果:

拷⻉后
edgar
18
riemann 
27 
修改⽼师的信息后—————— 
Games Games

结果分析:   两个引用student1和student2指向不同的两个对象,但是两个引用student1和student2中的 两个teacher引用指向的是同一个对象,所以说明是浅拷贝。

深拷贝

(1)、定义

深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对 象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

(2)、深拷贝实例

public class DeepCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        Teacher2 teacher = new Teacher2();
        teacher.setName("riemann");
        teacher.setAge(27);
        Student3 student1 = new Student3();
        student1.setName("edgar");
        student1.setAge(18);
        student1.setTeacher(teacher);
        Student3 student2 = (Student3) student1.clone();
        System.out.println("拷⻉后");
        System.out.println(student2.getName());
        System.out.println(student2.getAge());
        System.out.println(student2.getTeacher().getName());
        System.out.println(student2.getTeacher().getAge());
        System.out.println("修改⽼师的信息后——————");
        // 修改⽼师的信息
        teacher.setName("Games");
        System.out.println(student1.getTeacher().getName());
        System.out.println(student2.getTeacher().getName());
    }
}

class Teacher2 implements Cloneable {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class Student3 implements Cloneable {
    private String name;
    private int age;
    private Teacher2 teacher;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Teacher2 getTeacher() {
        return teacher;
    }

    public void setTeacher(Teacher2 teacher) {
        this.teacher = teacher;
    }

    public Object clone() throws CloneNotSupportedException {
        // 浅复制时:
        // Object object = super.clone();
        // return object;
        // 改为深复制:
        Student3 student = (Student3) super.clone();
        // 本来是浅复制,现在将Teacher对象复制⼀份并重新set进来
        student.setTeacher((Teacher2) student.getTeacher().clone());
        return student;
    }
}

输出结果:

拷⻉后
edgar
18
riemann 
27 
修改⽼师的信息后—————— 
Games riemann

结果分析:

两个引用student1和student2指向不同的两个对象,两个引用student1和student2中的两个teacher引用 指向的是两个对象,但对teacher对象的修改只能影响student1对象,所以说是深拷贝。

Java 中操作字符串都有哪些类?它们之间有什么区别?

String 、StringBuffer 、StringBuilder。

String 和 StringBuffer 、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新  的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer 、StringBuilder 可以在原有对象的基 础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。

StringBuffer 和 StringBuilder 最大的区别在于,  StringBuffer 是线程安全的,而 StringBuilder 是非线程 安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多 线程环境下推荐使用 StringBuffer。

String str=”a”与 String str=new String(“a”)一样吗?

不一样,因为内存的分配方式不一样。

Stringstr=”a”; -> 常量池     String str=new String(“a”) -> 堆内存

抽象类能使用 final 修饰吗?

不能。定义抽象类就是让其他类继承的,而 final修饰的类不能被继承。

static关键字5连问

(1)抽象的(abstract)方法是否可同时是静态的(static)?

抽象方法将来是要被重写的,而静态方法是不能重写的,所以这个是错误的。

(2)是否可以从一个静态  (static)方法内部发出对非静态方法的调用?

不可以,静态方法只能访问静态成员,非静态方法的调用要先创建对象。

(3) static 可否用来修饰局部变量?

static 不允许用来修饰局部变量

(4)内部类与静态内部类的区别?

静态内部类相对与外部类是独立存在的,在静态内部类中无法直接访问外部类中变量、方法。如果   要 访问的话,必须要new一个外部类的对象,使用new出来的对象来访问。但是可以直接访问静态的变量、调用静态的方法;

普通内部类作为外部类一个成员而存在,在普通内部类中可以直接访问外部类属性,调用外部类的方 法。

如果外部类要访问内部类的属性或者调用内部类的方法,必须要创建一个内部类的对象,使用该对象访 问属性或者调用方法。

如果其他的类要访问普通内部类的属性或者调用普通内部类的方法,必须要在外部类中创建一个普通内 部类的对象作为一个属性,外同类可以通过该属性调用普通内部类的方法或者访问普通内部类的属性

如果其他的类要访问静态内部类的属性或者调用静态内部类的方法,直接创建一个静态内部类对象即可。

(5) Java中是否可以覆盖(override) 一个private或者是static的方法?

Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定 的。 static方法跟类的任何实例都不相关,所以概念上不适用。

重载(Overload)和重写(Override )的区别。重载的方法能否根据返回类型进行区分?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载:重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同 或者二者都不同)则视为重载;

重写:重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方 法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更 多的异常(里氏代换原则)。

重载对返回类型没有特殊的要求,不能根据返回类型进行区分。

Java的四种引用

1、强引用

最普遍的一种引用方式,如String s = “abc”,变量s就是字符串“abc”的强引用,只要强引用存在,则垃 圾回收器就不会回收这个对象。

2、软引用(SoftReference)

用于描述还有用但非必须的对象,如果内存足够,不回收,如果内存不足,则回收。  一般用于实现内存 敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中。

3、弱引用(WeakReference)

弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。 在垃圾回收器线程扫描它所管辖的内存区域的过程中,  一旦发现了只具有弱引用的对象,不管当前内存 空间足够与否,都会回收它的内存。

4、虚引用( PhantomReference )

就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引 用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被 垃圾回收器回收的活动。

虚引用与软引用和弱引用的一个区别在于:

虚引用必须和引用队列   (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

Java 中Comparator 与Comparable 有什么不同?

Comparable 接口用于定义对象的自然顺序,是排序接口,而 Comparator 通常用于定义用户定制的顺序,是比较接口。我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口),那么我们就可以建立一个“该类的比较器”来进行排序。

  Comparable总是只有一个,但是可以有多个Comparator  来定义对象的顺序。

Java 序列化,反序列化?

Java 序列化就是指将对象转换为字节序列的过程,反序列化是指将字节序列转换成目标对象的过程。

什么情况需要Java序列化?

当 Java 对象需要在网络上传输或者持久化存储到文件中时。

序列化的实现?

让类实现Serializable接口,标注该类对象是可被序列。

如果某些数据不想序列化,如何处理?

在字段面前加 transient 关键字,例如:

transient  private  String  phone;//不参与序列化

Java泛型和类型擦除?

  • 泛型即参数化类型,在创建集合时,指定集合元素的类型,此集合只能传入该类型的参数。
  • 类型擦除: java编译器生成的字节码不包含泛型信息,所以在编译时擦除。

查看更多

滚动至顶部