Java并发编程(二)并发控制

并发控制

一、锁

        锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些所可以允许多个线程并发的访问共享资源,比如读写锁)。

1. Lock接口

1)Lock接口简介

        在Lock接口之前,Java程序中依靠synchronized关键字来实现锁功能。在Java5以后,新增了Lock接口,用来实现锁的功能。相较于synchronized关键字,Lock接口提供的方法需要显式地获取和释放锁,虽然不如synchronized简便,但是使用起来更加灵活。

2)Lock接口的特性
特性描述
尝试非阻塞的获取锁当前线程尝试获取锁,如果这一时刻所没有被其他线程获取到,则成功获取并持有锁
能中断的获取锁与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
超时获取锁在指定的截止时间之前获取锁,如果截止时间到了仍然无法获取锁,则返回
3)Lock接口的API
方法名描述异常
void Lock()获取锁,调用该方法的线程将会获得锁,当锁获得后,方法返回\
void lockInterruptibly()可中断的获得锁,和lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程InterruptedException
boolean tryLock()尝试非阻塞的获取锁,调用该方法后立即返回,如果能够获得锁则返回true,反之返回false\
boolean tryLock(long time, TimeUnit unit)超时的获取锁,当前线程在以下三种情况下会返回:
1)当前线程在超时时间内获得了锁
2)当前线程在超时时间内被中断
3)超时时间结束,返回false
InterruptedException
void unlock()释放锁\
Condition newCondition()获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后当前线程释放锁\

2. 重入锁

        重入锁ReentrantLock是Lock接口的常用实现之一,它表示该锁能够支持一个线程对资源的重复加锁。它支持获取锁时的公平和非公平选择。当一个线程再次执行到lock()方法获取锁时,可以顺利的获得锁而不会被阻塞。synchronized也是支持重入的锁,当synchronized修饰递归方法时,可以连续多次获得锁。
        重入锁存在公平性问题。如果在时间上,一个先申请锁的请求一定先被满足,那么这个锁就是公平的,反之则是不公平的。公平的获得锁,也就是等待时间最长的线程最优先获得锁,也可以说是获取时顺序的。非公平锁的往往效率高于公平锁,但公平锁能够减少“饥饿”问题发生的概率,等待越久的请求越是能够优先得到满足。
        在默认情况下ReetrantLock是非公平的。

1)重入锁的使用

        重入锁的使用方式与synchronized类似,

public class ReentrantLockTest {
    public static void main(final String[] args) throws Exception {
        ReentrantLock lock1 = new ReentrantLock();
        ReentrantLock lock2 = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            try {
                lock1.lock();
                System.out.println("Thread A get Lock1");
                TimeUnit.SECONDS.sleep(1);
                lock2.lock();
                System.out.println("Thread A get Lock 2");
            } catch (InterruptedException e) {}
        });
        Thread t2 = new Thread(() -> {
            try {
                lock2.lock();
                System.out.println("Thread A get Lock1");
                TimeUnit.SECONDS.sleep(1);
                lock1.lock();
                System.out.println("Thread A get Lock 2");
            } catch (InterruptedException e) {} 
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("end");
    }
}

3. 读写锁

        读写锁不是排它锁,即同一时刻允许多个线程进行访问。读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,只能允许一个写线程单独运行,其它线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性比一般的排他锁有了很大提升。
        在没有读写锁的时候,写线程之间要通过synchronized来进行同步,而读线程则需要使用等待通知机制,当写线程进入时,读线程等待,写线程结束时通知读线程。大多数场景下,读多与写,读写锁能够比排它锁拥有更好的并发性和吞吐量。

1)ReentrantReadWriteLock的特性
特性说明
公平性支持非公平(默认)和公平的获取锁方式
重进入该锁支持重进入,读线程可以再次获取读锁,写线程也可以再次获取写锁
锁降级遵循获取写锁、获取读锁、再释放写锁的次序,写锁能够降级成为读锁
2)读写锁的API
方法名称说明
ReentrantReadWriteLock.ReadLock readLock()返回一个支持多个线程获取的读锁
ReentrantReadWriteLock.WriteLock writeLock()返回一个写锁
int getReadLockCount()返回当前读锁被获取的次数,重入也会增加次数
int getReadHoldCount()返回当前线程获取锁的次数
boolean isWriteLocked()判断写锁是否被获取
int getWriteHoldCount()返回当前写锁被获取的次数
3)读写锁的使用
public class ReentrantReadWriteLockTest {
	static Map<String, Object> map = new HashMap<>();
	static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
	static Lock r = rwl.readLock();
	static Lock w = rwl.writeLock();
	public static final Object get(String key) {
		r.lock();
		try {
			return map.get(key);
		} finally {
			r.unlock();
		}
	}
	public static final Object put(String key, Object value) {
		w.lock();
		try {
			return map.put(key, value);
		} finally {
			w.unlock();
		}
	}
}

        上述代码摘自《Java并发编程的艺术》。上述代码逻辑简单,即用读写锁实现了对于一个map的并发访问控制。在读操作时获取读锁,这使得读线程不会被阻塞。在写操作时获取写锁,当写锁获得后,其他读写线程都会阻塞。

4. LockSupport工具

在以前我们阻塞和唤醒某一个线程有很多限制:
	1. 因为饿wait()方法需要释放锁,所以必须在synchronized代码块中使用,否则会抛出异常
	2. notify()方法必须在synchronized中使用
	3. synchronized、notify、wait对象必须一致,所以一个synchronized代码块中只能有一个线程调用wait()和notify()
在jdk1.6中引入了LockSupport这个同步工具,用来创建锁和其他同步工具类的基本线程阻塞原语。在jdk中很多地方的线程阻塞和唤醒都是用了LockSupport.park()和LockSupport.unpart()
1)LockSupport提供的阻塞和唤醒方法
方法名称描述
void park()阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法中返回
void parkNanos(long nanos)阻塞当前线程最长不超过nanos纳秒,返回条件在part()基础上增加了超时
void parkUntil(long deadline)阻塞当前线程,直到deadline时间
void unpark(Thread thread)唤醒处于阻塞状态的线程thread
2)LockSupport的使用
public class TestLockSupport {
    static Thread t1 = null, t2 = null;
    public static void main(String[] args) {
        int[] a = new int[]{1,2,3,4};
        char[] b = new char[]{'a', 'b', 'c', 'd'};
        t1 = new Thread(() -> {
            for (int i = 0; i < 4; i++) {
                System.out.println(a[i]);
                LockSupport.unpark(t2);
                LockSupport.park();
            }
        });
        t2 = new Thread(() -> {
            for (int i = 0; i < 4; i++) {
                LockSupport.park();
                System.out.println(b[i]);
                LockSupport.unpark(t1);
            }
        });
        t1.start();
        t2.start();
    }
}

5. Condition接口

        任意一个对象都拥有一组监视器方法,主要包括wait()、wait(long timeout)、notify()、notifyAll(),这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似的方法,与Lock配合可以实现等待/通知模式,但两者在使用方式和功能特性上有差别。

1)Object的监视器方法和Condition接口的对比
对比项ObjectCondition
前置条件获取对象的锁调用Lock.lock()获得锁
调用Lock.newCodition()获取Condition对象
调用方式直接调用
如:object.wait()
直接调用
如:condition.await()
等待队列个数一个多个
当前线程释放锁并进入等待状态支持支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断不支持支持
当前线程释放锁并进入超时等待状态支持支持
当前线程释放锁并进入等待状态知道某个时间不支持支持
唤醒等待队列中的一个线程支持支持
唤醒等待队列中的所有线程支持支持
2)Condition的部分方法
方法名称描述
void await()当前线程进入等待状态知道被通知(signal)或中断,当前线程从await()方法中返回
void awaitUninterruptibly()当前线程进入等待状态,直到被通知,这个方法对中断不敏感
long awaitNanos(long nanosTimeout)当前线程进入等待状态,直到被通知、中断或超市。返回值表示剩余的时间。如果返回值是0或负数表示已经超时。
boolean awaitUntil(Date deadline)当前线程进入等待状态直到被通知、中断或到某个时间。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间返回false。
void signal()唤醒一个等待在Condition的线程
void signalAll()唤醒所有等待在Condition上的线程
3)Condition的使用
public class TestCondition {
	public static void main(String[] args) {
        char[] a = "1234".toCharArray();
        char[] b = "abcd".toCharArray();
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        Thread t1 = new Thread(() -> {
            try {
                lock.lock();
                for (char c : a) {
                    System.out.println(c);
                    condition.signal();
                    condition.await();
                }
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                lock.lock();
                for (char c : b) {
                    System.out.println(c);
                    condition.signal();
                    condition.await();
                }
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });
        t1.start();
        t2.start();
    }
}

二、并发工具

1. CountDownLatch

        CountDownLatch按照字面意思叫做“倒数门闩”,允许一个或多个线程等待其他线程操作完成。
        CountDownLatch的构造函数接收一个int类型的参数N作为计数器,当我们调用CountDownLatch的countDown()方法时N就会减1,知道N变成0.当N变成0以后,调用CountDownLatch.await()方法并等待的线程就会被唤醒继续执行。

public class TestCountDownLatch {
	public static void main(String[] args) {
		CountDownLatch cdl = new CountDownLatch(1);
		Thread t = new Thread(() -> {
			try {
				Thread.sleep(1000);
				cdl.countDown();
			} catch(Exception e){}
		});
		cdl.await();
	}
}

2. CyclicBarrier

        同步屏障CyclicBarrier的字面意思是可循环使用的屏障。他要做的事情是让一组线程到达一个屏障(同步点)时被阻塞,直到所有线程到达屏障,屏障才会开门,所有被拦截的线程才会继续运行。
        CyclicBarrier默认的构造方法传入一个int类型的参数,表示屏障拦截的线程数,每个线程调用await()方法告诉CyclicBarrier我已经到达屏障,然后当前线程被阻塞。
        CyclicBarrier还有一种更高级的构造方法CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。

1)CyclicBarrier应用场景

        CyclicBarrier可以用于多线程计算,最后合并计算结果的场景。

2)CyclicBarrier和CountDownLatch的区别
  1. CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset方法重置。
  2. CyclicBarrier还提供了其他方法,如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量,isBroken方法来了解阻塞的线程是否被中断。
3)CyclicBarrier的使用
public class TestCyclicBarrier {
    public static void main(String[] args) {
        CyclicBarrier cb = new CyclicBarrier(2);
        new Thread(() -> {
            try {
                cb.await();
            } catch (InterruptedException | BrokenBarrierException e) {}
            System.out.println(1);
        }).start();

        try {
            cb.await();
        } catch (Exception e) {}    
        System.out.println(2);
    }
}

3. Semaphore

        Semaphore(信号量)使用来控制同事访问特定资源的线程数量,他通过协调各个线程,以保证合理的使用公共资源。

1)Semphore的应用场景

        Semaphore可以用于做流量控制(限流),特别是公用资源有限的应用场景,比如数据库连接。

2)常用方法
方法名描述
void acquire()获得许可证
void release()释放许可证
boolean tryAcquire()尝试获得许可
int availablePermits()返回可用许可证数目
boolean hasQueuedThreads()返回正在等待许可证的线程数
void reducePermits(int reduction)是否有线程正在等待许可证
Collection getQueueThreads()返回所有等待许可证的线程集合
3)Semphore的使用
public class TestSemaphore {
    public static void main(String[] args) {
        Semaphore s = new Semaphore(10);
        ExecutorService tpe = Executors.newFixedThreadPool(30);
        for (int i = 0; i < 30; i++) {
            tpe.execute(() -> {
                try {
                    s.acquire();
                    System.out.println("save data");
                    s.release();
                } catch (Exception e) {

                }
            });
        }
        tpe.shutdown();
    }
}

上述代码逻辑很简单,虽然有30个线程在执行,但是只允许10个并发执行。Semaphore(int permits)接收一个整形,表示可用的许可量。

4. Exchanger

        Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。两个线程可以通过exchange()方法来进行数据交换,如果其中一个线程执行了exchange()方法,她会一直等待另一个线程执行exchage()。当两个线程都到达同步点的时候,两个线程就可以进行数据交换。

public class TestExchange {
    public static void main(String[] args) {
        Exchanger<String> exgr = new Exchanger<>();
        ExecutorService tpe = Executors.newFixedThreadPool(2);
        tpe.execute(() -> {
            String a = "a";
            try {
                exgr.exchange(a);
            } catch (InterruptedException e){}
        });
        tpe.execute(() -> {
            String b = "b";
            try {
                String a = exgr.exchange("b");
                System.out.println("a=" + a + ", b=" + b);
            } catch (InterruptedException e){}
        });
        tpe.shutdown();
    }
}

三、原子操作

1. 原子更新基本类型类

AtomicBoolean:原子更新布尔类型
AtomicInteger:原子更新整形
AtomicLong:原子更新长整形

1.1 AtomicInteger的API

方法含义
int addAndGet(int delta)医院自方式将输入的数值与实例中的值相加,并返回结果
boolean compareAndSet(int expect, int update)如果输入的数值等于预期值,则以原子方式将该值设置为输入的值
int getAndIncrement()以原子方式将当前值加1
void lazySet(int newValue)最终会设置成为newValue

上表中给出了AtomicInteger类的方法,其他三个类与其使用方法类似。

1.2 AtomicInteger类使用

public class TestAtomicInteger {
	public static void main(String[] args) {
		AtomicInteger ai = new AtomicInteger(1);
		System.out.println(ai.getAndIncrement());
		System.out.println(ai.get());
	}
}

2. 原子更新数组

AtomicIntegerArray:原子更新整形数组里的元素
AtomicLongArray:原子更新长整形数组里的元素
AtomicReferenceArray:原子更新引用类型数组里的元素

2.1 AtomicIntegerArray的API

方法含义
int addAndGet(int i, int delta)以原子方式将输入值与数组中索引i的元素相加
boolean compareAndSet(int i, int expect, int update)如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值

上表给出了AtomicIntegerArray类的方法,其余两个类与其类似。

2.2 AtomicIntegerArray的使用

public class TestAtomicIntegerArray {
	public static void main(String[] args) {
		int[] value = new int[] {1,2};
		AtomicIntegerArray ai = new AtomicIntegerArray(value);
		ai.getAndSet(0, 3);
		System.out.println(ai.get(0));
		System.out.println(value[0]);
	}
}

AtomicIntegerArray类在构造时,会拷贝一份传入的参数数组,所有的原子操作都在拷贝的数组中进行操作,而不会影响传入的原数组。

3. 原子更新引用类型

AtomicReference:原子更新引用类型
AtomicReferenceFieldUpdater:原子更新引用类型列的字段
AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)

4. 原子更新字段类

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值