Java多线程与高并发四(Lock与CAS)

我们在《Java多线程与高并发一》中讲到怎样新建一个线程,用synchronized怎样给代码加锁,以及synchronized锁的优化升级。

在《Java多线程与高并发二》中,我们认识了Java的内存模型和计算机的内存架构,以及二者之间的不同,怎样弥补。

在《Java多线程与高并发三》中,我们我们认识了volatile关键字,以及其作用和深入原理。

《Java多线程与高并发四》给大家介绍Lock,在官方介绍中也提到了,Lock可以替代synchronized,有更好的语义,更灵活的方法,以及介绍Lock背后的原理。

Lock简单用

我们看一个多线程对int类型数进行自增的demo

import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * lock测试
 *
 * @author zab
 * @date 2019-11-03 11:22
 */
public class LockTest {
    static Semaphore semaphore1 = new Semaphore(0);
    static Semaphore semaphore2 = new Semaphore(0);
    static Lock lock = new ReentrantLock();
    static int i1 = 0;
    static int i2 = 0;

    public void f1() {
        try {
            lock.lock();
            for (int j = 0; j < 1000000; j++) {
                i1++;
            }
        } finally {
            lock.unlock();
        }

        semaphore1.release();
    }

    public void f2() {
        try {
            lock.lock();
            for (int j = 0; j < 1000000; j++) {
                i1++;
            }
        } finally {
            lock.unlock();
        }
        semaphore2.release();
    }

    public void f3() {

        for (int j = 0; j < 1000000; j++) {
            i2++;
        }

        semaphore1.release();
    }
    public void f4() {

        for (int j = 0; j < 1000000; j++) {
            i2++;
        }

        semaphore2.release();
    }


    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        new Thread(lockTest::f1).start();
        new Thread(lockTest::f2).start();
        new Thread(lockTest::f3).start();
        new Thread(lockTest::f4).start();

        try {
            semaphore1.acquire();
            semaphore1.acquire();
            semaphore2.acquire();
            semaphore2.acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(i1);
        System.out.println(i2);
    }


}

f1(),f2()方法是加锁的对i1进行自增

f3(),f4()方法是没加锁的对i2进行自增

我们在输出结果i1,i2前,用信号量对代码进行阻塞,也就是semaphore.acquire()方法,等四个线程都执行完了,对i1,i2进行输出。

可以看到i1是加到了200000,但是i2却始终没有200000.

这个例子在之前介绍synchronized有试验过,可以看到lock的基本用法与synchronized还是有区别:

1、lock需要手动上锁与解锁,而且建议在finally语句块里解锁。

2、lock只能在代码块中上锁,而不能直接修饰方法。

Lock之lock()

Lock的lock()方法,是获得锁,有三种情况:

第一如果没有其他线程持有锁,那么当前在设置完hold count为1后立马就返回了;

第二如果当前线程持有该锁,就会把hold count加1然后返回。

第三如果锁被另一个线程持有,则当前线程将因线程调度而禁用,并处于休眠状态,直到获得锁为止,此时锁hold count设置为1。

public void lock() {
    sync.lock();
}

 lock方法里直接调用sync.lock()方法,这个方法有公平锁和非公平锁两种实现,sync则是ReentrantLock的一个抽象内部类,其唯一一个抽象方法就是lock(),我们看看公平锁的实现:

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
//省略其他代码
}

这样看起来,这个Lock的lock()方法实现还真简单,就是调用acquire(1),表面上意思就是获得1。

这个acquire是AQS(AbstractQueuedSynchronizer),即CAS的核心类,的一个重要方法。我们看看这个方法的注释:

Acquires in exclusive mode, ignoring interrupts.  Implemented by invoking at least once {@link #tryAcquire}, returning on success.  Otherwise the thread is queued, possibly repeatedly blocking and unblocking, invoking {@link#tryAcquire} until success.  This method can be used to implement method {@link Lock#lock}.

翻译:以独占模式获取,忽略中断通过调用至少一次tryAcquire方法来实现,成功时返回。否则线程将排队,可能会重复阻塞和解除阻塞,调用tryAcquire直到成功,此方法可用于实现方法Lock的lock()方法。

从注释上来看,lock()方法的确在高并发的情况会大量堆积在队列里,并且不断尝试获得锁,增加CPU开销。我们本章不追究源码实现。

Lock之公平锁

Lock在new的时候可以指定是否是公平锁,像这样:

Lock lock = new ReentrantLock(true);

如果传true,表示是公平锁。

那么公平锁,非公平锁是什么概念呢??我们点进去看源码可以发现ReentrantLock的构造方法是这样的:

public ReentrantLock(boolean fair) {
     sync = fair ? new FairSync() : new NonfairSync();
}

这是一个三元运算符,true表示new了一个FairSync,我们继续跟。发现FariSync是这样的:

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

其公平的核心在于hasQueuedPredecessors()这个方法,我们跟进去看看怎么说:

Queries whether any threads have been waiting to acquire longer than the current thread.

翻译过来说:查询是否有任何线程等待获取的时间超过当前线程。

在这一章,我们不追究到AQS的源码,只知道公平锁会去检查队列是否有线程排队,如果有就会tryAcquire失败。

非公平锁会调用:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

可以看到第五行代码,这个方法判断c等于0过后,不管有没有队列,直接争抢,进行CAS操作。

Lock之tryLock()

从字面上看,Lock可以尝试获得锁,追进去,其实就是返回上面贴的nonfairTryAcquire()方法:

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

但是参数传了1。那可以这样理解,如果getState()这个方法返回值是0,(预告下,state是AQS抽象类里面的核心参数)

tryLock就是尝试把0改为1,改成功了就获得锁!

tryLock就是尝试把0改为1,改成功了就获得锁!

tryLock就是尝试把0改为1,改成功了就获得锁!

而更改方法是用的CAS的操作。

tryLock()方法可以带参数

lock.tryLock(1, TimeUnit.SECONDS);

 第一个参数是时间,第二个是时间单位。带时间的tryLock表示只要没被打断,就会在指定的时间内尝试获得锁。这个就给我们处理某些问题带来了好处,比如某个热点数据更新,一直拿不到锁在那死等,还不如等待几秒钟如果再拿不到就直接处理失败的逻辑。

Lock之Condition

lock可以获得condition,而condition可以在锁的代码块中实现类似wait和notify的功效。

看这段消费者和生产者代码:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest1 {

    static Lock lock = new ReentrantLock(true);
    static Condition condition1 = lock.newCondition();
    static Condition condition2 = lock.newCondition();

    static int i = 0;

    public void f1(){
        try {
            lock.lock();
            while (true) {
                while (i < 10) {
                    i++;
                    condition2.signal();
                    System.out.println("生产了"+i);
                }
                try {
                    condition1.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {

            lock.unlock();
        }
    }
    public void f2(){
        try{
            lock.lock();
            while(true){
                while (i>1){
                    i--;
                    condition1.signal();
                    System.out.println("消费了"+i);
                }
                try {
                    condition2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }finally {
            lock.unlock();
        }

    }

    public static void main(String[] args) {
        LockTest1 lockTest1 = new LockTest1();
        new Thread(lockTest1::f1).start();
        new Thread(lockTest1::f2).start();
    }


}

 main方法很简单,就是起了两个线程,分别执行LockTest1的f1和f2方法,f1和f2方法内部都用lock加锁,起一个死循环,在循环内部对公有变量i进行增增减减操作。当i的条件不满足死循环里层的while条件时,也就是大于等于10或者小于等于1时,线程就阻塞,但是while条件中会唤醒沉睡的另一方工作。

输出效果大致长这样:

 可以用condition来控制两线程的交替打印(面试题) 

什么是CAS

cas即compare and set(swap),就是比较并且设置(或者交换)。我们看一段伪代码:

m=0
m++
Expected=read m
CAS(Expected,NewValue){
    if m==Expected
    m=NewValue
}

代码表意,我们想要对m进行自增,首先需要读出期望值,也就是0;再者,在采用CAS修改m的时候,我们希望更改m前做一次判断,到底改之前,这个m还是不是期望的那个值0。如果m在修改前与期望值0相等,那就修改m的值。

整个CAS过程由CPU原语支持,不需要加锁。所以效率比较高,没有synchronized膨胀到重量级锁费时的操作系统内核态的转变过程。

那CAS有没有什么弊端呢?

弊端一:ABA问题

模拟一个场景:本人在银行里有两百块钱(真穷),欲约妹子去吃个麻辣烫,想要去取款机取一百作为约会基金,选择了100按下确定键,由于机器故障发送了两次扣款一百请求(场景需要,麻烦机器坏一次),假设机器扣款用的CAS操作(再麻烦机器配合表演下,假装是CAS扣款的),会出现以下伪代码操作

myMoney = 200
CAS(期望值200,新值100){
       if(myMoney==200){
               myMoney = 100
       }
}

假设这时候两次请求有一次出现不明原因的阻塞(再麻烦机器配合以下表演,卡死一个请求),以上伪代码只会运行一次,并且本人的银行余额已经被扣除了一百余额一百,那这时候恰好我妈给我打了一百块,这个请求先于阻塞的请求运行,那么本人的银行卡就余额两百,注意这个时候,就在这个时候,那阻塞的线程请求开始CAS操作(再次请出机器帮忙演戏),屁颠屁颠地看到我银行卡还有两百,就把我妈给我的一百块扣除了,又使我的银行卡余额变成一百(流氓ABA,还我吃麻辣烫的钱)!

ABA问题在某些情况下会造成问题,解决ABA的问题需要在操作的对象上加一个版本号,每次CAS操作不仅要对比期望值还要对比版本号,版本号和期望值都满足,才进行CAS操作。

弊端二:高并发情况下,多个线程都尝试去改某个值,改不成功,程序会一直尝试,CPU会大量消耗,这时候效率反而可能不如synchronized的重量级锁来的高。

弊端三:CAS只能对某个值进行修改,如果想要对多个值进行修改,就显得有点力不从心,想要锁多个值(其实就是锁代码块)并且还要线程安全,就用synchronized。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java多线程高并发Java中非常重要的概念和技术,它们可以让Java程序更加高效地利用计算机多核CPU的性能,提升程序的并发能力,提高程序的性能和响应速度。 Java多线程机制是基于线程对象的,它允许程序同时执行多个任务,从而提高程序的并发能力。Java中的线程有两种创建方式:继承Thread类和实现Runnable接口。其中继承Thread类是比较简单的一种方式,但是它会限制程序的扩展性和灵活性,因为Java只支持单继承。而实现Runnable接口则更加灵活,因为Java支持多实现。 Java高并发编程主要包括以下几个方面: 1. 线程池技术:线程池技术是Java中非常重要的高并发编程技术,它可以实现线程的复用,减少线程创建和销毁的开销,提高程序的性能和响应速度。 2. 锁机制:Java中的锁机制包括synchronized关键字、ReentrantLock锁、ReadWriteLock读写锁等,它们可以保证多个线程之间的互斥访问,避免数据竞争和线程安全问题。 3. CAS操作:CAS(Compare and Swap)操作是Java中一种非常高效的并发编程技术,它可以实现无锁并发访问,避免线程之间的互斥和阻塞,提高程序的性能和响应速度。 4. 并发集合类:Java中的并发集合类包括ConcurrentHashMap、ConcurrentLinkedQueue、CopyOnWriteArrayList等,它们可以实现线程安全的数据访问和操作,有效地提高程序的并发能力。 总之,Java多线程高并发Java编程中非常重要的概念和技术,掌握这些技术可以让Java程序更加高效地利用计算机多核CPU的性能,提升程序的并发能力,提高程序的性能和响应速度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值