线程
Q:进程和线程的区别
进程和线程的关系:
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
(4)处理机分给线程,即真正在处理机上运行的是线程。
(5)线程是指进程内的一个执行单元,也是进程内的可调度实体。
线程与进程的区别:
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
(4)系统开销:在创建或撤销进程的时候,由于系统都要为之分配和回收资源,导致系统的明显大于创建或撤销线程时的开销。但进程有独立的地址空间,进程崩溃后,在保护模式下不会对其他的进程产生影响,而线程只是一个进程中的不同的执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但是在进程切换时,耗费的资源较大,效率要差些。
进程是资源分配的最小单位,线程是程序执行的最小单位
进程有自己独立地址空间
线程间通信更方便
Q:并发与并行的区别
并行:同一个时刻,多条指令在多个处理器上同时执行
并发:同一时刻只能有一条指令执行,但是多个进程轮换执行
当有多个CPU时,可以并行
单个CPU是并发,多线程,也是把CPU运行时间划分成若干个时间段,分配个各个线程执行
Q:线程的五中状态(生命周期)
1.新建new
2.就绪,调用了start方法
3.运行,获得了CPU执行
4.阻塞,线程暂停运行
(1)wait,调用线程的wait()方法,让线程等待某工作完成
(2)同步阻塞,线程获取synchronized同步锁失败,锁被其他线程占用
(3)其他阻塞,调用sleep或IO请求进入阻塞
5.死亡,线程执行完run退出
五种状态涉及的关键函数、关键字
Object类:wait,notify,notifyAll
Thread类:sleep,interrupt中断,getName
Q:线程的三种创建方式
1.继承Thread,start启动
2.implements Runnable接口
Thread p1 = new Thread(new Producer(storage));
3.implements Callable接口,重新call()方法,有返回值
public class MyThread implements Callable{
public String call(){}
public static void main(String[] args){
Callable callable = new MyThread();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread mThread = new Thread(futureTask);
mThread.start();
}
}
Q:守护线程
守护线程:最低最低的优先级,用于为系统中的其它对象和线程提供服务。将一个用户线程设置为守护线程的方式是在线程对象创建之前调用线程对象的setDaemon(true)方法。典型的守护线程例子是JVM中的垃圾回收线程
守护线程:和普通线程本质没啥区别,唯一的区别之处就在虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了
线程关键字
Q:线程协作
Thread.sleep 暂停执行,不释放锁
list.wait() 释放list锁
只有调用了list.notify才告诉调用过wait的线程可以参与锁的竞争
join: t.join方法(等待t线程执行完)阻塞调用此方法的线程,直到线程t完成,再继续,常用在main主线程内
list.notify 唤醒一个正在等待的,如果有多个线程都在等,只能唤醒其中一个
list.notifyAll 唤醒所有
等到线程退出synchronized块释放锁,才可获得锁执行
线程可能会等待多个锁
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,在阻塞队列那一篇博文中就讲述到了,阻塞队列实际上是使用了Condition来模拟线程间协作。
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
Q:synchronized
多线程的时候,对同一个对象进行修改,造成内存错误
为了解决共享资源,共享资源其实就是对象,是内存的一块地址
当调用synchronized(对象)的时候,该对象会被加上,该对象上其他synchronized方法都不能调用,都要等共同的锁,等的同一个对象就不能调用
1.一个线程访问一个对象的synchronized方法时,其他线程对同一个方法将被阻塞,对该对象的其他synchronized方法也被阻塞
2.其他线程仍可以访问该对象的非synchronized方法
synchronized(this) 是锁住当前调用对象
synchronized代码块访问力度更细
Q:volatile
是因为,每个线程有自己的内存,为了提高性能,线程会在自己的内存中保持要访问变量的副本,但是可能两个线程的值是不同的,造成错误。
volatile标志的属性,意思就是这个值随时可能被其他线程修改,要用的话需要从主内存中读取,修改也要立刻更新
Q:ReentrantLock(可重入锁)
主要相同点:Lock能完成synchronized所实现的所有功能
主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。
synchronized可以做方法签名,获得该实例的对象锁,必须所有线程使用同一个对象实例,否则synchronized将失去意义,如果是类方法,static,线程会拥有该类的锁,synchronized代码块synchronized(obj){},就会拥有obj对象的对象锁,obj==this,代表当前调用该方法的实例对象
ReetrantLock还包含中断锁和定时锁,synchronized是JVM层实现的,系统可以监控锁释放与否,ReetrantLock使用代码实现,系统无法自动释放锁,要在finally子句中显示释放锁lock.unlock(),如果并发量小,用synchronized,并发量高,性能下降,用ReetrantLock
Q:ThreadLocal
ThreadLocal类可实现线程本地存储的功能,把共享数据的可见范围限制在同一个线程之内,无须同步就能保证线程之间不出现数据争用的问题,这里可理解为ThreadLocal帮助Handler找到本线程的Looper。
底层数据结构:每个线程的Thread对象中都有一个ThreadLocalMap对象,它存储了一组以ThreadLocal.threadLocalHashCode为key、以本地线程变量为value的键值对,而ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,也就包含了一个独一无二的threadLocalHashCode值,通过这个值就可以在线程键值值对中找回对应的本地线程变量。
为什么要有ThreadLock?怎么解决的?怎么用
线程局部变量,Looper在线程中的读取就是用了ThreadLock
一个对象,有set,get方法
一些变量是线程私有的,不想线程共享,可以保持一个键值对,key是当前线程,保持一个值
在Handler的使用,拿到当前线程的Looper
ThreadLocal<String> localName = new ThreadLocal();
localName.set(“aaa”);
String name = localName.get();
原理是有一个ThreadLocalMap,key是当前线程Thread
public void set(T value){
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
map.set(this,value);
}
public T get(){
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
TheatLocalMap.Entry e = map.getEntry(this);
T result = e.value;
}
Q:sleep和wait的区别
生产者消费者模型
1. wait(),notify()方法
public class Storage{
//仓库容量
private final int MAX_SIZE = 10;
//存放货物
private LinkedList<Object> list = new LinkedList<>();
//生产方法
public void produce(){
//锁这块内存,要读取这个对象,锁这个对象,阻塞在这行代码
synchronized(list) {
while(list.size()+1>MAX_SIZE){//仓库已满,释放这个锁
list.wait(); //释放list锁,是对象方法,对象做锁
}
//没有满就生产,生产完释放,通知所有等list的来竞争
list.add(new Object());
list.notifyAll(); 所有等待list的
}
}
//消费,消费者调用这个方法
public void consume(){
synchronized(list){
while(list.size() ==0){
list.wait();
}
list.remove();
list.notifyAll();
}
}
}
//生产者
public class Producer implements Runnable{
private Storage storage;
public Producer(){}
//告诉生产者仓库在哪
public Producer(Storage storage,String name){
this.storage = storage;
}
public void run(){
while(trure){
Thread.sleep(1000);
storage.produce(); //死循环的一直生产,要抢锁
}
}
}
//消费者
public class Consumer implements Runnable{
private Storage storage;
public Consumer(Storage storage,String name){
this.storagte = storage;
}
public void run(){
while(true){
Thread.sleep(3000);
storage.consume();
}
}
}
//主函数
public class Main(){
public static void main(String[] args){
Storage storage = new Storage();
Thread p1 = new Thread(new Producer(storage));
Thread p2 = new Thread(new Producer(storage));
Thread c1 = new Thread(new Consumer(storage));
Thread c2 = new Thread(new Consumer(storage));
p1.start();
p2.start();
c1.start();
c2.start();
}
}
2. await(),singnal()
//通过ReentranLock和Condition
//在Lock对象上调用newCondition()方法,将条件变量和锁对象绑定
public class Storage{
private final int MAX_SIZE = 10;
private LinkedList<Object> list = new LinkedList<>();
//锁
private final Locl lock = new ReentrantLock();
//仓库满的条件变量
private final Condition full = lock.newCondition();
private final Condition empty = lock.newCondition();
public void produce(){
lock.lock();
while(list.size()+1>MAX_SIZE){
full.await()
}
list.add(new Object());
empty.signalAll();
lock.unclock();
}
public void consume(){
lock.lock();
while(list.size() == 0){
empty.await();
}
list.remove();
full.signalAll();
lock.unlock();
}
}
3. BlockingQueue阻塞队列
已在内部实现了同步队列,用的是await,signal
put方法,容量最大,自动阻塞
take方法,消费,为0,自动阻塞
public class Storage{
private LinkedBlockingQueue<Object> list = new LinkedBlockingQueue<>(10);
public void produce(){
list.put(new Object());
}
public void consume(){
list.take();
}
}
4. 信号量
5.管道
线程安全
Q:线程安全
原子性操作、内存可见性和指令重排序是构成线程安全的三个主题
- 原子性
原子性:一个或几个操作只能在一个线程执行完之后,另一个线程才能开始执行,也就是这些操作不能被打断,不可以交替执行
Java自带了一些原子性操作,比如非long,double基本数据类型赋值或读取
i++不是原子操作,所以线程可能交叉进行,对于同一个变量,不同线程可以交叉三个步骤,导致计算不正确,所以提出了锁的概念
可以用synchronized,lock来保证大方位的原子性- 内存可见性
一个线程修改了共享变量的值,立刻更新到主内存,线程读取的时候要从主内存刷新变量的值
因此volatile保证了可见性,synchronized和lock也可以保证- 指令重排序
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程 程序的执行,却会影响到多线程并发执行的正确性。
线程池
Q:什么是线程池
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放众多(未死亡)的线程,池中线程的调度由池管理器来处理。当有线程任务时,从池中取一个,执行完线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源
Q:为什么设计了线程池这个东西?解决了什么问题,怎么设计解决的,你什么时候使用线程池
提升线程创建的性能
单个线程的弊端:
a. 每次new Thread新建对象性能差
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或者OOM,
c. 缺乏更多功能,如定时执行、定期执行、线程中断。
java提供的四种线程池的好处在于:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。
Q:创建线程池,Java通过Executors线程池有哪4种:
1.new FixThreadPool(int nThreads) 创建固定大小的线程池。可控制线程最大并发数,超出的线程会在队列中等待。
2.new SingleThreadExecutor() //创建只有一个线程的线程池,用唯一的工作线程来执行,保证任务按照指定书序(FIFO)先入先出执行
3.new CachedThreadPool() 缓存线程时,线程数量超过任务,可以回收线程,没有线程,创建
创建一个不限线程数上限的线程池,任何提交的任务都立即执行
4.new ScheduledThreadPool() 创建定长线程池,支持定时及周期性任务执行。创建一个定长线程池,支持定时及周期性任务执行。ScheduledExecutorService比Timer更安全,功能更强大
Q:可以向线程池提交任务的方法与区别
有两种Runnable和Callable
Callable的方法是call(),允许有返回值,允许抛异常
提交任务方法
线程池.submit(Callable task) 有返回结果,处理结果及异常都包装在Future,可以调用Future.get()方法获取,异常包装成ExecutionException
线程池.execute(Runnable command),提交任务执行
线程池.submit(Runnable task)
Q:java中的线程池是ThreadPoolExecutor类 构造方法参数的意义
7个参数:线程池长期维持的线程数,线程数的上线,线程保留的时长,任务的队列,任务工厂,拒绝策略
corePoolSize:线程池长期维持的线程数
maximumPoolSize:线程池最大线程数,表示线程池最多能创建多少个线程
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用
unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:天、小时、分钟、秒、毫秒
workQueue:阻塞队列,存储等待执行的任务,数据结构常用LinkedBlockingQueue,ArrayBlockingQueue,SynchronousQueue使用较少
threadFactory:线程工厂,主要用来创建线程
RejectedExecutionHandler handler:拒绝策略,有四种
1.丢弃任务并抛出Exception
2.丢弃任务,但是不抛出异常
3.丢弃队列最前面的任务,然后重新尝试执行任务
4.有调度线程处理该任务
Q:ThreadPoolExecutor执行任务时会遵循如下规则
如果线程池中的线程数量未达到核心线程的数量,那么会直接启动一个核心线程来执行任务。
如果线程池中的线程数量已经达到或则超过核心线程的数量,那么任务会被插入任务队列中排队等待执行。
如果在第2点无法将任务插入到任务队列中,这往往是由于任务队列已满,这个时候如果在线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务。
如果第3点中线程数量已经达到线程池规定的最大值,那么就拒绝执行此任务,ThreadPoolExecutor会调用RejectedExecutionHandler的rejectedExecution方法来通知调用者。
以运营一家装修公司做个比喻。公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。
Q:Java提供四种线程池的区别
锁
Q:Java中的锁
Java中的对象都可以作为一把锁给synchronized使用,者就是我们常说的内置锁,也就是监视锁
加静态发方法上面,表示监视Class对象
public static synchronized void fun()
加在实例方法上,监控this
public synchronized void fun()
代码块,加在对象上
Object obj = new Object();
public void fun(){
synchronized(obj){}
}
}
如果在一个类中存在两个临界区需要同步,即需要两把锁,Lock1 和 Lock2,那么此时一个this对象就不够用了
Q:可重入锁
就是一个synchronized方法调用另一个synchronized方法,可以直接获得
已经有了同一把锁,如果再次需要获得这把锁,不需要再次竞争,可以直接得到,就是可重入
public synchronized void test(){
reentrant();
}
public synchronized void reentrant(){}
test()和reentrant()都需要this对象上面的监视锁,由于synchronized是 可重入的,所以test()获取了锁之后,调用reentrant()时,需要再次获取锁,由于可重入性,test()方法是没有问题的。
Q:不公平锁
是否按请求顺序获取锁?不是
线程1使用锁完毕,开始释放锁,此时,JVM会唤醒那个线程呢?如果是按照加入阻塞队列的顺序来依次唤醒,那么就是公平锁;否则,就是非公平锁。公平锁有额外的消耗,实际上Java的内置锁在进入阻塞队列前,会使用自旋锁等待一段超时时间,这样这样就形成了后来先用,当然不公平啦。
Q:内置锁
java提供了一种内置的锁机制来支持原子性:同步代码块synchronized
每个java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路劲退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
Q:内置锁的三种状态
内置锁是通过JVM的monitorenter和monitorexit指令实现的,所以他的加锁和释放锁都从JVM中实现
在早期的JVM中,monitorenter和monitorexit比较严重的依赖于操作系统的互斥信号量
这样就须臾从用户态切换到内核态,这种切换存在性能问题
从1.6开始,进行了一些了优化,对锁的状态进行了区分
锁的状态流转就是对应多个线程对锁的竞争程度,如果对锁的竞争比较低,那么JVM不会通过系统信号量来实现同步,随着竞争加剧,获取锁的代价越来越大,最后腿化成依赖于系统信号量的重量级锁
锁的状态存在对象头中
对象在堆中由三部分组成:对象头,实例变量,填充数据
锁有四种状态
1.无锁
2.偏向锁,只需比较Thread ID
3.轻量级锁,自旋
4.重量级锁,依赖于mutex(操作系统的互斥)
随着锁的竞争加剧,锁的状态会从偏向锁升级到轻量级锁,最后到重量级锁
Q:偏向锁
大部分情况下,对一个锁的获取都是同一个线程(不存在竞争),为了减少获取锁的代价,引入偏向锁。因为每次加锁解锁都涉及到一些CAS操作,CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作
就是给锁对象设置个变量,线程在获取时,只需要看一下当前这个变量是不是自己,如果是自己,就不需要再去走获取锁的逻辑了。
获取偏向锁,就是看看当前锁是不是偏向当前线程,一条原则,如果获取偏向失败,那么就撤销锁的偏向,转入轻量级锁的路径来获取。
偏向锁一旦受到多线程竞争,就会膨胀为轻量级锁
Q:轻量级锁
CAS将对象头中Mark Word替换为指向锁记录的指针,如果获取失败,表示其他线程竞争锁,当前线程便尝试使用自选来获取锁,如果自选还是无法获取到锁,轻量级锁就膨胀为重量级锁
Q:重量级锁
通过系统 互斥信号量接介入来达到同步,代价最高,因为涉及到了当前线程在用户态和核心态的切换,这也是为什么Java中要做前面优化的原因。
Q:内置锁三种状态的比较
偏向锁
优点:代价最低,在低竞争的情况下,如果大部分情况都是同一个线程进入临界区,一旦锁对象进行了偏向,那么几乎没有什么成本(仅仅是多了一次是否是自己持有锁的判断);
缺点:如果锁上有激烈的竞争,那么偏向带来的性能优势就会消失殆尽,因为偏向之后,还需要撤销偏向。
轻量级锁
优点:竞争的线程不会阻塞,提高了程序的响应性
缺点:消耗CPU资源
重量级锁
优点:不用消耗CPU自旋
缺点:阻塞线程,响应时间长
Q:自旋锁(锁优化)
ynchronized在1.6之前称为重量级锁,因为挂起线程与恢复线程的操作都需要转入内核态中完成。从用户态转入内核态是比较耗费系统性能的。
研究表明,大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环,使当前线程不放弃处理器的执行时间(这也是称为自旋的原因),在经过若干次循环后,如果得到锁,就顺利进入临界区。
但是自旋不能代替阻塞,首先,自旋锁需要多处理器或一个处理器拥有多个核心的 CPU 环境,这样才能保证两个及以上的线程并行执行(一个是获取锁的执行线程,一个是进行自旋的线程)。除了对处理器数量的要求外,自旋虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间比较短,自旋的效果就比较好,否则只是白白占用了 CPU 资源,带来性能上的浪费。
那么自旋就需要有一定的限度,如果自旋超过了一定的次数后,还没有成功获取锁,就只能进行挂起了,这个次数默认是 10。
在 JDK 1.4.2 中引入了自旋锁,在 JDK 1.6 中引入了自适应自旋锁。自适应意味自旋的时间不再固定:
如果同一个锁对象上,自旋等待刚刚成功获取锁,并且持有锁的线程正在运行,那么虚拟机就会认为此次自旋也很有可能成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。如果对于某个锁,自旋很少成功获取过,那么在以后获取这个锁时将可能自动省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越精准,虚拟机也就会越来越“聪明”。
Q:锁消除(锁优化)
通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁的事件
锁消除的主要判定依据来源于逃逸分析技术的支持
StringBuffer 是一个线程安全的类,在它的 append 方法中有一个同步块,锁对象就是 sb,但是虚拟机观察变量 sb,发现它是一个局部变量,本身线程安全,并不需要额外的同步机制。因此,这里虽然有锁,但可以被安全的清除,在 JIT 编译之后,这段代码就会忽略掉所有的同步而直接执行。这就是锁消除。
Q:类锁、对象锁
synchronized加到static方法前面是给class加锁,即类锁
synchronized加到非静态方法前面是给对象加锁
对象锁对同一个实例对象起作用,类锁对该类所有对象起作用
Q:悲观锁
先获取锁,再操作,一锁二查三更新。悲观的认为获取锁是非常有可能失败的。到底有多少失败的可能性,决定用哪种锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等
synchronized和reentrantLock等独占锁就是悲观锁
多写的情况用悲观锁比较好
Q:乐观锁
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适
Q:CAS的原理(比较并交换)
版本号
业务操作进行前获取需要锁的数据的当前版本号,然后实际更新数据时再次对比版本号确认与之前获取的相同,并更新版本号+1,即可确认这之间没有发生并发的修改。如果更新失败即可认为老版本的数据已经被并发修改掉而不存在了,此时认为获取锁失败,需要回滚整个业务操作并可根据需要重试整个过程
CAS算法
compare and swap 比较再交换,是一种有名的无锁算法。非阻塞同步,即不使用锁的情况下实现线程之间的变量同步。
jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。
CAS算法涉及到三个操作数
需要读写的内存值 V
内存里的旧值 A
拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
先把就值备份给A,操作完出现一个新值,在存储的时候,看看现在内存里和备份的A值是不是一样
ABA问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题
循环时间长
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
CAS与synchronized的使用情景
简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)
对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
Q:CAS带来的3大问题
Q:CAS底层原理
Q:实现一个CAS的方式
Q:AQS的解析
Q:死锁
死锁的四个条件
1.互斥使用(资源独占):一个资源每次只能给一个进程使用
2.不可强占(不可剥夺):资源申请者不能强行从资源占有者手中夺取资源,资源只能由占有者自愿释放
3.占有并等待(部分分配,占有申请):一个进程在申请新资源的同时保持对原有资源的占有
4.循环等待:存在一个进程等待队列{P1 , P2 , … , Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路
预防
1.破坏占有并等待条件:要破坏这个条件,就要求每个进程必须一次性的请求它们所需要的所有资源,若无法全部获取就等待,直到满足为止,也可以采用事务机制,确保可以回滚,即把获取、释放资源做成原子性的。这个方法实现起来可能会比较困难,因为某些情况下,进程并不能事先直到自己需要哪些资源,也有时候并不需要分配到所有资源就可以运行。
2.破坏不可剥夺条件:一个已占有资源的进程若要再申请新的资源,它必须先释放已占有的资源。若随后再需要这些资源,需要重新申请。
3.破坏循环等待条件:将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作。