java多线程学习(一)

多线程学习(一)

1.线程与进程概念和区别

  • 进程是正在执行的程序,它是线程的集合,一个进程可以有多个线程;而线程则是一条正在独立运行的执行路径,每个线程之间独立运行,互不影响
  • 每个线程拥有自己一整套变量,而线程则共享数据,共享变量使得线程之间的通信比进程之间的更加有效、便捷。此外,线程是“轻量级”,创建、撤销一个线程比启动新进程的开销要少得多

面试题:线程和进程的区别?

  • 进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
  • 线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
  • 特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间内存共享,这使多线程编程可以拥有更好的性能和用户体验
  • 注意:多线程编程对于其它程序是不友好的,占据大量 cpu 资源。

2.创建线程的方式

  1. 继承Thread类
  2. 实现Runnable接口
  3. 使用匿名内部类
  4. 继承Callable接口
  5. 使用线程池

3.同步、异步的概念与区别

迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章

4.守护线程和非守护线程

  • 守护线程:为其他线程提供服务,它是随主线程一起的,主线程结束它也结束。每个程序都有一个GC线程,它主要是回收垃圾资源。除此之外,我们平时用的软件的后台下载功能,也是基于守护线程实现的
  • 非守护线程:和主线程独立运行,二者互不影响,当主线程结束时,它也会接着运行

5.多线程的生命状态图

线程具有:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态,尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

在这里插入图片描述

6.join方法的使用

join:等待一段时间知道第二个线程结束才继续执行,可类比于生活中的插队。那它具体是怎么样用的呢?这里有个实例说明一下:

  • 线程A需要在线程B执行结束之后再执行,于是在线程A里调用线程B的join方法,便可实现该需求
  • 一句话概括,谁调用join方法,谁就是插队者

7.实现同步的两种方式

在多线程并发情况下,为了解决线程安全问题,都是采用序列化访问共享资源的方案,就意味着同一时刻只能允许一个任务访问共享资源,而实现这种同步方案,我们一般有两种实现方式

  • 加synchronized同步关键字-----》自动挡
  • 使用显示的Lock锁–jdk1.5 并发包中----》手动挡

那么这有一个问题?多线程操作全部变量时,会产生线程安全问题,那么同理,操作局部变量时,也会产生线程安全吗?答案是不会的。

同步机制的实现原理:(可以类比于多个人抢厕所的生活场景)

  • 1.多个进程争夺锁,当有一个线程拿到锁之后,其他线程必须等待,直到该线程释放掉锁
  • 2.该线程代码执行完或者程序抛出异常,锁被释放
  • 3.其他线程又开始争夺锁,重复上面的逻辑

同步机制的缺点:很影响性能,因为涉及到争夺锁、加锁、释放锁

同步函数(用的是this锁)、同步代码块、静态同步函数(Class锁),常见问题:

  • 如果一个线程用同步函数,另一个用同步代码块(不用this锁),是不能是实现同步的

8.多线程产生死锁的原因

同步中嵌套同步,线程之间占据锁资源,互不释放而又互相等待对方释放资源,于是乎,便出现了死锁

  • 死锁产生的必要条件:
    • 互斥条件:线程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个线程所占有。此时若有其他线程请求该资源,则请求线程只能等待。
    • 不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来释放(只能是主动释放)。
    • 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
    • 循环等待条件:存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个线程所请求。

9.多线程三大特性

原子性、可见性、有序性

10.volatile关键字

保证线程之间可见,但不保证原子性

11.原子类

java内部实现好了的针对基本数据类型操作线程安全的工具类

死磕 java原子类之终结篇(面试题)

12.sleep()和wait()的区别

  1. sleep()是属于Thread类的,而wait()是属于Object对象的
  2. sleep()会导致线程暂停执行指定的时间,让出cpu给其它进程,但是它的监控状态依旧保持着,当时间到了之后又会自动恢复运行状态
  3. 在调用 sleep()方法的过程中,线程不会释放对象锁
  4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

13.线程局部变量 ThreadLocal

  • ThreadLocal 的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
  • 每个线程调用全局 ThreadLocal 对象的 set 方法,在 set 方法中,首先根据当前线程获取当前线程ThreadLocalMap对象,然后往这个 map 中插入一条记录,key 其实是 ThreadLocal 对象,value 是各自的 set方法传进去的值。也就是每个线程其实都有一份自己独享的 ThreadLocalMap对象,该对象的 Key 是ThreadLocal对象,值是用户设置的具体值。在线程结束时可以调用 ThreadLocal.remove()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。

14.线程池

  1. 为什么要用线程池:

    • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
    • 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)
  2. 常用的三种线程池

    核心是走ThreadPollExecutor这个构造方法

    //创建固定大小的线程池
    ExecutorService fPool = Executors.newFixedThreadPool(3);
    //创建缓存大小的线程池
    ExecutorService cPool = Executors.newCachedThreadPool();
    //创建单一的线程池(这个线程死亡或者关闭后,还会创建一个新线程来继续完成工作)
    ExecutorService sPool = Executors.newSingleThreadExecutor();
    
  3. 线程的关闭

    • shutdown 只是将空闲的线程 interrupt() 了,shutdown()之前提交的任务可以继续执行直到结束。
    • shutdownNow 是 interrupt 所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部 ,是因为 interrupt()方法能力有限。
  4. 面试题:

    • 线程池的作用:

      • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
      • 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
      • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
    • 什么是线程池,如何使用:

      线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率。在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法。

      ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
      ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
      ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
      ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
      

      然后调用他们的 execute 方法即可。

    • 常用的线程池有哪些:

      • newSingleThreadExecutor:
      • newFixedThreadPool:
      • newCachedThreadPool:
      • newScheduledThreadPool:
      • newSingleThreadExecutor:
    • 线程池的启动策略:

      1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
      2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
        • a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
        • b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
        • c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
        • d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
      3. 当一个线程完成任务时,它会从队列中取下一个任务来执行
      4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

15.并发队列-阻塞队列(一)

常用的并发队列有阻塞队列和非阻塞队列,前者使用实现,后者则使用 CAS 非阻塞算法实现。

  • 阻塞队列 (BlockingQueue):是 Java util.concurrent 包下重要的数据结构,BlockingQueue 提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue 实现的。

  • BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。

  • BlockingQueue队列无法插入 null,会抛出一个NullPointerException

  • 阻塞队列提供了四种处理方法:

在这里插入图片描述

  • BlockingQueue 接口的实现类:

    • ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个
      数组里。
    • DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现
      java.util.concurrent.Delayed 接口。
    • LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如
      果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
    • PriorityBlockingQueue : PriorityBlockingQueue 是 一 个 无 界 的 并 发 队 列 。 它 使 用 了 和 类
      java.util.PriorityQueue 一 样 的 排 序 规 则 。 你 无 法 向 这 个 队 列 中 插 入 null 值 。 所 有 插 入 到
      PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于
      你自己的 Comparable 实现。
    • SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
  • 公平模式和非公平模式的模型:

    • 公平模式下,底层实现使用的是 TransferQueue 这个内部队列(先进先出),它有一个 head 和 tail 指针,用于指向当前正在等待匹配的线程节点。公平策略总结下来就是:队尾匹配队头出队
    • 非公平模式下,底层的实现使用的是TransferStack,一个(先进后出),实现中用 head 指针指向栈顶队尾匹配队尾出栈

16.并发队列-非阻塞队列(二)

  • 非阻塞队列:首先我们要简单的理解下什么是非阻塞队列:与阻塞队列相反,非阻塞队列的执行并不会被阻塞,无论是消费者的出队,还是生产者的入队。在底层,非阻塞队列使用的是 CAS(compare and swap)来实现线程执行的非阻塞。

  • 非阻塞队列简单操作:

    • 入队方法:
      • add():底层调用 offer();
      • offer():Queue 接口继承下来的方法,实现队列的入队操作,不会阻碍线程的执行,插入成功返回 true;出队方法:
      • poll():移动头结点指针,返回头结点元素,并将头结点元素出队;队列为空,则返回 null;
      • peek():移动头结点指针,返回头结点元素,并不会将头结点元素出队;队列为空,则返回 null;
  • 非阻塞算法 CAS:

    • 首先我们需要了解悲观锁和乐观锁

      • 悲观锁:假定并发环境是悲观的,如果发生并发冲突,就会破坏一致性,所以要通过独占锁彻底禁止冲突发生。有一个经典比喻,“如果你不锁门,那么捣蛋鬼就回闯入并搞得一团糟”,所以“你只能一次打开门放进一个人,才能时刻盯紧他”。
      • 乐观锁:假定并发环境是乐观的,即,虽然会有并发冲突,但冲突可发现且不会造成损害,所以,可以不加任何保护,等发现并发冲突后再决定放弃操作还是重试。可类比的比喻为,“如果你不锁门,那么虽然捣蛋鬼会闯入,但他们一旦打算破坏你就能知道”,所以“你大可以放进所有人,等发现他们想破坏的时候再做决定”。通常认为乐观锁的性能比悲观所更高,特别是在某些复杂的场景。这主要由于悲观锁在加锁的同时,也会把某些不会造成破坏的操作保护起来;而乐观锁的竞争则只发生在最小的并发冲突处,如果用悲观锁来理解,就是“锁的粒度最小”。但乐观锁的设计往往比较复杂,因此,复杂场景下还是多用悲观锁。首先保证正确性,有必要的话,再去追求性能。
    • CAS:乐观锁的实现往往需要硬件的支持,多数处理器都都实现了一个 CAS 指令,实现“Compare And Swap”的语义(这里的 swap 是“换入”,也就是 set),构成了基本的乐观锁。CAS 包含 3 个操作数:

      • 需要读写的内存位置 V
      • 进行比较的值 A
      • 拟写入的新值 B

      当且仅当位置 V 的值等于 A 时,CAS 才会通过原子方式用新值 B 来更新位置 V 的值;否则不会执行任何操作。无论位置 V 的值是否等于 A,都将返回 V 原有的值。

17.java 的内存模型

在java内存模型中,分为:线程本地内存和主存,线程本地内存保存了线程的运行时变量的信息。当线程访问某个对象的值的时候,可以分为以下几个步骤:

  1. 根据对象的引用找到在对应堆内存中的值
  2. 然后把值load到线程本地内存,建立一个copy
  3. 之后线程便不跟对主存中的变量值有瓜葛,而是直接操作本地内存里的copy的变量副本
  4. 在修改完变量副本之后的某一时刻(线程退出之前),自动把线程本地内存里的变量副本的值写回到主存中去,就这样完成了修改

在这里插入图片描述

18.synchronized 和 volatile 关键字的作用 和区别

一个共享变量(类的成员变量、类的静态成员变量)被volatile关键字修饰了之后,就具备了以下两个语义:

  • 保证了不同线程对该变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是
    立即可见的。(不从本地线程内存中进行操作,直接在主存中操作)
  • 禁止进行指令重排序

volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;而synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

区别:

  1. volatile 只能作用于变量,而synchronized 可以作用于变量、方法、和类
  2. volatile 只能保证变量修改的可见性,不能保证原子性;synchronized则都可以保证
  3. volatile 不会导致线程阻塞;synchronized会导致线程阻塞
  4. volatile 标记的变量不会被编译器优化;synchronized 标记的变量则会被优化

19.线程之间进行通讯

  1. 生产者–消费者问题:会出现线程安全问题,解决办法:
    • 生产者线程生产一个,消费者线程立马进行消费
    • 生产者没有任何生产,消费者不能消费
    • 消费者没有消费完,生产者不能生产
  2. wait()、notify/notifyAll() 的使用(要在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。)
    • wait():释放当前的锁,然后让出CPU,进入等待状态。直到另一个线程调用该对象的notify/notifyAll方法。
    • notify/notifyAll():唤醒一个或多个正处于等待状态的线程
    • notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。

21.jdk1.5并发包 - Lock锁

Lock锁于synchronized同步锁 的区别

  • Lock锁就好比于开车的手动挡,自己上锁,自己解锁,灵活度高,效率也会更高一点;而且自己可以定义程序出错时,如何抛异常。它也具有等待(await)、唤醒(signal)的方法,在Condition类里
  • Lock可以非阻塞式的获取锁;若Lock锁的代码中发生异常,则不会释放锁资源
  • 而synchronized同步锁就相当于自动挡,自动上锁、解锁,代码更加剪辑,可读性比Lock锁高

这里推荐一篇博文:Java锁机制了解一下

22.中断线程的方法

interrupt()、isInterrupted()、interrupted()

23.CountDownLatch、CyclicBarrier以及Semaphore

  • CountDownLatch:计数器,一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
  • CyclicBarrier:一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
  • Semaphore:其实和锁有点类似,它一般用于控制对某组资源的访问权限。它可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

具体使用可参考:CountDownLatch CyclicBarrier和 Semaphore

24.java锁机制

悲观锁、乐观锁、重入锁、读写锁、CAS无锁、自旋锁、排它锁

  • 悲观锁、乐观锁:上面有

  • 重入锁:会传递给下一个方法,重复进行使用,例如:ReenTrantLock,synchronizd;而非重入锁则是递归使用同步,会产生死锁

  • 读写锁:读–读可以共存,读–写和写–写都不能共存

  • CAS:上面有

  • 自旋锁:让CPU空转

推荐博文:通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其Java实现!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值