Android实习面试准备——并发编程(一)

Android实习面试准备——并发编程(一),第1张

Android实习面试准备——并发编程(一)

1、假如只有一个cpu,单核,多线程还有用吗

        一般来说,一个任务既需要在CPU上花时间,也需要在IO上花时间,比如说读写文件等等。如果单核CPU、单线程的话,那么一个线程在处理的时候,CPU就是闲置的,这就浪费了CPU资源。而是多线程的话,一个CPU在等待IO的时候,另一个线程可以利用CPU进行计算,这样IO资源和CPU资源都得到了充分的利用。

2、sychronied修饰普通方法和静态方法的区别?什么是可见性

        sychronied修饰普通方法时,锁是对象锁。

        sychronied修饰静态方法时,锁是类锁,也就是类对应的class对象。每个类可以有不同的对象,但是class对象只有一个,所以如果不同的线程通过不同的对象调用方法不会造成线程阻塞,但是对于类锁而言,就算是不同的对象,多个线程调用该类的静态方法也会造成阻塞。

        可见性是指:多个线程访问同一个变量时,其中一个线程修改了这个变量的值,其它线程能够立即看到修改值。volatile可以保证可见性,它会保证修改的值会立即更新到主存,所以对其它线程是可见的。而普通的共享变量不能保证可见性,因为被修改后放在线程的工作内存里,何时更新到主存不确定,所以其它线程读取的时候可能得到的是未修改的值。

3、锁分哪几类

(1)互斥锁和自旋

        最基本的两种锁。互斥锁和自旋锁对于加锁失败之后的处理不一样。互斥锁加锁失败后,线程会释放CPU,给其他线程。自旋锁加锁失败后,线程会忙等待,直到它拿到锁。

(2)读写锁

        读写锁适用于能明确区分读 *** 作和写 *** 作的场景。写锁是独占锁,因为任意时刻只能有一个线程持有写锁,而读锁是共享锁,多个线程可以同时持有。读写锁可以根据应用场景选择互斥锁和自旋锁的一种来实现。

(3)乐观锁与悲观锁

        前面的互斥锁、自旋锁和读写锁都是悲观锁,悲观锁认为多线程同时修改共享资源的概率较高,很容易出现冲突,所以访问共享资源前就要上锁。乐观锁假定冲突的概率较低,所以她的工作方式是:先修改完共享资源,再验证这段时间有没有发生冲突,如果没有其它线程在修改资源,那么 *** 作完成;如果发现有其它线程在修改,这次 *** 作无效。

4、CAS无锁编程的原理

        CAS即compare and swap(比较与交换),无锁编程也就是在不使用锁的情况下实现多线程之间的变量同步,其涉及到三个 *** 作数:需要读写的内存值,预期值A,准备写入的新值B。工作原理是首先读取内存里的值V,和A进行比较,如果V=A,则将V更新为B,如果不相等,则 *** 作就失败,进入自旋等待状态。

        CAS是一种系统原语,是 *** 作系统硬件提供的功能,也就是说比较与交换是一种原子 *** 作,comapre成功了,swap也就成功了。

5、ReentrantLock和AQS实现原理

         首先是ReentrantLock、AQS等几个关键类的关系。然后从ReentrantLock.lock()来分析ReentrantLock和AQS的原理,ReentrantLock.lock()的调用关系如下图:

         先来看看最终的acquire方法,也就是有6个参数的方法。(网上关于这块的代码还是以前的版本,现在代码更新了,我这个菜鸡只能自己硬啃了,如果有什么地方错误或者要补充的,麻烦告诉我一下,谢谢大哥们)

       +------+  prev +-------+       +------+
       | head | <---- | first | <---- | tail |
       +------+       +-------+       +------+

final int acquire(AbstractQueuedSynchronizer.Node node, int arg, boolean shared,
                      boolean interruptible, boolean timed, long time) {
        Thread current = Thread.currentThread();
        byte spins = 0, postSpins = 0;   // 线程的自旋等待次数
        boolean interrupted = false;
        //是否是第一个节点,这里的第一个是指head的下一个,head是哨兵节点
        boolean first = false; 
        AbstractQueuedSynchronizer.Node pred = null;        // node的前驱结点

        for (;;) {
            //node不为first,pred不为null,也不为head
            if (!first && (pred = (node == null) ? null : node.prev) != null &&
                    !(first = (head == pred))) {  
                if (pred.status < 0) {  //如果前驱结点的状态变为了取消
                    cleanQueue();    //从尾部反复遍历,解开已取消的节点,直到没有找到为止
                    continue;
                } else if (pred.prev == null) {  
                //由于是多线程的,可能运行到这儿来时pred变成了head了(因为没有前驱结点了)
                    Thread.onSpinWait();    
                //pred变成head,意味着node为first,那么自旋等待临界区资源释放
                    continue;
                }
            }
            if (first || pred == null) {  //如果node是first,或者还没入队
                //尝试获取资源
                boolean acquired;
                try {
                    if (shared)
                        acquired = (tryAcquireShared(arg) >= 0);
                    else
                        acquired = tryAcquire(arg);  
                } catch (Throwable ex) {
                    cancelAcquire(node, interrupted, false);
                    throw ex;
                }
                //获取成功
                if (acquired) {
                    if (first) {
                        node.prev = null;
                        head = node;
                        pred.next = null;
                        node.waiter = null;
                        if (shared)
                            signalNextIfShared(node);
                        if (interrupted)
                            current.interrupt();
                    }
                    return 1;
                }
            }
            if (node == null) {    //创建节点node
                if (shared)
                    node = new AbstractQueuedSynchronizer.SharedNode();
                else
                    node = new AbstractQueuedSynchronizer.ExclusiveNode();
            } else if (pred == null) {          // 尝试入队
                node.waiter = current;
                AbstractQueuedSynchronizer.Node t = tail;
                node.setPrevRelaxed(t);         // avoid unnecessary fence
                if (t == null)
                    tryInitializeHead();
                else if (!casTail(t, node))
                    node.setPrevRelaxed(null);  // back out
                else
                    t.next = node;
            } else if (first && spins != 0) { //如果是first,并且自旋次数不为0,那么自旋等待
                --spins;                   
                Thread.onSpinWait();
            } else if (node.status == 0) {
                node.status = WAITING;          // 修改为等待状态,等着之后阻塞
            } else {
                long nanos;
                //计算自旋次数,因为线程不能一直霸占CPU,自旋次数是有限的
                //自旋次数用完了就继续阻塞,并且增加下一次的自旋次数
                spins = postSpins = (byte)((postSpins << 1) | 1);  
                if (!timed)
                    LockSupport.park(this);   //将等待状态的node的线程阻塞
                else if ((nanos = time - System.nanoTime()) > 0L)
                    LockSupport.parkNanos(this, nanos);
                else
                    break;
                node.clearStatus();   //当调用unpark唤醒后,清除标志位WAITING
                if ((interrupted |= Thread.interrupted()) && interruptible)
                    break;
            }
        }
        return cancelAcquire(node, interrupted, interruptible);
    }

         这里说一下大致流程,首先第一次进入该方法时,first=false,pred=null,首先会尝试一次获取资源,如果成功就直接返回了;如果失败,就进入各种条件判断,大致是:

        先new一个结点;循环一次后,将其入队,入队的过程是通过CAS对尾节点进行赋值更新;再循环一次后,将结点的状态改为WAITING;再循环一次后,计算自旋次数并将状态为WAITING的线程阻塞,等待唤醒。唤醒后,当结点变为first时,临界区资源又没有释放时,就开始按照自旋次数来进行自旋等待,如果自旋次数用尽后,临界区资源还没有释放,那么又进入阻塞状态,并且自旋次数又在原来的基础上*2+1。

        这里我之前也有一个疑问,在判断语句if(first || pred==null)里,明明还没有入队,也就是pred==null,都没有前驱结点,为啥就要去获取资源,这不是变成了非公平锁了吗,那在ReentrantLock的子类FairSync是怎么体现出公平锁的呢。这里我的理解是,FairSync和NonFairSync的公平和非公平是体现在各自的initialTryLock()和tryAquire()方法上的。先看一看initialTryLock()方法:

FairSync:
final boolean initialTryLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedThreads() && compareAndSetState(0, 1)) {  //区别在这儿
                    setExclusiveOwnerThread(current);
                    return true;
                }
            } else if (getExclusiveOwnerThread() == current) {
                if (++c < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(c);
                return true;
            }
            return false;
        }

NonFairSync:
final boolean initialTryLock() {
            Thread current = Thread.currentThread();
            if (compareAndSetState(0, 1)) { // first attempt is unguarded
                setExclusiveOwnerThread(current);
                return true;
            } else if (getExclusiveOwnerThread() == current) {
                int c = getState() + 1;
                if (c < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(c);
                return true;
            } else
                return false;
        }

Sync:
final void lock() {
            if (!initialTryLock())
                acquire(1);
        }

         可以看到区别就在于FairSync会先判断有没有等待状态的结点在,有的话就不会采用CAS *** 作获取资源,而在NonFairSync方法中。则是直接尝试获取资源。另外,这里还可以看到initialTryLock()方法还进行了一个可重入锁的判断。而在tryAcquire方法中:

FairSync:
protected final boolean tryAcquire(int acquires) {
            if (getState() == 0 && !hasQueuedPredecessors() && //区别在这儿
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

NonFairSync:
protected final boolean tryAcquire(int acquires) {
            if (getState() == 0 && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        这个同样如此,没有排队的前驱结点时FairSync才会进行CAS *** 作获取资源。

        好的,到这里差不多理顺了,ReentrantLock的两个子类分别实现了公平锁和非公平锁,而在获取资源失败时,都会采用AQS的acquire方法,将相应线程封装成结点后,放入FIFO队列尾部,阻塞等待唤醒,唤醒后再自旋等待资源释放。而怎么分别得到这两个子类,看一看ReentrantLock的构造方法:

public ReentrantLock() {
        sync = new NonfairSync();
    }

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

         当采用默认构造方法时是创建非公平子类,带有一个boolean参数的构造方法,会根据fair来创建不同的子类。最后来看一看ReentrantLock的unlock方法:

         unlock的方法就很简单了,先去调用了Sync.tryRelease方法,当state=0,也就是没有线程占用资源的时候,才会返回true;当tryRelease方法返回true时就调用signalNext方法区唤醒处于等待状态的线程。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (getExclusiveOwnerThread() != Thread.currentThread())
                throw new IllegalMonitorStateException();
            boolean free = (c == 0);
            if (free)
                setExclusiveOwnerThread(null);
            setState(c);
            return free;
        }

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            signalNext(head);
            return true;
        }
        return false;
    }

private static void signalNext(Node h) {
        Node s;
        if (h != null && (s = h.next) != null && s.status != 0) {
            s.getAndUnsetStatus(WAITING);
            LockSupport.unpark(s.waiter);
        }
    }

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5708759.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存