锁的基础知识:
锁的类型:
悲观锁和乐观锁:
乐观锁:认为读多写少,遇到并发写的可能性极低,即每次去拿数据的时候认为别人不会修改,所以不会上锁,但是在更新的时候会判断在此期间有没有 人更新这个数据。
判断依据:在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样就更新),如果失败就重复 读 -> 比较 ->写的操作
Java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败
CAS:
简单的来说,CAS有三个操作数,内存值 V, 旧的预期值 A,要修改的新值B。当且仅当预期值A与内存值V相等时,将内存值修改为B,否则返回V。
这是一种乐观锁的思路。它相信在它之前没有线程去修改内存值
缺点:会发生 ABA问题,即A被修改成B,然后又被修改成A,不能感知到修改
悲观锁:认为写多读少,遇到并发写的可能性高,每次在读写数据的时候都会上锁,如果别的线程想读写这个数据就会block直到拿到锁
1.Synchronized底层实现(*****):
Java对象头:锁的对象保存在对象头中,synchronized锁的是对象
锁的状态分为:自旋锁,偏向锁,轻量级锁,重量级锁)
自旋锁:加锁后,只有一个线程进入代码块,其他线程等待(自旋等待),为重量级锁(monitorenter,重量级锁标志)
自旋锁:1.相当于怠速停车,具有不公平性,处于自旋状态的锁比重量级锁(它处于阻塞状态)更容易获得锁
2.自旋锁不会引起调用者立即睡眠,如果自旋锁已经被别的执行单元保持,调用者不放弃处理器的执行时间,进行忙循环(自旋),
跑的是无用的线程,JDK1.6后默认开启了自旋锁,自旋次数默认10次
3.自旋锁一直占用CPU,在未获得锁的情况下,一直进行运行 - - - 自旋,若不能在很短的时间内获得锁,将会使CPU效率降低
自适应自旋:1.减少无用线程占用CPU的问题
2.自旋的时间不再是固定的,由前一个在同一个锁上的自旋时间及锁拥有者的状态来决定。
3.如果在同一个锁对象上,自选等待刚好成功获得锁,并且持有锁的线程正在运行中,那么虚拟机就认为这次自旋很有可能会获得锁,
将会允许自旋等待更长的时间
重量级锁(Java头中的monitorenter对象):os需要从用户态 -> 内核态,开销较大。同一时刻多个线程竞争资源
轻量级锁(CAS操作):多线程在不同时刻访问共享资源,乐观锁的一种
偏向锁:更为乐观的锁,假定从始至终都是同一个线程在访问共享资源
JDK中锁只有升级过程,没有降级过程。 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
锁消除:消除类似于Vector等线程安全集合不存在共享资源竞争时,JVM将代码优化,解锁
2.synchronized 与 ReentrantLock 区别
synchronized:
synchronized是Java中最基本同步互斥的手段,可以修饰代码块,方法,类
在修饰代码块的时候需要一个reference对象作为锁的对象
在修饰方法的时候默认当前对象作为锁的对象
修饰类的时候默认当前类的class对象作为锁的对象
synchronized会在进入同步块的前后分别形成monitorenter和monitorexit字节码指令,在执行monitorenter指令时会尝试获取对象的锁,如果
此对象没有被锁,或此对象已被当前线程锁住,则锁的计数器+1,如果monitorexit被锁的对象的计数器-1,直到为0就释放该对象的锁,由此
synchronized是可重入的,不会将自己锁死。
ReentrantLock:
除了synchronized的功能,多了三个高级功能
1.等待可中断 2.公平锁 3.绑定多个Condition
1.等待可中断
在持有锁的线程长时间不释放锁的时候,等待的线程可以放弃等待. tryLock(long timeout,TimeUnit unit)
2.公平锁
按照申请锁的顺序来一次获得锁称为公平锁,synchronized的是非公平锁,ReentrantLock可以通过构造函数实现公平锁
new ReentrantLock(boolean fair)
3.绑定多个Condition
通过多次newCondition可以获得多个Condition对象,可以简单的实现比较复杂的线程同步的功能
总的来说,lock更加灵活
二者相同点: Lock能完成synchronized所实现的所有功能
3.Lock(AQS: AbstractQueuedSynchronized 核心概念:一个是表示(锁)状态的变量、一个是队列)
4.Lock与Synchronized的区别
a.synchronized内置关键字,在JVM层面实现,发生异常时,会自动释放线程占有的锁,因此不会发生死锁现象。
Lock在发生异常时,如果没有主动通过unLock去释放锁,很可能产生死锁现象,因此使用Lock时需要在finally块中释放锁
b. Lock具有高级特性: 时间锁等候,可中断锁等候
c.当竞争资源非常激烈时(即有大量线程同时竞争时),此时Lock的性能要远远优于synchronized
5.线程池
JDK内置的四大线程池:
1.创建无大小限制的线程池:
public static ExecutorService newCachedThreadPool()
2.创建固定大小的线程池:
public static ExecutorService newFixedThreadPool(int nThreads)
3.单线程池
public static ExecutorService newSingleThreadExecutor()
4.创建调度线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
线程池的三大优点:
a.降低资源消耗:通过重复利用已创建的线程降低线程创建与销毁带来的消耗
b.提高响应速度:当任务到达时,不需要等待线程创建就可以立即执行
c.提高线程创建的可管理性:使用线程池可以统一进行线程分配、调度和监控
线程池的组成:
1. corePool:核心线程池
2. BlockingQueue: 阻塞队列
3. MaxPool: 线程池容纳的最大线程容量
a. 如果当前执行的线程数 < corePoolSize ,则创建新的线程执行任务,然后将其放入corePool(需要全局锁)
b. 如果当前线程数 >= corePoolSize,将任务放入阻塞队列等待调度执行 (95%)
c. 如果阻塞队列已满,则试图创建新的线程来执行任务(需要全局锁)
d. 如果创建线程后,总线程数 > maxPoolSize,则任务被拒绝,调用拒绝策略返回给用户
工作线程:
线程池创建线程时,将线程封装为worker,worker在执行完任务后,还会循环从工作队列中取得任务来执行
手工创建线程池的六个参数:
ThreadPoolExecutor tye = new ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler
)
1. corePoolSize(核心线程池): 当提交一个任务到线程池时,线程池会创建一个新的线程执行这个任务,即使其他基本线程也可以执行这个任务也会创建 新的线程,直到当前线程池中的线程数量大于基本大小
2. BlockingQueue(阻塞队列): 用于保存等待执行任务的阻塞队列
a. ArrayBlockingQueue: 基于数组的有界阻塞队列。按照FIFO对元素排序
b. LinkedBlockingQueue: 基于链表的无界阻塞队列,吞吐量高于 ArrayBlockingQueue
FixedThreadPool(),SingleThreadPool() 都用此队列
c. SynchronousQueue: 不存储元素的阻塞队列,每个插入操作必须等待另一个线程移除操作,否则插入操作一直处于阻塞状态
吞吐量 > LinkedBlockingQueue
cachedThreadPool()采用此队列
d. PriorityBlockingQueue: 具有优先级的无界阻塞队列
3.keepAliveTime(线程活动保持时间): 线程的工作线程空闲后,保持存活的时间
4.TimeUnit : KeepAliveTime 的时间单位
5.RejectedExecutionHandler(饱和策略)(拒绝策略):线程池满时无法处理新任务的执行策略
线程池默认采用 AbortPolicy (抛出异常)-----可以省略此参数
6.死锁的产生原因(四大条件)以及处理方案(银行家算法)
a. 互斥条件:进程对所分配的资源进行排他性使用,即一段时间内,某资源只能由一个进程占用,如果此时还有其他进程请求该资源,则请求者只能等 待
b. 请求和保持条件:即进程已经保持至少保持一个资源,但是还在请求别的资源,但此时别的资源被其他线程持有,此时请求进程阻塞,此进程也不 想放弃所持有的资源
c. 不剥夺条件:进程已经获得的资源,不能被剥夺,只能由进程使用完自己释放
d. 环路等待条件:在发生死锁时,必然存在一个竞争资源的环形链。即进程集合中 {p0,p1,p2,...,pn},p0等待p1的资源,pn等待p0的资源
银行家算法:
是用来避免操作系统出现死锁的有效算法
7. volatile 两层语义 - 懒汉式单例为何使用双重加锁
a.禁止指令重排
b.保证内存可见性
// 懒汉式单例模式
// 声明 : 代码中出现的问题前提是 第二行代码未被 volatile修饰
public class Singleton{ // 1
private static volatile Singleton singleton; // 2
private Singleton(){}
public static Singleton getInstance(){ // 3
// 双重检查
if(singleton==null){ // 4 第一次检查
synchronized(Singleton.class){ // 5 加锁
if(singleton==null){ // 6 第二次检查
return singleton = new Singleton(); // 7 问题在这里
}
}
}
return singleton;
}
}
在多线程情况下,上面第七行代码 singleton = new Singleton(); 创建一个对象分为三步
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
singleton = memory; // 3.设置 singleton 指向 刚分配的内存地址
上面三行代码可能会被重排序:
memory = allocate(); // 1:分配对象的内存空间
singleton = memory; // 3.设置 singleton 指向 刚分配的内存地址
ctorInstance(memory); // 2:初始化对象
这里由一个问题就是,在还没有初始化对象的时候就将singleton指向了内存地址
8.NIO(Netty)
NIO如何实现多路复用,BIO,NIO,AIO特点
多路复用:在Java 1.4中引入了NIO框架(Java.nio包),提供了Channel,Selector,Buffer等新的抽象类,可以构建多路复用的,同步非阻塞IO程序,同时提供了更接近操作系统底层的高性能数据操作方式
AIO:在JDK1.7中,NIO有了新一步改进,既 NIO2 ,引入了异步非阻塞IO方式,也叫AIO,
异步IO操作基于事件和回调机制,可以简单理解为,应用操作直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后 续工作
BIO,NIO,AIO 特点:
BIO:该方式适用于数目比较小且固定的架构,这种方式对于服务器资源要求比较高,并发局限于应用中,是JDK1.4以前唯一的选择,但程序直观简单易理解
NIO: 该方式适用于数目比较多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4以后支持
AIO: 适用于数目比较多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参加并发操作,编程比较复杂,JDK1.7开始支持