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); } }
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)