并发编程java实现原理

volatile
保证了有序性和可见性,不保证原子性(读取、修改、写入,这三步仍然是分开的),所以并不是线程安全的。
三重功效:64位写的原子性(仅仅是写的原子性,读和写仍然分开)、内存可见性、禁止重排序(通过内存屏障禁止)。
当CPU写数据时,如果发现一个变量在其它CPU中存在副本,那么会发出信号量通知其它CPU将该副本对应的缓存行置为无效状态,其它CPU读取变量副本时会发现缓存行是无效的,然后它会从主内存重新读取。
参考:AQS-volatile、CAS - 光何 - 博客园 (cnblogs.com)
Volatile禁止指令重排序(三) - MXC肖某某 - 博客园 (cnblogs.com)
volatile禁止重排序的原理-内存屏障_binga的博客-CSDN博客_volatile重排序原理

重排序分类:编译器重排序、指令重排序、内存重排序(指令的执行顺序和写入主内存的顺序不完全一致)

CAS
在Unsafe包中,通过native类型方法实现原子写。读和写分开,乐观锁。

synchronized
java对象头里面会保存锁标志位以及当前持有锁的线程ID,包括无锁、偏向锁、轻量级锁、重量级锁等类型。
监视器锁monitorEnter、monitorExit
另,wait()、notify()必须和synchronized一起使用,因为调用wait()、notify()的对象本身也需要同步。wait()做了3件事:1)释放锁,2)阻塞等待notify()通知,3)再次获取锁。

ReentrantLock
锁本身不是单例,只是通过CAS保证了线程安全。
Concurrent包中的锁都是可重入锁。核心要素:
1、state记录是否有线程持有锁,以及重入次数。
2、exclusiveOwnerThread记录哪个线程持有锁。
3、park、unpark控制线程阻塞与唤醒。
4、queue,需要一个队列维护所有阻塞的线程。

Sync类为ReentrantLock的静态内部类。
一旦内部类使用static修饰,那么此时这个内部类就升级为顶级类。也就是说,除了写在一个类的内部以外,static内部类具备所有外部类的特性。
参考:Java中静态内部类和非静态内部类有什么区别?_vcliy的博客-CSDN博客_静态内部类

// 对于非公平锁,先插队,插队不成功再排队。
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

// 对于公平锁
        final void lock() {
            acquire(1);
        }

// AQS的acquire()方法
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

// tryAcquire动态绑定到NonfairSync或者FairSync里面的实现。
        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;
        }

// 公平锁
        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;
        }
    }

// 循环等待,被唤醒之后,检查是否排到自己,如果没有排到自己,继续将自己阻塞等待。
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // park()自己阻塞自己,可以通过unpark()或者interrupt()唤醒。unpark()可以唤醒指定线程,而notify做不到。
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

// 解锁不区分公平锁和非公平锁,先释放锁state--,然后唤醒下个线程。
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
synchronizedlock
java的关键字一个接口
不需要手动释放,包括异常也会自动释放锁需要手动释放unlock
无法获取锁的状态可以判断锁的状态
悲观锁采用CAS乐观锁实现
可重入、不可中断、非公平可重入、可中断、可公平
需要内核态和用户态切换CAS不会切到内核态

Condition
类似于synchronized,await()、signalAll也必须和Lock一起使用。synchronized和wait()、notify()作用的对象必须是同一个,强绑定了只能有一个状态,无法区分队列空和队列满两种状态,而Condition就可以解决该问题,一把锁,可以创建多个条件对象。
读写锁的读锁不支持Condition,写锁和互斥锁都支持Condition。
Condition之所以必须和Lock一起使用,是因为Condition的实现也是Lock的一部分。同样通过AQS队列同步器实现。

// park阻塞,添加到等待队列
        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 判断node节点是否在同步队列中,初始的时候,只在Condition的队列中,signal之后,才放在同步队列中。
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

// 唤醒。signalAll会把Condition队列中的所有node都放在同步队列中,signal只会把头节点放在同步队列中。然后才会唤醒节点对应的线程。
        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }

Q:为什么不直接放在同步队列里面排队呢,而要另起一个Condition队列呢?
A:放在同步队列里面,都是已经具备运行条件的,只差获取锁了。而Condition的,还要等待条件满足,才满足运行条件。

ReentrantReadWriteLock
ReadLock和WriteLock看起来是两把锁,实际上只是同一把锁的两个不同视图。
把state变量拆成两半,高16位记录读锁,低16位记录写锁。为什么不是使用两个int变量呢?因为CAS为了保证原子性,不能同时操作两个int变量。

对于公平锁,不管是读锁还是写锁,都不能插队,每次都去排队没有问题。
但是对于非公平锁,如果读锁每次都去插队,可能会导致写锁永远得不到锁。所以要对读锁进行约束,只有队列里面的头节点(注意,只是看头节点,如果是公平锁,只要没有排队到自己,都不能加读锁)不是写锁时,才能去插队。

StampedLock
jdk8新引入的,不仅读读不互斥,也能做到读写不互斥,只有写写才互斥。
采用了乐观读的策略,读的时候不加读锁,读出来之后,如果发现版本号改变了,再加读锁升级为悲观读。类似于MVCC。
这里的悲观读锁并不是基于AQS实现的,AQS的锁是通过阻塞实现的,而这里是自适应自旋锁。

Semaphore
和锁非常类似,基于AQS实现,有公平和非公平之分,但不可重入。使用state变量保存资源总数。acquire()/release()。

CountDownLatch
基于AQS实现,但是不区分是否公平。使用state变量保存资源总数,只要state不为0,就一直阻塞。想阻塞同步的线程调用await(),其他线程调用countDown()。

CyclicBarrier
基于ReentrantLock和Condition实现,await()会先判断所有线程是否执行完,如果没有执行完,主动阻塞自己;如果执行完了,会发送signalAll()唤醒所有线程。
1、和CountDownLatch相比,CyclicBarrier可以一直重复同步。
2、支持回调函数,每次到达同步点时执行一次。
3、CyclicBarrier还支持中断。

如何等待所有线程结束
1、join()
2、CountDownLatch
3、CyclicBarrier
4、线程池
参考:Java多线程--等待所有子线程执行完的五种方法 - MengJH - 博客园 (cnblogs.com)

线程安全的容器类
Vector:。
Hashtable:key、value都不可以为空。get()、put()使用synchronized同步全局加锁。
ConcurrentHashMap:key、value都不可以为空。
1、jdk7通过分段锁实现:
分了2^n个segement,每个segement再构造一个哈希表,并继承自ReentrantLock,put时通过该锁保证多线程不冲突,ReentrantLock的数量和segement的数量相等。
每个segement的初始化通过CAS乐观锁防止重复初始化,初始容量和segement[0]的相同。
segement的个数,即为并发级别数,初始化之后不可以扩容。但是每个segement可以单独按2倍扩容。
segement的put时,要先尝试获取锁,获取失败时,会做两件事:1、会先自旋一定次数之后,再阻塞;2、遍历链表,发现该节点不存在时,提前新建一个节点。
get时不需要加锁,volatile保证不会读取到过期数据。
参考:ConcurrentHashMap实现原理及源码分析 - dreamcatcher-cx - 博客园 (cnblogs.com)
2、jdk8的实现:
在HashMap的基础上,对数组元素的每个头节点使用synchronized进行并发控制。每个数组元素有一把锁,并发度等于数组长度。
构造函数,按(1.5倍的初始容量+1)往上取2^n。
初始化时,对sizeCtl使用CAS乐观锁对数组进行初始化。
put每个数组元素的首个节点时,使用CAS乐观锁。
扩容时,新建一张哈希表,采用多线程并发扩容,对于扩容完成的数组元素添加一个转移节点,转移到新的哈希表上,可以进行get()。
参考:深入解析 ConcurrentHashMap 实现内幕,吊打面试官,没问题 - 平头哥的技术博文 - 博客园 (cnblogs.com)
为何弃用分段锁?
1)并发度提升了,每次扩容都会提升并发度。
2)put时竞争同一个锁的概率非常小,分段锁反而会造成更新的长时间等待。
3)减少内存空间的浪费,ReentrantLock内存开销。
4)synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。而ReentrantLock是API级别的,优化空间小,产生依赖。
参考:Java8开始ConcurrentHashMap,为什么舍弃分段锁_天才小站-CSDN博客

非线程安全容器类
HashMap:key、value都可以为空。初始容量为16,按2倍扩容,方便扩容之后的数据搬移,在扩容时,不需要重新计算元素的hash了,只需要判断最高位是1还是0就好了,也就是和旧的容量进行位与看是否为0。数组的每个节点是维护一个链表,当节点数达到8个时,转为红黑树。另外计算hash值的时候会通过把高位和低位相异或加扰动,防止散列化不均匀。
jdk7扩容时采用头插法,会改变链表顺序,并发下可能出现环形链表问题(参考:【HashMap】扩容机制中尾插法造成环形链表导致死循环问题_numbbe的博客-CSDN博客),而jdk8采用尾插,解决了环链问题,但是仍然不是线程安全的,因为很多数据结构的设计并没有考虑并发场景。
ArrayList:维护了一个数组,初始容量为10,然后按1.5倍扩容,扩容之后,将原来的数组内容复制到新数组上。可以指定扩容的最小值,扩容之后的大小取max(1.5*oldCapacity, minCapacity)。
PriorityQueue:内部维护了一个数组,通过该数组实现了一个二叉堆,然后做堆排序。插入方法的时间复杂度o(logn)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值