java-base-four

miku831 / 2023-07-29 / 原文

  1. JVM内存区域






     

    程序计数器
    程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

    为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。

    如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。

    程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。

    虚拟机栈(JVM Stacks)
    虚拟机栈线程私有,生命周期与线程相同。

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。
    栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

     

    局部变量表
    栈帧中,由一个局部变量表存储数据。
    局部变量表中存储了
    基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、
    和对象的引用(String、数组、对象等),但是不存储对象的内容。
    局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小

    局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。以下简称 Slot 。

    JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。

     

    操作数栈
    操作数栈是一个后进先出栈。操作数栈的元素可以是任意的Java数据类型。方法刚开始执行时,操作数栈是空的,在方法执行过程中,通过字节码指令对操作数栈进行压栈和出栈的操作。通常进行算数运算的时候是通过操作数栈来进行的,又或者是在调用其他方法的时候通过操作数栈进行参数传递。操作数栈可以理解为栈帧中用于计算的临时数据存储区

    Java堆
    堆是Java虚拟机所管理的内存中最大的一块存储区域。堆内存被所有线程共享
    主要存放使用new关键字创建的对象。所有对象实例以及数组都要在堆上分配。
    垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)。

    Java堆分为年轻代(Young Generation)和老年代(Old Generation);
    年轻代又分为伊甸园(Eden)和幸存区(Survivor区);
    幸存区又分为From Survivor空间和 To Survivor空间。

    年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。

    老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。

    方法区(Method Area)

    方法区同 Java 堆一样是被所有线程共享的区间,
    用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
    更具体的说,静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中。常量池是方法区的一部分

     

     


  2. 只有输出流才需要flush()刷新,输入流都没有flush()方法

    1. 为什么没有输入流,因为所有输入流都没flush()方法。
      flush()方法是定义在OutputStream和Writer这两个输出流的超类里面的方法,这是因为flush方法的作用和功能是向外做输出,将内存中的数据提交输出到外部文件当中,在BufferedWriter中,因为是对外做写操作,因此是有提交输出的flush方法的;而在BufferedReader对象中,因为它的作用是用来从外部读取文件内容到内存中,因此不存在向外做提交输出的操作,因此没有flush方法。

    所以以下的流均包含了flush()方法:
    FileOutputStream, 字节流输出流
    FileWriter, 字符输出流
    BufferedOutputStream, 缓冲字节输出流
    BufferedWriter, 缓冲字符输出流
    OutputStreamWriter, 转换输出流
    ObjectOutputStream 序列化输出流

     

    只有字节流输出流FileOutputStream和序列化输出流ObjectOutputStream不需要flush()就能直接写入文件
    FileWriter字符输出流,BufferedOutputStream缓冲字节输出流,BufferedWriter缓冲字符输出流,OutputStreamWriter转换输出流都需要刷新flush方法才能写到文件上。

    所以总结一下:如果是和Writer相关(内置缓冲区)或者是内置了缓冲数组的流都需要flush()这个过程才能写入文件

    FileOutPutStream继承outputStream,并不提供flush()方法的重写所以无论内容多少write都会将二进制流直接传递给底层操作系统的I/O,flush无效果而Buffered系列的输入输出流函数单从Buffered这个单词就可以看出他们是使用缓冲区的,应用程序每次IO都要和设备进行通信,效率很低,因此缓冲区为了提高效率,当写入设备时,先写入缓冲区,等到缓冲区有足够多的数据时,就整体写入设备

    使用BufferedXXXStream。默认缓冲区大小是8K。读的时候会一直填满缓冲区(或者文件读取完毕),写的时候也是等缓冲区满了之后(或者执行flush操作)才将内容送入内核缓冲区。效率高的原因就是避免了每读一个字节都要陷入操作系统内核(这是个耗时的操作)

     


        1. 不会初始化子类的几种情况
          1. 调用的是父类的static方法或者字段
          2.调用的是父类的final方法或者字段
          3. 通过数组来引用

          虚拟机规范严格规定了有且只有五种情况必须立即对类进行“初始化”:

          1.      使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候,已经调用一个类的静态方法的时候。

          2.      使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。

          3.      当初始化一个类的时候,如果发现其父类没有被初始化就会先初始化它的父类。

          4.      当虚拟机启动的时候,用户需要指定一个要执行的主类(就是包含main()方法的那个类),虚拟机会先初始化这个类;

          5.      使用Jdk1.7动态语言支持的时候的一些情况。

           

          除了这五种之外,其他的所有引用类的方式都不会触发初始化,称为被动引用。下面是被动引用的三个例子:

          1.      通过子类引用父类的的静态字段,不会导致子类初始化。

             2.      通过数组定义来引用类,不会触发此类的初始化。

          public class NotInitialization { 

              public static void main(String[] args) { 

                  SuperClass[] sca = new SuperClass[10]; 

              }   

          }

          3.      常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

          public class ConstClass { 

              static { 

                  System.out.println("ConstClass init!"); 

              } 

              public static final int value = 123; 

          public class NotInitialization{ 

              public static void main(String[] args) { 

                  int x = ConstClass.value; 

              } 

  3. Java类初始化的时机详解
    https://www.jianshu.com/p/3afa5d24bf71

  4. String.intern() 是一个 Native 方法,它的作用是:
    如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;
    如果没有,则在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用。



  5. 序列化的是对象,不是类,类变量不会被序列化
    序列化保存的是对象的状态,静态变量属于类的状态,因此,序列化并不保存静态变量。所以i是没有改变的
    Java在序列化时不会实例化static变量和transient修饰的变量,因为static代表类的成员,transient代表对象的临时数据,被声明这两种类型的数据成员不能被序列化
  6. wait()、notify()和notifyAll()是 Object类 中的方法

    从这三个方法的文字描述可以知道以下几点信息:

    1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。

    2)调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁)

    3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;

    4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程;
     
    上面已经提到,如果调用某个对象的wait()方法,当前线程必须拥有这个对象的monitor(即锁),

    因此调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。
    调用某个对象的wait()方法,相当于让当前线程交出此对象的monitor,然后进入等待状态,
    等待后续再次获得此对象的锁(Thread类中的sleep方法使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它并不释放对象锁);
    notify()方法能够唤醒一个正在等待该对象的monitor的线程,当有多个线程都在等待该对象的monitor的话,则只能唤醒其中一个线程,具体唤醒哪个线程则不得而知。同样地,调用某个对象的notify()方法,当前线程也必须拥有这个对象的monitor,因此调用notify()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。
    nofityAll()方法能够唤醒所有正在等待该对象的monitor的线程,这一点与notify()方法是不同的。
    Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,在阻塞队列那一篇博文中就讲述到了,阻塞队列实际上是使用了Condition来模拟线程间协作。
    • Condition是个接口,基本的方法就是await()和signal()方法;
    • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
    • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用Conditon中的await()对应Object的wait(); Condition中的signal()对应Object的notify(); Condition中的signalAll()对应Object的notifyAll()
     

  7. ceil:中文的意思是天花板,意思就是向上取整;返回的类型是:双精度浮点型double;

    例如:Math.ceil(11.0000001);其结果等于12.0;

    对于负数来说可以这样理解:取整:Math.ceil(-11.999999999);其结果等于-11.0;

    floor:中文的意思是地板,向下取整;返回的类型是:单精度浮点型float;

    例如:Math.floor(11.999999);其结果等于11.0;

    对于负数来说:Math.floor(-11.00001);其结果等于-12.0;

    round:意思是四舍五入;round返回的类型是长整型long;

    Math.round(-11.00033);其结果等于-11;

    Math.round(11.00033);其结果等于11;

    Math.round(-11.58888);其结果等于-12;

    Math.round(11.58888);其结果等于12;

  8. Java网络接口支持IP以上的所有协议
    java.net 包中提供了两种常见的网络协议的支持:TCP、UDP,TCP和UDP是同一层的,位于IP层上层

  9. ResultSet

    • ---- 表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。 
    • ResultSet 接口提供用于从当前行获取列值的获取 方法(getBoolean、getLong 等)。可以使用列的索引编号或列的名称获取值。一般情况下,使用列索引较为高效。列从 1 开始编号。为了获得最大的可移植性,应该按从左到右的顺序读取每行中的结果集列,每列只能读取一次

     

  10. Servlet的生命周期分为5个阶段:加载、创建、初始化、处理客户请求、卸载。
    (1)加载:容器通过类加载器使用servlet类对应的文件加载servlet
    (2)创建:通过调用servlet构造函数创建一个servlet对象
    (3)初始化:调用init方法初始化
    (4)处理客户请求:每当有一个客户请求,容器会创建一个线程来处理客户请求
    (5)卸载:调用destroy方法让servlet自己释放其占用的资源

  11. >> 右移 高位补符号位 算术右移
    >>> 右移 高位补0        逻辑右移
    算术右移不改变原数的符号,而逻辑右移不能保证这点。
  12. 四种引用类型
    JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态,这将无法描述某些特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。
    所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。
    一,强引用
    Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收 obj = null; //手动置null
    只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
    二,软引用
    软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
    在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
    三,弱引用
    弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
    四,虚引用
    虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
  13.  

     两个最基本的java回收算法:复制算法和标记清理算法

    复制算法:两个区域A和B,初始对象在A,继续存活的对象被转移到B。此为新生代最常用的算法
    标记清理:一块区域,标记可达对象(可达性分析),然后回收不可达对象,会出现碎片,那么引出
    标记-整理算法:多了碎片整理,整理出更大的内存放更大的对象
    两个概念:新生代和年老代
    新生代:初始对象,生命周期短的
    永久代:长时间存在的对象
    整个java的垃圾回收是新生代和年老代的协作,这种叫做分代回收。
    P.S:Serial New收集器是针对新生代的收集器,采用的是复制算法
    Parallel New(并行)收集器,新生代采用复制算法,老年代采用标记整理
    Parallel Scavenge(并行)收集器,针对新生代,采用复制收集算法
    Serial Old(串行)收集器,新生代采用复制,老年代采用标记整理
    Parallel Old(并行)收集器,针对老年代,标记整理
    CMS收集器,基于标记清理
    G1收集器:整体上是基于标记 整理 ,局部采用复制

    因为新生代里绝大部分都是垃圾对象,可以使用复制算法将小部分存活对象复制到另一个区域,然后留下来的都是垃圾对象,可以一网打尽,一下子清除掉。因为存活的对象少,所以“复制”的次数少;虽然留下来的垃圾对象多,但是可以一网打尽,所以“删除”的次数也少。就像你有一个文件夹,里面有 1000 张图片,其中只有 3 张是有用的,你要删除余下的 997 张垃圾图片,你会怎么删除呢?你肯定不会一张一张的去删除 997 张垃圾图片,这样需要删除 997 次,你显然认为把 3 张有用的图片复制出去,然后将整个文件夹干掉来的快。这也就是为什么新生代使用复制算法合适、使用标记清除算法不合适。

    因为老年代里都是些“老不死”的对象,假设你有一个文件夹,里面有 1000 张图片,其中 997 张是有用的,你会怎样删除这其中的 3 张垃圾图片呢?你肯定不会将 997 张有用的图片复制出去,然后将整个文件夹干掉,因为复制的代价太大了,耗时久,而且 997 张图片的位置都变了,反应在 java 对象上,就是 997 个 java 对象相互之间引用的地址都得换个遍。相反,你会挨个去删除 3 张垃圾图片,因为删除次数少,也不需要大量移动文件。所以老年代适合使用标记清除算法、不适合使用复制算法


    综上:新生代基本采用复制算法,老年代采用标记整理算法。cms采用标记清理。

  14. synchronized保证三大性,原子性,有序性,可见性
    volatile保证有序性,可见性,可禁止指令重排序,不能保证原子性

    为什么Synchronized无法禁止指令重排,却能保证有序性?

    为了进一步提升计算机各方面能力,在硬件层面做了很多优化,如处理器优化和指令重排等,但是这些技术的引入就会导致有序性问题。

     

    我们也知道,最好的解决有序性问题的办法,就是禁止处理器优化和指令重排,就像volatile中使用内存屏障一样。

     

    但是,虽然很多硬件都会为了优化做一些重排,但是在Java中,不管怎么排序,都不能影响单线程程序的执行结果。这就是as-if-serial语义,所有硬件优化的前提都是必须遵守as-if-serial语义

     

    as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会 干扰他们,也无需担心内存可见性问题。

     

    再说下synchronized,他是Java提供的锁,可以通过他对Java中的对象加锁,并且他是一种排他的、可重入的锁。

     

    所以,当某个线程执行到一段被synchronized修饰的代码之前,会先进行加锁,执行完之后再进行解锁。在加锁之后,解锁之前,其他线程是无法再次获得锁的,只有这条加锁线程可以重复获得该锁。

     

    synchronized通过排他锁的方式就保证了同一时间内,被synchronized修饰的代码是单线程执行的。所以呢,这就满足了as-if-serial语义的一个关键前提,那就是单线程,因为有as-if-serial语义保证,单线程的有序性就天然存在了


  15. Hashmap中的value可以为null,get(key)==null有两种情况,一是key不存在,二是该key中存的是null,所以应该使用map.containskey(key)返回的true/false来判断是否存在这个key。
  16. enum AccountType
    {
        SAVING, FIXED, CURRENT;
        private AccountType()
        {
            System.out.println("It is a account type");
        }
    }
    class EnumOne
    {
        public static void main(String[]args)
        {
            System.out.println(AccountType.FIXED);
        }
    }
     

     枚举类在后台实现时,实际上是转化为一个继承了java.lang.Enum类的实体类,原先的枚举类型变成对应的实体类型,上例中AccountType变成了个class AccountType,并且会生成一个新的构造函数,若原来有构造函数,则在此基础上添加两个参数,生成新的构造函数,如上例子中:

    private AccountType(){ System.out.println(“It is a account type”); } 
    会变成:
    private AccountType(String s, int i){
        super(s,i); System.out.println(“It is a account type”); }
    而在这个类中,会添加若干字段来代表具体的枚举类型:
    public static final AccountType SAVING;
    public static final AccountType FIXED;
    public static final AccountType CURRENT;

    而且还会添加一段static代码段:
    static{
        SAVING = new AccountType("SAVING", 0);
        ...  CURRENT = new AccountType("CURRENT", 0);
       $VALUES = new AccountType[]{
             SAVING, FIXED, CURRENT
        } }
    以此来初始化枚举中的每个具体类型。(并将所有具体类型放到一个$VALUE数组中,以便用序号访问具体类型)
    在初始化过程中new AccountType构造函数被调用了三次(因为有 SAVING, FIXED, CURRENT;三个枚举参数),所以Enum中定义的构造函数中的打印代码被执行了3遍。

  17.  java多态有两种情况:重载和重写
     在重写中,运用的是动态单分配,是根据new的类型确定对象,从而确定调用的方法;
     在重载中,运用的是静态多分派,即根据静态类型确定对象,因此不是根据new的类型确定调用的方法

    另外
    1.成员变量:编译和运行都参考左边。
    2.成员函数(非静态):编译看左边,运行看右边
    3.静态函数:编译和运行都看左边。

  18. Java一律采用Unicode编码方式,每个字符(char)无论中文还是英文字符都占用2个字节
  19. 关于抽象类
    JDK 1.8以前,抽象类的方法默认访问权限为protected
    JDK 1.8时,抽象类的方法默认访问权限变为default

    关于接口
    JDK 1.8以前,接口中的方法必须是public的
    JDK 1.8时,接口中的方法可以是public的,也可以是default的
    JDK 1.9时,接口中的方法可以是private的
  20. Java中的byte,short,char进行计算时都会提升为int类型。
  21. synchronized不是修饰变量的 它修饰方法或代码块或对象
  22. 方法调用时,会创建栈帧在栈中,调用完是程序自动出栈释放,而不是gc释放
  23.   intValue()是把Integer对象类型变成基本数据类型
    valueOf()是把String基本类型变成Integer对象类型
    parseInt()是把String变成Int基本类型
  24. 创建Statement是不传参的,PreparedStatement是需要传入sql语句
    java,exe是java虚拟机
    javadoc.exe用来制作java文档
    jdb.exe是java的调试器
    javaprof,exe是剖析工具



  25. A.Java系统提供3种类加载器:启动类加载器(Bootstrap ClassLoader)  扩展类加载器(Extension ClassLoader) 应用程序类加载器(Application ClassLoader). A正确

    B.《深入理解Java虚拟机》P228:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类必定不相等。接口类是一种特殊类,因此对于同一接口不同的类装载器装载所获得的类是不相同的。B错误

    C.类只需加载一次就行,因此要保证类加载过程线程安全,防止类加载多次。C正确

    D. Java程序的类加载器采用双亲委派模型,实现双亲委派的代码集中在java.lang.ClassLoader的loadClass()方法中,此方法实现的大致逻辑是:先检查是否已经被加载,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFoundException异常。D错误

    E.双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。E正确

    F.应用程序类加载器(Application ClassLoader)负责加载用户类路径(ClassPath)上所指定的类库,不是所有的ClassLoader都加载此路径。F错误


  26. 前台线程又叫用户线程,后台线程又叫守护线程
    守护线程(Daemon Thread)也被称之为后台线程或服务线程
    守护线程是为用户线程服务的,当程序中的用户线程全部执行结束之后,守护线程也会跟随结束。 
    守护线程的角色就像“服务员”,而用户线程的角色就像“顾客”,当“顾客”全部走了之后(全部执行结束),那“服务员”(守护线程)也就没有了存在的意义,
    所以当一个程序中的全部用户线程都结束执行之后,那么无论守护线程是否还在工作都会随着用户线程一块结束,整个程序也会随之结束运行。
    用户线程
            main默认为用户线程,使用Thread建立的线程默认情况下是用户线程

    用户线程与守护线程的区别
            用户线程在Java程序中非常重要,JVM一定会等到所有的用户线程执行完之后才会自然结束,而守护线程就不一样了,守护线程的存在是为了给用户线程服务的,所以当所有的用户线程都执行完毕之后,不管守护线程是否还在执行,JVM都会推出执行。


  27.  

    A.  semaphore:信号量。用于表示共享资源数量。用acquire()获取资源,用release()释放资源。
    B.  CyclicBarrier  线程到达屏障后等待,当一组线程都到达屏障后才一起恢复执行
    C. CountDownLatch  初始时给定一个值,每次调用countDown值减1,当值为0时阻塞的线程恢复执行



    http://www.manongjc.com/detail/41-pdrwxufmtwhidbf.html

     


  28. 1.使用new创建对象会自动调用构造方法
       Person person=new Person();
    2.使用Class类的newInstance方法会调用构造方法
       Person person=(Person) Class.forName("com.Person").newInstance();
    3.用反射中的constructor类 的newInstance 调用构造方法
    Constructor  constructor=Person.class.getConstructor ();
    Person person=constructor.newInstance();

    4. 在类的其他构造方法中可以用this()的方式调用其他构造方法;
    5. 在类的子类中则可以通过super调用父类中指定的构造方法
    2.3均属于反射机制
    上面三中创建对象的时候回自动调用构造方法,
    但不是所有的创建对象的时候会调用,比如clone方法(对象的复制),
    序列化的时候也会创建对象,这是有jvm创建的,所以也不会调用

  29. 常见代码优化技术有:

    复写传播,删除死代码, 强度削弱,归纳变量删除
  30.  



     Ant 没有生命周期,你必须定义目标和目标之间的依赖。你必须手工为每个目标附上一个任务序列。

    Ant的作用:是一种基于Java的build工具

    1. 能够用ant编译java类。生成class文件
    2. ant能够自己定义标签、配置文件,用于构建。
    3. ant能够把相关层构建成jar包 。
    4. ant把整个项目生成web包。并公布到Tomcat  

    Ant的长处:

    1. 跨平台性:Ant是纯Java语言编写的,因此具有非常好的跨平台性。
    2. 操作简单:Ant是由一个内置任务和可选任务组成的。Ant执行时须要一个XML文件(构建文件)。
    1. Ant通过调用target树,就能够运行各种task:每一个task实现了特定接口对象。因为Ant构建文件时XML格式的文件。所以非常easy维护和书写,并且结构非常清晰。
    2. Ant能够集成到开发环境中:因为Ant的跨平台性和操作简单的特点。它非常easy集成到一些开发环境中去。 

    Maven的作用: 除了以程序构建能力为特色之外,还提供高级项目管理工具。 

    Maven除了具备Ant的功能外。还添加了下面基本的功能:

    1. 使用Project Object Model来对软件项目管理。
    2. 内置了很多其它的隐式规则,使得构建文件更加简单。
    3. 内置依赖管理和Repository来实现依赖的管理和统一存储;
    4. 内置了软件构建的生命周期;

    Maven的长处:

    1. 拥有约定,知道你的代码在哪里,放到哪里去
    2. 拥有一个生命周期,比如运行 mvn install就能够自己主动运行编译,測试。打包等构建过程
    3. 仅仅须要定义一个pom.xml,然后把源代码放到默认的文件夹,Maven帮你处理其它事情
    4. 拥有依赖管理。仓库管理 

    总体的比較:

           Ant将提供了非常多能够重用的task,比如 copy, move, delete以及junit单元測试Maven则提供了非常多能够重用的过程

  31. Java的赋值运算符=有返回值的,为所赋值,赋了什么值,就返回什么值

     

  32.  

     



     A选项使用了jsp中动作标签的包含标签,这里是动态包含。原理是包含与被包含的页面单独翻译成不同的java文件,然后运行时合并在一起。因为是存在域中的数据,故刚开始就直接翻译数据还不存在,因此浏览器上不能显示出HAHA。

    B选项使用了jsp三大指令中的包含指令,这里是静态包含。原理是直接把包含与被包含页面的内容先合并在一起,然后翻译成一个java源文件,最后编译执行。故可以在浏览器上显示出HAHA。

    C和D选项分别使用了跳转和重定向,我们知道jsp中有四个域对象,从小到大分别为:
    • page域:在同一个jsp页面中数据有效
    • request域:在同一个请求中数据有效
    • session域:在用一个会话中数据有效
    • application域:在同一个网站中数据有效
    题中使用的是page域对象:pageContext,而C选项应该使用request域对象:HttpServletRequest,而D选项应该至少使用session域对象:HttpSession(如果处于同一会话中)。

    AC选项都是报此错误,说明不可以这么使用.
    图片说明
    A选项正确用法
    图片说明
    接收参数得改成这样:
    图片说明
    C选项正确用法
    图片说明
    接收参数也得改成这样:
    图片说明
    D选项,response.sendredirect(url); 对服务器的响应进行重定向。当server作出响应后,client客户端的请求的生存周期就终止了。
    这个时候再用request.getparameter()或request.getattribute()得到的只能是null。
    图片说明
    B选项,正确显示
    图片说明

  33. A DriverManager.getConnection方法返回一个Connection对象,这是加载驱动之后才能进行的

    加载驱动方法
    1.Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
    2. DriverManager.registerDriver(new com.mysql.jdbc.Driver());
    3.System.setProperty("jdbc.drivers", "com.mysql.jdbc.Driver");

  34. [://]
    
    这是个很明显的陷阱
    在正则表达式中,[]表示其中的元素任意一个,但是不允许重复,所以等价于[:/]
    +号表示可以匹配一次或多次前面的原子
    如果[ ]后面没有带+号的话,是不会贪婪匹配的,就只能匹配到://中的其中一个 
  35.  

    1、jps:查看本机java进程信息。

    2、jstack:打印线程的信息,制作线程dump文件。

    3、jmap:打印内存映射,制作dump文件

    4、jstat:性能监控工具

    5、jhat:内存分析工具

    6、jconsole:简易的可视化控制台

    7、jvisualvm:功能强大的控制台


  36. A、标准ASCII只使用7个bit,扩展的ASCII使用8个bit。

    B、ANSI通常使用 0x00~0x7f 范围的1 个字节来表示 1 个英文字符。超出此范围的使用0x80~0xFFFF来编码,即扩展的ASCII编码。不同 ANSI 编码之间互不兼容。
    在简体中文Windows操作系统中,ANSI 编码代表 GBK 编码;在繁体中文Windows操作系统中,ANSI编码代表Big5;在日文Windows操作系统中,ANSI 编码代表 Shift_JIS 编码。
    C、ANSI通常使用 0x00~0x7f 范围的1 个字节来表示 1 个英文字符,即ASCII码
    D、ASCII码包含一些特殊空字符

  37. Arrays.asList方法返回 java.util.Arrays.ArrayList对象

     




  38. off-heap叫做堆外内存,将你的对象从堆中脱离出来序列化,然后存储在一大块内存中,这就像它存储到磁盘上一样,但它仍然在RAM中。
    对象在这种状态下不能直接使用,它们必须首先反序列化,也不受垃圾收集。序列化和反序列化将会影响部分性能(所以可以考虑使用FST-serialization)使用堆外内存能够降低GC导致的暂停。
    堆外内存不受垃圾收集器管理,也不属于老年代,新生代。


  39.  




    22 34 17 

    思考和解决这个题的主要核心在于对java多态的理解。个人理解时,执行对象实例化过程中遵循多态特性 ==>
    调用的方法都是将要实例化的子类中的重写方法,只有明确调用了super.xxx关键词或者是子类中没有该方法时,才会去调用父类相同的同名方法。

    Step 1:  new B()构造一个B类的实例

    此时super(5)语句调用显示调用父类A带参的构造函数,该构造函数调用setValue(v),
    这里有两个注意点一是虽然构造函数是A类的构造函数,但此刻正在初始化的对象是B的一个实例,因此这里调用的实际是B类的setValue方法
    于是调用B类中的setValue方法 ==> 而B类中setValue方法显示调用父类的setValue方法,将B实例的value值设置为2 x 5 = 10
    紧接着,B类的构造函数还没执行完成,继续执行setValue(getValue()- 3) // 备注1语句。

    先执行getValue方法,B类中没有重写getValue方法,因此调用父类A的getValue方法。这个方法比较复杂,需要分步说清楚:

    1. 调用getValue方法之前,B的成员变量value值为10。
    2. value++ 执行后, B的成员变量value值为11,此时开始执行到return语句,将11这个值作为getValue方法的返回值返回出去
    3. 但是由于getValue块被try finally块包围,因此finally中的语句无论如何都将被执行,所以步骤2中11这个返回值会先暂存起来,到finally语句块执行完毕后再真正返回出去。
    4. 这里有很重要的一点:finally语句块中 this.setValue(value)方法调用的是B类的setValue方法。为什么?
      因为此刻正在初始化的是B类的一个对象(运行时多态),就像最开始第一步提到的一样(而且这里用了使用了this关键词显式指明了调用当前对象的方法)。因此,此处会再次调用B类的setValue方法,同上,super.关键词显式调用A的setValue方法,将B的value值设置成为了2 * 11 = 22
    5. 因此第一个打印项为22。  
    6. finally语句执行完毕 会把刚刚暂存起来的11 返回出去,也就是说这么经历了这么一长串的处理,getValue方法最终的返回值是11。

    回到前面标注了 //备注1 的代码语句,其最终结果为setValue(11-3)=>setValue(8)
    而大家肯定也知道,这里执行的setValue方法,将会是B的setValue方法。 之后B的value值再次变成了2*8 = 16;

    Step2:  new B().getValue()

    B类中没有独有的getValue方法,此处调用A的getValue方法。同Step 1,

    1. 调用getValue方法之前,B的成员变量value值为16
    2. value++ 执行后, B的成员变量value值为17,此时执行到return语句,会将17这个值作为getValue方法的返回值返回出去
    3. 但是由于getValue块被try finally块包围而finally中的语句无论如何都一定会被执行,所以步骤2中17这个返回值会先暂存起来,到finally语句块执行完毕后再真正返回出去。
    4. finally语句块中继续和上面说的一样: this.setValue(value)方法调用的是B类的setValue()方法将B的value值设置成为了2 * 17 = 34
    5. 因此第二个打印项为34。
    6. finally语句执行完毕 会把刚刚暂存起来的17返回出去。
    7. 因此new B().getValue()最终的返回值是17.

    Step3:  main函数中的System.out.println

    将刚刚返回的值打印出来,也就是第三个打印项:17

    最终结果为 22 34 17。 

  40. replace和replaceAll是Java中常用的替换字符的方法,它们的区别是:

    1)replace的参数是char和CharSequence,即可以支持字符的替换,也支持字符串的替换(CharSequence即字符串序列的意思,说白了也就是字符串);

    2)replaceAll的参数是regex,即基于正则表达式的替换,比如,可以通过replaceAll("\\d", "*")把一个字符串所有的数字字符都换成星号;

     
  41. HttpServlet容器响应Web客户请求流程如下

    1)Web客户向Servlet容器发出Http请求;

    2)Servlet容器解析Web客户的Http请求;

    3)Servlet容器创建一个HttpRequest对象,在这个对象中封装Http请求信息;

    4)Servlet容器创建一个HttpResponse对象;

    5)Servlet容器调用HttpServlet的service方法,这个方法中会根据request的Method来判断具体是执行doGet还是doPost,把HttpRequest和HttpResponse对象作为service方法的参数传给HttpServlet对象;

    6)HttpServlet调用HttpRequest的有关方法,获取HTTP请求信息;

    7)HttpServlet调用HttpResponse的有关方法,生成响应数据;

    8)Servlet容器把HttpServlet的响应结果传给Web客户。



  42. A.
    子进程得到的是除了代码段是与父进程共享以外,其他所有的都是得到父进程的一个副本,子进程的所有资源都继承父进程,得到父进程资源的副本,子进程可获得父进程的所有堆和栈的数据,但二者并不共享地址空间。
    两个是单独的进程,继承了以后二者就没有什么关联了,子进程单独运行;
    进程的线程之间共享由进程获得的资源,但线程拥有属于自己的一小部分资源,就是栈空间,保存其运行状态和局部自动变量的。

    B.线程之间共享进程获得的数据资源,所以开销小,但不利于资源的管理和保护;而进程执行开销大,但是能够很好的进行资源管理和保护。

    C.线程的通信速度更快,切换更快,因为他们共享同一进程的地址空间。

    D.一个进程可以有多个线程,线程是进程的一个实体,是CPU调度的基本单位。
  43. 事务属性

    事务属性的种类:传播行为、隔离级别、只读和事务超时

     

    a)   传播行为定义了被调用方法的事务边界。

     

    传播行为

    意义

    PROPERGATION_MANDATORY

    表示方法必须运行在一个事务中,如果当前事务不存在,就抛出异常

    PROPAGATION_NESTED

    表示如果当前事务存在,则方法应该运行在一个嵌套事务中。否则,它看起来和 PROPAGATION_REQUIRED 看起来没什么俩样

    PROPAGATION_NEVER

    表示方法不能运行在一个事务中,否则抛出异常

    PROPAGATION_NOT_SUPPORTED

    表示方法不能运行在一个事务中,如果当前存在一个事务,则该方法将被挂起

    PROPAGATION_REQUIRED

    表示当前方法必须运行在一个事务中,如果当前存在一个事务,那么该方法运行在这个事务中,否则,将创建一个新的事务

    PROPAGATION_REQUIRES_NEW

    表示当前方法必须运行在自己的事务中,如果当前存在一个事务,那么这个事务将在该方法运行期间被挂起

    PROPAGATION_SUPPORTS

    表示当前方法不需要运行在一个是事务中,但如果有一个事务已经存在,该方法也可以运行在这个事务中

     

     

     

     

    b)   隔离级别

    在操作数据时可能带来 3 个副作用,分别是脏读、不可重复读、幻读。
    为了避免这 3
    中副作用的发生,在标准的 SQL 语句中定义了 4 种隔离级别,分别是未提交读、已提交读、可重复读、可序列化。而在 spring 事务中提供了 5 种隔离级别来对应在 SQL 中定义的 4 种隔离级别,如下:

    隔离级别

    意义

    ISOLATION_DEFAULT

    使用后端数据库默认的隔离级别

    ISOLATION_READ_UNCOMMITTED

    允许读取未提交的数据(对应未提交读),可能导致脏读、不可重复读、幻读

    ISOLATION_READ_COMMITTED

    允许在一个事务中读取另一个已经提交的事务中的数据(对应已提交读)。可以避免脏读,但是无法避免不可重复读和幻读

    ISOLATION_REPEATABLE_READ

    一个事务不可能更新由另一个事务修改但尚未提交(回滚)的数据(对应可重复读)。可以避免脏读和不可重复读,但无法避免幻读

    ISOLATION_SERIALIZABLE

    这种隔离级别是所有的事务都在一个执行队列中,依次顺序执行,而不是并行(对应可序列化)。可以避免脏读、不可重复读、幻读。但是这种隔离级别效率很低,因此,除非必须,否则不建议使用。

     

     

     

     

    c)    只读

    如果在一个事务中所有关于数据库的操作都是只读的,也就是说,这些操作只读取数据库中的数据,而并不更新数据,那么应将事务设为只读模式( READ_ONLY_MARKER ) , 这样更有利于数据库进行优化

    因为只读的优化措施是事务启动后由数据库实施的,因此,只有将那些具有可能启动新事务的传播行为 (PROPAGATION_NESTED 、 PROPAGATION_REQUIRED 、 PROPAGATION_REQUIRED_NEW) 的方法的事务标记成只读才有意义。

    如果使用 Hibernate 作为持久化机制,那么将事务标记为只读后,会将 Hibernate 的 flush 模式设置为 FULSH_NEVER, 以告诉 Hibernate 避免和数据库之间进行不必要的同步,并将所有更新延迟到事务结束。

    d)   事务超时

    如果一个事务长时间运行,这时为了尽量避免浪费系统资源,应为这个事务设置一个有效时间,使其等待数秒后自动回滚。与设

    置“只读”属性一样,事务有效属性也需要给那些具有可能启动新事物的传播行为的方法的事务标记成只读才有意义。

  44. 解决Hash冲突四种方法

    1、开放定址法:我们在遇到哈希冲突时,去寻找一个新的空闲的哈希地址。

    (1)线性探测法

    (2)平方探测法(二次探测)

    2、再哈希法:同时构造多个不同的哈希函数,等发生哈希冲突时就使用第二个、第三个……等其他的哈希函数计算地址,直到不发生冲突为止。虽然不易发生聚集,但是增加了计算时间。

    3、链地址法:将所有哈希地址相同的记录都链接在同一链表中。

    4、建立公共溢出区:将哈希表分为基本表和溢出表,将发生冲突的都存放在溢出表中。

    HashMap采用了链地址法,ThreadLocalMap则是开放地址法。

    开放定址法(Open Addressing)
    一旦产生了冲突(该地址已有其它元素),就按某种规则去寻找另一空地址

    若发生了第 i 次冲突,试探的下一个地址将增加di,基本公式是:
    hi(key) = (h(key)+di) mod TableSize ( 1≤ i < TableSize )
    di 决定了不同的解决冲突方案:线性探测、平方探测、双散列。
    线性探测:di = i
    平方探测:di = ± i2( +12, -12, +22, -22……)
    双散列:di = i * h2(key)

    链地址法:将所有哈希地址相同的记录都链接在同一链表中。

     


  45. 对于D
    服务端accept后才创建Socket对象

    ServerSocket ss=new ServerSocket(3000);
    Socket s=ss.accept();//当有客户端连接时才创建Socket对象,而不是new ServerSocket时创建

    创建是在accept()中实现的

        public Socket accept() throws IOException {
            if (isClosed())
                throw new SocketException("Socket is closed");
            if (!isBound())
                throw new SocketException("Socket is not bound yet");
            Socket s = new Socket((SocketImpl) null);
            implAccept(s);
            return s;
        }
    另外,创建ServerSocket有两种方式,第一种无参构造函数是未绑定的,还需要进一步绑定端口才能accept.
    ```
    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress(8081));
        ServerSocket serverSocket1 = new ServerSocket(8081);
        serverSocket.accept();
    } 
    ```

     



  46. 先分析一下里面各个参数的含义: 
    -Xms:1G , 就是说初始堆大小为1G 
    -Xmx:2G , 就是说最大堆大小为2G 
    -Xmn:500M ,就是说年轻代大小是500M(包括一个Eden和两个Survivor) 
    -XX:MaxPermSize:64M , 就是说设置持久代最大值为64M 
    -XX:+UseConcMarkSweepGC , 就是说使用使用CMS内存收集算法 
    -XX:SurvivorRatio=3 , 就是说Eden区与Survivor区的大小比值为3:1:1
    题目中所问的Eden区的大小是指年轻代的大小,直接根据-Xmn:500M和-XX:SurvivorRatio=3可以直接计算得出
    500M*(3/(3+1+1)) 
    =500M*(3/5) 
    =500M*0.6 
    =300M  
    所以Eden区域的大小为300M。

    Xms 起始内存

    Xmx 最大内存

    Xmn 新生代内存

    Xss 栈大小。 就是创建线程后,分配给每一个线程的内存大小

    -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

    -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

    -XX:MaxPermSize=n:设置持久代大小

    收集器设置
    -XX:+UseSerialGC:设置串行收集器
    -XX:+UseParallelGC:设置并行收集器
    -XX:+UseParalledlOldGC:设置并行年老代收集器
    -XX:+UseConcMarkSweepGC:设置并发收集器
    垃圾回收统计信息
    -XX:+PrintGC
    -XX:+PrintGCDetails
    -XX:+PrintGCTimeStamps
    -Xloggc:filename
    并行收集器设置
    -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
    -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
    -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
    并发收集器设置
    -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
    -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数

  47. super和this都只能位于构造器的第一行,而且不能同时使用,这是因为会造成初始化两次。
    this用于调用重载的构造器,super用于调用父类被子类重写的方法

  48. 1.Log4j在运行期间不可重置;
    2.日志级别ERROR>WARN>INFO>DEBUG 只建议使用这个四个级别
    日志级别分为 OFF>FATAL>ERROR>WARN>INFO>DEBUG>TRACE>ALL


  49.  

    三元操作符如果遇到可以转换为数字的类型,会做自动类型提升
  50.  

    HttpServletResponse(接口)提供如下方法声明来设置响应头的数据

    addHeader:添加一个新的请求头字段。(一个请求头中允许有重名字段。)

    setHeader:设置一个请求头字段,有则覆盖,无则添加。

     

     

  51. final 定义的变量,可以在不是必须要在定义的同时完成初始化,也可以在构造方法或代码块中完成初始化

  52. 首先,^表示匹配输入的开始$表示匹配输入的结束

    每个选项从前向后看,http都能够严格匹配
    ?表示匹配某元素0次或1次,这里四个选项都没有问题,能够匹配0次或1次字符s
    接下来:严格匹配,\/\/严格匹配两个//
    接着往下看,[]表示字符集合,它用在正则表达式中表示匹配集合中的任一字符
    A D 选项中的 [a-zA-Z\d] 表示匹配一个小写字母 或者 大写字母 或者 数字
    B C 选项中的 \w 表示匹配字母数字或下划线(注意这里比A D中能多匹配下划线类型)
    +表示匹配某元素1次或多次,到这里四个选项都能够完美匹配字符www
    .可以匹配除了换行符\n \r外的任何字符
    接下来我们看选项A,bilibili com video av都严格匹配,而 \D 表示匹配一个非数字字符而非数字字符,av后的数字是无法匹配成功的,A错误
    B选项,\d匹配数字,{m,n}表示最少匹配m次,最多匹配n次,\/?能匹配末尾的0个或1个/字符,B正确
    C选项,*表示匹配某元素0次或多次,但 \w 并不能匹配字符 /,C错误
    D选项,前面都对,错在最后的\/+至少要匹配一个/,而原字符串最后并没有/

     



  53. doGet/doPost与Http协议有关,是在 javax.servlet.http.HttpServlet 中实现的
    GenericServlet 抽象类 给出了设计 servlet 的一些骨架,定义了 servlet 生命周期,还有一些得到名字、配置、初始化参数的方法,其设计的是和应用层协议无关的

     

     




  54. ServerSocket(int port) 是服务端绑定port端口,调用accept()监听等待客户端连接,它返回一个连接队列中的一个socket。

    Socket(InetAddress address , int port) 是创建客户端连接主机的socket流,其中InetAddress是用来记录主机的类,port指定端口。
    socket和servletSocket的交互如下图所示

     

     

只有输出流才需要flush()刷新,输入流都没有flush()方法

  1. 为什么没有输入流,因为所有输入流都没flush()方法。
    flush()方法是定义在OutputStream和Writer这两个输出流的超类里面的方法,这是因为flush方法的作用和功能是向外做输出,将内存中的数据提交输出到外部文件当中,在BufferedWriter中,因为是对外做写操作,因此是有提交输出的flush方法的;而在BufferedReader对象中,因为它的作用是用来从外部读取文件内容到内存中,因此不存在向外做提交输出的操作,因此没有flush方法。

所以以下的流均包含了flush()方法:
FileOutputStream, 字节流输出流
FileWriter, 字符输出流
BufferedOutputStream, 缓冲字节输出流
BufferedWriter, 缓冲字符输出流
OutputStreamWriter, 转换输出流
ObjectOutputStream 序列化输出流

wait()、notify()和notifyAll()是 Object类 中的方法

从这三个方法的文字描述可以知道以下几点信息:

1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。


2)调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁)

3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;

4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程;

有朋友可能会有疑问:为何这三个不是Thread类声明中的方法,而是Object类中声明的方法

(当然由于Thread类继承了Object类,所以Thread也可以调用者三个方法)?其实这个问

题很简单,由于每个对象都拥有monitor(即锁),所以让当前线程等待某个对象的锁,当然

应该通过这个对象来操作了。而不是用当前线程来操作,因为当前线程可能会等待多个线程

的锁,如果通过线程来操作,就非常复杂了。

上面已经提到,如果调用某个对象的wait()方法,当前线程必须拥有这个对象的monitor(即

锁),因此调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者

synchronized方法)。

调用某个对象的wait()方法,相当于让当前线程交出此对象的monitor,然后进入等待状态,

等待后续再次获得此对象的锁(Thread类中的sleep方法使当前线程暂停执行一段时间,从

而让其他线程有机会继续执行,但它并不释放对象锁);

notify()方法能够唤醒一个正在等待该对象的monitor的线程,当有多个线程都在等待该对象

的monitor的话,则只能唤醒其中一个线程,具体唤醒哪个线程则不得而知。

同样地,调用某个对象的notify()方法,当前线程也必须拥有这个对象的monitor,因此调用

notify()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。

nofityAll()方法能够唤醒所有正在等待该对象的monitor的线程,这一点与notify()方法是不同的。
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,在阻塞队列那一篇博文中就讲述到了,阻塞队列实际上是使用了Condition来模拟线程间协作。
  • Condition是个接口,基本的方法就是await()和signal()方法;
  • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
  • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用Conditon中的await()对应Object的wait(); Condition中的signal()对应Object的notify(); Condition中的signalAll()对应Object的notifyAll()


作者:zhisheng_blog
链接:https://www.nowcoder.com/exam/test/71372156/submission?examPageSource=Intelligent&pid=51273070&testCallback=https%3A%2F%2Fwww.nowcoder.com%2Fexam%2Fintelligent%3FquestionJobId%3D10
来源:牛客网