06-并发编程

什么是并发编程?
	并发编程是指多个任务在同一时间段内同时执行。

为什么要用并发编程?
 	并发编程将多核CPU的计算能力发挥到极致,性能得到提升。

并发编程的缺点:
 	CPU不断切换线程造成性能损耗;会出现线程安全问题。
 	解决方案:
	  1. 无锁并发编程:ConcurrentHashMap锁分段,减少上下文切换时间。
	  2. CAS算法:使用乐观锁,减少不必要的锁竞争带来的上下文切换。
	  3. 使用最少线程:避免创建不需要的线程。
	  4. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

一、多线程

1、线程与进程

进程:指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程至少有一个线程。

2、线程调度

 1. 分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
 2. 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个线程调度。

对于单核CPU,某个时刻只能执行一个线程。多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

3、同步与异步

同步:排队执行,效率低但是安全
异步:同时执行,效率高但是不安全

4、并发与并行

并发:指两个或多个事件在同一个时间段内发生
并行:指两个或多个时间在同时发生

5、守护线程

线程分守护线程和用户线程,守护线程需要等全部用户线程结束才能结束。比如GC线程就是守护线程。

6、线程的状态

 1. NEW:尚未启动的线程
 2. RUNNABLE:在Java虚拟机中执行的线程
 3. BLOCKED:被阻塞等待监视器锁定的线程
 4. WAITING:正在等待另一个线程执行特定动作的线程
 5. TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程
 6. TERMINATED:已退出的线程

7、创建线程的方式

 1. 继承Thread类:
	- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
​	- 创建Thread子类的实例,即创建了线程对象。	
​	- 调用线程对象的start()方法来启动该线程。

 2. 实现Runnable接口:
	- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
​	- 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
​	- 调用线程对象的start()方法来启动该线程。

 3. 实现Callable接口:
	- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
​	- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
​	- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
​	- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

8、Runnable和Callable的区别

 1. Runnable接口中的run()的返回值是void,它做的事情只是纯粹地去执行run()中的代码。
​ 2. Callable接口中的call()是有返回值并且可以抛出异常,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

9、sleep和wait的区别

 1. sleep():是Thread类的静态方法,让调用线程进入睡眠状态,让出执行机会给其它线程,等到睡眠结束后进入就绪状态和其它线程一起竞争CPU的执行时间片。因为是静态方法,不能改变对象的锁,所以线程虽然进入阻塞状态,但是不会释放持有当前对象的锁,其它线程依然无法方法该对象。
 2. ​wait():是Object类的方法,当线程调用wait()会进入到阻塞状态并且释放当前持有对象的锁,需要通过notify()或notifyAll()唤醒。

10、notify和notifyAll的区别

 1. notify()只会唤醒一个等待线程让其进入锁池,去竞争当前对象的锁。
 2. notifyAll()会唤醒所有线程,它们都会进入到锁池,并且竞争当前对象的锁,没有竞争到的线程会一直留在锁池中,只有再次调用wait()才会进入到等待池中。

​ 注意:优先级高的线程竞争到对象锁的概率大。

11、run和start的区别

 1. 每个线程都是通过某个特定的Thread对象所对应的run()来完成其操作的,run()称为线程体。通过调用Thread类的start()来启动一个线程。
 2. ​start()用来启动一个线程,正真实现了多线程运行。此时无需等待run()方法体代码执行完毕,可以直接继续执行下面的代码。
 3. run()只是线程里的一个函数,而不是多线程的。如果直接调用run(),相当于调用一个普通方法。

12、四种线程池

 1. 缓存线程池:
	newCachedThreadPool():创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
 2. 定长线程池:
	newFixedThreadPool(int nThreads):创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
 3. 单线程线程池:
	newSingleThreadExecutor():这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
 4. 周期定长线程池:
	newScheduledThreadPool(int corePoolSize):创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

13、线程池的状态

 1. RUNNING状态:线程池的初始化状态是RUNNING,能够接收新任务,以及对已添加的任务进行处理。
 2. SHUNDOWN状态:不接收新任务,但能处理已添加的任务。调用线程池的shutdown(),线程池由RUNNING→SHUNDOWN
 3. STOP状态:不接收新任务,不处理已添加的任务,中断正在处理的任务。调用线程池的shutdownNow(),线程池由RUNNING→STOP
 4. TIDYING状态:当所有的任务已终止,ctl记录的任务数量为0,线程池就会变成TIDYING,会执行钩子函数terminated()。当线程池在SHUNDOWN状态下,阻塞队列和线程池中执行的任务都为空时,线程池由SHUNDOWN→TIDYING;当线程池在STOP状态下,线程池由STOP→TIDYING。
 5. TERMINATED状态:线程池彻底终止。线程池处在TIDYING状态,执行完terminated()后,TIDYING→TERMINATED。

14、submit和execute的区别

 1. submit()有返回值,方便Exception处理,一般用来执行Callable。
​ 2. execute()没有返回值,一般用来执行Runnable。

二、线程安全

1、原子性、可见性、有序性

为了保证并发程序的正确执行,必须确保原子性、可见性、有序性。
	1. 原子性:一个或多个操作要么全部执行,并且在执行过程中不会被任何因素打断,要么都不执行。
	2. 可见性:多个线程同时访问共享数据时,某个线程修改了这个变量,其它线程能够立即看到修改的值。
	3. 有序性:有顺序的执行,遵循happens-before原则。

确保原子性、可见性、有序性的原因:
	1. 由于并发编程,多个任务同时执行,当某个任务没有执行完就被挂起,另外的线程开始执行就会出现数据不一致的情况。
	2. 在多核CPU中,每个线程可能运行在不同的CPU中,不同线程的内存空间不同,在线程运行的时候会将主存中的共享数据复制一个副本到高速缓存中,多个线程之间对共享变量的修改没有即使刷新到主存中,造成数据不一致的情况。
	3. 由于JVM虚拟机会进行指令重排序。

怎么确保原子性、可见性、有序性:
	1. 确保原子性:Atomic原子类,sychronized关键字加锁,Lock锁。
	2. 确保可见性:volatile关键字,sychronized关键字,Lock锁。
	3. 确保有序性:(volatile),JMM(Java内存模型)的happens-before原则,synchronized关键字加锁,Lock锁。

happens-before原则:
	1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
	2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
	3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
	4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
	5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
	6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
	7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
	8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

2、死锁

死锁是指两个或两个以上线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。

3、防止死锁

形成死锁需要同时满足以下四个条件,所有防止死锁应该让其四种条件不同时满足即可。
	1. 互斥条件:线程对所分配到的资源不允许其它线程进行访问,其它线程只能等待当前线程释放资源。
	2. 请求和保持条件:线程获得一定的资源后,对当前获得的资源保持不放,同时对其它资源发出请求,但其它资源可能被其它线程占用,此时请求阻塞。
	3. 不可剥夺条件:线程获取到资源后,在未完成使用前不可被剥夺,只能在使用完成后自己释放。
	4. 环形等待条件:是指线程发生死锁后,若干线程之间形成一种头尾相接的循环等待资源关系。

避免逻辑中多个线程互相持有对方线程所需要的独占锁的情况,就可用避免死锁。

4、synchronized

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的原子性,可见性,有序性。

synchronized锁对象:
	- 普通同步方法:锁是当前实例对象。
	- 静态同步方法:锁是当前类的Class对象。
	- 同步代码块:锁是括号里面的对象。

synchronized底层实现原理:
 1. 同步代码块:通过monitorenter和monitorexit控制。每个对象都有一个monitor(监视器锁),执行monitorenter指令尝试获取monitor的所有权。
	  - monitor进入数为0,则该线程进入并将进入数设置为1,如果该线程已经占有该monitor,重新进入只需要将monitor进入数加1。
	  - monitor进入数不为0,则表示其它线程已经占用,进入阻塞状态,直到monitor的进入数为0,再尝试获取。
	执行monitorexit指令时,monitor的进入数减1,减1后进入数为0则退出当前线程,其它阻塞线程可以尝试获取monitor锁的所有权。

 2. 同步方法:通过ACC_SYCHRONIZED标识符,JVM根据该标识符实现同步方法。当方法调用时,检测方法的标识符是否被设置了,如果设置了,线程将获取monitor的所有权,方法执行完再释放monitor的所有权,在执行期间,其它线程无法获得该monitor。

5、Lock

Lock是一个接口,接口的实现类ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWrite.WriteLock。

四种获取锁的方法:
 1. lock():用来获取锁,如果锁被其它线程获取则等待。
 2. tryLock():表示用来尝试获取锁,如果获取成功则返回true,获取失败则返回false。
		if(lock.tryLock()){
	    	try{
	        
	   		}catch{
	        
	    	}finally{
	        	lock.unlock();
	    	}
		}else{
	    	//如果不能获取锁,则直接做其它事情,不会等待。
		}
 3. tryLock(long time,TimeUnit unit):和tryLock()类似,区别在于拿不到锁会等待一定时间,在期间拿不到锁再返回false。
 4. lockInterruptibly():通过这个方法获取锁时,如果线程没有获取到锁,正在等待获取锁,则此线程能够响应中断,即中断线程的等待状态。

注意:当线程获取锁后是不会被interrupt()方法中断的。interrupt()方法只能中断阻塞过程中的锁。
	 synchronized修饰的锁不能被中断。

ReentrantLock锁常用方法:
	- getXxx方法:
	  - int getHoldCount():当前线程调用lock()方法的次数。
	  - int getQueueLength():返回正在等待获取此锁定的线程预估数。
	  - int getWaitQueueLength(Condition condition):返回与此锁定相关的约定condition的线程预估数。
	- hasXxx方法:
	  - boolean hasQueuedThread(Thread thread):查询当前线程是否在等待获取此锁。
	  - boolean hasQueuedThreads():查询是否有线程在等待获取此锁。
	  - boolean hasWaiters(Condition condition):查询是否存在指定condition的线程正在等待此锁。
	- isXxx方法:
	  - boolean isFair():判断是否为公平锁。
	  - boolean isHeldByCurrentThread():查询当前线程是否保持此锁。
	  - boolean isLocked():判断此锁是否由任意线程保持。 

ReentrantReadWriteLock实现了ReadWriteLock接口,两个主要方法:readLock()、writeLock(),实现了读写分离。读读共享,读写互斥,写写互斥。

6、Condition

 1. ReentrantLock与Condition结合,实现线程等待与唤醒机制。一个Lock对象可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的condition中,从而可以有选择性的进行线程通知,在线程调度上更加灵活。
​ 2. 在使用notify/notifyAll方法进行通知时,被通知的线程是由JVM选择的,使用ReentrantLock类结合Condition实例可以实现“选择性通知”。

7、synchronized和volatile的区别

 1. volatile本质是告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其它线程被阻塞。
​ 2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别。
​ 3. volatile仅能实现变量的修改可见性,不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
​ 4. volatile不会造成线程阻塞;synchronized可能会造成线程阻塞。
 5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

8、synchronized和Lock的区别

 1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
 2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
 3. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
 4. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
 5. synchronized的锁可重入、不可判断、非公平,而Lock锁可重入、可判断、可公平(默认非公平);
 6. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

9、synchronized 和 ReentrantLock 的区别

 1. synchronized是关键字。
 2. ReentrantLock是类,它提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上: 
 3. ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁 。
 4. ReentrantLock可以获取各种锁的信息。
 5. ReentrantLock可以灵活地实现多路通知 。
 6. 二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。

10、atomic的原理

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,一直等到执行成功。

11、CAS

CAS(Compare and Swap),比较再交换。CAS算法实现了区别于synchronized同步锁的一种乐观锁。	        CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存		值V相同时,将内存值V修改为B,否则什么都不做。

缺点:
	循环时间长开销大;
	只能保证共享变量的原子性;
	ABA问题;

ABA问题:
	当一个线程对共享变量进行操作时,另一个线程也对共享变量进行操作,将内存值修改后又再一次修改回原来的值,这时第一个线程的预期值和内存值仍然相等。基本数据类型的影响不大,如果是引用数据类型则可能引用的内容发生了改变。解决方法是使用AtomicStampedReference控制变量值的版本号或者加锁。

12、ThreadLocal

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

​经典的使用场景:
	为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

13、锁的四种状态及锁升级原理

synchronized优化后,把锁分为无锁、偏向锁、轻量级锁、重量级锁四种状态。锁的升级只能从高到低:无锁→偏向锁→轻量级锁→重量级锁

 1. 无锁:CAS算法就是一种无锁算法。
 2. 偏向锁:一直被同一线程执行,不存在竞争,该线程不会主动释放锁,只有其它线程尝试竞争偏向锁时才会释放。升级为轻量级锁。
 3. 轻量级锁:在偏向锁的情况下,只有一个等待线程,偏向锁升级为轻量级锁,等待线程通过自旋尝试获取锁,线程不会阻塞。当只有一个等待线程,自旋超过一定次数升级为重量级锁;当不止一个等待线程时升级为重量级锁。
 4. 重量级锁:当一个线程获取锁后,其余所有线程处于阻塞状态。

在这里插入图片描述

14、锁的分类

 1. Synchronized:非公平,悲观,独享,互斥,可重入的重量级锁。
 2. ReentrantLock:默认非公平但可实现公平的,悲观,独享,互斥,可重入的重量级锁。
 3. ReentrantReadWriteLock:默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁

猜你喜欢

转载自blog.csdn.net/rookie__zhou/article/details/110139092