java里的多线程总结

一.多线程:

  • 多线程
    • 用空间(cpu占用率)换时间(响应时间),期望以更多的资源消耗换取更快的响应时间的技术
  • 线程和进程的区别
    • 进程:是程序的一次执行,会向系统索取资源,进程之间内存空间是独立的
    • 线程:是进程内部的最小执行单元,线程之间堆内存是共享的,栈内存是独立的。
  • 创建多线程的几种方式
    • 继承Thread类,重写run方法
    • 实现Runnable接口以及run方法
      • 创建一个类实现Runnable接口
      • 创建匿名内部类实现Runnable接口
      • 通过lambdas表达式实现Runnable接口
    • 实现Callable接口,通过创建FutureTask,包装到线程中执行
      • FutureTask本质上也是Runnable
  • 线程之间的通信
    • 一个线程执行,另一个线程可以睡眠等待
      • 缺点:不好确定睡眠时间
    • 由于jvm中至少有两个线程,一个主线程,一个垃圾回收线程,所以可以通过一个while循环判断当前jvm中激活的线程数量是否为2判断线程是否执行完毕
      • 缺点:如果当前系统比较复杂,线程数量比较多,用途也多样,不好确定剩下多少个线程是合适的,测试时候使用
    • 通过CountDownLatch解决(以后会详细讲)
      • 优点:可以解决一个线程甚至多个线程的通信
      • 优点:不需要while不断尝试,节省系统资源
      • 缺点:拿不到线程的执行结果
    • 通过实现Callable接口,以及FutureTask的异步通知来实现
      • 优点:可以解决一个线程等待另一个线程的执行结果后再执行
      • 优点:可以拿到另一个线程的执行结果
      • 优点:不需要while不断尝试,节省系统资源
      • 缺点:FutureTask只能被一个线程执行
  • 用户线程和守护线程
    • 用户线程会不随着主线程终止而终止,一般的线程都是用户线程
    • 守护线程会随着主线程终止而终止,可以通过Thread#setDaemon(true)将用户线程转化为守护线程

二.线程安全

  • 什么是线程安全
    • 一段代码如果在多线程执行的情况下可能发生和预期结果不一致的情况,就说代码不是线程安全的,线程安全的问题通常都伴随着多线程对共享的变量的访问。
  • 解决线程安全
    • 加锁
      • 加synchronized同步锁
        • synchronized锁的是对象
        • 特点
          • 独占:被synchronized加锁的区域只能被一个线程访问
          • 可重入:同一个线程获取到了synchronized锁以后可以再次获取同一把锁
          • 非公平:被阻塞的线程都有可能竞争到锁,并不是谁等待时间就谁先获取锁
        • 同步方法的锁对象
          • 如果是非静态方法加synchronized锁,锁的是this对象
          • 如果是静态方法加synchronized锁,锁的是当前类的class对象
      • 加lock锁(以后详细讲)
  • 死锁
    • 产生的原因:多个线程互相等待对方释放锁
    • 解决办法:不要同步中嵌套同步
    • 生产环境发生了死锁如何定位:到讲jvm的时候会系统说明

三.线程状态

  • 什么是线程状态
    • 从线程创建到死亡的生命周期内可能存在一系列状态
      • new:线程刚被创建,还没有start运行
      • Runnable:线程是可运行的
        • ready:线程准备就绪,但是没有运行,cpu没有调度到该线程
        • running:线程正在运行当中
        • 由于以上两个状态切换十分的频繁,所以统称为Runnable
      • Blocked:线程被阻塞,线程尝试获取一把锁,但是获取不到
      • waiting:程会一直等待,知道外部给予唤醒才会继续执行
        • 调用锁对象的wait方法
          • 当同一把锁对象调用了notify或者notifyAll会唤醒一个/多个同一把锁waiting的线程
        • join
        • LockSupport.park():挂起线程
          • LockSupport.unpark():唤醒线程
      • timed_waiting:计时等待
        • 执行了Thread.sleep(time)
          • 线程会等待time指定的时间单位毫秒,时间到了被唤醒继续执行
        • 锁对象.wait(time)
          • 线程会等待time指定的时间,并释放锁,时间到了以后获取到锁才能继续执行
        • join(time)
          • 时间到了以后继续执行
      • TERMINATED:线程被终止了
        • run方法执行完毕
  • 锁对象调度线程的方法
    • wait():让线程释放锁并进入waiting状态
    • notify():唤醒一个处于同一把锁的waiting状态下的线程被
    • notifyAll():唤醒多个处于同一把锁的waiting状态下的线程被
  • wait和sleep的区别
    • sleep属于Thread类,wait属于Object
    • sleep通常只是让当前线程停止一段时间但是不会释放锁;wait方法让线程无线等待或者计时等待,会释放锁,当线程被唤醒必须获取到锁之后才能继续执行。
  • 线程停止
    • 线程是一个伺服线程,就是一个while循环,一般来说需要给线程一个退出while的标识
      • 自定义退出标识
        • 线程每次循环都要判断一下标识是否符合条件,当外部改变了标识之后会让线程停止运行
      • 使用线程内部的退出标识
        • 线程正常运行
          • 通过判断Thread.currentThread().isInterrupted()如果返回true,说明线程已经在外部被停止了,break跳出即可
        • 线程处于阻塞状态(waiting,timed_waiting,blocking)
          • 线程会抛出异常,捕获异常之后,break跳出
        • 相比于自定义标识来说,好处就是当线程处于阻塞状态下依然可以立刻响应退出,不需要等待

四.线程其他方法

  1. 设置优先级

    • 可以设置线程执行的优先级setPriority(),值得范围是1-10,默认值是5
    • 优先级是一个相对的概念,如果一个线程的优先级高于另一个,cpu调度到高优先级的线程的概率更大,从宏观角度来说(给它一个执行的时间),高优先级的线程执行的次数更多
  2. 线程join方法

    • 可以让并行执行的线程变成串行执行,也可以设置一个时间,时间到了以后等待的线程继续执行
    • ex:
      • 线程B在执行的时候调用线程A.join,线程A先执行,执行完了在执行线程B
      • 线程B在执行的时候调用线程A.join(1000),线程A先执行1s,无论1s之后线程A是否执行完毕,都会执行线程B
  3. yield方法

    • 可以将当前状态从running->ready,但是可能没有效果,因为让步的线程可能再次被cpu选中继续执行

五。线程安全的三个特性

  1. 多线程在执行过程中,如果对共享变量的访问不能满足三个特性中的任意一个就说代码不是线程安全的。
    1. 原子性:线程在执行一段代码的过程中不能被其他线程所干扰
      1. i++为什么不是线程安全的
        • 因为i++实际执行过程中有三个步骤
          • 将i读到副本变量
          • 修改i+1
          • 写回主内存
        • 由于在执行过程中有可能一个线程在执行到第三步之前被其他线程进来执行了,导致执行了两次结果只加了1
    2. 可见性:一个线程修改了共享变量的值能够立刻被其他线程所看见
      1. 和jvm内存模型有关(副本变量)
    3. 有序性:代码指令在执行过程中可能会发生指令重排,重排结果和单线程下顺序执行的结果是一致的,但是多线程的情况下不能保证是一致的,这也是多线程执行过程中由于有序性导致线程安全问题发生的根源。

六.volatile

  1. 解决可见性的问题
    • 对volatile修饰的变量的操作要遵循jmm语法规范
      • 在发生了volatile写操作后会立刻将副本变量的值更新到主内存
      • 会让其他cpu缓存区域的该变量的副本变量的值无效(通过缓存一致性协议实现)
  2. 内存屏障防止指令重排
    • 在写操作之后会有一个store屏障,读操作之前有一个load屏障
    • 屏障的作用防止指令越过屏障发生指令重排,保证即使发生指令重排对volatile修饰的变量是没有任何影响的。
  3. volatile的使用场景
    • 只要变量的操作满足原子性就可以使用volatile保证线程安全
    • 原子性操作
      • 除了lang和double以外的基本类型赋值操作
      • 引用类型的赋值操作
      • atomic包下的原子类的方法都是原子操作
      • cas是原子操作
  4. volatile和synchronized对比
    • 共同点:都遵循了jmm语法集规范
    • 不同点:
      • volatile不会引起上下文切换,资源消耗低;synchronized会引起上下文切换,资源消耗高
      • volatile能保证可见性和一定程度的有序性,synchronized保证原子性,有序性,可见性

七.synchronized

  1. 解决可见性的问题

    • jmm语法集规范对synchronized有如下规范
      • 在进入synchronized的时候会将副本变量清空
      • 在出synchronized的时候会将副本变量修改后的值刷到主内存
  2. synchronized同步原理

    1. 依靠对锁对象monitor和markword访问配合完成
    2. 从无锁状态->偏向锁->轻量级锁->重量级锁随着竞争的激烈程度不断升级,锁一旦升级就不能降级
  3. 锁优化

    1. 偏向锁

      • 一个线程尝试获取锁对象的时候,如果锁标志位01,表示无锁/偏向锁
      • 通过cas操作修改偏向锁标志位从0->1,同时修改偏向锁线程id从null->自己的id
        • 如果失败,就说明之前有其他线程获取到了偏向锁,意味着发生了锁竞争,当前的锁对象的锁会升级到轻量级锁
        • 如果成功,当前线程就获取到该锁对象的偏向锁,可以继续执行
        • 如果重入(偏向锁的线程id是当前线程),就继续执行不做任何操作
    2. 轻量级锁

      • 将markword拷贝到当前线程的栈内存空间,通过cas尝试将锁对象的markword中部分内容修改为当前线程的副本markword内存地址
        • 如果成功,就修改锁标志位00,继续执行
        • 如果失败,开始自旋
    3. 适应性自旋锁

      • 不断的重复轻量级锁的过程,如果失败次数达到最大允许自旋次数,就会把线程挂起
      • 每失败一次,都会减少最大允许自旋次数
      • 每成功一次,都会增加最大允许自旋次数
      • 如果线程自旋多次自选失败,最大允许自旋次数减到0,就会升级到重量级锁
    4. 重量级锁

      1. 线程会将markword的锁标志位设置为10
      2. 将markword的部分内容指向monitor
      3. 尝试将自己线程的id设置为monitor的owner,表示当前线程获取到锁
      4. 如果重入会累计重入次数到Nest。
      5. 重量级锁的过程会导致线程从用户态切换到核心态,以获取更高的访问操作系统的权限,但这个切换动作需要传递大量的参数,非常消耗资源
    5. 锁消除

      1. 当jvm检测到锁只会被一个线程锁访问,就会将锁消除
    6. 锁粗化

      1. 当jvm检测到连续的加锁解锁操作,会将连续的操作和并为一个更大范围的加锁解锁操作
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值