为什么需要多线程

  • CPU:对于CPU的计算速度来说,其它的操作都太慢了
    • CPU经常需要等待网络,内存,磁盘IO等操
    • 1GHZ -> 1G(10^9) 时钟周期 / 秒 -> 1纳秒
  • 现代CPU都是多核了
  • Java的执行模型是同步/阻塞(block)的
  • 默认情况下只有一个线程
    • 处理问题非常自然
    • 但是有严重的性能问题

开启一个新线程:Thread

  • Java中只有Thread这么一种东西代表线程
  • start方法才能并发执行,run方法是等这步执行完后才进行下一步
  • 每多开一个线程,就多一个执行流
  • 方法栈(局部变量)是线程私有的
  • 静态变量/类变量是被所有线程共享的
    • 共享变量可以收集所有线程的结果
    • 变量的共享,是多线程几乎所有坑的来源

多线程的难点

  • 线程困难的本质:同一份代码,你要想象不同的人在疯狂地以乱序执行它
  • 非原子操作,都是线程不安全的
    • 比如i=0;一千个线程执行i++操作,最后得到的i小于1000
    • 因为操作系统在不停切换线程,这个线程事情没做完,切到另一个线程做了些事情再切回来
      image.png

多线程适用的场景&带来的性能提升

1. 适合的场景

  • IO密集型应用

    • 网络IO(通常包括数据库)
    • 文件IO

      2.不适合的场景

  • 对于CPU密集型应用稍有折扣

  • 因为多线程的本意是CPU运算速度对于其他操作太快了
    • 在等待其他操作时让CPU不要闲着
    • 从而提示性能
  • CPU密集型本来CPU就被占满了,多线程带来的提示有限

    3. 性能提升的上限在哪?

  • 单核CPU: 100%

  • 多核CPU: N*100%(N核跑满)

线程的昂贵性

  • 线程无法无穷无尽地提升性能
  • 线程的昂贵性在于:
    • CPU切换上下文很慢
    • 线程需要占用内存等系统资源
  • 如果你的应用用户数量较低:
    • new Thread().start()
  • 如果应用负载很高:
    • 使用线程池:JUC包

线程安全

  • 享用了线程的便利需要付出代价
    • 原子性
    • 共享变量
    • 默认的实现几乎都不是线程安全的

线程不安全的表现

1. 数据错误

  • i++
  • if-then-do

    2. 死锁

  • 产生原理:

    • 两个或者以上线程阻塞着等待其他处于死锁状态的线程所持有的锁
    • 通常发生在多个线程同时以不同顺序请求同一组锁的时候
  • 死锁的简单实现:
  1. public class Main {
  2. private static final Object lock1 = new Object();
  3. private static final Object lock2 = new Object();
  4. public static void main(String[] args) throws InterruptedException {
  5. // 两个进程以不同顺序获取lock
  6. // Thread2等不到lock1,Thread1等不到lock2
  7. // start方法才能并发执行,run方法是等这步执行完后才进行下一步
  8. new Thread1().start();
  9. new Thread2().start();
  10. }
  11. static class Thread1 extends Thread {
  12. @Override
  13. public void run() {
  14. synchronized (lock1) {
  15. System.out.println("Thread1 get lock1");
  16. try {
  17. Thread.sleep(500);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. synchronized (lock2) {
  22. System.out.println("Thread1 get lock2");
  23. }
  24. }
  25. }
  26. }
  27. static class Thread2 extends Thread {
  28. @Override
  29. public void run() {
  30. synchronized (lock2) {
  31. System.out.println("Thread2 get lock2");
  32. try {
  33. Thread.sleep(100);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. synchronized (lock1) {
  38. System.out.println("Thread2 get lock1");
  39. }
  40. }
  41. }
  42. }
  43. }

查看Java进程

  • Linux命令查看进程: ps | grep java
  • Java自带命令查看所有Java进程: jps
  • jstack + 查看进程中的所有堆栈信息
    • JVM所有对象都在堆(heap)上分配
    • 每个线程独享一个方法栈
    • 每个方法调用别的方法,就在栈上加一层
    • 每个线程最底下就是Thread.run()方法
      image.png

实现线程安全的基本手段

1. 不可变类:Integer、String…

  • 两个以上线程修改一个对象就可能出问题
  • 不可变类只可读,不可改,就不会出现两个以上线程修改一个对象

    2. synchronized 同步块

  • 同步块同步了什么东⻄?

    • synchronized(Object) 把这个Object当成锁
    • static synchronized ⽅法 把Class对象当成锁
      • private synchronized static void func() {}
    • 实例的 synchronnized⽅法 把该实例当成锁
  • Collections.synchronizedCollection/List/Map/…

    3. JUC包

  • AtomicInteger/Boolean/…

  • ConcurrentHashMap
    • 任何使⽤HashMap有线程安全问题的地⽅
    • 都⽆脑地使⽤ConcurrentHashMap替换即可。
  • ReentrantLock
    • 可重入锁,与synchronnized相似,但是更强大

Object类中的线程方法

1. 线程的历史

  • Java一开始就把线程作为语言特性,提供语言级的支持

    2. 为什么Java中的所有对象都可以成为锁?

  • Object.wait()/notify()/notifyAll()方法

  • 线程的状态与调度

经典的生产/消费者模型

三种实现方法

  • wait/notify/notifyAll
  • Lock/Condition
  • BlockingQueue

线程池与Callable/Future

1. 什么事线程池

  • 线程是昂贵的(Java线程模型的缺陷,依赖于操作系统的线程调度)
  • 线程池是预先定义好的若干个线程
  • Java中的线程池
    • Executors

      2. Callable/Future