多线程、锁、CAS和AQS(1)

多线程、锁、CAS和AQS(1)线程安全

1、什么是线程安全性?

当多个线程访问某个类时,不管运行时环境采用何种调度方式,并且主调代码中不需要使用任何额外的同步或者协同,这个类的输出总是正确的的。

  • 原子性:互斥访问,同一时刻只能有一个线程对它操作

  • 可见性:一个线程对主内存的修改可以及时的被其他线程观察到

  • 有序性:

2、什么是竞态条件?

在多线程环境下,由于不恰当的执行顺序而出现不正确的结果。换句话说,就是正确的结果取决于运气。这种情况就叫竞态条件,出现了竞态条件,就代表了线程不安全。


3、原子性 Atomic包

熟知的Atomic包与线程安全的原子性相关,原因是Atomic包中对数据的操作实现了了大名鼎鼎的compareAndSwapInt,CAS操作

原子类AtomicInteger的实现原理

源码如下:

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;
    }

  • var1:当前对象
  • var2:offset,对象偏移量,通过var1, var2可以取到底层中对应的值
  • var5:底层的值,var5 = this.getIntVolitale(var1, var2)的意思是,var1:当前对象+var2:偏移量 ,可以取到底层中的实际值。
  • var4:增长的值

源码分析:

  • 通过var1(当前对象)加上var2(偏移量地址),定位到底层中对应的值,然后把这个值和var5(之前定位底层的值)进行比,如果一样,就更新底层中对应的值。

  • 如果不一样说明己经被更新过了,这一步CAS没有成功,那就采用自旋的方式继续进行CAS操作。这块是一个CPU指令完成的,依旧是原子操作。

类AtomicInteger与LongAdder对比

  • LongAdder的思想是将热点数据分离,放入数组,相当于将AtomicInteger单点更新的压力分散到了多点,提高性能。

  • LongAdder的缺点是统计数据可能会有误差,不建议使用

4、原子性 synchronize

原子性同样可以使用synchronize关键字保证,synchronize关键字可以修饰代码块、方法、静态方法和类


5、原子性 lock类

原子性可以使用lock来实现,ReentrantLock是Lock的实现类,也是Lock唯一的实现类

Lock和synchronized的区别

Lock提供了比synchronized更多的功能:

  • Lock可以让等待线程只等待一定的时间或者响应中断,Synchronized则是无限等下去。

  • Lock可以让多个线程只是进行读操作的时候共享锁,Synchronized则是一个线程读操作时,其他线程只能等待。

  • Lock可以知道线程有没有成功获取到锁,Synchronized则不行。

  • 但是Lock必须用户手动写代码释放锁,如果没有主动释放锁,就有可能导致出现死锁现象,因此使用Lock时需要在finally块中释放锁。而synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。

Lock中声明的方法

  • Lock():是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

  • tryLock():方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

  • tryLock(long time, TimeUnit unit):方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

  • 所以,一般情况下通过tryLock来获取锁时是这样使用的:

  • lockInterruptibly():方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。因此lockInterruptibly()一般的使用形式如下:




6、CAS(Compare and swap)分析

  • CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。

  • 现在CPU内部已经执行原子的CAS操作,Java5+中内置的CAS特性可以让你利用底层的你的程序所运行机器的CPU的CAS特性,这会使代码运行更快。

  • Java5以来,你可以使用java.util.concurrent.atomic包中的一些原子类来使用CPU中的这些功能

CAS实例:

public class CountExample2 {
    private static Logger log = LoggerFactory.getLogger(CountExample2.class);

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    private static void add() {
        count.incrementAndGet();
    }
}

上面的代码使用AtomicInteger实现了并发状态下的累加,保证了线程的原子性,count变量不再是int类型而是AtomicBoolean。这个类中有一个AtomicInteger()方法,它使用一个期望值和AtomicInteger实例的值比较,若两者相等,则使用一个新值替换原来的值。

CAS用于同步(乐观锁的机制就是CAS)


  • 通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

  • 类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法可以对该操作重新计算。

CAS存在的问题(三个)

ABA问题

  • 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

  • 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销大

  • 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
  • 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

只能保障一个变量的原子操作

  • 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。

  • 这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

7、synchronize底层实现原理

对象的同步Synchronized的底层是通过monitor来完成

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:


  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

释放锁则是通过monitorexit指令,执行monitorexit的线程必须是objectref所对应的monitor的所有者,指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

方法的synchronized同步

相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。


8、ReentrantLock(Lock接口类)底层实现原理

整体来看Lock主要是通过两个东西来实现的分别是CAS和ASQ,如下:

  • AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

  • 对于刚来竞争的线程首先会通过CAS设置状态,如果设置成功那么直接获取锁,执行临界区的代码;

  • 反之如果已经存在Running线程,那么CAS肯定会失败,则新的竞争线程会通过CAS的方式被追加到队尾。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。






全部评论

相关推荐

点赞 1 评论
分享
牛客网
牛客企业服务