28、29多线程初步

为什么需要多线程

  • CPU :你们都慢!死!了!
  • 现代CPU都是多核的,Java的执行模型是同步/阻塞(block)的,默认情况下只有一个线程,处理问题非常自然,但是具有严重的性能问题
  • 提高cpu利用率

开启一个新的线程

Thread

  • Java中只有这么一种东西代表线程
  • start方法才能并发执行!
  • 每多开一个线程,就多一个执行流
  • 方法栈(局部变量)是线程私有的(为了保证线程中的局部变量不被别的线程访问到)
  • 静态变量/类变量是被所有线程共享的
  • run()与start()的区别
    start() 启动了一个新的线程,run()普通成员方法,不会启动新的线程

基本API:

  • isAlive()测试线程是否处于活动状态
  • sleep()让“正在执行的线程”休眠
  • getId()取得线程唯一标识
  • yield()放弃当前的CPU资源

线程难的本质原因是

  • 你要看着同一份代码,想象不同的人在疯狂地以乱序执行它(线程的随机性)

演示多线程带来的性能提升

  • 对于IO密集型应用极其有用
  • 网络IO(通常包括数据库)
  • 文件IO
  • 对于CPU密集型应用稍有折扣
  • 性能提升的上限在哪里?
  • 单核CPU 100%
  • 多核 CPU N*100%

昂贵的线程

  • 线程的昂贵性在于
    • 第一,CPU切换上下文很慢
    • 第二,线程需要占用内存等系统资源
    • 如果你的应用一天才几个用户
      • new Thread().start()
    • 如果你的应用负载很高
      • 使用线程池:JUC包

      上下文切换
      线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

线程安全

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

线程不安全的表现

  • 数据错误

    • I++
    • if-then-do

    同类的多个线程共享进程的堆和方法区资源

死锁

  • 著名的HashMap的死循环问题
  • 预防死锁产生的原则:
    • 所有的线程都按照相同的顺序获得资源的锁
  • 死锁问题的排查
  • 多线程的经典问题:哲学家用餐

线程安全

  • 实现线程安全的基本手段
    • 不可变类
      • Integer/String/....
    • synchronized 同步块
    • 同步块同步了什么东西?
      • synchronized(—个对象)把这个对象当成锁
      • Static synchronized方法把Class对象当成锁
      • 实例的synchronnized方法把该实例当成锁
    • Collections.synchronized
    • JUC包
      • Atomiclnteger/...
      • ConcurrentHashMap
        • 任何使用HashMap有线程安全问题的地方,都无脑地使用ConcurrentHashMap替换即可。
      • ReentrantLock 可重入锁
        • 可以使用tryLock()尝试获取锁。
        • 必须先获取到锁,再进入try {...}代码块,最后使用finally保证释放锁
        • 尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。

线程的生命周期和状态

状态名称 说 明
NEW 初始状态,线程被构建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
BLOCKED 阻塞状态,表示现成阻塞于锁
WAITING 等待状态,表示线程进入等待状态.进入该状态表示当前线程需要等待其他线程 做出一些特定动作(通知或中断)
TIME_WAITING 超时等待状态,该状态不同 WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,当前线程已经执行完毕

Object类里的线程方法

  • 线程的历史
    • Java从一开始就把线程作为语言特性,提供语言级的支持
  • 为什么Java中的所有对象都可以成为锁?
    • Object.wait()/notify()/notifyAII()方法
  • 线程的状态与线程调度

    【扩展】为什么说Java的线程

多线程的经典问题 生产者/消费者模型

  • 使用三种方法来解决它

    1. wait/notify/notifyAII (等待/通知机制)
      名称 说 明
      wait 使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒
      notify 随机唤醒等待队列中等待同一共享资源的 “一个线程”,并使该线程退出等待队列,进入可运行状态
      notifyAII 使所有正在等待队列中等待同一共享资源的 “全部线程” 退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现
    2. Lock/Condition

      • Lock()调用了的线程就持有了“对象监视器”
      • 使用Condition实现等待/通知:比wait()和notify()/notyfyAll()更灵活,比如可实现多路通知。
      • 调用condition.await()前须先调用lock.lock()获得同步监视器
    3. BIockingQueue(接口)
      • java.util.concurrent包,并发容器,基本上线程安全,容易死锁
      • take()对列空就等待,直到有元素可用,再从里面拿东西
      • put()队列满了就等待,直到有空间可用再往里面放东西
      • 建议使用链表的实现:LinkedBlockingQueue

ThreadLocal类:每个线程绑定自己的值

  • 覆写该类的initialValue()方法可以使变量初始化,从而解决get()返回null的问题
    InheritableThreadLocal类可在子线程中取得父线程继承下来的值。

线程池与 Callable/Future

  • 什么是线程池
    • 线程是昂贵的(Java线程模型的缺陷)
    • 线程池是预先定义好的若干个线程
    • Java中的线程池
  • Callable/Future
    • 类比Runnable,Callable可以返回值,抛出异常
    • Future代表一个“未来才会返回的结果
  • 实战:多线程的WordCount
点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注