java高并发编程(四)——JDK并发包

一、同步控制(lock的使用)

  重入锁(lock)可以完全替代synchronized关键字,重入锁的性能要优于synchronized,但是在JDK 6.0开始,JDK对synchronized上做了大量优化,因此两者差距不大。
下面是使用lock的案例。

public class LockTest implements Runnable {
	private static Lock lock =new ReentrantLock();
	
	public static int apple = 0;
	
	public void run() {
		for(int i = 0;i<10;i++) {
			lock.lock();
			apple++;
			lock.unlock();
			}
		
	}
	
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(new LockTest());
		Thread t2 = new Thread(new LockTest());
		
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.err.println(apple);
		
	}
}

  在重入锁的使用过程中,要明确需要获取锁的位置,当操作完成之后要释放锁,因此在逻辑的灵活性上重入锁的使用要优于synchronized关键字。重入锁是可以反复进入的锁,它可以写成以下形式。

lock.lock();
			lock.lock();
			apple++;
			lock.unlock();
			lock.unlock();

  当线程多次获得锁的时候,获取了几次锁,就要释放几次锁,释放少了,线程依然持有锁,其他线程依然无法获取锁,释放多了会得到一个java.lang.IllegalMonitorStateException的异常。此外重入锁还有一些高级功能。功能如下。

(1)中断相应

  中断相应指的是当几个线程处于死锁的时候,某一个线程可以放弃对锁的请求,退出锁池,让其他线程可以获取原本这线程占用的锁,继续执行从而解决死锁问题。

public class LockInterrputTest {
	public static ReentrantLock lock1 = new ReentrantLock();
	public static ReentrantLock lock2 = new ReentrantLock();
	static class MyLock implements Runnable{
		private int lock;
		
		public MyLock(int lock) {
			this.lock = lock;
		}
		
		public void run() {
			if(lock ==1) {
				try {
					lock1.lockInterruptibly();
					Thread.sleep(2000);
					lock2.lockInterruptibly();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}else {
				try {
					lock1.lockInterruptibly();
					Thread.sleep(2000);
					lock2.lockInterruptibly();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			if(lock1.isHeldByCurrentThread()) {
				lock1.unlock();
			}
			if(lock2.isHeldByCurrentThread()) {
				lock2.unlock();
			}
			System.err.println("线程"+lock+"退出");
		}
	}
	
	public static void main(String[] args) {
		Thread t1 = new Thread(new MyLock(1));
		Thread t2 = new Thread(new MyLock(1));
		
		t1.start();
		t2.start();
		t2.interrupt();
	}
}

  lock.isHeldByCurrentThread()方法允许线程中断对锁的请求。t1线程首先获取lock1,并期待获取lock2,而t2线程首先获取lock2,并期待获取lock1,形成死锁。然后让t2线程放弃对lock1的获取并退出,t1线程获取到lock2然后执行完毕并退出。整个流程中只有t1是执行完毕才退出的,这种方式提供了一种死锁的解决方案。

(二、锁申请等待限时)

  除了等待外部通知,来让死锁中的某一线程放弃获取锁,从而解决死锁问题之外,限时等待也是解决死锁问题的一种有效方法。线程在形成死锁的情况之下一直期待获取所需要的锁,等待限时(tryLock()方法)可以让线程在锁池长时间获取不到锁的情况下,自动放弃锁的获取。

public class TimeLockTest {
	public static class MyTimeLock implements Runnable{
		private ReentrantLock lock =new ReentrantLock();
		public void run() {
			try {
				if(lock.tryLock(5, TimeUnit.SECONDS)) {
					System.err.println(Thread.currentThread().getName()+"线程获取到锁");
					Thread.sleep(6000);
				}else {
					System.err.println(Thread.currentThread().getName()+"未获取到锁");
				}
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally {
				if(lock.isHeldByCurrentThread()) {
					lock.unlock();
				}
			}
		}
	}
	
	public static void main(String[] args) {
		MyTimeLock m1 = new MyTimeLock();
		Thread t1 = new Thread(m1,"AAA");
		Thread t2 = new Thread(m1,"BBB");
		
		t1.start();
		t2.start();
	}
}

  上述代码中t1线程获取锁之后随即等待6秒,由于sleep()过程中不会释放锁,因此t2线程在等待了5秒之后并没有获得锁,tryLock()返回flase。

(三)公平锁

  在大多数情况下,线程请求锁的结果是随即的。A线程先请求锁,B线程后请求锁,两个线程都在锁池中等待获取锁。当其他线程释放这两个线程需要的锁的时候,A,B两个线程中随机一个获取该锁并开始执行。而公平的锁并不是这样的,它会按照时间的先后顺序保证线程获取锁是先来后到的,即先进入锁池的先获取锁,后进入的后获取。公平锁的一大特点是它不会产生饥饿。public ReentrantLock(boolean fair)它的构造函数中fair为true时锁是公平的,反之就是不公平的。要实现公平锁,必定需要一个有序队列,因此公平锁的实现成本比较高,性能相对比较低下,因此,默认情况下锁是不公平的,如果没有什么特殊请况也不需要运用公平锁。公平锁和非公平锁在调度上面也是不一样的。

public class FairLock {
	private static ReentrantLock  fairLock = new ReentrantLock(true);
	
	static class MyFairLock implements Runnable{
		
		public void run() {
			fairLock.lock();
			System.err.println(Thread.currentThread().getName()+"开始执行");
			fairLock.unlock();
		}
	}
	
	public static void main(String[] args) {
		Thread[] t1 = new Thread[10];
		for(int i = 0;i<10;i++) {
			t1[i] = new Thread(new MyFairLock(),"第"+i+"线程");
		}
		
		for(int i = 0;i<10;i++) {
			t1[i].start();
		}
	}
}

输出如下

0线程开始执行
第1线程开始执行
第3线程开始执行
第4线程开始执行
第5线程开始执行
第2线程开始执行
第7线程开始执行
第6线程开始执行
第8线程开始执行
第9线程开始执行

公平锁使更早等待锁的线程先获取到锁,并按顺序依次使线程获取锁。如果换为非公平锁(初始 化锁的时候构造器里的true改为flase或者直接空着),线程执行的顺序就会随机无序的进行。

二、重入锁与condition对象

  condition对象的作用和wait(),notify()相似。wait()和notify()是和synchronized关键字配合使用的,condition对象是和lock重入锁一起使用的。通过Lock的newCondition()接口就能新建一个与当前lock绑定的condition对象。condition对象可以让线程在合适的时间等待,或者在特殊的时间得到通知。
  condition的常用方法如下

void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanoTimeout) throws InterruputedException
boolean await(long time,TimeUnit unit ) throws InterruptedException;
boolean awaitUnit(Date deadline) throws InterruptedException;
void signal();
void signalAll();

  await()和signal()的使用方法与wait()和notify类似。awaitUninteruptibly()方法与await()相似,但是他并不会在等待过程中中断响应。下面是使用lock和condition实现生产者消费者模型的示例。

public class MyConditionTest {
	private static ReentrantLock lock = new ReentrantLock();
	private static Condition ProCondition = lock.newCondition();
	private static Condition ConCondition = lock.newCondition();
	
	static class Box {
		public int apple;
		
		public void increase() throws InterruptedException {
			lock.lock();
			while(apple == 5) {
				ProCondition.await();
			}
			
			apple++;
			System.err.println(Thread.currentThread().getName()+"生产产品成功,库存剩余:"+apple);
			ConCondition.signal();
			ProCondition.signal();
			lock.unlock();
		}
		
		private void decrease() throws InterruptedException {
			lock.lock();
			while(apple == 0) {
				ConCondition.await();
			}
			
			apple--;
			System.err.println(Thread.currentThread().getName()+"消费产品成功,库存剩余:"+apple);
			ConCondition.signal();
			ProCondition.signal();
			lock.unlock();
		}
	}
	
	static class Producer implements Runnable{
		private Box box;
		public Producer(Box box) {
			this.box = box;
		}
		
		@Override
		public void run() {
			for(int i = 0;i<10;i++) {
				
			try {
				box.increase();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			}
		}
	}
	
	static class Consumer implements Runnable{
		private Box box;
		public Consumer(Box box) {
			this.box = box;
		}
		@Override
		public void run() {
			for(int i = 0;i<10;i++) {
			try {
				box.decrease();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			}
		}
	}
	
	public static void main(String[] args) {
		Box box = new Box();
		Thread t1 = new Thread(new Producer(box),"AAA");
		Thread t2 = new Thread(new Consumer(box),"BBB");
		Thread t3 = new Thread(new Producer(box),"CCC");
		Thread t4 = new Thread(new Consumer(box),"DDD");
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

通过lock.condition()方法将ProCondition和ConCondition与之绑定起来。当线程执行await()方法时,该线程被挂起并释放锁,其他线程获取该锁并开始执行。相对于synchronized关键字,lock可以创建多个condition,在唤醒时可以指定特定的condition被唤醒,实现部分唤醒功能。

三、信号量(semaphore)

  信号量为多线程协作提供了更强大的控制方法。广义上说,信号量是对锁的扩展。无论是synchronized关键字还是重入锁,同一时间只能允许一个线程访问一个资源。而信号量却可以指定多个线程,同时访问某一资源。信号量的主要构造函数如下。

public Semaphore (int permits)
public Semaphore (int permits,boolean fair)

在声明一个信号量的时候必须指定同一时刻准入数,即同时可以访问的线程的个数,当每个线程同时只申请一个许可的时候,这就相当于指定了同时有多少个线程可以访问某一资源。信号量的主要方法如下。

public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout,TimeUnit unit)
public void release()

aquire()方法尝试获得一个准入许可。若无法获得则线程会等待,知道有现成释放一个许可或者当前线程被中断,acquireUniterruptibly()方法和acquire()方法类似,但是响应不中断,tryAcquire()尝试获得一个许可,如果成功返回true,失败返回false,它不会进行等待,立即返回。release()释放一个许可,以使其他等待许可的线程可以进行资源访问。

public class SemaphoreTest {
	
	static class MyThread implements Runnable{
		private Semaphore se = new Semaphore(5);
		@Override
		public void run() {
			try {
				se.acquire();
				System.err.println(Thread.currentThread().getName()+"开始执行");
				Thread.sleep(5000);
				se.release();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally {
			}
			
		}
	}
	
	public static void main(String[] args) {
		ExecutorService exec = Executors.newFixedThreadPool(20);
		final MyThread demo = new MyThread();
		for(int i = 0;i<20;i++) {
			exec.submit(demo);
		}
	}
	
}

  se的Semaphore实例允许同一时间5个线程同时执行,每个线程执行之后挂起5s。运用线程池同时新建20个线程,第一次5个线程开始执行,每有一个线程执行完之后,等待区的一个未执行的线程就开始执行。最后20个线程都执行完毕后,释放信号量。

四、读写锁(ReadWriteLock)

  读写分离锁可以有效的减少锁竞争,以提高系统性能。读写锁可以使多个线程只执行读取操作的时候可以同时读,但是考虑到数据安全和读取数据的准确性,包含写操作(比如读-写,写-写)的线程还是需要相互等待持有锁。比如A1,A2,A3线程进行写操作,B1,B2,B3线程进行读操作。如果B1,B2,B3同时读的话,可以使这三个线程真正的做到并行。如果系统中读的操作远多于写,那么就可以使用读写锁。

public class ReadWriteLockTest {
	private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	private static ReadLock readlock = readWriteLock.readLock();
	private static WriteLock writeLock = readWriteLock.writeLock();
	
	static class Box{
		public void read(Lock lock) throws InterruptedException{
			lock.lock();
			System.err.println("读操作");
			Thread.sleep(1000);
			lock.unlock();
		}
		
		public void write(Lock lock) throws InterruptedException {
			lock.lock();
			System.err.println("写操作");
			Thread.sleep(1000);
			lock.unlock();
		}
	}
	
	public static void main(String[] args) {
		Box box = new Box();
		Runnable readDemo = new Runnable() {
			public void run() {
				try {
					box.read(readlock);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		};
		
		Runnable writeDemo = new Runnable() {
			public void run() {
				try {
					box.write(writeLock);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		};
		ExecutorService exec = Executors.newFixedThreadPool(1000);
		for(int i = 0;i<10000;i++) {
			exec.submit(readDemo);
			//new Thread(readDemo).start();
		}
		
		for(int i = 0;i<10;i++) {
			exec.submit(writeDemo);
		}
		
		exec.shutdown();
	}
}

  读写锁的使用方法与重入锁差不多,在需要获取锁的时候获取并在执行完成之后释放锁。上述代码在read和write操作中分别加入读锁(readLock)和写锁(writeLock)。然后新建了10000个读操作线程和10个写操作线程,通过一个容量为1000的线程池完成线程的创建和执行,10000个读操作分为10次(线程池中线程总数为1000,因此分为10次)每次一秒执行完毕,10个写线程分为十次每次一秒执行完毕。由此可见读写锁对于有大量读操作线程的系统,优化能力非常高。

五、倒计时器(CountDownLatch)

  倒计时器是一个用来控制线程等待的工具,它可以使一个线程在指定个数线程结束之后再开始执行。典型的倒计时器运用场景就火箭点火,在完成指定道检查程序之后,点火程序才开始执行。下面是倒计时器的使用案例。

public class CountDownLatchTest {
	private static CountDownLatch count = new CountDownLatch(10);
	private static ReentrantLock lock = new ReentrantLock();
	static class MyThread implements Runnable{
		@Override
		public void run() {
			lock.lock();
			System.err.println(Thread.currentThread().getName()+"working");
			try {
				Thread.sleep(1000);
				count.countDown();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}finally {
				lock.unlock();
			}
		}
	}
	
	public static void main(String[] args) {
		MyThread m = new MyThread();
		
		ExecutorService exec = Executors.newFixedThreadPool(10);
		for(int i = 0;i<10;i++) {
			exec.submit(m);
		}
		
		try {
			count.await();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.err.println("主程序开始运行");
		exec.shutdown();
	}
}

  上述代码中主程序作为倒计时器结束后执行的线程,在MyThread类的run()方法中的count.countDown()作用为每当有该线程的实例完成一次操作,倒计时器计数就减1,当10个线程都执行完毕之后主程序才开始执行,并输入“主程序还是运行”。

六、循环栅栏(CyclicBarrier)

  CyclicBarrier是另一种非常实用的线程控制工具。它和CountDownLatch比较相似。它可以实现线程间的计数等待,但它的功能比CountDownLatch更强大一些。CyclicBarrier可以用来组织线程继续执行,要求其他线程进行等待,而单词cyclic的意思即为循环,因此CyclicBarrier这个计数器是可以重复使用的。比如将计数器记为10,则系统会当每十个线程执行完毕之后在宣布执行完成。相当于一种捆绑的作用,把是个线程作为一个整体。CyclicBarrier可以接收一个参数作为barrierAction,即当计数器一次计数完成之后系统会执行的指令。

public class CyclicBarrierTest {
	
	static class MyThread implements Runnable{
		private CyclicBarrier cyclic = new CyclicBarrier(10);
		public int num;
		MyThread(Integer num,CyclicBarrier cyclic){
			this.num = num;
			this.cyclic = cyclic;
		}
		@Override
		public void run() {
			try {
				cyclic.await();
				System.err.println("线程"+num+":开始执行");
				Thread.sleep(1000);
				cyclic.await();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (BrokenBarrierException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	
	static class AfterAwait implements Runnable{
		boolean f;
		int N;
		public AfterAwait(boolean f,int N) {
			this.f = f;
			this.N = N;
		}
		@Override
		public void run() {
			if(f) {
				System.err.println(10+"个线程执行完成");
			}else {
				System.err.println(10+"个线程准备就绪");
				f = true;
			}
		}
		
	}
	
	public static void main(String[] args) {
			final int N = 10;
			Thread t[] = new Thread[N];
			boolean f = false;
			CyclicBarrier cyclic = new CyclicBarrier(N,new AfterAwait(f,N));
			for(int i = 0;i<10;i++) {
				t[i] = new Thread(new MyThread(i,cyclic));
				t[i].start();
			}
	}
}

  该示例中创建了10个线程,每一个线程的run()方法中都设置了CyclicBarrier.await()方法,即等到10个线程全部运行到第一个cyclic.await();时所有的线程再继续运行(即栅栏作用,相当于10个人跑步,跑的快的人在这个栅栏处等其他九个人都到了,大家再一起往前跑),AtferAwait类指定了在cyclicBarrier计数器计数满一次之后要执行的操作。因此是个线程都开始运行,然后计数器第一次记满10后运行AfterAwait的run()方法,此时的f传值为false,因此输出“10个线程准备就绪”,然后把f置为true,这样下次计数器记满之后的操作就是if(true)的操作了。输出了“10个线程准备就绪之后,10个线程都开始往下进行,按照随机顺序输出“线程n开始执行””然后再次等待计数器记满十次之后,执行AfterAwait中的run()方法,这是f为true,因此输出“是个线程执行完成”。

七、线程阻塞工具类(LockSupport)

  LockSupport是一个十分方便的线程阻塞工具,它可以在线程内任何位置让线程zuse。和Thread.suspend()比他弥补了resume()在前发生,导致线程无法继续执行的情况,和wait()相比它不需要先获得锁,也不会抛出interruptedException。LockSupport的静态方法park()可以阻塞当前线程,类似的方法还有parkNanos();parkUntil();等,这些方法提供一个限时的等待。

猜你喜欢

转载自blog.csdn.net/qq_34459728/article/details/87971157