为什么要使用并发编程?
1. 使用并发编程可以提升多核CPU的利用率:可以在一个多核的CPU主机上创建多个线程,将多个线程分配给不同的CPU去执行。
2.方便进行业务拆分: 面对复杂业务模型,可以对业务模块进行拆分,从而提升响应速度。而进行拆分时可以使用多线程技术来完成。
并发编程的三要素是什么?
原子性:一个操作是不可以再分的要么全部成功,要么全部失败。
可见性:某个线程对共享数据修改后,其他线程再调用该共享变量时可以立刻看到该变量的最新值。
顺序性:程序执行的顺序按代码的先后顺序执行。
指令重排:虚拟机在进行代码编时,对于那些改变顺序后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码顺序来执行,有可能将他们重排序。实际上对有些代码进行重排序后,虽然对变量的值没有造成影响,但有可能出现线程安全问题。
并行,并发,串行的区别?
并行:并行操作在时间上是重叠的,两个任务可以在同一时刻互不干扰的进行。
并发: 多个任务在同一个CPU上运行,按CPU时间片的轮转来轮流执行。在同一时刻只能由一个任务执行。
串行:在时间上是不会发生重叠的,后面的任务需要等待前面的任务完成才能执行。
什么是进程和线程?两者的区别是什么?
进程是系统进行资源管理和分配的基本单位,进程是动态的,系统运行一个程序就是一个进程从创建到运行到消亡的过程。
线程是进程划分的一个更小的运行单位,是进程中的一个执行任务。一个进程可以产生多个线程,一个进程至少有一个线程,多个线程可以共享进程中的数据。
根本区别就是:进程是系统管理和分配资源的一个基本单位,而线程是处理器任务调度和执行的基本单位,在内存方面,进程和进程之间的内存资源是独立的,而同一个进程之间的线程的资源是共享的。
如何理解线程安全?如何实现线程安全?
如果多个进程同时同时调用同一个数据对象,而该对象不需要进行额外的同步控制或者其他操作,每一个调用都可以得到正确的结果,那么我们就可以说这个对象是安全的。
方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用JVM提供的自动锁 synchronized。
方法三:使用JDK提供的手动锁 Lock。
synchronized的作用?
synchronized关键字可以把任意一个非null对象当作锁,它的作用是解决多个进程之间资源访问的同步性,被synchronized关键字修饰的方法或者代码块在同一时刻只运行一个进程执行。
三种使用方式:
修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
volatile 关键字的作用
Java 提供了 volatile 关键字是线程同步的轻量级实现,用来保证可见性和有序性(禁止指令重排),volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
对于加了 volatile关键字的成员变量,在对这个变量进行修改时,全直接将CPU高级缓存中的数据送回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性
在对 volatile修饰的成员变量进行读写时,会插入内存屏障,而内存屏障可以达到禁止重排序的效果,从而可以保证有序性。
synchronized 关键字和 volatile 关键字的区别
volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比synchronized关键字要好 。
volatile 关键字只能用于变量,而synchronized 关键字可以修饰方法以及代码块 。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
锁分类
是否锁同步资源:乐观锁、悲观锁
乐观锁:一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
悲观锁:一种悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是synchronized;
线程是否阻塞:自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
线程自旋是需要消耗 CPU 的,说白了就是让 CPU在做无用功,如果一直获取不到锁,那线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
自旋锁的优点:
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
线程竞争时是否排队:公平锁、非公平锁
公平锁:锁的分配机制是公平的,加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得
非公平锁:加锁时不考虑排队等待问题,JVM 按随机、就近原则分配锁的机制则称为不公平锁,非公平锁实际执行的效率要远远超出公平锁(5-10倍),除非程序有特殊需要,否则最常用非公平锁的分配机制。
线程中的多个流程是否可以获取同一把锁:可重入锁(递归锁)
可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。
多线程之间能不能共享同一把锁:共享锁和排他锁
共享锁:共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
排他锁:每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
锁状态:无锁、偏向锁、轻量级锁、重量级锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
锁升级:随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,
也就是说只能从低到高升级,不会出现锁的降级)。
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能
轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
死锁
两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法让程序进行下去!