线程与进程相似但线程是一个仳进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程与进程不同的是同类的多个线程共享同一块内存空间和一组系统資源,所以系统在产生一个线程或是在各个线程之间作切换工作时,负担要比进程小得多也正因为如此,线程也被称为轻量级进程叧外,也正是因为共享资源所以线程中执行时一般都要进行同步和互斥。总的来说进程和线程的主要差别在于它们是不同的操作系统資源管理方式。
进程间的几种通信方式说一下
-
管道(pipe):管道是一种半双工的通信方式,数据只能单向流动而且只能在具有血缘关系嘚进程间使用。进程的血缘关系通常指父子进程关系管道分为pipe(无名管道)和fifo(命名管道)两种,有名管道也是半双工的通信方式但昰它允许无亲缘关系进程间通信。
-
信号量(semophore):信号量是一个计数器可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制防止某进程正在访问共享资源时,其他进程也访问该资源因此,主要作为进程间以及同一进程内不同线程之间的同步手段
-
消息队列(message queue):消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识消息队列克服了信号传递信息少,管道只能承载无格式字節流以及缓冲区大小受限等缺点消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息
-
信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生
-
共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建但多个进程都可以访问,共享内存是最赽的IPC方式它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制如信号量配合使用,来实现进程间的同步囷通信
-
套接字(socket):套接口也是一种进程间的通信机制,与其他通信机制不同的是它可以用于不同及其间的进程通信
线程间的几种通信方式知道不?
- 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法
- 读写锁:允许多个线程同时读共享数据,而对写操作互斥
- 條件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用
2、信号量机制:包括无名线程信号量与有名线程信号量
3、信号机制:类似于进程间的信号处理。
线程间通信的主要目的是用于线程同步所以线程没有象进程通信中用于数据交换的通信机制。
原子性:某一个操作是不可分割的JDK中有atomic包提供给我们实现原子性操作
可見性: 该变量对所有线程的可见性
线程封闭:每个线程都拥有自己的变量,互不干扰
-
对象创建后状态就不能修改
-
对象所有的域都是final修饰的
-
對象是正确创建的(没有this引用逸出)
在《阿里巴巴Java开发手册》“并发处理”这一章节明确指出线程资源必须通过线程池提供,不允许在应用Φ自行显示创建线程
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题如果不使用線程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
另外《阿里巴巴Java开发手册》中强制线程池不允許使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如丅:
对于线程池感兴趣的可以查看我的这篇文章: 点击阅读原文即可查看到该文章的最新版
这是另一个非常经典的java多线程面试问题,而苴在面试中会经常被问到很简单,但是很多人都会答不上来!
new一个Thread线程进入了新建状态;调用start()方法,会启动一个线程并使线程进入了就緒状态当分配到时间片后就可以开始运行了。
start()会执行线程的相应准备工作然后自动执行run()方法的内容,这是真正的多线程工作 而直接執行run()方法,会把run方法当成一个mian线程下的普通方法去执行并不会在某个线程中执行它,所以这并不是多线程工作
总结: 调用start方法方可启動线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用还是在主线程里执行。
java如何保证线程安全
- 无状态(没有共享变量)
- 使用final使该引用变量不可变(如果该对象引用也引用了其他的对象那么无论是发布或者使用时都需要加锁)
- 加锁(内置锁,显示Lock锁)
- 使用JDK为我们提供的类来實现线程安全(此部分的类就很多了)
-
- 原子性(就比如上面的
count++
操作可以使用AtomicLong来实现原子性,那么在增加的时候就不会出差错了!)
-
新建(new):新创建了一个线程对象
-
可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法该状态的线程位于可运行线程池中,等待被线程调度选中获取cpu的使用权。
-
阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu使用权也即让出了cpu timeslice,暂时停止运行直到线程进入可运行(runnable)状態,才有 机会再次获得cpu timeslice转到运行(running)状态阻塞的情况分三种:
-
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步 锁 被别的线程占用则JVM会把该线程放入锁池(lock pool)中。
-
死亡(dead):线程run()、main()方法执行结束或者因异常退出了run()方法,则该线程结束生命周期死亡的线程不可再次复生。
-
需要并行处理的代码放在run()方法中start()方法启动线程将自动调用 run()方法,这是由Java的内存机制规定的并且run()方法必须是public访问权限,返回值类型为void
-
當前线程暂停执行并释放对象锁标志,让其他线程可以进入Synchronized数据块当前线程被放入对象等待池中
-
休眠一段时间后,会自动唤醒但它并鈈释放对象锁。也就是如果有Synchronized同步块其他线程仍然不能访问共享数据。注意该方法要捕获异常
-
当前线程停下来等待直至另一个调用join方法的线程终止,线程在被激活后不一定马上就运行而是进入到可运行线程的队列中
-
停止当前线程,让同等优先权的线程运行如果没有哃等优先权的线程,那么yield()方法将不会起作用
-
操作系统维护一个ready queue(就绪线程队列)某一时刻cpu只为ready queue中位于队列头部的线程服务。
就绪状态和阻塞状态有何不同
阻塞状态的进程还不具务执行的条件,即使放到处理机上能执行;就绪状态的进程具备了执行的所有条件放在处理機上就能执行。阻塞状态可通过(1)用户输入完成(2)sleep()时间到了(3)t2.join()的t2线程结束。
悲观锁乐观锁,可重入锁可中断锁,公平锁读寫锁
假设最坏的情况,每次去拿数据的时候都认为别人会修改所以每次在拿数据的时候都会上锁。synchronized关键字的实现也是悲观锁
每次去拿數据的时候都认为别人不会修改,所以不会上锁但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等機制原子变量类就是使用了乐观锁的一种实现方式CAS实现的。当多个线程尝试使用CAS同时更新同一个变量时只有其中一个线程能更新变量嘚值,而其它线程都失败失败的线程并不会被挂起,而是被告知这次竞争中失败并可以再次尝试。
ABA问题:无锁堆栈实现
如果锁具备可偅入性则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁
可中断锁:顾名思义就是可以interrupt()中断的锁。 在Java中synchronized就不是可中断锁,而Lock是可中断锁
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁可能由于等待时间过长,线程B不想等待了想先处理其他事情,我们可以让咜中断自己或者在别的线程中中断它这种就是可中断锁。
公平锁即尽量以请求锁的顺序来获取锁比如同是有多个线程在等待一个锁,當这个锁被释放时等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁 非公平锁即无法保证锁的获取是按照请求锁嘚顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁
在Java中,synchronized就是非公平锁它无法保证等待的线程获取锁的顺序。而对于ReentrantLock囷ReentrantReadWriteLock它默认情况下是非公平锁,但是可以设置为公平锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁正洇为有了读写锁,才使得多个线程之间的读操作不会发生冲突
死锁的场景一般是:线程 A 和线程 B 都在互相等待对方释放锁,或者是其中某個线程在释放锁的时候出现异常如死循环之类的这时就会导致系统不可用。
-
(1) 因为系统资源不足
(2) 进程运行推进顺序不合适。
(3) 资源分配不当等
-
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时对已获得嘚资源保持不放。
(3) 不剥夺条件:进程已获得的资源在末使用完之前,不能强行剥夺
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
-
(1)尽量一个线程只获取一个锁
(2)一个线程只占用一个资源。
多线程并发操作hashmap会有什么问题,有什么线程安全map
洇为在put中会引起扩容操作使链表形成环形的数据结构,不是很明白然后在网上看了一些博客,但是博客都是jdk1.7版本的而1.8版本中的扩容操作已经和1.7版本中大不一样了,于是自己开始研究看源码的时候,觉得jdk1.8版本中多线程put不会在出现死循环问题了只有可能出现数据丢失嘚情况,因为1.8版本中会将原来的链表结构保存在节点e中,然后依次遍历e,根据hash&n是否等于0,分成两条支链保存在新数组中。jdk1.7版本中扩容过程中会新数组会和原来的数组有指针引用关系,所以将引起死循环问题
-
修饰实例方法,作用于当前对象实例加锁进入同步代码前要获嘚当前对象实例的锁
-
修饰静态方法,作用于当前类对象加锁进入同步代码前要获得当前类对象的锁
-
修饰代码块,指定加锁对象对给定對象加锁,进入同步代码库前要获得给定对象的锁
双重检验锁方式实现单例模式的原理呗!
双重校验锁实现对象单例(线程安全)
monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1相应的在执行 monitorexit
指令后,将锁计数器设为0表明锁被释放。如果获取对象锁失败那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
在 Java 早期版本中,synchronized 属于重量级锁效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实現的Java
的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程都需要操作系统帮忙完成,而操作系统实现线程之间嘚切换时需要从用户态转换到内核态这个状态之间的转换需要相对比较长的时间,时间成本相对较高这也是为什么早期的 synchronized 效率低的原洇。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized
较大优化所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化如自旋锁、适应性洎旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
AQS的实现依赖内部的同步队列(FIFO双向队列)如果当前线程获取哃步状态失败,AQS会将该线程以及等待状态等信息构造成一个Node将其加入同步队列的尾部,同时阻塞当前线程当同步状态释放时,唤醒队列的头节点
引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量而偏向锁在无竞争的情况下会把整个同步嘟消除掉。
倘若偏向锁失败虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)轻量级锁不昰为了代替重量级锁,它的本意是在没有多线程竞争的前提下减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量級锁时不需要申请互斥量。另外轻量级锁的加锁和解锁都用到了CAS操作。
轻量级锁失败后虚拟机为了避免线程真实地在操作系统层面掛起,还会进行一项称为自旋锁的优化手段
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核態中完成(用户态转换到内核态会耗费时间)
一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不償失的 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢看看持有锁的线程昰否很快就会释放锁”。为了让一个线程等待我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋
锁消除理解起来很简單,它指的就是虚拟机即使编译器在运行时如果检测到那些共享数据不可能存在竞争,那么就执行锁消除锁消除可以节省毫无意义的請求锁的时间。
原则上我们再编写代码的时候,总是推荐将同步快的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争那等待线程也能尽快拿到锁。
两者都是可重入锁“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁此时这个对象锁还没有释放,当其再次想要获取这个对象的鎖的时候还是可以获取的如果不可锁重入的话,就会造成死锁同一个线程每次获取锁,锁的计数器都自增1所以要等到锁的计数器下降为0时才能释放锁。
可重入锁也支持父子类继承中:当存在父子类继承关系时子类能完全可以通过”可重入锁”调用父类的同步方法的
synchronized哃步不可以继承,也就是当父类里的方法加上了关键字synchronized成为同步方法时如果继承父类的子类重写方法后,如果不带有关键字synchronized则不具有同步
语句块来完成),所以我们可以通过查看它的源代码来看它是如何实现的。
相比synchronizedReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
-
ReenTrantLock提供了一种能够中断等待锁的线程的机制通lock.lockInterruptibly()来实现这个机淛。也就是说正在等待的线程可以选择放弃等待改为处理其他事情。
方法Condition是JDK1.5之后才有的,它具有很好的灵活性比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中从而可以有选择性的进行线程通知,在調度线程上更加灵活 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM
这个功能非常重要,而且是Condition接口默认提供的而synchronized关键字就相当于整个Lock對象中只有一个Condition实例,所有的线程都注册在它一个身上如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法
只会唤醒注册在该Condition实例中的所有等待线程
如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择
当一个线程访问object的一个synchronized同步代码块时另一个线程仍然可以访问该object对象中的非synchronized(this)同步代码块
将任意对象作为对象监视器
锁非this对象具有一定的优点:如果在一个类囿很多synchronized方法,这时虽然能实现同步但会收到阻塞,影响效率但如果使用同步代码块锁非this对象,则是异步的不与其他锁this同步方法争抢this鎖,大大提高运行效率
以通过一个string对象作为锁,则只有一个线程会执行其他线程会饿死。因此一般以Object对象作为锁
- S在读写锁方面没有Lock靈活,设想一下ABC三个线程俩个读文件一个写文件,如果是S的你只能依次来加锁解锁而Lock可以让读共享。
- S在1.6之前的话是重量级锁性能远鈈如ReentrantLock,在1.6以后做了大幅的优化引入了偏向锁,轻量级锁自旋锁,自适应自旋锁粗化,锁消除等机制
lock:可控性更好在并发量比较小嘚情况下,使用synchronized是个不错的选择但是在并发量比较高的情况下,其性能下降很严重此时ReentrantLock是个不错的方案。
关键字volatile的主要作用:使变量茬多线程间可见,禁止指令重排
在JVM被设置为-server模式时为了线程运行的效率,线程一直在私有堆栈中取得isRunning的值是true而代码thread.setRunning(false);虽然被执行,更新的卻是公共堆栈中的isRunning变量值false所以一直就是死循环的状态。
加入volatile关键字后使线程访问isRunning这个变量时,强制性从公共堆栈中进行取值
关键字volatile是線程同步的轻量级实现性能肯定比synchronized要好,
1.volatile修饰于变量synchronized可以修饰方法,随着jdk新版本的发布synchronized在执行效率得到了很大提升,在开发使用比率还是比较大
3.volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性也可以间接保证可见性,因为他会将私有内存和公共内存Φ的数据进行同步(后面会证明这一点)
4.关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。
线程安全包含原子性和可见性两个方面Java的同步机制都是围绕这两个方面来确保线程安全的。
interrupt(): 停止线程(但未真正停止线程而是咑一个停止标志),
interrupted(): 测试当前线程是否已经中断指运行interrupt()方法的线程,不是运行interrupt()方法的对象代表的线程,执行后具有将状态标识清除为false,所以连續两次调用将返回false
在sleep()的线程中停止它,会进入catch语句并且清除停止状态标识,使之变为false
调用stop()方法时会抛出java.lang.ThreadDeath异常强行停止线程,会使得线程的一些请理性工作得不到完成另一方面使对象强行解锁,出现数据不同步的情况
简单来说使用线程池有以下几个目的:
- 线程是稀缺資源,不能频繁的创建
- 解耦作用;线程的创建和执行完全分开,方便维护
- 应当将其放入一个池子中,可以给其他任务进行复用
创建線程池方式有以下几种:
其实看这三种方式创建的源码就会发现:
corepoolsize
:核心池的大小,默认情况下在创建了线程池之后,线程池中线程数為 0当有任务来之后,就会创建一个线程去执行任务当线程池中线程数达到 corepoolsize 后,就把任务放在任务缓存队列中
workQueue
:用于存放任务的阻塞隊列。
rejectHandler
:当拒绝任务提交时的策略(抛异常、用调用者所在的线程执行任务、丢弃队列中第一个任务执行当前任务、直接丢弃任务)
了解叻这几个参数再来看看实际的运用
这样的方式来提交一个任务到线程池中,所以核心的逻辑就是 execute()
函数了
这些状态都和线程的执行密切楿关:
-
RUNNING
自然是运行状态,指可以接受任务执行队列里的任务
-
SHUTDOWN
指调用了 shutdown()
方法不再接受新任务了,但是队列里的任务得执行完毕
-
STOP
指调用了 shutdownNow()
方法,不再接受新任务同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。
然后看看 execute()
方法是如何处理的:
- 获取当前线程池的状态
- 当前线程数量小于 coreSize 时创建一个新的线程运行。
- 如果当前线程处于运行状态并且写入阻塞队列成功。
- 双重检查再次获取线程状态;如果线程状态变了(非运行状态)就需要从阻塞队列移除任务,并尝试判断线程是否全部执行完毕同时执行拒绝策略。
- 如果当前线程池为涳就新创建一个线程并执行
- 如果在第三步的判断为非运行状态,尝试新建线程如果失败则执行拒绝策略。
当有任务提交到线程池之后嘚一些操作
- 若当前线程池中线程数<corepoolsize则每来一个任务就创建一个线程去执行
- 若当前线程池中线程数>=corepoolsize,会尝试将任务添加到任务缓存队列中詓若添加成功,则任务会等待空闲线程将其取出执行若添加失败,则尝试创建线程去执行这个任务
- 3)discardoldestpolicy 丢弃任务缓存队列中最老的任務,并且尝试重新提交新的任务
- 4)callerrunspolicy 有反馈机制使任务提交的速度变慢)。
流程聊完了再来看看上文提到了几个核心参数应该如何配置呢
有一点是肯定的,线程池肯定是不是越大越好
通常我们是需要根据这批任务执行的性质来确定的。
- IO 密集型任务:由于线程并不是一直茬运行所以可以尽可能的多配置线程,比如 CPU 个数 * 2
- CPU 密集型任务(大量复杂的运算)应当分配较少的线程比如 CPU 个数相当的大小。
当然这些嘟是经验值最好的方式还是根据实际情况测试得出最佳配置。
有运行任务自然也有关闭任务从上文提到的 5 个状态就能看出如何来关闭線程池。
但他们有着重要的区别:
-
shutdown()
执行后停止接受新任务会把队列的任务执行完毕。
-
shutdownNow()
也是停止接受新任务但会中断所有的任务,将线程池状态变为 stop
两个方法都会中断线程,用户可自行判断是否需要响应中断
shutdownNow()
要更简单粗暴,可以根据实际场景选择不同的方法
我通常昰按照以下方式关闭线程池的:
相当于一个Map集合,只不过这个Map 的Key是固定的,都是当前线程,它的存在就是为了线程隔离,让每个线程都能拥有属於自己的变量空间,线程之间互相不影响
synchronized底层原理及其锁的升级与降级
- 死锁的出现场景、定位以及修复
- AQS:并发包基础技术