欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

带你快速搞定java多线程(3)

程序员文章站 2023-01-16 13:55:47
目录一、锁的概念先来聊聊这几个概念,总不能聊起来的时候啥也不知道,只知道干活也没有用。公平锁:当线程a获取访问该对象,获取到锁后,此时内部存在一个计数器num+1,其他线程想访问该对象,就会进行排队等...

一、锁的概念

先来聊聊这几个概念,总不能聊起来的时候啥也不知道,只知道干活也没有用。

公平锁:当线程a获取访问该对象,获取到锁后,此时内部存在一个计数器num+1,其他线程想访问该对象,就会进行排队等待(等待队列最前一个线程处于待唤醒状态),直到线程a释放锁(num = 0),此时会唤醒处于待唤醒状态的线程进行获取锁的操作,一直循环。如果线程a再次尝试获取该对象锁时,会检查该对象锁释放已经被占用,如果还是当前线程占用锁,则直接获得锁,不用进入排队。

非公平锁:当线程a在释放锁后,等待对象的线程会进行资源竞争,竞争成功的线程将获取该锁,其他线程继续睡眠。

公平锁是严格的以fifo的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,所以非公平锁可能会有“饥饿”的问题。但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统的影响是比较大的,所以非公平锁的吞吐量是大于公平锁的,这也是为什么jdk将非公平锁作为默认的实现。

悲观锁:总是假设最坏的情况,每次想要使用数据的时候就恰好别人也要修改数据,一切是以安全第一,所以在每次操作资源的时候都会先加锁,不管有没有人抢,然后独占资源。java中synchronized和reentrantlock等独占锁就是悲观锁思想的实现

乐观锁:乐观锁和悲观锁刚好相反,假定自己使用资源的时候没有人抢,所以不需要上锁。乐观锁的实现方案一般来说有两种:版本号机制 和 cas实现 。下期可能会讲。

在java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式cas实现的。

二、synchronized 的使用方式

场景 具体分类 锁对象 代码示例
修饰方法 实例方法 当前实例对象 public synchronized void method () { ... }
... 静态方法 当前类的class对象 public static synchronized void method () { ... }
修饰代码块 代码块 ( )中配置的对象 synchronized(object) { ... }

三、synchronized 的实现原理列

想知道原来先去底层看下,看看字节码是什么样子的,let's go!

  private static object lock = new object();
  public static synchronized void testsyn() {
      system.out.println("香菜");
  }
  public synchronized void testsyn2() {
      system.out.println("香菜");
  }
  public static void testobj() {
      synchronized (lock) {
          system.out.println("香菜");
      }
  }

看下字节码:

带你快速搞定java多线程(3)

可以看到synchronized 的地方使用的是monitorenter指令,每个对象都和一个monitor对象关联,主要用来控制互斥资源的访问,如果你想要加锁必须先获得monitor的批准,如果现在正有线程访问,会把申请的线程加入到等待队列。

小结

1、 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对class对象的锁,该类所有的对象同一把锁。2、每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。

3、实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制,避免做嵌套synchronized 的使用。

4、synchronized 要尽量控制范围,不能范围太大,否则会损失系统性能。

四、线程池是什么

线程池就是一个对象持有一堆线程,举个例子就是饿了么养的骑手团队。线程池就是这个团队,每个骑手都是一个线程。

五、为什么要用线程池?

假如现在商家有外卖单子,需要骑手去送单,这个时候的外卖任务就会派单给骑手,为什么要用线程池呐?

有几个好处,第一就是骑手的招聘是有成本的,等你有了外卖订单再去招聘,来不及了,不如平常养一些骑手,线程的创建和销毁的开销是巨大的。

第二就是不能一个单子来了就来一个骑手,这样的话骑手的数量很难控制,对于派单来说也存在很大的压力,会造成整个骑手团队的崩溃,对应的就是可以通过线程池控制系统内的线程数量,有效的避免大量的线程池争夺cpu资源而造成堵塞。

第三如果养了一个骑手团队,这样在骑手的管理上可以规范,以便提供更好的外卖服务,比如这种外卖超时,骑手打星等。对比线程池就是线程池可以提供定时、定期、单线程、并发数控制等功能。

六、看下类图,从整体上理解下

带你快速搞定java多线程(3)

七、线程池的创建

带你快速搞定java多线程(3)

线程池主要使用的四种

固定数量的线程池(fixedthreadpool

定时线程池(scheduledthreadpool

可缓存线程池(cachedthreadpool

单线程化线程池(singlethreadexecutor

八、线程池核心参数说明

首先看下如何构造一个线程池

  public static executorservice newfixedthreadpool(int nthreads, threadfactory threadfactory) {
      return new threadpoolexecutor(nthreads, nthreads,
                                    0l, timeunit.milliseconds,
                                    new linkedblockingqueue<runnable>(),
                                    threadfactory);
      public threadpoolexecutor(int corepoolsize,
                            int maximumpoolsize,
                            long keepalivetime,
                            timeunit unit,
                            blockingqueue<runnable> workqueue,
                            threadfactory threadfactory,
                            rejectedexecutionhandler handler)

核心参数说明:

带你快速搞定java多线程(3)

九、几个疑问点

9.1、是怎么保证线程不销毁的?

核心线程会阻塞等待workqueue

9.2 提交任务有哪几种方式?

带你快速搞定java多线程(3)

9.3 拒绝策略都有哪些?

拒绝策略(handler)当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现rejectedexecutionhandler接口,并实现rejectedexecution(runnable r, threadpoolexecutor executor)方法。不过executors框架已经为我们实现了4种拒绝策略:

abortpolicy(默认):丢弃任务并抛出rejectedexecutionexception异常。

callerrunspolicy:由调用线程处理该任务。

discardpolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。

discardoldestpolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

9.4 线程池的关闭

关闭线程池可以调用shutdownnow和shutdown两个方法来实现

shutdownnow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。

shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。

9.5 初始化线程池时线程数的选择

如果任务是io密集型,一般线程数需要设置2倍cpu数以上,以此来尽量利用cpu资源。

如果任务是cpu密集型,一般线程数量只需要设置cpu数加1即可,更多的线程数也只能增加上下文切换,不能增加cpu利用率。

具体问题具体分析。

十、总结

线程池是项目中常用的,需要理解线程池的应用场景和构造函数,正确的使用线程池。

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注的更多内容!

相关标签: java 多线程