线程池的学习(六)

公平锁和非公平锁

  1. 公平锁:是指多个线程按照申请锁的顺序来获取锁
  2. 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获锁。
非公平锁一上来就尝试占用锁,如果尝试占用失败,就采用公平锁的方式到末尾排队。
在高并发的情况下,有可能造成优先级反转或饥饿现象
非公平锁的优点在于吞吐量比公平锁大。
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问题的复现。

posted @ 2020-03-29 01:55  An-Optimistic-Person  阅读(89)  评论(0编辑  收藏  举报