上一篇文章,小编介绍到了一些关于锁策略的内容。

那么这篇文章,小编继续分享,由锁策略延伸出的一些内容。

比如,上篇讲过,synchronized中

加锁过程中,是有无锁==》偏向锁==》轻量级锁==》重量级锁==》

那么这个轻量级锁呢,会使用CAS,对操作进行原子化,保证数据安全,从而避免阻塞状态。

那么这个CAS,到底是什么呢?

CAS

全称:compare and swap

字面意思就是比较交换。

它的核心操作就是:

假设内存中有元数据A,寄存器1:旧的预期值V,寄存器2:需要修改的值B

1.比较A与V的值

2.如若相等,此时就把B的值写进A中

3.然后返回操作是否成功信息

交换过程:

那么这里提供了一个伪代码:

这里的CAS,是将操作变成原子化执行,原理呢就是通过底层“一条CPU指令“执行。

既然是一条cpu指令执行操作,就是很大程度上避免了多线程编程,也多线程编程提供这样的一个思路,就是无锁化编程,毕竟加锁操作,是要增加资源开销的。

对于一些判定再赋值、++、--操作,CAS是非常适用这里的,毕竟那些操作,在多线程环境下,存在多线程安全问题。

那么这个CAS会有哪些应用呢?

1.原子类

原子类是java标准库中,提供了一种以线程安全的方式执行单一变量操作的类。

位于java.until.concurrent.atomic包下

常见的一些基本类型的原子类:、

AtomicInteger:对整数类型的原子操作。

AtomicLong:提供对长整型的原子操作。

AtomicBoolean:提供对布尔值的原子操作。

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray:分别提供对整数数组、长整型数组和对象引用数组的原子操作。

这里提供一个如何使用的例子:

代码例子:

public class Demo31 {
    private static AtomicInteger count=new AtomicInteger();
    public static void main1(String[] args) throws InterruptedException {
      Thread t1=new Thread(()->{
         for(int i=0;i<5000;i++){
             count.getAndIncrement();
         }
      });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println("count:"+count);
    }
}

例子,实现的是count++操作到10000次,由两个线程执行。

那么用原子类实现的话,cout++ ==> count.getAndIncrement();

同理,其他的一些操作

count-- ==>count.getAndDecrement();

++count ==> count.incrementAndGet();

--count ==> count.decrementAndGet();

那么要是给到我们模拟实现通过CAS实现原子类呢?

那么此时,这里也是给到一个伪代码实现

 class AtomicInteger{
        //这个是内存中的值public int value;
        public int getIntIncrement(){
            //oldValue指的是寄存器1中int oldValue=value;
            while (CAS(value,oldValue,oldValue+1)!=true){
                //oldValue+1指的是寄存器2的值
                oldValue=value;
            }
            return oldValue;
        }
    }

那么对于CAS呢,还一个应用的地方。

实现自旋锁

这个自旋锁就前面一篇文章分享过了,就是当这个线程尝试去获取锁的时候,如若此时的锁获取失败了,它任然是不释放资源,继续去尝试获取锁。

这里也是提供一个伪代码实现

class SpinLock{
        //表示该线程没有持有该锁public Thread owner=null;
        public void lock(){
            while(!CAS(this.owner,null,Thread.currentThread())){
                //如若当前的锁,任然是null,即没有被其他线程获取到,设置当前线程为持有该锁状态//如若被获取到了,就不断自旋
            }

        }
        public void unlock(){
            this.owner=null;
        }
   }

ok,一套下来,诶发现这个CAS是不是还挺好用的,但是呢,一个事物有好的一面,也有不好的一面。

CAS是会存在一个关键性问题。

ABA问题

CAS之所以可以保证线程安全,是因为,可以感知到当前的内存值有没有被别的线程修改。

但是此时此刻,如若是一个线程修改了当前内存值,但是,这个线程又修改回去了。

那么此时,CAS它是感觉不到的。

举个极端例子

银行转账

假设我们的转账功能代码如下:

 public void TransferCount(int n){
        int oldvalue=value;
        if(!CAS(value,oldvalue,oldvalue-n)){
            System.out.println("转账失败");
        }else{
            System.out.println("转账成功");
        }
   }

转账过程如下,由两个线程控制:

那么此时,就是一个bug的出现

CAS感知不到,value值是不是被修改了。

那么对于这个情况,也不是没有办法去解决。

我们可以引入版本号去解决

代码实例:

 public void TransferCount(int n){
        int oldVersion=version;
        if(!CAS(version,oldVersion,oldVersion+1)){
            System.out.println("转账失败");
        }else{
            value-=n;
        }
    }

因为版本号,是一直递增/递减,所以,即使是中间修改value值,

但是版本号变了,和原来的不一致,所以CAS是可以感知到的。

其实,对于CAS的ABA问题呢,其实一般是bug不太大。

就像上面举的例子,也是在极端情况下进行的。

那么接下来分享的是java.until.concurrent一些类

这个java.until.concurrent可以简称为JUC

JUC

首先第一个是Callable

Callable

它是一个接口,类似于Runnable接口

不过它呢,是带有返回值的。

它的一个call()方法是可以返回当前的值,这个call()方法还可以抛出受检异常。

那么举个例子来示范下它是怎么用的吧

比如我们计算从0-5000的值

代码范例:

 public static void main2(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable=new Callable<Integer>() {
            @Overridepublic Integer call() throws Exception {
                int sum=0;
                for(int i=0;i<5000;i++){
                    sum+=i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask=new FutureTask<>(callable);
        Thread t=new Thread(futureTask);
        t.start();
        System.out.println(futureTask.get());
    }

值得注意的是,这个Callable是要带一个泛型参数的,并且还是里面的call()方法返回类型

也是和泛型参数是一致的。

但为什么多出了一个FutureTask来了呢?

主要是Thread不能接收callable作为参数,

所以引入了这个FutureTask

这个FutrueTask是什么呢?

它实现了Runnable和Futrue接口,可以看作为一个任务存储的东西。

它把存储到的任务交给线程执行。

同时自身带了一个get()方法用来返回任务类型的中的值

ReentrantLock

可重入互斥锁,与synchronized类似,实现互斥效果,也是为了保证线程安全。

它呢,常用的有三个用法

lock():加锁,长时间获取不到锁就死等

try lock(超时时间):加锁,如果获取不到锁,等待一段时间后,就会放弃加锁

unlock():解锁,通常是放在了finall{}中的。

但既然有了synchronized后,为什么还需要这个Reentrantlock呢?

这是因为和synchronized是有些区别的

1.synchronized是在jvm内部,大部分是通过c++代码实现的

而Reentrantlock则是在jvm外,通过java代码实现

2.synchronized使用时,不是手动释放锁,而Reentrantlock则是可以通过unlock手动去释放锁,

使用起来,较为灵活。

3.synchronized 在获取不到锁的时候,是死等,Reentrantlock则是通过try lock操作等待一段时间释放锁

4.synchronized是非公平锁,Reentrantlock通过传入true给构造方法,开启公平锁模式。

5.同时Reentrantlock还提供了更强的”等待通知机制“

基于Condition 类,可以实现通知到某一个线程。

Semaphore

信号量

用来表示 可用资源个数,本质上是一个计数器来的。

增加操作就是+1,称为P操作

减少操作就是-1,称为V操作

有几个常用操作

acquire():从信号量获取一个许可,如果没有可用的许可,则当前线程等待直到有许可可用或者当前线程被中断。

release():释放一个许可,将其返回给信号量

tryAcquire():尝试获取一个许可,如果有立即返回true,否则立即返回false,不会阻塞当前线程。

availabePermits():返回此信号量中当前可用的许可数

drainPermits():获取并返回立即可用的所有许可,即消耗所有许可,即原本有三个信号量,那就调用这个方法后,所有许可没有了,执行acquire就会阻塞,直到释放一个许可。

值得注意的是,当信号设置为1的时候,也是可以当一个”锁“来使用的

那么这里给到一个例子,也是计算count++一万次,通过两个线程进行操作

  public static int count1=0;
    public static void main3(String[] args) throws InterruptedException {
     

        Semaphore semaphore=new Semaphore(1);
        Thread t1=new Thread(()->{
            for(int i=0;i<5000;i++){

                try {
                    semaphore.acquire();
                    count1++;
                    semaphore.release();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                try {
                    semaphore.acquire();
                    count1++;
                    semaphore.release();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count1);
    }

CountDownLatch

这个类,作用就是允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。

举个代码例子:

public static void main(String[] args) throws InterruptedException {
        ExecutorService pool=Executors.newFixedThreadPool(4);
        CountDownLatch countDownLatch=new CountDownLatch(20);
        for(int i=0;i<20;i++){
            int id=i;
            pool.submit(()->{
                System.out.println("任务执行:"+id);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("任务结束:"+id);
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        System.out.println("所有任务执行完毕!");
    }
}

这个代码就是说,让一共有20个任务,然后线程池中存储4个任务,submit执行完任务后,

countDownLatch-1,然后在主线程中,countDownLatch.await()阻塞主线程执行

直到countDownLatch减到0。

这个JUC相关类就介绍到这

接下来来分享下

线程安全中的一些集合类

我们知道

这个ArrayLsit是线程不安全的。

有一些类,比如Vetor,stack,Hashtable是线程安全

它们内置了synchronized

但是里面呢,有点像是”无脑“加锁的感觉,所以对于一些日常开发呢,

并不是很推荐使用。

话说回来,既然这个ArrayList是线程不安全的,那么如若想要它变得安全

我们确实是可以使用这个加锁操作。

但是呢,线程的标准库中,也是提供了一个带锁的List

比如:

那么和vector有什么区别呢?

最大的区别就是,synchronizedList提供了一种方式,使得可以在需要的时候进行加锁操作

而vector呢,则是所有方法都带锁,在有些时候不需要锁的情况下,也是会引入的。

所以对资源的利用率不是很高。

除了这一个SynchronizedList之外,还有一些方法:

当前,除了这样的类型除外,还有另一个类可以使用,这个也是可以保证线程安全的。

比如CopyOnWriteArrayList。

这个是位于java.util.concurrent包中

CopyOnWriteArrayList这个集合类,并没锁,它是通过”写时拷贝“来实现线程安全。

通过这个”写时拷贝“,可以避免两个线程同时修改一个变量的情况。

那这个”写时拷贝“到底是怎么样的呢?

值得注意的是,赋值操作是原子性,就是将新的数组赋值到ArrayList引用过程中

写时拷贝也是有缺点的。

1.无法应对多个线程修改的情况

2.数组内容数据量过大,拷贝非常慢

ok,这个东西呢,就暂且分享到这。

如若是多线程使用队列的话,那么这里推荐使用BlockingQueue

多线程使用哈希表的话,那么推荐使用这个ConcurrentHashMap

即使是HashMap也是一个可选项,但是这个ConcurrentHashMap是改进过后的。

HashMap是对put、get等方法也是加上锁了,整个hash对象就是一把”锁“,对其一些操作,会触发锁竞争

ConcurrentHashMap改进的核心就是:

1.优化了锁的粒度

具体来说呢,

就是对hash表中的”链表“进行加锁,从而变成一个个锁桶。

只有进行两次对同一个元素修改的时候,才会触发到锁竞争,所以这时候是降低了锁冲突的概率。

其次,还有一些改进了的地方。

2.ConcurrentHashMap引入了CAS原子化操作

针对size++/--操作,借助CAS原子化操作,而不是真正的加锁。

3.针对这个读操作进行一些特殊处理。

就是引入了volatile和关键字和其他一些精巧涉及,使其读操作,不会出现”读一半“情况。

4.针对其Hash扩容,也进行了一定的优化

以前的Hash表,进行扩容的时候

是把整个数据都扩容过去了,虽然是一次性完毕,但是数据量过大,造成耗时过长。

所以,ConcurrentHashMap操作呢,就是每次操作的时候,会触发一部分key进行搬运

直至所有的key搬运完成。

值得注意的是,

修改/删除/查询操作在新的扩容表和旧的扩容表中操作

而插入操作是在,新的扩容表中操作。

ok,那么小编就暂且分享到这里。

完!

文章作者: 南汐
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 www.phblog.cloud
JavaSE JavaSE
喜欢就支持一下吧