并发编程经验总结

AtomicMarkableReference描述<Object,Boolean>,原子修改Object或者Boolean值,在缓存或状态描述比较有用,修改Object/Boolean可有效的提高吞吐量
AtomicStampedReference维护<Object,int>,用原子方式更新,对对象的并发技术,可以携带对象Object引用,对对象和计数进行原子操作。
AtomicMarkableReference/AtomicStampedReference在解决ABA很有用

锁机制的问题:

  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  • 一个线程持有锁会导致其它所有需要此锁的线程挂起。
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是synchronized轻量的用法,但不能保证原子性,synchronized是悲观锁,导致其他线程挂起,等待持有锁线程释放锁,另一个高效的锁是乐观锁,不加锁,如果冲突失败就重试,直到成功。volatile不能保证线程安全,使用原则如下。

  • 写入变量不依赖此变量的值,或者只有一个线程修改此变量
  • 变量的状态不需要与其它变量共同参与不变约束
  • 访问变量不需要加锁
    volatile语义特征:
  • jvm不会对volitale进行重排序
  • volatile变量不会被缓存在寄存器中(只有拥有线程可见),每次从主存取变量,读。
volatile boolean done = false;
…
    while( ! done ){
        dosomething();
    }

happens-before完整规则:

(1)同一个线程中的每个Action都happens-before于出现在其后的任何一个Action。
(2)对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁。
(3)对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作
(4)Thread.start()的调用会happens-before于启动线程里面的动作。
(5)Thread中的所有动作都happens-before于其他线程检查到此线程结束或者Thread.join()中返回或者Thread.isAlive()==false。
(6)一个线程A调用另一个另一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())。
(7)一个对象构造函数的结束happens-before与该对象的finalizer的开始
(8)如果A动作happens-before于B动作,而B动作happens-before与C动作,那么A动作happens-before于C动作。

非阻塞算法:一个线程失败挂起不应该影响其他线程的失败或挂起。
**示例:**拿AtomicInteger来说

//用volatile来保持数据可读
private volatile int value;
//直接读取
public final int get() {
        return value;
    }
//用了cas操作,每次从内存读数据然后和+1的结果进行cas操作,成功就返回,否则重试到成功为止
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
//CAS操作用JNI完成CPU操作
public final boolean compareAndSet(int expect, int update) {  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

整个J.U.C在性能上有很大的提升,倒是基于CAS上完成的,尽管如此,CAS会出现ABA问题

锁Lock

  • void lock();

  • 获取锁

  • 锁不可用,禁用当前线程,获取锁前,线程休眠

  • void lockInterruptibly() throws InterruptedException

  • 当前锁未中断

  • 锁可用否,可用,获取锁,立即返回

  • 锁可用否,不可用,禁用当前线程,如果发生以下两种情况将处于休眠状态
    1.锁由当前线程获得
    2.其他某线程中断当前线程,并支持对锁获取的中断
    如果当前线程:进入方法前设置中断状态,或者获取时被中断 ,抛出InterruptedException,并清除中断状态

Boolean tryLock(long time,TimeUnit unit) throws InterruptedException
当前线程未中断,锁在给定时间内空闲,获取锁;
锁可用,方法返回true,不可用,禁用该线程,在三种情况下休眠:1.锁由当前线程获得 2.某个线程中断当前线程,支持锁中断 3.超过指定等待时间

如果当前线程:进入方法前设置中断状态,或者获取时被中断 ,抛出InterruptedException,并清除中断状态

void unlock();
释放锁,对应于lock()、tryLock()、tryLock(xx)、lockInterruptibly()等操作,如果成功的话应该对应着一个unlock(),这样可以避免死锁或者资源浪费。

public static void main(String[] args) throws Exception{
     final int max = 10;
     final int loopCount = 100000;
     long costTime = 0;
     for (int m = 0; m < max; m++) {
         long start1 = System.nanoTime();
         final AtomicIntegerWithLock value1 = new AtomicIntegerWithLock(0);
         Thread[] ts = new Thread[max];
         for(int i=0;i<max;i++) {
             ts[i] = new Thread() {
                 public void run() {
                     for (int i = 0; i < loopCount; i++) {
                         value1.incrementAndGet();
                     }
                 }
             };
         }
         for(Thread t:ts) {
             t.start();
         }
         for(Thread t:ts) {
             t.join();
         }
         long end1 = System.nanoTime();
         costTime += (end1-start1);
     }
     System.out.println("cost1: " + (costTime));
     //
     System.out.println();
     costTime = 0;
     //
     final Object lock = new Object();
     for (int m = 0; m < max; m++) {
         staticValue=0;
         long start1 = System.nanoTime();
         Thread[] ts = new Thread[max];
         for(int i=0;i<max;i++) {
             ts[i] = new Thread() {
                 public void run() {
                     for (int i = 0; i < loopCount; i++) {
                         synchronized(lock) {
                             ++staticValue;
                         }
                     }
                 }
             };
         }
         for(Thread t:ts) {
             t.start();
         }
         for(Thread t:ts) {
             t.join();
         }
         long end1 = System.nanoTime();
         costTime += (end1-start1);
     }
     //
     System.out.println("cost2: " + (costTime));
}


static int staticValue = 0;

上面测试,每次启动10个线程,每个线程计算10w次,重复测试10次,结果cost1:624071136 cost2:2057847833 lock性能比synchronized好得多

AQS
在这里插入图片描述
AbstractQueuedSynchronizer是CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore的基础,AbstractQueuedSynvhronizer是Lock/Executor实现的前提
代码思路是:1.先判断是否允许获取锁,是就获取,否则阻塞或者获取失败,是独占锁就可能阻塞,共享锁就失败

while(synchronization state does not allow acquire){
    enqueue current thread if not already queued;
    possibly block current thread;
}

dequeue current thread if it was queued;
//释放锁:这个过程就是修改状态位,如果有线程因为状态位阻塞的话就唤醒队列中的一个或者更多线程。
update synchronization state;
if(state may permit a blocked thread to acquire)
    unlock one or more queued threads;

阻塞和唤醒线程

  • LockSupport.park()
  • LockSupport.park(Object)
  • LockSupport.parkNanos(Object, long)
  • LockSupport.parkNanos(long)
  • LockSupport.parkUntil(Object, long)
  • LockSupport.parkUntil(long)
  • LockSupport.unpark(Thread)
    park()是在当前线程调用,导致阻塞,Object是挂起对象,这样监视的时候就知道此线程是因为什么资源阻塞,park()立即返回,需要在循坏中检测资源来决定是否进行下一次阻塞,park返回原因有仨:
  • 其他线程将当前线程作目标调用unpark
  • 其他线程中断当前线程
  • 该调用不合逻辑的返回

接下来讲讲public void java.util.concurrent.locks.ReentrantLock.lock()

  • 该锁没有被另外一个线程保持,获取锁就返回,设count为1
  • 当前线程保持该锁,count+1,返回
  • 该锁被另一个线程保持,禁用当前线程,获取锁前,线程一直休眠,锁保持计数设置为1。
    公平锁:获取锁是按照请求顺序,否则非公平锁。非公平锁能子啊前面休眠的线程恢复前拿到锁,保证了性能,公平锁是有序的。
  • tryAcquire(int)
  • tryRelease(int)
  • tryAcquireShared(int) :共享模式获取状态
  • tryReleaseShared(int):设置状态翻译官共享模式的释放
  • isHeldExclusively() :线程是独占true
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. 如果tryAcquire(arg)成功,那就没有问题,已经拿到锁,整个lock()过程就结束了。如果失败进行操作2。
  2. 创建一个独占节点(Node)并且此节点加入CHL队列末尾。进行操作3。
  3. 自旋尝试获取锁,失败根据前一个节点来决定是否挂起(park()),直到成功获取到锁。进行操作4。
  4. 如果当前线程已经中断过,那么就中断当前线程(清除中断位)。
protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (isFirst(current) &&
                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;
    }
}

对于公平锁来说,这里的isFirst判断AQS是否为空或者当前线程是否在队列头

  1. 如果当前锁有其它线程持有,c!=0,进行操作2。否则,如果当前线程在AQS队列头部,则尝试将AQS状态state设为acquires(等于1),成功后将AQS独占线程设为当前线程返回true,否则进行2。这里可以看到compareAndSetState就是使用了CAS操作。
  2. 判断当前线程与AQS的独占线程是否相同,如果相同,那么就将当前状态位加1(这里+1后结果为负数后面会讲,这里暂且不理它),修改状态位,返回true,否则返回false。这里之所以不是将当前状态位设置为1,而是修改为旧值+1呢?这是因为ReentrantLock是可重入锁,同一个线程每持有一次就+1。
    cas如下线程1要改10+1,可是线程2抢先一步,改成11,Acompare一下10和11不等,就不改了
    在这里插入图片描述
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

对比之下,公平锁多了一个isFirst实现,判断是否队列头,这就保证了是否按照请求锁的顺序来决定,非公平锁在第一次获取锁,或者其他线程释放锁采用CASS设置AQS独占线程持有锁,更加高效

如果tryAcquire失败意味着已经进入队列,AQS队列中节点Node发挥作用
java.util.concurrent.locks.AbstractQueuedSynchronizer.Node中有两个常量

static final Node EXCLUSIVE = null; //独占节点模式
static final Node SHARED = new Node(); //共享节点模式
private Node addWaiter(Node mode) { //mode是节电模式参数
     Node node = new Node(Thread.currentThread(), mode);
     // Try the fast path of enq; backup to full enq on failure
     Node pred = tail;
     if (pred != null) {
         node.prev = pred;
         if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     enq(node);
     return node;
}

上面是节点如队列的一部分,仅当队列不空,新节点插入尾部成功后直接返回新节点

final boolean acquireQueued(final Node node, int arg) {
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } catch (RuntimeException ex) {
        cancelAcquire(node);
        throw ex;
    }
}

自旋请求所,如果可能的话挂起线程,知道获得锁,返回当前线程是否中断,否则如果park过或者中断的话,就返回true,
1.当前节点AQS队列头结点,就尝试获取锁,成功了九江头结点设置为当前节点,返回中断,否则执行2
2.检测当前节点是否应该park(),应该就挂起线程,修改中断位状态,进行操作1

那么一个节点收i否该park方法由java.util.concurrent.locks.AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire(Node, Node)实现的。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int s = pred.waitStatus;
    if (s < 0) return true;
    if (s > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else compareAndSetWaitStatus(pred, 0, Node.SIGNAL);
    return false;
}
  1. 当前节点等待状态小于0,那就是前面节点还没有活得锁,返回true,应该park阻塞
  2. 前一个节点的等待状态waitStatus>0,那么就是前一个节点被取消了,那么将前一个节点去掉,递归操作知道所有前一个节点waitStatus<=0,进行4,否则3
  3. 前一个节点等待状态waitStatus=0,修改前一个节点状态为SINGAL,表示后面有节点等待处理,根据状态来决定是否park。进行4
  4. 返回false,不该park
selfInterrupt()

private static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

如果线程中断或阻塞过,那就再中断一次,中断两次意思清除中断位。有了中断位,我们就可以用判断来中断程序,增加了灵活性。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值