Java基础篇——线程、并发编程知识点全面介绍(面试、学习的必备索引)
原创不易,如需转载,请注明出处https://www.cnblogs.com/baixianlong/p/10739579.html,希望大家多多支持!!!
一、线程基础
1、线程与进程
- 线程是指进程中的一个执行流程,一个进程中可以运行多个线程。
- 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,即进程空间或(虚空间),比如一个qq.exe就是一个进程。
2、线程的特点
- 线程共享分配给该进程的所有资源
- 线程之间实际上轮换执行(也就是线程切换)
- 一个程序至少有一个进程,一个进程至少有一个线程
- 线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
- 线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程包含以下内容:
- 一个指向当前被执行指令的指令指针
- 一个栈
- 一个寄存器值的集合,定义了一部分描述正在执行线程的处理器状态的值
- 一个私有的数据区
3、线程的作用
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率(并发执行)
4、线程的创建
- 继承Thread类
- 实现Runnable接口
- 通过ThreadPool获取
5、线程的状态(生命周期)
-
创建:当用new操作符创建一个线程时。此时程序还没有开始运行线程中的代码
-
就绪:当start()方法返回后,线程就处于就绪状态
-
运行:当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法
-
阻塞:所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态
-
死亡:1、run方法正常退出而自然死亡;2、一个未捕获的异常终止了run方法而使线程猝死
6、线程的优先级、线程让步yield、线程合并join、线程睡眠sleep
- 优先级:线程总是存在优先级,优先级范围在1~10之间(数值越大优先级越高),线程默认优先级是5,优先级高的理论上先执行,但实际不一定。
- 线程让步:yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态(使用yield()的目的是让相同优先级的线程之间能适当的轮转执行)。
- 线程合并:保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。
- 线程睡眠:sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。
7、线程的分类(以下两者的唯一区别之处就在虚拟机的离开)
- 守护线程: thread.setDaemon(true),必须在thread.start()之前设置,GC线程就是一个守护线程。
- 普通线程
8、正确结束线程(给出一些方案)
- 使用Thread.stop()方法,但是该方法已经被废弃了,使用它是极端不安全的,会造成数据不一致的问题。
- 使用interrupt()方法停止一个线程,直接调用该方法不会终止一个正在运行的线程,需要加入一个判断语句才可以完成线程的停止。
- 使用共享变量的方式,在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。
二、线程同步
1、线程同步的意义
- 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏
2、锁的原理
- Java中每个对象都有一个内置锁,内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁。
- 当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
- 当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。
- 一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
- 释放锁是指持锁线程退出了synchronized同步方法或代码块。
3、锁和同步的理解
- 只能同步方法,而不能同步变量和类。
- 每个对象只有一个锁,当提到同步时,应该清楚在什么上同步,也就是说,在哪个对象上同步。
- 不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
- 如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
- 如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
- 线程睡眠时,它所持的任何锁都不会释放。
- 线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
- 同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
- 在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。
4、对象锁和类锁的区别
- 对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。
- 类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
5、线程的死锁及规避
- 死锁是线程间相互等待锁锁造成的,一旦程序发生死锁,程序将死掉。
- 如果我们能够避免在对象的同步方法中调用其它对象的同步方法,那么就可以避免死锁产生的可能性。
6、volatile关键字(推荐大家一片文章)
- 正确使用 volatile变量
- 与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。
- 如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile 代替 synchronized 来简化代码。
三、线程的交互
1、线程交互的基础知识
- void notify()——唤醒在此对象监视器上等待的单个线程。
- void notifyAll()——唤醒在此对象监视器上等待的所有线程。
- void wait()——导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法。
- void wait(longtimeout)——导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者超过指定的时间量。
- void wait(longtimeout, int nanos)——导致当前的线程等待,直到其他线程调用此对象的 notify()方法或 notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
2、注意点
- 必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。
- 当在对象上调用wait()方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用notify()时,并不意味着这时线程会放弃其锁。如果线程仍然在完成同步代码,则线程在移出之前不会放弃锁。因此,调用notify()并不意味着这时该锁变得可用。
四、线程池
1、好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
2、线程池的创建使用
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
}
- 参数介绍:
- corePoolSize:核心池的大小
- maximumPoolSize:线程池最大线程数
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止
- unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
- TimeUnit.DAYS; //天
- TimeUnit.HOURS; //小时
- TimeUnit.MINUTES; //分钟
- TimeUnit.SECONDS; //秒
- TimeUnit.MILLISECONDS; //毫秒
- TimeUnit.MICROSECONDS; //微妙
- TimeUnit.NANOSECONDS; //纳秒
- workQueue:一个阻塞队列,用来存储等待执行的任务
- threadFactory:线程工厂,主要用来创建线程
- handler:表示当拒绝处理任务时的策略,有以下四种取值:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
3、java默认实现的4中线程池(不建议使用)
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
- newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。
- newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
五、并发编程相关内容
1、synchronized 的局限性 与 Lock 的优点
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
2、Lock 和 ReadWriteLock
-
Lock接口,ReentrantLock(可重入锁)是唯一的实现类。
public interface Lock { void lock(); //lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。 //也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。 void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
-
ReadWriteLock接口,ReentrantReadWriteLock实现了ReadWriteLock接口。
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
- 读读共享
- 写写互斥
- 读写互斥
- 写读互斥
3、信号量(Semaphore)
-
介绍:Java的信号量实际上是一个功能完毕的计数器,对控制一定资源的消费与回收有着很重要的意义,信号量常常用于多线程的代码中,并能监控有多少数目的线程等待获取资源,并且通过信号量可以得知可用资源的数目等等。
-
特点:
- 以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
- 单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。
- 信号量解决了锁一次只能让一个线程访问资源的问题,信号量可以指定多个线程,同时访问一个资源。
-
分为公平模式和非公平模式(默认非公平)
public Semaphore(int permits) { sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
区别在于:公平模式会考虑是否已经有线程在等待,如果有则直接返回-1表示获取失败;而非公平模式不会关心有没有线程在等待,会去快速竞争资源的使用权。
说到竞争就得提到AbstractQueuedSynchronizer同步框架,一个仅仅需要简单继承就可以实现复杂线程的同步方案,建议大家去研究一下。
4、闭锁(CountDownLatch)
-
介绍:闭锁是一种同步工具,可以延迟线程的进度直到终止状态。可以把它理解为一扇门,当闭锁到达结束状态之前,这扇门一直是关闭的,没有任何线程可以通过。当闭锁到达结束状态时,这扇门会打开并允许所有线程通过,并且闭锁打开后不可再改变状态。
闭锁可以确保某些任务直到其他任务完成后才继续往下执行。 -
使用介绍
- 构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。
- 与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。
- 其他N 个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调 用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。
4、栅栏(CyclicBarrier)
-
介绍:栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。
-
CyclicBarrier和CountDownLatch的区别:
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;
- CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
- CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
5、原子量 (Atomic)
-
介绍:Atomic一词跟原子有点关系,后者曾被人认为是最小物质的单位。计算机中的Atomic是指不能分割成若干部分的意思。如果一段代码被认为是Atomic,则表示这段代码在执行过程中,是不能被中断的。通常来说,原子指令由硬件提供,供软件来实现原子方法(某个线程进入该方法后,就不会被中断,直到其执行完成)
-
特性:在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。
-
注意:原子量虽然可以保证单个变量在某一个操作过程的安全,但无法保证你整个代码块,或者整个程序的安全性。因此,通常还应该使用锁等同步机制来控制整个程序的安全性。
6、Condition
- 介绍:在Java程序中,任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object类上),主要包括wait()、wait(long)、notify()、notifyAll()方法,这些方法与synchronized关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有区别的。
- Condition与Object中的wati,notify,notifyAll区别:
- Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。
不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。 - Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。如果采用Object类中的wait(),notify(),notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程",而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。
- Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。
7、并发编程概览
六、总结
- 到此线程的基本内容介绍就差不多了,这篇文章偏理论一些,每个知识点的介绍并不全面。
- 大家可以以此篇文章为索引,来展开对并发编程的深入学习,细细咀嚼每个知识点,相信你会有巨大的收获!!!
个人博客地址:
cnblogs:https://www.cnblogs.com/baixianlong
csdn:https://blog.csdn.net/tiantuo6513
segmentfault:https://segmentfault.com/u/baixianlong
github:https://github.com/xianlongbai