多线程学习(一)
1.线程与进程概念和区别
- 进程是正在执行的程序,它是线程的集合,一个进程可以有多个线程;而线程则是一条正在独立运行的执行路径,每个线程之间独立运行,互不影响
- 每个线程拥有自己一整套变量,而线程则共享数据,共享变量使得线程之间的通信比进程之间的更加有效、便捷。此外,线程是“轻量级”,创建、撤销一个线程比启动新进程的开销要少得多
面试题:线程和进程的区别?
- 进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独立单位。
- 线程:是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。
- 特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间内存共享,这使多线程编程可以拥有更好的性能和用户体验
- 注意:多线程编程对于其它程序是不友好的,占据大量 cpu 资源。
2.创建线程的方式
- 继承Thread类
- 实现Runnable接口
- 使用匿名内部类
- 继承Callable接口
- 使用线程池
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内部实现好了的针对基本数据类型操作线程安全的工具类
12.sleep()和wait()的区别
- sleep()是属于Thread类的,而wait()是属于Object对象的
- sleep()会导致线程暂停执行指定的时间,让出cpu给其它进程,但是它的监控状态依旧保持着,当时间到了之后又会自动恢复运行状态
- 在调用 sleep()方法的过程中,线程不会释放对象锁
- 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
13.线程局部变量 ThreadLocal
- ThreadLocal 的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
- 每个线程调用全局 ThreadLocal 对象的 set 方法,在 set 方法中,首先根据当前线程获取当前线程ThreadLocalMap对象,然后往这个 map 中插入一条记录,key 其实是 ThreadLocal 对象,value 是各自的 set方法传进去的值。也就是每个线程其实都有一份自己独享的 ThreadLocalMap对象,该对象的 Key 是ThreadLocal对象,值是用户设置的具体值。在线程结束时可以调用 ThreadLocal.remove()方法,这样会更快释放内存,不调用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。
14.线程池
-
为什么要用线程池:
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
- 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)
-
常用的三种线程池
核心是走ThreadPollExecutor这个构造方法
//创建固定大小的线程池 ExecutorService fPool = Executors.newFixedThreadPool(3); //创建缓存大小的线程池 ExecutorService cPool = Executors.newCachedThreadPool(); //创建单一的线程池(这个线程死亡或者关闭后,还会创建一个新线程来继续完成工作) ExecutorService sPool = Executors.newSingleThreadExecutor(); -
线程的关闭
- shutdown 只是将空闲的线程 interrupt() 了,shutdown()之前提交的任务可以继续执行直到结束。
- shutdownNow 是 interrupt 所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部 ,是因为 interrupt()方法能力有限。
-
面试题:
-
线程池的作用:
- 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
-
什么是线程池,如何使用:
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 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:
-
线程池的启动策略:
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
- a. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- b. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
- c. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务;
- d. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程无事可做,超过一定的时间(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 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
- ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个
-
公平模式和非公平模式的模型:
- 公平模式下,底层实现使用的是 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内存模型中,分为:线程本地内存和主存,线程本地内存保存了线程的运行时变量的信息。当线程访问某个对象的值的时候,可以分为以下几个步骤:
- 根据对象的引用找到在对应堆内存中的值
- 然后把值load到线程本地内存,建立一个copy
- 之后线程便不跟对主存中的变量值有瓜葛,而是直接操作本地内存里的copy的变量副本
- 在修改完变量副本之后的某一时刻(线程退出之前),自动把线程本地内存里的变量副本的值写回到主存中去,就这样完成了修改

18.synchronized 和 volatile 关键字的作用 和区别
一个共享变量(类的成员变量、类的静态成员变量)被volatile关键字修饰了之后,就具备了以下两个语义:
- 保证了不同线程对该变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是
立即可见的。(不从本地线程内存中进行操作,直接在主存中操作) - 禁止进行指令重排序
volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;而synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
区别:
- volatile 只能作用于变量,而synchronized 可以作用于变量、方法、和类
- volatile 只能保证变量修改的可见性,不能保证原子性;synchronized则都可以保证
- volatile 不会导致线程阻塞;synchronized会导致线程阻塞
- volatile 标记的变量不会被编译器优化;synchronized 标记的变量则会被优化
19.线程之间进行通讯
- 生产者–消费者问题:会出现线程安全问题,解决办法:
- 生产者线程生产一个,消费者线程立马进行消费
- 生产者没有任何生产,消费者不能消费
- 消费者没有消费完,生产者不能生产
- 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空转

被折叠的 条评论
为什么被折叠?



