多线程和锁

ThreadLocal 用过么,用途是什么,原理是什么,用的时候要注意什么?

 (1)每个Thread维护着一个ThreadLocalMap的引用

(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。

(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中

(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。

(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

CAS机制?

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。CAS机制当中使用了3个基本操作数:

(1)内存地址V,也就是AtomicInteger中的valueOffset。

(2)旧的预期值A,也就是getAndIncrement方法中的current。

CAS机制中,更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。下面我们来看一个具体的例子:

(1)在内存地址V当中,存储着值为10的变量。

(2)此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

(3)但是,在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

(4)此时,线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

(5)线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

(6)这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

      对比Synchronized,我们可以发现,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS机制缺点:

(1)ABA问题

       如果V的初始值是A,在准备赋值的时候检查到它仍然是A,那么能说它没有改变过吗?也许V经历了这样一个过程:它先变成了B,又变成了A,使用CAS检查时,以为它没变,其实却已经改变过了。

(2)CPU开销较大

     在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

(3)不能保证代码块的原子性

     CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

怎么解决ABA?

使用比较旧的内存地址中的值以及版本号一起比较就好了AtomicStampedReference类就实现了用版本号做比较的CAS机制

 

 

AtomicInteger(AtomicLong、AtomicReference)的内部实现:存在ABA问题

public final int getAndIncrement() {
         for (;;) {
             int current = get();
             int next = current + 1;
             if (compareAndSet(current, next))
                 return current;
         }
 }

这个方法的做法为先获取到当前的 value 属性值,然后将 value 加 1,赋值给一个局部的 next 变量,然而,这两步都是非线程安全的,但是内部有一个死循环,不断去做compareAndSet操作,直到成功为止,也就是修改的根本在compareAndSet方法里面。compareAndSet()方法的代码如下:

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
 }

传入的为执行方法时获取到的 value 属性值,update为加 1 后的值,compareAndSet所做的为调用 Sun 的 UnSafe 的 compareAndSwapInt方法来完成,此方法为 native 方法,compareAndSwapInt 基于的是CPU 的 CAS指令来实现的

线程池的关闭方式:

shutdown()调用后,不可以再 submit 新的 task,已经 submit 的将继续执行
shutdownNow()调用后,试图停止当前正在执行的 task,并返回尚未执行的 task 的 list

Lock与Sychronized区别?

  1. 来源:
    lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

  2. 异常是否释放锁:
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

  3. 是否响应中断
    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

  4. 是否知道获取锁
    Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

  5. Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

  6. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  7. synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度

如果让你实现一个并发安全的链表,你会怎么做?

可以使用读写锁ReentrantReadWriteLock(int 高低16位来实现)

static Map<String, Object> map = new HashMap<>();

static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

static Lock r = rwl.readLock();

static Lock w = rwl.writeLock();//获取一个key对应的value

public static final Object get(String key){

r.lock();

try { return map.get(key);

} finally {

r.unlock();

}

}//设置key对应的value,并返回旧的value

public static final Object put(String key,Object value){

w.lock();

try {

return map.put(key, value);

} finally {

w.unlock();

}}

锁升级过程?

 

markword 中存储有

无锁

偏向锁

  (存储threadid,每次比较threadid 来判断要不要升级为轻量级)、

轻量级锁

  (自旋,没有获取到的线程自旋,而不是阻塞,如果很多自旋次数很多,就会升级重量锁)

重量级锁

直接阻塞,阻塞需要从用户态到内核态转换,消耗cpu)

jvm按照线程并发的情况选择 四种的一种锁 

countdowlatch 和 cyclicbarrier 的内部原理和用法,以及相互之间的差别(比如countdownlatch 的 await 方法和是怎么实现的)?

1.CountDownLatch减计数,CyclicBarrier加计数。
2.CountDownLatch是一次性的,CyclicBarrier可以重用。

 

CountDownLatch Await方法解析:

// 此方法用来让当前线程阻塞,直到count减小为0才恢复执行

public void await() throws InterruptedException {

// 这里直接调用sync的acquireSharedInterruptibly方法,这个方法定义在AQS中

// 方法的作用是尝试获取共享锁,若获取失败,则线程将会被加入到AQS的同步队列中等待

// 直到获取成功为止。且这个方法是会响应中断的,线程在阻塞的过程中,若被其他线程中断,

// 则此方法会通过抛出异常的方式结束等待。

sync.acquireSharedInterruptibly(1);

}

 

/** 此方法是AQS中提供的一个模板方法,用以获取共享锁,并且会响应中断 */

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {

     // 首先判断当前线程释放被中断,若被中断,则直接抛出异常结束

     if (Thread.interrupted()) throw new InterruptedException();

     // 调用tryAcquireShared方法尝试获取锁,这个方法被Sycn类重写了,

     // 若count == 0,则这个方法会返回1,表示获取锁成功,则这里会直接返回,线程不会被阻塞

     // 若count < 0,将会执行下面的doAcquireSharedInterruptibly方法,

     // 此处请去查看Sync中tryAcquireShared方法的实现

     if (tryAcquireShared(arg) < 0)

     // 下面这个方法的作用是,线程获取锁失败,将会加入到AQS的同步队列中阻塞等待,

     // 直到成功获取到锁,而此处成功获取到锁的条件就是count == 0,若当前线程在等待的过程中,

     // 成功地获取了锁,则它会继续唤醒在它后面等待的线程,也尝试获取锁,

     // 这也就是说,只要count == 0了,则所有被阻塞的线程都能恢复运行

     doAcquireSharedInterruptibly(arg); }

CountDownLatch CountDown方法解析:

/** * 此方法的作用就是将count的值-1,如果count等于0了,就唤醒等待的线程 */

public void countDown() {

     // 这里直接调用sync的releaseShared方法,这个方法的实现在AQS中,也是AQS提供的模板方法,

    // 这个方法的作用是当前线程释放锁,若释放失败,返回false,若释放成功,则返回false,

    // 若锁被释放成功,则当前线程会唤醒AQS同步队列中第一个被阻塞的线程,让他尝试获取锁

     // 对于CountDownLatch来说,释放锁实际上就是让count - 1,只有当count被减小为0,

     // 锁才是真正被释放,线程才能继续向下运行

    sync.releaseShared(1);

}

 

public final boolean releaseShared(int arg) {

     // 调用tryReleaseShared尝试释放锁,这个方法已经由Sycn重写,请回顾上面对此方法的分析

     // 若tryReleaseShared返回true,表示count经过这次释放后,等于0了,于是执行doReleaseShared 

     if (tryReleaseShared(arg)) {

     // 这个方法的作用是唤醒AQS的同步队列中,正在等待的第一个线程

     // 而我们分析acquireSharedInterruptibly方法时已经说过,

     // 若一个线程被唤醒,检测到count == 0,会继续唤醒下一个等待的线程

    // 也就是说,这个方法的作用是,在count == 0时,唤醒所有等待的线程

     doReleaseShared();

     return true;

     }

     return false;

}

了解AQS?(队列同步器,非常方便的帮我们实现共享锁和排它锁,如countdownlatch利用的就是AQS的共享锁)

 

AQS通过一个同步队列来维护当前获取锁失败,进入阻塞状态的线程。这个同步队列是一个双向链表,获取锁失败的线程会被封装成一个链表节点,加入链表的尾部排队,而AQS保存了链表的头节点的引用head以及链表的尾节点引用tail

 

独占锁获取锁源码分析

public final void acquire(int arg) {

    if (!tryAcquire(arg) &&

        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

        selfInterrupt();

}

上面的方法执行流程如下:

  1. 首先调用tryAcquire(是一个模板方法,我们自己去实现)尝试获取一次锁,若返回true,表示获取成功,则acquire方法将直接返回;若返回false,则会继续向后执行acquireQueued方法;

  2. tryAcquire返回false后,将执行acquireQueued,但是这个方法传入的参数调用了addWaiter方法;

  3. addWaiter方法的作用是将当前线封装成同步队列的节点,然后加入到同步队列的尾部进行排队,并返回此节点;

  4. addWaiter方法执行完成后,将它的返回值作为参数,调用acquireQueued方法。acquireQueued方法的作用是让当前线程在同步队列中阻塞,然后在被其他线程唤醒时去获取锁;

  5. 若线程被唤醒并成功获取锁后,将从acquireQueued方法中退出,同时返回一个boolean值表示当前线程是否被中断,若被中断,则会执行下面的selfInterrupt方法,响应中断;

addWaiter方法逻辑:将新线程封装成一个节点,加入到同步队列的尾部,若同步队列为空,则先在其中加入一个默认的节点,再进行加入;若加入失败,则使用死循环(也叫自旋)不断尝试,直到成功为止

acquireQueued方法逻辑:让线程在同步队列中阻塞,直到它成为头节点的下一个节点,被头节点对应的线程唤醒,然后开始获取锁,若获取成功才会从方法中返回

 

释放独占锁源码分析:

public final boolean release(int arg) {
    // 调用tryRelease尝试修改state释放锁,若成功,将返回true,否则false
    if (tryRelease(arg)) {
        // 若修改state成功,则表示释放锁成功,需要将当前线程移出同步队列
        // 当前线程在同步队列中的节点就是head,所以此处记录head
        Node h = head;
        // 若head不是null,且waitStatus不为0,表示它是一个装有线程的正常节点,
        // 在之前提到的addWaiter方法中,若同步队列为空,则会创建一个默认的节点放入head
        // 这个默认的节点不包含线程,它的waitStatus就是0,所以不能释放锁
        if (h != null && h.waitStatus != 0)
            // 若head是一个正常的节点,则调用unparkSuccessor唤醒它的下一个节点所对应的线程
            unparkSuccessor(h);
        // 释放成功
        return true;
    }
    // 释放锁失败
    return false;
}

release也是一个模板方法,其中通过调用tryRelease尝试释放锁,而tryRelease也需要使用者自己实现。在之前也说过,头节点释放锁时,需要唤醒它的下一个节点对应的线程,让这个线程不再等待,去获取锁,而这个过程就是通过unparkSuccessor方法实现的。

自己实现一个共享锁(读锁)?(利用AQS即可)代码如下:

/**
 * 抽象队列同步器(AQS)使用:
 *      实现一个同一时刻至多只支持两个线程同时执行的同步器
 */

// 让当前类继承Lock接口
        public class TwinLock implements Lock {

            // 定义锁允许的最大线程数
            private final static int DEFAULT_SYNC_COUNT = 2;
            // 创建一个锁对象,用以进行线程同步,Sync继承自AQS
            private final Sync sync = new Sync(DEFAULT_SYNC_COUNT);

            // 以内部类的形式实现一个同步器类,也就是锁,这个锁继承自AQS
            private static final class Sync extends AbstractQueuedSynchronizer {

                // 构造方法中指定锁支持的线程数量
                Sync(int count) {
                    // 若count小于0,则默认为2
                    if (count <= 0) {
                        count = DEFAULT_SYNC_COUNT;
                    }
                    // 设置初始同步状态
                    setState(count);
                }

                /**
                 * 重写tryAcquireShared方法,这个方法用来修改同步状态state,也就是获取锁
                 */
                @Override
                protected int tryAcquireShared(int arg) {
                    // 循环尝试
                    for (; ; ) {
                        // 获取当前的同步状态
                        int nowState = getState();
                        // 计算当前线程获取锁后,新的同步状态
                        // 注意这里使用了减法,因为此时的state表示的是还能支持多少个线程
                        // 而当前线程如果获得了锁,则state就要减小
                        int newState = nowState - arg;

                        // 如果newState小于0,表示当前已经没有剩余的资源了
                        // 则当前线程不能获取锁,此时将直接返回小于0的newState;
                        // 或者newState>0,就会执行compareAndSetState方法修改state的值,
                        // 若修改成功将,将返回大于0的newState;
                        // 若修改失败,则表示有其他线程也在尝试修改state,此时循环一次后,再次尝试
                        if (newState < 0 || compareAndSetState(nowState, newState)) {
                            return newState;
                        }
                    }
                }

                /**
                 * 尝试释放同步状态
                 */
                @Override
                protected boolean tryReleaseShared(int arg) {
                    for (; ; ) {
                        // 获取当前同步状态
                        int nowState = getState();
                        // 计算释放后的新同步状态,这里使用加法,
                        // 表示有线程释放锁后,当前锁可以支持的线程数量增加了
                        int newState = nowState + arg;
                        // 使用CAS修改同步状态,若成功则返回true,否则自旋
                        if (compareAndSetState(nowState, newState)) {
                            return true;
                        }
                    }
                }

            }

            /**
             * 获取锁的方法
             */
            @Override
            public void lock() {
                // 这里调用的是AQS的模板方法acquireShared,
                // 在acquireShared中将调用我们重写的tryAcquireShared方法
                // 传入参数为1表示当前线程,当前线程获取锁后,state将-1
                sync.acquireShared(1);
            }

            /**
             * 解锁
             */
            @Override
            public void unlock() {
                // 这里调用的是AQS的模板方法releaseShared,
                // 在acquireShared中将调用我们重写的tryReleaseShared方法
                // 传入参数为1表示当前线程,当前线程释放锁后,state将+1
                sync.releaseShared(1);
            }
        }

LinkedBlockingQueue与ConcurrentLinkedQueue区别?都是线程安全的队列

  • 阻塞队列,典型例子是 LinkedBlockingQueue适用阻塞队列的好处:多线程操作共同的队列时不需要额外的同步,另外就是队列会自动平衡负载,即哪边(生产与消费两边)处理快了就会被阻塞掉,从而减少两边的处理速度差距。采用Lock,condition实现的
  • 非阻塞队列,典型例子是 ConcurrentLinkedQueue当许多线程共享访问一个公共集合时,ConcurrentLinkedQueue 是一个恰当的选择。Queue 中元素按 FIFO 原则进行排序。采用 CAS操作,来保证元素的一致性

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值