首页 » java

Java 里 synchronized、wait()/notify() 相关的知识很琐碎,看懂难,会用更难。但实际上 synchronized、wait()、notify() 不过是操作系统领域里管程模型的一种实现而已,Java SDK 并发包里的条件变量 Condition 也是管程里的概念

1.并发编程可以总结为3个核心问题:分工、同步、互斥

  • 分工:指的是如何高效地拆解任务并分配给线程,类似于现实中一个组织完成一个项目,项目经理要拆分任务,安排合适的成员去完成。
  • 例如 Fork/Join 框架就是一种分工模式
  • 著名数学家华罗庚曾用“烧水泡茶”的例子通俗地讲解了统筹方法(一种安排工作进程的数学方法),“烧水泡茶”这么简单的事情都这么多说道,更何况是并发编程里的工程问题呢。
  • Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是一种分工方法。
  • 并发编程领域还总结了一些设计模式,基本上都是和分工方法相关的,例如生产者 - 消费者、Thread-Per-Message、Worker Thread 模式等都是用来指导你如何分工的。
  • 其实这就是生产者 - 消费者模式的一个优点,生产者一个一个地生产数据,而消费者可以批处理,这样就提高了性能。
  • 同步:指的是线程之间如何协作,分好工之后,就是具体执行了。在项目执行过程中,任务之间是有依赖的,一个任务结束后,依赖它的后续任务就可以开工了,后续工作怎么知道可以开工了呢?这个就是靠沟通协作了,这是一项很重要的工作。
  • 在并发编程领域里的同步,主要指的就是线程间的协作,本质上和现实生活中的协作没区别,不过是一个线程执行完了一个任务,如何通知执行后续任务的线程开工而已。
  • 协作一般是和分工相关的。Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是分工方法,但同时也能解决线程协作的问题。例如,用 Future 可以发起一个异步调用,当主线程通过 get() 方法取结果时,主线程就会等待,当异步执行的结果返回时,get() 方法就自动返回了。主线程和异步线程之间的协作,Future 工具类已经帮我们解决了。除此之外,Java SDK 里提供的 CountDownLatch、CyclicBarrier、Phaser、Exchanger 也都是用来解决线程协作问题的。
  • 工作中遇到的线程协作问题,基本上都可以描述为这样的一个问题:当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行。例如,在生产者 - 消费者模型里,也有类似的描述,“当队列满时,生产者线程等待,当队列不满时,生产者线程需要被唤醒执行;当队列空时,消费者线程等待,当队列不空时,消费者线程需要被唤醒执行。”
  • 在 Java 并发编程领域,解决协作问题的核心技术是管程monitor,上面提到的所有线程协作技术底层都是利用管程解决的。管程是一种解决并发问题的通用模型,除了能解决线程协作问题,还能解决下面我们将要介绍的互斥问题。可以这么说,管程是解决并发问题的万能钥匙。
  • 互斥则是保证同一时刻只允许一个线程访问共享资源,分工、同步主要强调的是性能,但并发程序里还有一部分是关于正确性的,用专业术语叫“线程安全”。
  • 并发程序里,当多个线程同时访问同一个共享变量的时候,结果是不确定的。不确定,则意味着可能正确,也可能错误,事先是不知道的。而导致不确定的主要源头是可见性问题、有序性问题和原子性问题
  • 为了解决这三个问题,Java 语言引入了内存模型,内存模型提供了一系列的规则,利用这些规则,我们可以避免可见性问题、有序性问题,但是还不足以完全解决线程安全问题。解决线程安全问题的核心方案还是互斥
  • 所谓互斥,指的是同一时刻,只允许一个线程访问共享变量。实现互斥的核心技术就是锁,
  • 虽说锁解决了安全性问题,但同时也带来了性能问题
  • Java SDK 里提供的 ReadWriteLock、StampedLock 就可以优化读多写少场景下锁的性能。还可以使用无锁的数据结构,例如 Java SDK 里提供的原子类都是基于无锁技术实现的。
  • 除此之外,还有一些其他的方案,原理是不共享变量或者变量只允许读。这方面,Java 提供了 Thread Local 和 final 关键字,还有一种 Copy-on-write 的模式。
  • 使用锁除了要注意性能问题外,还需要注意死锁问题。
  1. Java SDK 并发包其余的一部分则是并发容器和原子类,这些比较容易理解,属于辅助工具,其他语言里基本都能找到对应的。

其实这三个词关系不是很大,但是因为长的像,所以在中国是常见的面试题。哈哈

1.final 修饰变量/参数,不可更改(指针),如果时list,则里面还是能添加对象的,这点和immutable不同啊。
2.finally 和try配合使用,除非try里面用了System.exit()否则,finally一定会执行。
3.finalize 是给垃圾回收器用的。但是垃圾回收的时间是不确定的,所以可能造成OOM哦。而且jvm在调用finalize的时候会吞掉里面的任何异常,造成不可预知的结果。JDK9已经标记这个方法为depreciate。如果想用,可以用java.lang.ref.Cleaner,虽然单独出线程来处理了。不会造成死锁,但是还是建议不要用cleaner。最好的方法还是用完显式的释放。

Exception和Error都继承自Throwable类,可以被throw和catch,那么他们有什么区别呢

1.Exception是程序正常运行中,可以预料的意外情况,可能而且应该被catch,进行处理
2.Error是不大可能出现的情况,绝大部分Error会导致JVM处于非正常的不可恢复的状态,如OOM,不便于或者也不需要捕获。
3.Exception分为可检查异常和不检查异常。可检查异常是编译器检查的一部分,必须显示的进行捕获处理,如IO异常。不检查异常是运行时异常,可以再运行时用代码判断是否捕获。如空指针,数组超界。常见问题参考--https://blog.csdn.net/bryantlmm/article/details/78118763
4.注意try catch是有性能损耗的,不要用try包括大量的代码,用if else会更高效。对于追求极致性能的底层类库,会使用创建不进行栈快照的Exception。
5.Reactive 编程时,不能简单的抛异常,异常处理也要非常小心,很容易导致其他问题

说起java平台,最多的是跨平台,一次编译,到处运行。java代码用javac编译成bytecode 的class文件,然后运行时,解释器转换成最终的机器码。JIT会把热点代码编译执行,而不是解释执行。

1.类加载器,Bootstrap,Application,Extension class loader,参考周志明《深入理解java虚拟机》。经过加载,验证,链接,初始化。
2.常见的垃圾收集器,SerialGC,Parallel GC,CMS,G1
3.java分为编译期和运行时。

  • 编译生成的.class文件是平台无关的,会屏蔽具体的硬件细节和操作系统细节,这是“一次编译,到处运行”的基础
  • 运行时,通过类加载器Class Loader加载字节码(.class)文件。可以解释加载也可以编译加载。jdk 8 默认的是混合模式,即java运行指令-Xmixed,但是Client模式和Server模式有明显的区别
  • client模式以启动快速为目的,启动时 会进行1500次(上限)调用来进行编译。这里的JIT是C1
  • Server模式以长时间运行为目的,启动时 会进行上万次调用,来确定搞笑的编译,这里的JIT是C2,C2默认采用所谓的分层编译TieredComplilation
  • 当然可以再运行时指定-Xcomp,告诉jvm关闭解释器,编译运行,但这种未必是最搞笑的,而且启动会慢很多
  • 也可以指定-Xint,只解释运行
  • 除了编译和解释,还有一种aot模式(oracle jdk 9引入),将某个模块编译成机器代码,可以减少启动预热开销,使用jaotc工具来使用.注意分层编译可以和AOT一起使用,不是非此即彼的。
    jaotc --output libHelloWorld.so  HelloWorld.classs
    java -XX:AOTLibrary=./libHelloWorld.so HelloWorld