地址

Java 核心技术面试精讲

第1讲 谈谈你对Java平台的理解

1. Java是解释执行还是编译执行?

我们开发的 Java 的源代码,首先通过 Javac 编译成为字节码(bytecode),然后,在运行时,通过 Java 虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码,这种情况属于编译执行。但是常见的 JVM,比如我们大多数情况使用的 Oracle JDK 提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)编译器,也就是通常所说的动态编译器,JIT 能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于编译执行,而不是解释执行了。

第2讲 Exception和Error有什么区别?

  1. Exception 和 Error 都是继承了Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型
  2. Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
  3. Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
  4. Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误。
  5. NoClassDefFoundError 和 ClassNotFoundException 的区别:前者实现项目编译时没有找到class, 后者是在代码运行的时候没有找到class,比如调用class.forName(“xxx”)。
  6. try-with-resource: JDK7之后Java新增语法,是一个声明一个或多个资源对象的 try语句,其中每个对象所在类需要实现AutoCloseable接口,在语句执行完毕后,每个资源都被自动关闭。
public class MyAutoClosable implements AutoCloseable {public void doIt() {System.out.println("MyAutoClosable doing it!");}@Overridepublic void close() throws Exception {System.out.println("MyAutoClosable closed!");}public static void main(String[] args) {try(MyAutoClosable myAutoClosable = new MyAutoClosable()){myAutoClosable.doIt();} catch (Exception e) {e.printStackTrace();}}
}
  1. 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。

第3讲 谈谈final、finally、 finalize有什么不同?

  1. final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。
  2. finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。
  3. finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,因为我们无法保证 finalize 什么时候执行,执行的是否符合预期。使用不当会影响性能,导致程序死锁、挂起等,并且在 JDK 9 开始被标记为 deprecated。
  4. System.exit,退出虚拟机,无论状态码为多少,都是不会执行finally的。
import java.util.Objects;public class MyAutoClosable implements AutoCloseable {@Overridepublic void close() throws Exception {System.out.println("MyAutoClosable closed!"); // 不会打印}public static void main(String[] args) {Objects.requireNonNull(args);try(MyAutoClosable myAutoClosable = new MyAutoClosable()){//            return ;System.exit(0);} catch (Exception e) {e.printStackTrace();} finally {System.out.println("finally"); // 不会打印}}
}
  1. final只能约束对象的引用不被改变。

第4讲 强引用、软引用、弱引用、幻象引用有什么区别?

  1. 强引用:就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。
  2. 软引用:相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
  3. 弱引用:并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。
  4. 虚引用:也叫幻象引用,不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。

第5讲 String、StringBuffer、StringBuilder有什么区别?

  1. String: 典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。
  2. StringBuffer: 为解决上面提到拼接产生太多中间对象的问题而提供的一个类,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销。
  3. StringBuilder: Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。
  4. String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。但在JDK6中,并不推荐大量使用 intern,因为被缓存的字符串是存储在永久代中,只会被FULLGC收集。在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题。
  5. JDK9之前String是基于char[]实现,每一个char占用两个字节;JDK9之后,String变为一个 byte 数组加上一个标识编码的所谓 coder实现。

第7讲 int和Integer有什么区别?

  1. int是基本数据类型,Integer是引用数据类型。在JDK5中,Interger新增了静态工厂方法 valueOf,在调用它的时候如果传入的值在-128到127之间,会利用一个缓存机制返回对应的Interger对象。

第8讲 对比Vector、ArrayList、LinkedList有何区别?

  1. Vector: Java 早期提供的线程安全的动态数组,内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。
  2. ArrayList: 应用更加广泛的动态数组实现,它本身不是线程安全的,也是可以根据需要调整容量,不过与Vector的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。
  3. LinkedList: Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。

第9讲 对比HashTable、HashMap、TreeMap有什么不同?

  1. HashTable: 早期 Java 类库提供的一个哈希表实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用。
  2. HashMap: HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能。
  3. TreeMap: 基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断。
  4. LinkedHashMap: 通常提供的是遍历顺序符合插入顺序,它的实现是通过为条目(键值对)维护一个双向链表。注意,通过特定构造函数,我们可以创建反映访问顺序的实例,所谓的 put、get、compute 等,都算作“访问”。
  5. HashMap 内部的结构:它可以看作是数组(Node[] table)和链表结合组成的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组的寻址;哈希值相同的键值对,则以链表形式存储。
  6. 为什么 HashMap 要树化呢?本质上这是个安全问题。因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,则会形成一个链表,而链表查询是线性的,会严重影响存取的性能。而在现实世界,构造哈希冲突的数据并不是非常复杂的事情,恶意代码就可以利用这些数据大量与服务器端交互,导致服务器端 CPU 大量占用(查询缓慢),这就构成了哈希碰撞拒绝服务攻击。

第10讲 如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?

  1. 我们可以调用 Collections 工具类提供的包装方法,来获取一个同步的包装容器(如 Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。
  2. 更加普遍的选择是利用并发包提供的线程安全容器类:
    a. 各种并发容器,比如 ConcurrentHashMap、CopyOnWriteArrayList。
    b. 各种线程安全队列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。
    c. 各种有序容器的线程安全版本等。
  3. 在 Java 8 和之后的版本中,ConcurrentHashMap 发生了哪些变化呢?
    a. 总体结构上,它的内部存储变得和HashMap 结构非常相似,同样是大的桶(bucket)数组,然后内部也是一个个所谓的链表结构(bin),同步的粒度要更细致一些。
    b. 其内部仍然有 Segment 定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构上的用处。因为不再使用 Segment,初始化操作大大简化,修改为 lazy-load 形式,这样可以有效避免初始开销。
    c. 数据存储利用 volatile 来保证可见性。
    d. 使用 CAS 等操作,在特定场景进行无锁并发操作。
    e. 使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。
  4. JDK8中,ConcurrentHashMap使用syncronized而不是ReentrantLock进行同步,为什么?因为现代 JDK 中,synchronized 已经被不断优化,可以不再过分担心性能差异,另外,相比于 ReentrantLock,它可以减少内存消耗。

第11讲 Java提供了哪些IO方式? NIO如何实现多路复用?

  1. BIO: 传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
  2. NIO: 在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 ==Channel、Selector、Buffer ==等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。
  3. == NIO2(AIO): 在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。==
  4. 同步/异步与阻塞/非阻塞的区别:同步/异步说明的对象是线程之间的协作关系,线程B必须等待线程A完成才开始执行,则线程A和B是同步的关系;阻塞/非阻塞说明的是某一线程内部的关系,比如线程A此时正在执行读取文件操作,线程A无法再做其他事情,只能等待读取结束,此时就称线程A处于阻塞状态。
  5. Reader/Writer 是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader/Writer 相当于构建了应用逻辑和原始数据之间的桥梁。

第12讲 Java有几种文件拷贝方式?哪一种最高效?

  1. 利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个 FileOutputStream,完成写入工作。
public static void copyFileByStream(File source, File dest) throws IOException {try (InputStream is = new FileInputStream(source); OutputStream os = new FileOutputStream(dest)) {byte[] buffer = new byte[1024];int length;while ((length = is.read(buffer)) > 0) {os.write(buffer, 0, length);}}}
  1. 利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现。
public static void copyFileByChannel(File source, File dest) throwsIOException {try (FileChannel sourceChannel = new FileInputStream(source).getChannel();FileChannel targetChannel = new FileOutputStream(dest).getChannel()) {for (long count = sourceChannel.size(); count > 0; ) {long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel);sourceChannel.position(sourceChannel.position() + transferred);count -= transferred;}}}
  1. NIO transferTo/From 的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换。
  2. 当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。

    5.== 而基于 NIO transferTo 的实现方式,在 Linux 和 Unix 上,则会使用到零拷贝技术,数据传输并不需要用户态参与==,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。注意,transferTo 不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行 Socket 发送,同样可以享受这种机制带来的性能和扩展性提高。
  3. 如何提高类似拷贝等 IO 操作的性能,有一些宽泛的原则:
    a. 在程序中,使用缓存等机制,合理减少 IO 次数(在网络通信中,如 TCP 传输,window 大小也可以看作是类似思路)。
    b. 使用 transferTo 等机制,减少上下文切换和额外 IO 操作。
    c. 尽量减少不必要的转换过程,比如编解码;对象序列化和反序列化,比如操作文本文件或者网络通信,如果不是过程中需要使用文本信息,可以考虑不要将二进制信息转换成字符串,直接传输二进制信息。

第13讲 谈谈接口和抽象类有什么区别?

  1. 接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java 标准类库中,定义了非常多的接口,比如 java.util.List。
  2. 抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。Java 标准库中,比如 collection 框架,很多通用部分就被抽取成为抽象类,例如 java.util.AbstractList。
  3. 从 Java 8 开始,interface 增加了对 default method 的支持。Java 9 以后,可以定义 private default method。Default method 提供了一种二进制兼容的扩展已有接口的办法。
  4. 基本的设计原则:S.O.L.I.D 原则。
    a. 单一职责(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。
    b. 开闭原则(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题。
    c. 里氏替换(Liskov Substitution),这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换。
    d. 接口分离(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响。
    e. 依赖倒置(Dependency Inversion),实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。

第14讲 谈谈你知道的设计模式?

  1. 多线程安全下实现工厂模式的两种方案:
    a. volatile + double check:
public class Singleton {private static volatile Singleton singleton = null;private Singleton() {}public static Singleton getSingleton() {if (singleton == null) { // 尽量避免重复进入同步块synchronized (Singleton.class) { // 同步.class,意味着对同步类方法调用if (singleton == null) {singleton = new Singleton();}}}return singleton;}
}
b. ==内部类==
public class Singleton {private Singleton(){}public static Singleton getSingleton(){return Holder.singleton;}private static class Holder {private static Singleton singleton = new Singleton();}
}

第15讲 synchronized和ReentrantLock有什么区别呢?

  1. synchronized: Java 内建的同步机制,提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在那里。在 Java 5 以前,synchronized 是仅有的同步手段,在代码中, synchronized 可以用来修饰方法,也可以使用在特定的代码块儿上,本质上 synchronized 方法等同于把方法全部语句用 synchronized 块包起来。
  2. ReentrantLock: 通常翻译为可重入锁(当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功),是 Java 5 提供的锁实现,它的语义和 synchronized 基本相同。通过代码直接调用 lock() 方法获取,代码书写也更加灵活。与此同时,ReentrantLock 提供了很多实用的方法,能够实现很多 synchronized 无法做到的细节控制,比如可以控制 fairness,也就是公平性,或者利用定义条件等。但是,编码中也需要注意,必须要明确调用 unlock() 方法释放,不然就会一直持有该锁。

第16讲 synchronized底层如何实现?什么是锁的升级、降级?

  1. synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。
  2. 在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。现代的(Oracle)JDK 中,JVM 对此进行了大刀阔斧地改进,提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
  3. 当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
  4. 如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
  5. 锁降级是会发生的,当 JVM 进入安全点(SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。
  6. 偏向锁升级:对象头记录的线程与当前线程不同,如果原线程依然存在,则升级为轻量级锁。此时使用cas进行锁竞争。
  7. 轻量级锁升级:有2个线程正在用cas锁竞争,如果有第3个线程来争夺资源,则升级为重量级锁。cas本质是cpu自旋,如果线程2自旋次数过多,也会升级。升级为重量级锁后,等待的线程阻塞,不再cas,防止cpu空转。
  8. StampedLock: 不支持可重入的读写锁,性能比ReentrantReadWriteLock高。在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着读,然后通过 validate 方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。其中,long writeLockTimestamp = sl.writeLock(); sl.unlockWrite(writeLockTimestamp);sl.unlockWrite(writeLockTimestamp);sl.unlockRead(readLockTimestamp);必须成对调用。

第17讲 一个线程两次调用start()方法会出现什么情况?

  1. Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。
  2. 关于线程生命周期的不同状态,在 Java 5 以后,线程状态被明确定义在其公共内部枚举类型 java.lang.Thread.State 中,分别是:
  • 新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个 Java 内部状态。
  • 就绪(RUNNABLE),表示该线程已经在 JVM 中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它 CPU 片段,在就绪队列里面排队。在其他一些分析中,会额外区分一种状态 RUNNING,但是从 Java API 的角度,并不能表示出来。
  • 阻塞(BLOCKED),这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待 Monitor lock。比如,线程试图通过 synchronized 去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。
  • 等待(WAITING),表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似 notify 等动作,通知消费线程可以继续工作了。Thread.join() 也会令线程进入等待状态。
  • 计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如 wait 或 join 等方法的指定超时版本。
  • 终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行。
  1. 守护线程:有的时候应用中需要一个长期驻留的服务程序,但是不希望其影响应用退出,就可以将其设置为守护线程,如果 JVM 发现只有守护线程存在时,将结束进程。

第18讲 什么情况下Java程序会产生死锁?如何定位、修复?

  1. 死锁的发生是因为:
  • 互斥条件,类似 Java 中 Monitor 都是独占的,要么是我用,要么是你用。
  • 互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其他线程抢占。
  • 循环依赖关系,两个或者多个个体之间出现了锁的链条环。

第19讲 Java并发包提供了哪些并发工具类?

  1. 提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatch、CyclicBarrier、Semaphore等,可以实现更加丰富的多线程操作,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。
  2. 各种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcurrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组 CopyOnWriteArrayList 等。
  3. 各种并发队列实现,如各种 BlockingQueue 实现,比较典型的 ArrayBlockingQueue、 SynchronousQueue 或针对特定场景的 PriorityBlockingQueue 等。
  4. 强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。
  5. Semaphore示例:

import java.util.concurrent.Semaphore;
public class UsualSemaphoreSample {public static void main(String[] args) throws InterruptedException {System.out.println("Action...GO!");Semaphore semaphore = new Semaphore(5);for (int i = 0; i < 10; i++) {Thread t = new Thread(new SemaphoreWorker(semaphore));t.start();}}
}
class SemaphoreWorker implements Runnable {private String name;private Semaphore semaphore;public SemaphoreWorker(Semaphore semaphore) {this.semaphore = semaphore;}@Overridepublic void run() {try {log("is waiting for a permit!");semaphore.acquire();Thread.sleep(5000);log("acquired a permit!");log("executed!");} catch (InterruptedException e) {e.printStackTrace();} finally {log("released a permit!");semaphore.release();}}private void log(String msg){if (name == null) {name = Thread.currentThread().getName();}System.out.println(name + " " + msg);}
}
  1. CountDownLatch 是不可以重置的,所以无法重用;而 CyclicBarrier 则没有这种限制,CyclicBarrier则会自动重置,可以重用。
  2. CountDownLatch 的基本操作组合是 countDown/await。调用 await 的线程阻塞等待 countDown 足够的次数,不管你是在一个线程还是多个线程里 countDown,只要次数足够即可。
  3. CyclicBarrier 的基本操作组合,就是 await,当所有的伙伴(线程)都调用了 await,这些线程才会继续进行任务,并自动进行重置。注意,正常情况下,CyclicBarrier 的重置都是自动发生的,如果我们调用 reset 方法,但还有线程在等待,就会导致等待线程被打扰,抛出 BrokenBarrierException 异常。CyclicBarrier 侧重点是线程,而不是调用事件,它的典型应用场景是用来等待并发线程结束。
  4. CountDownLatch: 多个线程使用countDown()对计数减1,只有当计数为0时,调用await()方法的线程继续执行。

import java.util.concurrent.CountDownLatch;
public class LatchSample {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(6);for (int i = 0; i < 6; i++) {Thread t = new Thread(new FirstBatchWorker(latch));t.start();}for (int i = 0; i < 5; i++) {Thread t = new Thread(new SecondBatchWorker(latch));t.start();}Thread.sleep(5000);}
}
class FirstBatchWorker implements Runnable {private CountDownLatch latch;public FirstBatchWorker(CountDownLatch latch) {this.latch = latch;}@Overridepublic void run() {latch.countDown();System.out.println("First batch executed!");}
}
class SecondBatchWorker implements Runnable {private CountDownLatch latch;public SecondBatchWorker(CountDownLatch latch) {this.latch = latch;}@Overridepublic void run() {try {latch.await();System.out.println("Second batch executed!");} catch (InterruptedException e) {e.printStackTrace();}}
}
  1. CyclicBarrier: 等待所有线程都调用了await()方法后,开始继续这些线程后续的功能。

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierSample {public static void main(String[] args) throws InterruptedException {CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {@Overridepublic void run() {System.out.println("Action...GO again!");}});for (int i = 0; i < 5; i++) {Thread t = new Thread(new CyclicWorker(barrier));t.start();}}static class CyclicWorker implements Runnable {private CyclicBarrier barrier;public CyclicWorker(CyclicBarrier barrier) {this.barrier = barrier;}@Overridepublic void run() {try {for (int i=0; i<3 ; i++){int randomTime = new Random().nextInt(10);System.out.println(Thread.currentThread().getName() + " will sleep " + randomTime + " seconds");Thread.sleep(randomTime * 1000);barrier.await();System.out.println(Thread.currentThread().getName() + " execute");}} catch (BrokenBarrierException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}}}
}
  1. 如果我们的应用侧重于 Map 放入或者获取的速度,而不在乎顺序,大多推荐使用 ConcurrentHashMap,反之则使用 ConcurrentSkipListMap;如果我们需要对大量数据进行非常频繁地修改,ConcurrentSkipListMap 也可能表现出优势。
  2. CopyOnWrite: 任何修改操作,如 add、set、remove,都会拷贝原数组,修改后替换原来的数组,通过这种防御性的方式,实现另类的线程安全。

第20讲 并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?

  1. 并发包中的 ConcurrentLinkedQueue 和 LinkedBlockingQueue 有什么区别?
  • Concurrent 类型基于 lock-free(CAS),在常见的多线程访问场景,一般可以提供较高吞吐量。
  • LinkedBlockingQueue 内部则是基于锁,并提供了 BlockingQueue 的等待性方法。
  1. java.util.concurrent 包提供的容器(Queue、List、Set)、Map,从命名上可以大概区分为 Concurrent*、CopyOnWrite和 Blocking等三类,同样是线程安全容器,可以简单认为:
  • Concurrent 类型没有类似 CopyOnWrite 之类容器相对较重的修改开销。
  • Concurrent 往往提供了较低的遍历一致性。你可以这样理解所谓的弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。
  • 与弱一致性对应的,就是同步容器常见的行为“fail-fast”,也就是检测到容器在遍历过程中发生了修改,则抛出 ConcurrentModificationException,不再继续遍历。
  • 弱一致性的另外一个体现是,size 等操作准确性是有限的,未必是 100% 准确。
  • 与此同时,读取的性能具有一定的不确定性。
  1. ArrayBlockingQueue: 是最典型的的有界队列,其内部以 final 的数组保存数据,数组的大小就决定了队列的边界,所以在创建 ArrayBlockingQueue 时,都要指定容量。
  2. LinkedBlockingQueue:容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被设置为 Integer.MAX_VALUE,成为了无界队列。
  3. SynchronousQueue:这是一个非常奇葩的队列实现,每个删除操作都要等待插入操作,反之每个插入操作也都要等待删除动作。那么这个队列的容量是多少呢?是 1 吗?其实不是的,其内部容量是 0。同时,它也是 Executors.newCachedThreadPool() 的默认队列。比如如下代码,将永远不会往queue中插入数据:
public class QueueTest {public static void main(String[] args) {SynchronousQueue<Integer> queue = new SynchronousQueue();new Thread(new Runnable() {@Overridepublic void run() {try {int num = 1;queue.put(num);System.out.println("put num:" + num);} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}
  1. PriorityBlockingQueue: 是无边界的优先队列,虽然严格意义上来讲,其大小总归是要受系统资源影响。
  2. DelayedQueue 和 LinkedTransferQueue: 同样是无边界的队列。对于无边界的队列,有一个自然的结果,就是 put 操作永远也不会发生其他 BlockingQueue 的那种等待情况。
  3. ConcurrentLinkedQueue:基于 CAS 的无锁技术,不需要在每个操作时使用锁,所以扩展性表现要更加优异。
  4. 通用场景中,LinkedBlockingQueue 的吞吐量一般优于 ArrayBlockingQueue,因为它实现了更加细粒度的锁操作(头尾操作用不同的锁)。

第21讲 Java并发类库提供的线程池有哪几种? 分别有什么特点?

  1. Executors 目前提供了 5 种不同的线程池创建配置:
  • newCachedThreadPool(): 它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用(corePoolSize为0,maxPoolSize为Integer.MAX_VALUE),当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。
  • newFixedThreadPool(int nThreads): 重用指定数目(nThreads)的线程(corePoolSize和maxPoolSize都是nThreads),其背后使用的是无界的工作队列,任何时候都有 nThreads 个工作线程是活动的。
  • newSingleThreadExecutor: 它的特点在于工作线程数目被限制为 1((corePoolSize和maxPoolSize都是1)),操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
  • newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize): 创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
  • newWorkStealingPool(int parallelism): 这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
  1. 创建线程池的两种方法:
  • 通过实例化ThreadPoolExecutor对象创建。
  • 通过Executors的静态工厂方法创建。

第22讲 AtomicInteger底层实现原理是什么?如何在自己的产品代码中应用CAS操作?

  1. AtomicIntger 是对 int 类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于 CAS(compare-and-swap)技术。

第23讲 请介绍类加载过程,什么是双亲委派模型?

  1. Java 的类加载过程分为三个主要步骤:加载、链接、初始化。
  2. 加载阶段:是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
  3. 链接阶段:这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:
  • 验证(Verification)。这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
  • 准备(Preparation)。创建类或接口中的静态变量,并初始化静态变量的初始值(默认值)。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于这里只会分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
  • 解析(Resolution)。在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。在Java 虚拟机规范中,详细介绍了类、接口、方法和字段等各个方面的解析。
  1. 初始化阶段(initialization):这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
  2. 双亲委派模型:简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做(构造函数也是优先调用父类构造函数)。使用委派模型的目的是避免重复加载 Java 类型。

第24讲 有哪些方法可以在运行时动态生成一个Java类?

  1. 类从字节码(class文件)到 Class 对象的转换,通过java.lang.ClassLoader#defineClass实现。
  2. 可以通过ASM框架在Java运行时动态生成一个Java类。

第25讲 谈谈JVM内存区域的划分,哪些区域可能发生OutOfMemoryError?

  1. 程序计数器(PC,Program Counter Register),线程独有。任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。
  2. Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈,线程独有。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等。
  3. 堆(Heap),线程共享。它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。
  4. 方法区(Method Area),线程共享。用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。Oracle JDK 8 中将方法区移除,同时增加了元数据区(Metaspace)。
  5. 本地方法栈(Native Method Stack),线程独有。它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。
  6. 除了程序计数器,其他区域都有可能会因为可能的空间不足发生 OutOfMemoryError。

第26讲 如何监控和诊断JVM堆内和堆外内存使用?

  1. 堆上新生代Eden区中有一块区域TLAB(Thread Local Allocation Buffer),这是 JVM 为每个线程分配的一个私有缓存区域,否则,多线程同时分配内存时,为避免操作同一地址,可能需要使用加锁等机制,进而影响分配速度。TLAB 仍然在堆上,其内部结构比较直观易懂,start、end 就是起始地址,top(指针)则表示已经分配到哪里了。所以我们分配新对象,JVM 就会移动 top,当 top 和 end 相遇时,即表示该缓存已满,JVM 会试图再从 Eden 里分配一块。
  2. 普通的对象会被分配在 TLAB 上;如果对象较大,JVM 会试图直接分配在 Eden 其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代。

第27讲 Java常见的垃圾收集器有哪些?

  1. Serial GC:单线程垃圾收集器,Client 模式下 JVM 的默认选项。其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法,区别于新生代的复制算法。
  2. ParNew GC:Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作。
  3. CMS(Concurrent Mark Sweep) GC:老年代垃圾收集器,基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间。但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。
  4. Parallel GC:在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称作是吞吐量优先的 GC。它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的。
  5. G1 GC:这是一种兼顾吞吐量和停顿时间的 GC 实现,是== Oracle JDK 9 以后的默认 GC 选项==。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 Region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。== CMS 已经在 JDK 9 中被标记为废弃(deprecated)==。
  6. 引用计数算法:为对象添加一个引用计数,用于记录对象被引用的情况,如果计数为 0,即表示对象可回收。
  7. 可达性分析算法:通常又叫作追踪性垃圾收集。将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和 GC Roots 之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。JVM 会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,作为 GC Roots。
  8. 三种常见的垃圾收集算法:
  • 复制(Copying)算法:新生代 GC,基本都是基于复制算法,将活着的对象复制到 to 区域,拷贝过程中将对象顺序放置,就可以避免内存碎片化。这么做的代价是,既然要进行复制,既要提前预留内存空间,有一定的浪费;另外,对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,这个开销也不小,不管是内存占用或者时间开销。
  • 标记 - 清除(Mark-Sweep)算法:首先进行标记工作,标识出所有要回收的对象,然后进行清除。这么做除了标记、清除过程效率有限,另外就是不可避免的出现碎片化问题,这就导致其不适合特别大的堆;否则,一旦出现 Full GC,暂停时间可能根本无法接受。
  • 标记 - 整理(Mark-Compact):类似于标记 - 清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。
  1. 年轻代的GC称为Minor GC,老年代的GC称为Major GC,整个堆的GC称为Full GC。

第28讲 谈谈你的GC调优思路?

  1. 从性能的角度看,调优通常关注三个方面,内存占用(footprint)、延时(latency)和吞吐量(throughput) 。
  2. G1: region 的大小是一致的,数值是在 1M 到 32M 字节之间的一个 2 的幂值数,JVM 会尽量划分 2048 个左右、同等大小的 region。年代是个逻辑概念,一部分 region 是作为 Eden,一部分作为 Survivor,一部分是Old,G1 会将超过 Region 50% 大小的对象(在应用中,通常是 byte 或 char 数组)归类为 Humongous 对象,一个 Humongous 对象可能会占用多个region。逻辑上,Humongous Region 算是老年代的一部分,因为复制这样的大对象是很昂贵的操作,并不适合新生代 GC 的复制算法。
  3. 从 GC 算法的角度,G1 选择的是复合算法,可以简化理解为:
  • 在新生代,G1 采用的仍然是并行的复制算法,所以同样会发生 Stop-The-World 的暂停。
  • 在老年代,大部分情况下都是并发标记,而整理(Compact)则是和新生代 GC 时捎带进行,并且不是整体性的整理,而是增量进行的。

第29讲 Java内存模型中的happen-before是什么?

  1. Happen-before 关系,是Java 内存模型中保证多线程操作可见性的机制,也是对早期语言规范中含糊的可见性概念的一个精确定义。具体表现形式:
  • 线程内执行的每个操作,都保证 happen-before 后面的操作,这就保证了基本的程序顺序规则,这是开发者在书写程序时的基本约定。
  • 对于 volatile 变量,对它的写操作,保证 happen-before 在随后对该变量的读取操作。
  • 对于一个锁的解锁操作,保证 happen-before 加锁操作。
  • 对象构建完成,保证 happen-before 于 finalizer 的开始动作。
  • 线程内部操作的完成,保证 happen-before 其他 Thread.join() 的线程等。

第30讲 Java程序运行在Docker等容器环境有哪些新问题?

  1. 在Docker中,内存、CPU 等资源限制是通过 CGroup(Control Group)实现的,早期的 JDK 版本(8u131 之前)并不能识别这些限制,进而会导致一些基础问题:
  • 如果未配置合适的 JVM 堆和元数据区、直接内存等参数,Java 就有可能试图使用超过容器限制的内存,最终被容器 OOM kill,或者自身发生 OOM。
  • 错误判断了可获取的 CPU 资源,例如,Docker 限制了 CPU 的核数,JVM 就可能设置不合适的 GC 并行线程数等。
  1. 如何解决这些问题呢?
    升级到最新的 JDK 版本。JDK 9 中引入了一些实验性的参数,以方便 Docker 和 Java“沟通”。这两个参数是顺序敏感的,并且只支持 Linux 环境。而对于 CPU 核心数限定,Java 已经被修正为可以正确理解“–cpuset-cpus”等设置,无需单独设置参数。
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap

如果是 JDK 10 或者更新的版本,Java 对容器(Docker)的支持已经比较完善,默认就会自适应各种资源限制和实现差异。上面的实验性参数“UseCGroupMemoryLimitForHeap”已经被标记为废弃。与此同时,新增了参数用以明确指定 CPU 核心的数目。-XX:ActiveProcessorCount=N

  1. 如果只能使用老版本的 JDK 怎么办?
  • 明确设置堆、元数据区等内存区域大小,保证 Java 进程的总大小可控,保证JVM堆内存小于docker容器内存。在容器启动时加上-e JAVA_OPTIONS='-Xmx300m'.
  • 明确配置 GC 和 JIT 并行线程数目,以避免二者占用过多计算资源。 -XX:ParallelGCThreads=N -XX:CICompilerCount=N
  • 禁止使用swap。指定JVM参数:-XX:MaxRAM=`cat /sys/fs/cgroup/memory/memory.limit_in_bytes. 或者指定docker参数: --memory-swappiness=0.

学习笔记之极客时间《Java 核心技术面试精讲》相关推荐

  1. 【算法笔记】极客时间 算法面试通关40讲 笔记  覃超

    [算法笔记]极客时间 算法面试通关40讲 覃超 [算法笔记]极客时间 算法面试通关40讲 覃超 相关链接 在leetcode 上的题号 数组.链表: (堆)栈stack.队列queue 优先队列 哈希 ...

  2. 《Java核心技术面试精讲--杨晓峰》学习笔记目录

    这仅仅是我个人的理解,想要仔细了解请去 极客时间 购买阅读 笔记会写的很多,也是一个夯实基础的过程吧 笔记的内容大部分是从专栏以及留言区摘录 10 11 12 中涉及到的 IO 与 NIO 知识点我许 ...

  3. Nginx学习笔记2--(极客时间-陶辉)

    1⃣️ nginx进程结构 nginx是多进程结构模型,由master作为父进程,启动多个子进程,通过信号管理. Master进程 << 监控worker进程:CHLD(子进程终止的时候会 ...

  4. Nginx学习笔记4--(极客时间-陶辉)

    main http { #HTTP模块upstream { ... } #HTTP模块自己的配置块split_clients {...} map {...}geo {...}server { #根据域 ...

  5. Nginx学习笔记5--(极客时间-陶辉)

    正则表达式 ?\ 转意字符:取消原字符的特殊含义 ?()分组与取值$ ?验证正则表达式工具:pcretest 找到处理请求的server指令块 server_name指令 指令可以跟多个域名,第一个是 ...

  6. Nginx学习笔记3--(极客时间-陶辉)

    nginx官方文档 nginx中文文档 nginx模块 ?nginx源码的/objs/ngx_modules.c中*ngx_modules[]数组代表了编译进nginx的模块. nginx连接池 ?每 ...

  7. 《Java核心技术面试精讲》23讲学习总结

    第22讲心得 该讲介绍了类加载过程,什么是双亲委派模型?. 一般来说,我们把 Java 的类加载过程分为三个主要步骤:加载.链接.初始化,具体行为在Java 虚拟机规范里有非常详细的定义.首先是加载阶 ...

  8. java核心技术面试精讲

    前言 大厂面试真题向来都是各大求职者的最佳练兵场,而今天小编带来的便是"HUAWEI"面经!这是一次真实的面试经历,虽然不是我自己亲身经历但是听当事人叙述后便会深有同感(因为我朋友 ...

  9. 极客时间 Redis核心技术与实战 笔记(基础篇)

    Redis 概览 Redis 知识全景图 Redis 问题画像图 基础篇 基本架构 数据结构 数据类型和底层数据结构映射关系 全局哈希表 链式哈希解决哈希冲突 渐进式 rehash 不同数据结构查找操 ...

  10. Java程序设计当中包的使用_【学习笔记】 唐大仕—Java程序设计 第4讲 类、包和接口之4.2 类的继承...

    [学习笔记] 唐大仕-Java程序设计 第4讲 类.包和接口之4.2 类的继承 super的使用 1.使用super访问父类的域和方法 注意:正是由于继承,使用this可以访问父类的域和方法.但是有时 ...

最新文章

  1. passwd文件详解
  2. 内容推荐 | 生信技术与前沿内容知识库
  3. Windows用管理员方式启动cmd (全面)
  4. 用户日志留存所采用的技术手段
  5. 深入理解InnoDB(6)—独立表空间
  6. win10 如何锁定计算机,Win10 1909 专业版怎么锁定计算机屏幕
  7. Mr.J-- jQuery学习笔记(二)--核心函数jQuery对象
  8. 机器学习之支持向量机(SVM)小结
  9. Mac系统下替换百度云的倍速播放器-Quicktime player 的使用方法
  10. TVS二极管和稳压二极管区别和原理
  11. Educational Codeforces Round 101 (Rated for Div. 2)
  12. unity 摄像头跟着鼠标移动_unity第三视角移动,摄像机跟随
  13. NFT 地板价计算方法
  14. 16进制的 RBG值 颜色 转换
  15. 趋高智能机器开发工业相机ccd视觉检测系统定制软件硬件
  16. 跨平台应用开发进阶(五十四):Android APP调试工具:ADB
  17. php chrome.crx,从PHP生成Chrome .crx
  18. 《道德经》(王弼本)
  19. java 创建和读取Excel表单
  20. “C语言已经老掉牙了,很快就会被淘汰”?通过这篇文章向你展示C伟大的一面

热门文章

  1. Busybox下tftp命令使用详解
  2. redis雪崩 击穿 穿透
  3. 如何生成希尔伯特矩阵
  4. 图书销售系统系统设计说明书
  5. 计算机仿真在机械应用,机械系统计算机仿真
  6. 时间管理_个人计划表
  7. 浅谈两轮平衡车的控制原理(续)
  8. Android基础教程(奋斗之小鸟)_PDF 电子书
  9. 基于三极管的电平转换电路
  10. HTML5新增标签--canvas之绘制你画我猜