线程池的学习(六)
参考一:原文链接: https://blog.csdn.net/qq_41282026/article/details/98304960
参考二:https://www.cnblogs.com/maohuidong/p/10027104.html
公平锁和非公平锁;
- 公平锁:是指多个线程按照申请锁的顺序来获取锁
- 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获锁。
非公平锁一上来就尝试占用锁,如果尝试占用失败,就采用公平锁的方式到末尾排队。
在高并发的情况下,有可能造成优先级反转或饥饿现象
非公平锁的优点在于吞吐量比公平锁大。
ReentrantLock:可以指定构造方法的boolean类型来指定是公平锁还是非公平锁,默认是非公平锁
synchronized:是一种非公平锁
可重入锁(又名递归锁)
可重入锁:指的是同一线程外层方法获得锁之后,内层递归方法仍然能够获得该锁的代码
在同一个线程在外层方法获取锁的时候,在进入内层方法的时候会自动获取锁;也就是说,线程可以进入任何一个它自己已经拥有的锁所同步着的代码块;可重入锁的最大作用是避免死锁。
ReentrantLock和synchronized就是一个典型的可重入锁
class People{ Lock lock = new ReentrantLock(); public void get(){ lock.lock(); try{ System.out.println(Thread.currentThread().getName()+" get"); set(); }finally { lock.unlock(); } } public void set(){ lock.lock(); try{ System.out.println(Thread.currentThread().getName()+" set"); }finally { lock.unlock(); } } } public class ReentrantLockDemo { public static void main(String[] args) throws InterruptedException { People people = new People(); new Thread(()->{ people.get(); },"t3").start(); new Thread(()->{ people.get(); },"t4").start(); } }
自旋锁:是指尝试获取锁的线程不会立即被阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线上下文切换的消耗,缺点是循环会消耗CPU。
CAS操作中的自旋:
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
独占锁:是指该锁一次只能被一个线程所持有。ReentrantLock和synchronized都是独占锁
共享锁:是指该锁可以被多个线程所持有 ReentrantReadWriteLock:其读锁是共享锁,其写锁是独占锁;读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的
public class ReadWriteLock { public static void main(String[] args) { Resource resource = new Resource(); //5个线程进行写入 for(int i = 1;i <= 5;i++){ final int tem = i; new Thread(()->{ resource.putV(tem+"",tem+""); },"put-"+i).start(); } //5个线程进行读取 for(int i = 1;i <= 5;i++){ final int tem = i; new Thread(()->{ resource.getV(tem+""); },"get-"+i).start(); } } } class Resource{ public volatile Map<String,String> map = new HashMap<>(); ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); //写入操作 public void putV(String key,String value){ try{ lock.writeLock().lock(); String name = Thread.currentThread().getName(); System.out.println(name + "\t 开始写入值..." + "\t "+key); TimeUnit.MILLISECONDS.sleep(300); map.put(key,value); System.out.println(Thread.currentThread().getName() + "\t 完成写入值..."); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } } //读取操作 public void getV(String key) { try{ lock.readLock().lock(); String name = Thread.currentThread().getName(); System.out.println(name + "\t 开始读取值..."); Object result = map.get(key); TimeUnit.MILLISECONDS.sleep(300); System.out.println(Thread.currentThread().getName() + "\t 完成读取值..." + "\t "+result); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.readLock().unlock(); } } }
------------------------------------------------------------------------------------------------------------------
有锁与无锁
悲观锁与乐观锁
数据库有两种锁,悲观锁的原理是每次实现数据库的增删改的时候都进行阻塞,防止数据发生脏读;乐观锁的原理是在数据库更新的时候,用一个version字段来记录版本号,然后通过比较是不是自己要修改的版本号再进行修改。这其中就引出了一种比较替换的思路来实现数据的一致性,事实上,cas也是基于这样的原理。
CAS(compare and set) ,也就是比较替换技术,·它包含三个参数,CAS(V,E,N),其中V(variile)表示欲更新的变量,E(Excepted)表示预期的值,N(New)表示新值,只有当V等于E值的时候吗,才会将V的值设为N,如果V值和E值不同,则说明已经有其它线程对该值做了更新,则当前线程什么都不做,直接返回V值。
举个例子,假如现在有一个变量int a=5;我想要把它更新为6,用cas的话,我有三个参数cas(5,5,6),
我们要更新的值是5,找到了a=5,符合V值,预期的值也是5符合,然后就会把N=6更新给a,a的值就会变成6;
CAS的优点:CAS是以乐观的态度运行的,它总是认为当前的线程可以完成操作,当多个线程同时使用CAS的时候只有一个最终会成功,而其他的都会失败。
这种是由欲更新的值做的一个筛选机制,只有符合规则的线程才能顺利执行,而其他线程,均会失败,但是失败的线程并不会被挂起,仅仅是尝试失败,并且允许再次尝试(当然也可以主动放弃);
CAS可以发现其他线程的干扰,排除其他线程造成的数据污染
AtomicInteger与unsafe类
CAS在jdk5.0以后就被得到广泛的利用,而AtomicInteger是很典型的一个类,接下来我们就来着重研究一下这个类:
AtomicInteger:关于Integer,它是final的不可变类,AtomicInteget可以把它视为一种整数类,它并非是fianl的,但却是线程安全的,而它的实现就是著名的CAS了,下面是一些它的常用方法:
public final int getAndSet(int newValue);
public final boolean compareAndSet(int expect, int update);
public final boolean weakCompareAndSet(int expect, int update);
public final int getAndIncrement();
public final int getAndDecrement();
public final int addAndGet(int delta);
public final int decrementAndGet();
public final int incrementAndGet();
其中主要的方法就是compareAndSet,我们来测试一下这个方法,首先先给定一个值是5,我们现在要把它改成2,如果expect传的是1,程序会输出什么呢?
public class TestAtomicInteger { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(5); boolean isChange = atomicInteger.compareAndSet(1, 2); int i = atomicInteger.get(); System.out.println("是否变化:"+isChange); //false System.out.println(i);//5 } }
如果我们把期望值改成5的话,boolean isChange = atomicInteger.compareAndSet(5, 2);
最后的输出结果将是:是否变化:true 2
结论:只有当期望值与要改的值一致的时候,cas才会替换原始的值,设置成新值
经典的ABA问题与解决方法
要了解什么是ABA问题,首先我们来通俗的看一下这个例子,
一家火锅店为了生意推出了一个特别活动,凡是在五一期间的老用户凡是卡里余额小于20的,赠送10元,但是这种活动没人只可享受一次。
然后火锅店的后台程序员小王开始工作了,很简单就用cas技术,先去用户卡里的余额,然后包装成AtomicInteger,写一个判断,开启10个线程,然后判断小于20的,一律加20,
然后就很开心的交差了。可是过了一段时间,发现账面亏损的厉害,老板起先的预支是2000块,因为店里的会员总共也就100多个,就算每人都符合条件,最多也就2000啊,怎么预支了这么多。
小王一下就懵逼了,赶紧debug,tail -f一下日志,这不看不知道,一看吓一跳,有个客户被充值了10次! 阐述:假设有个线程A去判断账户里的钱此时是15,满足条件,直接+20,这时候卡里余额是35.但是此时不巧,正好在连锁店里,这个客人正在消费,又消费了20,此时卡里余额又为15,
线程B去执行扫描账户的时候,发现它又小于20,又用过cas给它加了20,这样的话就相当于加了两次,这样循环往复肯定把老板的钱就坑没了!
本质:ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成casd多次执行的问题。
解决方案:使用AtomicStampReference
AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:
AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:
//参数代表的含义分别是 期望值,写入的新值,期望标记,新标记值 public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp); public V getRerference(); public int getStamp(); public void set(V newReference,int newStamp);
我们定义了一个money值为19,然后使用了stamp这个标记,这样每次当cas执行成功的时候都会给原来的标记值+1。
而后来的线程来执行的时候就因为stamp不符合条件而使cas无法成功,这就保证了每次只会被执行一次。
代码走起:
public class AtomicStampReferenceDemo { static AtomicStampedReference<Integer> money =new AtomicStampedReference<Integer>(19,0); public static void main(String[] args) { for (int i = 0; i < 3; i++) { int stamp = money.getStamp(); System.out.println("stamp的值是"+stamp); new Thread(){ //充值线程 @Override public void run() { while (true){ Integer account = money.getReference(); if (account<20){ if (money.compareAndSet(account,account+20,stamp,stamp+1)){ System.out.println("余额小于20元,充值成功,目前余额:"+money.getReference()+"元"); break; } }else { System.out.println("余额大于20元,无需充值"); } } } }.start(); } new Thread(){ @Override public void run() { //消费线程 for (int j = 0; j < 100; j++) { while (true){ int timeStamp = money.getStamp();//1 int currentMoney =money.getReference();//39 if (currentMoney>10){ System.out.println("当前账户余额大于10元"); if (money.compareAndSet(currentMoney,currentMoney-10,timeStamp,timeStamp+1)){ System.out.println("消费者成功消费10元,余额"+money.getReference()); break; } }else { System.out.println("没有足够的金额"); break; } try { Thread.sleep(1000); }catch (Exception ex){ ex.printStackTrace(); break; } } } } }.start(); } }.....
这样实现了线程去充值和消费,通过stamp这个标记属性来记录cas每次设置值的操作,而下一次再cas操作时,由于期望的stamp与现有的stamp不一样,因此就会设值失败,从而杜绝了ABA问题的复现。