Java笔记(十五) 并发包

摘要:
Java中还有一个并发工具包,位于Java包中。util。同时发生的它包括许多易于使用的高性能并发开发工具。1、 为什么原子变量和CAS需要原子变量?因为对于count++等操作使用synchronized太昂贵了。Java的基本原子变量是:AtomicBoolean、AtomicInteger、AtomitLong和AtomicReference(原子引用类型
并发包

Java中还有一套并发工具包,位于包java.util.concurrent下,里面包括很多易用

且很多高性能的并发开发工具。

一、原子变量和CAS

为什么需要原子变量,因为对于例如count++这种操作,使用

synchronized成本太高了。Java并发包的基本原子变量有:

AtomicBoolean、AtomicInteger、AtomitLong和AtomicReference(原子引用类型)

一)AtomicInteger

1.用法 

构造方法:

public AtomicInteger(int initialValue)
public AtomicInteger() //初始值为0

get/set方法:

public final int get()
public final void set(int newValue)

之所以称为原子变量,是因为它们包含一些以原子方式实现组合操作的方法。部分方法如下:

//以原子方式获取旧值并设置新值
public final int getAndSet(int newValue)
//以原子方式获取旧值并给当前值加一
public final int getAndIncrement()
//以原子方式获取旧值并给当前值减一
public final int getAndDecrement()
//以原子方式获取旧值并给当前值加delta
public final int getAndAdd(int delta)
//以原子方式给当前值加一并获取新值
public final int incrementAndGet()
//以原子方式给当前值减一并获取新值
public final int decrementAndGet()
//以原子方式给当前值加delta并获取新值
public final int addAndGet(int delta)

以上方法都依赖于方法:

/**
* @param expect 如果当前值等于expect,则更新当前值为update
* @param update 更新后的值
* @return 更新成功返回true,否则返回false
* */
public final boolean compareAndSet(int expect, int update)

compareAndSet是一个非常重要的方法,比较并设置,我们以后简称为CAS。

应用示例:

public class AtomicIntegerDemo {
    private static AtomicInteger counter = new AtomicInteger(0);
    static class Vistor extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
         int num = 1000;
         Thread[] threads = new Thread[num];
         for (int i = 0; i < num; i++) {
             threads[i] = new Vistor();
             threads[i].start();
             threads[i].join();
         }
         System.out.println(counter.get()); //1000000
    }
}

2.基本原理和思维 

主要内部成员:

private volatile int value; //volatile可保证内存可见性
public final int incrementAndGet() {
    for(;;) {
        //获取当前值
        int current = get();
        //计算更新值
        int next = current + 1;
        //如果更新没有成功,说明value被别的线程改了,则再去取最新值
        //并尝试更新直到成功为止
        if(compareAndSet(current, next))
            return next;
    }
}

原子变量的更新逻辑是非阻塞式,更新冲突的时候,它就重试,

不会阻塞,不会有上下文切换开销。对于大部分比较简单的操作,

这种方式性能都远高于使用阻塞的方式。

接下来是compareAndSet实现源代码:

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

其中usafe是sun.misc.Unsafe类型,定义为:

private static final Unsafe unsafe = Unsafe.getUnsafe();

一般应用程序不应该直接使用。原理上,一般的计算机系统都在硬件层次上直接支持CAS指令,

Java的实现都会利用这些特殊的指令。

3.实现锁 

基于CAS,除了可以实现乐观非阻塞外,还可以实现悲观阻塞式算法,比如锁。

实际上,Java并发包的所有阻塞式工具、容器、算法都是基于CAS的(不过,也需要一些别的支持)。

我们自己来实现一个简单的锁:

/**
 * 一般而言这种阻塞方式过于消耗CPU,实际开发中应该使用并发包中的类
 * 这里只是一个演示
 */
public class MyLock {
    //status表示锁的状态,0表示未锁定,1表示锁定
    private AtomicInteger status = new AtomicInteger(0);
    public void lock() {
        //更新成功后才退出
        while (!status.compareAndSet(0, 1)){
            Thread.yield();
        }
    }
    public void unlock() {
        status.compareAndSet(1, 0);
    }
}

4.ABA问题 

假设当前值为A,如果另一个线程先将A修改成B,再修改A,

当前线程的CAS操作无法分辨当前值发生变化。解决办法

是使用AtomicStampedReference,在修改值的同时附件一个时间戳,

只有值和时间戳都相同才能进行修改,其CAS声明方法为:

public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
Pair pair = new Pair(100, 200);
int stamp = 1;
AtomicStampedReference<Pair> pairRef = new AtomicStampedReference<Pair>(pair, stamp);
int newStamp = 2;
pairRef.compareAndSet(pair, new Pair(200, 200), stamp, newStamp);

 二、显式锁

Java并发包中的显式锁接口和类位于包java.util.concurrent.locks下,主要接口和类如下:

1)锁接口Lock,主要实现类是ReentrantLock;

2)读写锁和接口:ReadWriteLock,主要实现类是ReentrantReadWriteLock;

一)接口Lock

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

1)lock()/unlock():获取锁和释放锁,lock会阻塞直到成功。

2)lockInterruptibly():与lock()不同的是,它可以响应中断,如果被其他线程中断了,则抛出InterruptedException.

3)tryLock():只是尝试获取锁,立即返回,不阻塞,如果成功,返回true,否则返回false。

4)tryLock(long time, TimeUnit unit):在限定时间内等待,如果成功返回true,否则阻塞,在等待同时响应中断,超出时间没有获得锁,抛出false。

5)newCondition():

二)可重入锁ReentrantLock

1.基本用法 

该类方法lock()和unlock()实现了synchronized一样的语义,但:

1)可重入,一个线程在持有锁的情况下,可以继续获得该锁;

2)可以解决竞态条件问题;

3)可以保证内存可见性;

ReentrantLock有两个构造方法:

//默认为不公平锁
public ReentrantLock()
//设定true为公平锁
public ReentrantLock(boolean fair)

所谓公平是指,等待时间最长的线程优先获得锁。保证公平会英雄性能,一般也不需要,所以默认不保证。

public class Counter {
    private final Lock lock = new ReentrantLock();
    private volatile int count;
    public void incr() {
        lock.lock();
        try {
            count ++;
        //注意一定不要忘记释放锁
        } finally {
            lock.unlock();
        }
    }
    public int getCount() {
        return count;
    }
}

2.使用tryLock()避免死锁 

在持有一个锁同时获取另一个锁而获取不到的时候,可以释放

已持有的锁,给其他线程获取锁的机会,然后重试获取所有锁。

账户之间转账的例子:

public class Account {
    private Lock lock = new ReentrantLock();
    private volatile double money;
    public Account(double money) {
        this.money = money;
    }
    public void add(double money) {
        lock.lock();
        try {
            this.money += money;
        } finally {
            lock.unlock();
        }
    }
    public void reduce(double money) {
        lock.lock();
        try {
            this.money -= money;
        } finally {
            lock.unlock();
        }
    }
    public double getMoney() {return this.money;}
    void lock() {
        lock.lock();
    }

    void unlock() {
        lock.unlock();
    }

    boolean tryLock() {
        return lock.tryLock();
    }
}
public class AccountManager {
    public static class NoEnoughMoneyException extends Exception {}
    //该方法问题:如果两个账户同时给对方转账,都先获得了第一个锁,则会发生死锁
    public static void transfer(Account from, Account to, double money) throws NoEnoughMoneyException {
        from.lock();
        try {
            to.lock();
            try {
                if (from.getMoney() >= money) {
                    from.reduce(money);
                    to.add(money);
                } else {
                    System.out.println("The from account money is " + from.getMoney());
                    System.out.println("The money is " + money);
                    throw new NoEnoughMoneyException();
                }
            } finally {
                to.unlock();
            }
        } finally {
            from.unlock();
        }
    }

    //避免死锁的方法
    public static boolean tryTransfer(Account from, Account to, double money) throws NoEnoughMoneyException {
        if (from.tryLock()) {
            try {
                if (to.tryLock()) {
                    try {
                        if (from.getMoney() >= money) {
                            from.reduce(money);
                            to.add(money);
                        } else {
                            throw new NoEnoughMoneyException();
                        }
                        return true;
                    } finally {
                        to.unlock();
                    }
                }
            } finally {
                from.unlock();
            }
        }
        return false;
    }

    //模拟账户转账的死锁过程
    public static void simulateDeadLock() throws InterruptedException {
        final int accountNum = 10;
        final Account[] accounts = new Account[accountNum];
        final Random random = new Random();
        for (int i = 0; i < accountNum; i++) {
            accounts[i] = new Account(random.nextInt(1000000));
            System.out.println("account[" + i + "]" + accounts[i].getMoney());
        }


        int threadNum = 100;
        Thread[] threads = new Thread[threadNum];
        for (int i = 0; i < threadNum; i++) {
            threads[i] = new Thread() {
                @Override
                public void run() {
                    int loopNum = 100;
                    for (int k = 0; k < loopNum; k++) {
                        int i = random.nextInt(accountNum);
                        int j = random.nextInt(accountNum);
                        int money = random.nextInt(10);
                        if (i != j) {
                            try {
                                System.out.println( "Thread account[" + i + "]"+ accounts[i].getMoney());
                                //会发生死锁
                                transfer(accounts[i], accounts[j], money);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            };
            threads[i].start();
        }
    }

    public static void main(String[] args) {
        try {
            simulateDeadLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.ReentrantLock的实现原理 

在最底层,该类依赖于CAS方法,另外,它依赖于LockSupport的一些方法。

1)LockSupport

LockSupport类的基本方法:

//使当前线程放弃CPU,进入等待(WAITING)状态
public static void park()
//使指定线程恢复可运行状态
public static void unpark(Thread thread)
Thread t = new Thread(){
    @Override
    public void run() {
        LockSupport.park();
        System.out.println("exit");
    }
};
t.start();
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
LockSupport.unpark(t);

park不同于Thread.yield(),yield只是告诉操作系统可以先让其他线程运行,

但自己仍然是可运行状态。另外,park是响应中断的,当有中断发生时,park

会返回,线程的中断状态会被设置。另外,还需要说明的,park可能会无缘无故

地返回,程序应该重新检查park等待条件是否满足。park的两个变体:

//可指定最长等待时间,参数是相对于当前时间的纳秒数
public static void parkNanos(long nanos)
//指定最长等待到什么时候,参数是绝对时间
public static void parkUntil(long deadline)

当等待超时,它们也会返回。

也可以指定一个对象,表示是由于该对象而进行等待,以便调试:

public static void park(Object blocker)

返回一个线程的blocker对象:

public static Object getBlocker(Thread t)

2)AQS

Java提供了一个抽象类AbstractQueuedSynchronizer简称AQS,用于各种锁的代码复用。

AQS封装了一个状态,给子类提供查询和设置状态的方法:

private volatile int state;
protected final int getState()
protected final void setState(int newState)
protected final boolean compareAndSetState(int expect, int update)

用于实现锁时,AQS可以保存锁的当前持有线程,提供了方法进行查询和设置:

private transient Thread exclusiveOwnerThread();
protected final void setExclusiveOwnerThread(Thread t);
protected final Thread getExclusiveOwnerThread();

下面,我们以ReentrantLock的使用为例简要介绍AQS的原理:

ReentrantLock内部使用AQS,有三个内部类:

abstract static class Sync extends AbstractQueuedSynchronizer
//fair为false使用该类
static final class NonfairSync extends Sync
//fair为true时使用该类
static final class FairSync extends Sync

ReentrantLock内部有一个Sync成员:

private final Sync sync;

在构造方法中被赋值:

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

lock/unlock方法的实现:

public void lock() {
    sync.lock();
}

sync默认类型是NonfairSync,NonfairSync的lock代码为:

final void lock() {
    if(compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

ReentrantLock使用state表示是否被锁和持有数量,如果当前未被锁定,则立即获得锁,

否则调用acquire(1)获得锁。acquire()是AQS中的方法,代码为:

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

 它调用tryAcquire()获取锁,tryAcquire必须被子类重写。NofairSync的实现为:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //如果未被锁定,则进行锁定
    if(c == 0) {
        if(compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果已经锁定,增加锁定次数,这里state存放了锁定次数
    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;
}

如果tryAcquire()返回false,则AQS会调用:

//addWaiter()会新建一个节点Node,代表当前线程,然后加入内部等待队列
//放入等待队列后,调用acquireQueued尝试获得锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
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;
            }
            //否则最终调用LockSupport.park放弃CPU,进入等待,
            //被唤醒后检查是否中断,记录中断标志
            if(shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if(failed)
            cancelAcquire(node);
    }
}
private static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

总结lock方法的基本过程:能获得锁就立即获得,否则加入等待队列,被唤醒后检查

自己是否是第一个等待的线程,如果是且能获得锁,则返回,否则继续等待。这个过程

如果发生了中断,lock会记录中断标志位,但不会提前返回或抛出异常。

ReentrantLock的unlock代码为:

public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    //tryRelease会修改状态释放锁
    if(tryRelease(arg)) {
        Node h = head;
        if(h != null && h.waitStatus != 0)
            //会调用LockSupport.unpark将第一个等待的线程唤醒
            unparkSuccessor(h);
        return true;
    }
    return false;
}

4.对比ReentrantLock和synchronized 

相比synchronized,ReentrantLock,支持以非阻塞方式获取锁,

可以响应中断,可以限时,更为灵活。不过synchronized使用更简单

性能通常更好,总之能用synchronized就不要用ReentrantLock。

三、显式条件

1.用法

锁用于解决竞态条件问题,条件是线程间的协作机制。显式锁与synchronized相对应,

而显式条件与wait/notify相对应。显式锁与显式条件配合使用。Lock接口中定义的创建

显示条件的方法:

Condition newCondition();

Condition接口:

public interface Condition {
    //对应于Object的wait
    void await() throws InterruptedException;
    //不响应中断的等待,该方法不会由于中断结束,但当它返回时
    //如果等待过程中发生中断,中断标志会被设置
    void awaitUninterruptibly();
    //等待相对时间,单位纳秒
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    //等待相对时间
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    //等待绝对时间
    boolean awaitUntil(Date deadline) throws InterruptedException;
    //对应于notify
    void signal();
    void signalAll();
}

与Object的wait方法一样,调用await方法前先要索取锁,如果没有锁会抛出异常。

当await在进入等待队列后,会释放锁,释放CPU,当其他线程唤醒它后,或等待

超时后,或发生中断异常后,它都需要重新获取锁,获取锁后,才会从await方法中退出。

 另外,与Object的wait方法一样,await返回后就不一定代表等待的条件就满足了,

通常要将await的调用放到一个循环内,只有条件满足后才退出。示例:

public class WaitThread  extends Thread{
    private volatile boolean fire = false;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    @Override
    public void run() {
        try {
            lock.lock();
            try {
                while (!fire){
                    condition.await();
                }
            } finally {
                lock.unlock();
            }
            System.out.println("fired");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void fire() {
        lock.lock();
        try {
            this.fire = true;
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        WaitThread waitThread = new WaitThread();
        waitThread.start();
        Thread.sleep(1000);
        System.out.println("fire");
        waitThread.fire();
    }
}

2.生产者/消费者模式

在使用wait/notify实现生产者/消费者的时候有一个局限就是只能有一个条件队列,

而使用显式锁和条件不存在这个问题,例如:

public class MyBlockingQueue<E> {
    private Queue<E> queue = null;
    private int limit;
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();

    public MyBlockingQueue(int limit) {
        this.limit = limit;
        queue = new ArrayDeque<E>(limit);
    }

    public void put(E e) throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (queue.size() == limit) {
                notFull.await();
            }
            queue.add(e);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public E take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            E e = queue.poll();
            notFull.signal();
            return e;
        } finally {
            lock.unlock();
        }
    }
}

3.实现原理

ReentrantLock的newCondition():

public Condition newCondition() {
    return sync.newCondition();
}

sync的newCondition方法:

final ConditionObject newCondition() {
    return new ConditionObject();
}

ConditionObject是AQS的一个内部类,其内部有一个队列,

表示条件等待队列,其成员声明:

//条件队列的头节点
private transient Node firstWaiter;
//条件队列的尾节点
private transient Node lastWaiter;
public final void await() throws InterruptedException {
    //如果等待前中断标志位已经被设置,直接抛异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //1.为当前进程创建节点,加入条件等待队列
    Node node = addConditionWaiter();
    //2.释放持有的锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //3.放弃CPU进行等待,直到被中断或isOnSyncQueue变为true
    //isOnSyncQueue,表示该节点被其他线程从条件等待队列
    //移到了外部锁等待队列,等待的条件已经满足
    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);
}

免责声明:文章转载自《Java笔记(十五) 并发包》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇VMware——在CentOS中安装VMware Toolsredis未授权访问漏洞利用获得shell下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

C#:总结页面传值几种方法

 小知识点: 1.  W7自带 .NetFrameWork 3.5, 兼容模式为 高版本号兼容低版本号; 2. WF和WPF都是基于XAML的,可是两者的用途不同。 WF是一种开发框架,将工作流嵌入在.NET Framework应用程序中,所主要用于开发创建工作流应用程序。WF:http://msdn.microsoft.com/zh-cn/librar...

Spring-AOP(面向切面编程)

什么是AOP   AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入...

selenium 定位方式

在使用selenium webdriver进行元素定位时,通常使用findElement或findElements方法结合By类返回的元素句柄来定位元素。其中By类的常用定位方式共八种,现分别介绍如下。 1. By.name() 假设我们要测试的页面源码如下: <button aria-label="Google Search" name="bt...

redis学习(一)

一.redis简介 Redis是基于内存、可持久化的日志型、key-value高性能存储系统。关键字(Keys)是用来标识数据块。值(Values)是关联于关键字的实际值,可以是任何东西。有时候你会存储字符串,有时候是整数,还有时候你会存储序列化对象(使用JSON、XML等)。在大多数情况下,Redis会把值看做是一个字节序列,而不会关注它们实质上是什么。...

mysql last_insert_id() (转载)

先来看看官方的说明 The ID that was generated is maintained in the server on a per-connection basis. This means that the value returned by the function to a given client is the first AUTO_I...

iOS设置启动页后的广告页

转载请注明出处!!! 很多app(如淘宝、美团等)在启动图加载完毕后,还会显示几秒的广告,一般都有个跳过按钮可以跳过这个广告,有的app在点击广告页之后还会进入一个广告页面,点击返回进入首页。就像下面的效果。这个功能现在很常见,那么它是如何实现的呢? 思路1.广告页加载思路。广告页的内容要实时显示,在无网络状态或者网速缓慢的情况下不能延迟加载,或者等到首...