Java同步和多线程2:JUC锁

通过前面的文章我们知道了,可以通过synchronized关键字来进行同步,实现对竞争资源的互斥访问的锁。同步锁的原理是,对于每一个对象,有且仅有一个同步锁;不同的线程能共同访问该同步锁,但是在同一个时间点,该同步锁能且只能被一个线程获取到。这样,获取到同步锁的线程就能进行CPU调度,从而在CPU上执行;而没有获取到同步锁的线程,必须进行等待,直到获取到同步锁之后才能继续运行。这就是,多线程通过同步锁进行同步的原理。其实使用synchronized关键字也被称为内置锁,因为他是基于某个对象的。但是,这个锁的功能还是太单一了一点。而为了实现更强大的锁,需要用到java.util.concurrent(JUC)包中的各种锁。记住,对于reentrantLock来说,也是一个对象只有一个锁。就算在对象中有多个lock()和unlock(),只要lock()了一次,其他的lock()和unlock()之间包围起来的地方也一样是被锁住了。另外,跟synchronized一样,一个线程一旦在临界区某个地方被wait等释放了锁,挂起了,那他下次获得锁后,会从同一个地方继续执行,而不是重新从临界区一开始执行。这些都是在编码时候要注意的。

一、ReentrantLock

ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”,他实现了Lock接口。互斥意思是一个时间点只能被一个线程锁持有,而可重入指的是可以被单个线程多次获取锁。而对应地,也必须同样次数地释放锁,否则其他线程也是无法获取这个锁的。ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentraantLock是通过一个FIFO的等待队列来管理可以获取该锁所有线程的。弱锁此时被占用,当其他线程请求锁的时候,会把这些线程挂起,一个个放进FIFO队列中等待。在“公平锁”的机制下,如果某个线程请求锁,此时锁是被释放的,这个线程只有是队列头才能获得锁;在“非公平锁”的机制下,如果队列中某个线程请求获得锁,此时锁可用,那这个线程就算不是在队列头,也可以直接获取锁,也就是允许插队。非公平锁一般比公平锁效率高。ReentrantLock默认是非公平锁。他有两个构造方法:

public ReentrantLock():创建一个ReentrantLock实例,相当于public ReentrantLock(false)

public ReentrantLock(boolean fair):如果fair是true则是公平锁,否则是非公平锁

1、加锁和释放锁

ReentrantLock()为声明了他的一个实例的对象或类加锁和释放锁。例子如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class JUCTest {
	public static void main(String[] args) throws InterruptedException {
		thread myThread=new thread();
		Thread t1=new Thread(myThread,"t1");
		Thread t2=new Thread(myThread,"t2");
		Thread t3=new Thread(myThread,"t3");
		t1.start();
		t2.start();
		t3.start();
	}
}
class thread implements Runnable{
	private Lock lock=new ReentrantLock();
	public void run() {
		try {
			lock.lock();//当前执行的线程获取这个代码段的锁
			for(int i=0;i<5;i++) {
				System.out.print(Thread.currentThread().getName()+" "+i+"  ");
				Thread.sleep(1000);//可以看到,虽然令当前线程sleep了,但是当前线程是不会释放锁的,所以其他线程
				//并无法获得锁,无法进行下去
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();//当前执行的线程释放锁
		}
	}
}

程序的输出是t1 0  t1 1  t1 2  t1 3  t1 4  t3 0  t3 1  t3 2  t3 3  t3 4  t2 0  t2 1  t2 2  t2 3  t2 4  。由于t1到t3线程共同使用一个对象myThread,而run代码段已经被上锁了,所以其他线程无法打断当前运行的线程的执行。如果把获取锁和释放锁的代码段去掉,会出现这个结果:

t1 0  t2 0  t3 0  t2 1  t1 1  t3 1  t2 2  t1 2  t3 2  t2 3  t1 3  t3 3  t2 4  t3 4  t1 4  

要注意的是,unlock一定要在finally代码块中调用,意味着一定要释放锁,否则锁有可能永远得不到释放。

2、条件

如果锁只能这样单一得获取和释放,那锁的功能就太单调,太不灵活了。锁其实还可以绑定多个条件。条件是一定和锁绑定在一起的。在这里,我们用一个经典的单生产者单消费者模型来作为例子。注意,生产者消费者模型是多线程同步的一个经典模型。对这个模型的理解,非常重要。

import java.util.Random;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class JUCTest {
	public static void main(String[] args) throws InterruptedException {
		Depot depot=new Depot(20);
		Thread consumer=new Thread(new Consumer(depot),"consumer");
		Thread producer=new Thread(new Producer(depot),"producer");
		consumer.start();
		producer.start();
	}
}
class Depot{//仓库类,生产者进程和消费者进程共享一个仓库实例
	private int capability;//仓库的总容量能装多少
	private int size;//仓库目前的容量
	private Lock lock;
	private Condition full;
	private Condition empty;
	public Depot(int capability){
		this.capability=capability;
		this.size=0;
		this.lock=new ReentrantLock();
		this.full=lock.newCondition();
		this.empty=lock.newCondition();
	}
	public void produce() throws Exception {//生产
		try {
			lock.lock();//先上锁
			Random random=new Random();
			int producenum=random.nextInt(10);//每次生产0-9之间的一个随机数量
			if(this.size+producenum>this.capability) {//如果生产的东西会令仓库溢出,那就只生产能装下的部分
				producenum=this.capability-this.size;
			}
			this.size+=producenum;
			System.out.println("produce "+producenum+" size is "+this.size);
		}finally {
			lock.unlock();
		}
	}
	public void consume() throws Exception{
		try {
			lock.lock();
			Random random=new Random();
			int consumenum=random.nextInt(10);
			if(this.size-consumenum<0) {
				consumenum=this.size;
			}
			this.size=0;
			System.out.println("consume "+consumenum+" size is "+this.size);
		}finally {
			lock.unlock();
		}
	}
}
class Producer implements Runnable{
	private Depot depot;
	public Producer(Depot depot) {
		this.depot=depot;
	}
	public void run() {
		for(int i=0;i<10;i++) {
			try {
				depot.produce();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}
class Consumer implements Runnable{
	private Depot depot;
	public Consumer(Depot depot){
		this.depot=depot;
	}
	public void run() {
		for(int i=0;i<10;i++) {
			try {
				depot.consume();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}

这个模型比较简单,没有用到条件。消费者和生产者的行为不可控,也就是说消费者和生产者都不知道什么时候会来消费和生产,而是如果生产的数量会超出仓库范围,那就只生产仓库可以装下的物品,如果消费的数量多过仓库的数量,那就只消费仓库有的数量。这个模型比较简陋,在这种设定下根本不需要使用条件。这是因为可以生产或者消费0个物品,而且每次执行生产或者消费,必然可以到达lock.unlock()的finally,也就是必然有机会释放锁。所以在程序结果中,并不是一直生产100次,再消费100次,或者一直消费100次,再生产100次,而是有交替。我们接下来来弄一个改进的模型。

我们假设生产者和消费者能生产特定次数。如果生产者生产了一个很多的数量,仓库放不下了,那生产者就认为这次生产计划过剩了,那就先让消费者消费,自己先不把生产的东西放进去,而等下次有机会了再把这次生产的东西放进去。对于消费者同理。同时,我们举两个例子。第一个例子是要一直生产或一直消费,知道放不下或者没得消费了,再消费或者生产,第二个例子是消费和生产是随机的。他们的区别是锁划定的临界区范围。

public class JUCTest {
	public static void main(String[] args) throws InterruptedException {
		Depot depot=new Depot(20);
		Thread consumer=new Thread(new Consumer(depot),"consumer");
		Thread producer=new Thread(new Producer(depot),"producer");
		consumer.start();
		producer.start();
	}
}
class Producer implements Runnable{
	private Depot depot;
	public Producer(Depot depot) {
		this.depot=depot;
	}
	public void run() {
		try {
			depot.produce();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
class Consumer implements Runnable{
	private Depot depot;
	public Consumer(Depot depot){
		this.depot=depot;
	}
	public void run() {
		try {
			depot.consume();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

第一个例子:由于lock()和unlock()包围的范围是整个生产过程,连通while都包含进去了,这说明,如果一个线程进入了生产或者消费,这个线程会一直持有锁,因此另一个线程是无法执行的。但是,一旦到达了仓库溢出或者不满,就通过条件的await()方法,令该线程wait,因此挂起。同时,该线程释放锁,另一个线程就能获得锁。

	public void produce() throws Exception {//生产
		try {
			lock.lock();
			while(true) {
				Thread.sleep(1000);
				Random random=new Random();
				int producenum=random.nextInt(9)+1;//生产1-10中的一个随机数
				while(this.size+producenum>this.capability)
					full.await();
				this.size+=producenum;
				System.out.println("produce "+producenum+" , size is "+this.size);
				empty.signal();
				//lock.unlock();
			}
		}finally {
			lock.unlock();
		}
	}
	public void consume() throws Exception{
		try{	
			lock.lock();
			while(true) {
				Thread.sleep(1000);
				Random random=new Random();
				int consumenum=random.nextInt(9)+1;//生产1-10中的一个随机数
				while(this.size-consumenum<0)
					empty.await();
				this.size-=consumenum;
				System.out.println("consume "+consumenum+" , size is "+this.size);
				full.signal();
				//lock.unlock();
			}
		}finally {
			lock.unlock();
		}
	}

produce 1 , size is 1

produce 3 , size is 4

produce 6 , size is 10

produce 9 , size is 19

consume 1 , size is 18

consume 5 , size is 13

consume 7 , size is 6

produce 9 , size is 15

produce 2 , size is 17

consume 9 , size is 8

consume 7 , size is 1

produce 6 , size is 7

produce 3 , size is 10

produce 1 , size is 11

第二个例子,因为lock和unlock()包围的范围不是整个while,因此在代码跑出临界区的时候,另一个线程是有机会获得锁的。

	public void produce() throws Exception {//生产
		try {
			while(true) {
				Thread.sleep(1000);
				lock.lock();
				Random random=new Random();
				int producenum=random.nextInt(9)+1;//生产1-10中的一个随机数
				while(this.size+producenum>this.capability)
					full.await();
				this.size+=producenum;
				System.out.println("produce "+producenum+" , size is "+this.size);
				empty.signal();
				lock.unlock();
			}
		}finally {
			//lock.unlock();
		}
	}
	public void consume() throws Exception{
		try{	
			while(true) {
				Thread.sleep(1000);
				lock.lock();
				Random random=new Random();
				int consumenum=random.nextInt(9)+1;//生产1-10中的一个随机数
				while(this.size-consumenum<0)
					empty.await();
				this.size-=consumenum;
				System.out.println("consume "+consumenum+" , size is "+this.size);
				full.signal();
				lock.unlock();
			}
		}finally {
			//lock.unlock();
		}
	}

produce 7 , size is 7

consume 5 , size is 2

produce 9 , size is 11

consume 2 , size is 9

produce 3 , size is 12

consume 2 , size is 10

produce 6 , size is 16

consume 1 , size is 15

consume 9 , size is 6

produce 7 , size is 13

consume 8 , size is 5

produce 9 , size is 14

注意的是,条件和Lock是绑定的。signal后,哪些线程能获得锁,看是公平锁,还是非公平锁。总的来说,Condition的作用是对锁进行更精确的控制。Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的wait(),notify(),notifyAll()方法是和”同步锁”(synchronized关键字)捆绑使用的;而Condition是需要与”互斥锁”/”共享锁”捆绑使用的。

二、LockSupport

主要是LockSupport.park()和unpark(Thread t)这两个方法的应用。unpark方法为线程提供“许可(permit)”,线程调用park方法则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。当一个线程有许可,他才可以运行。例如,有线程A和B。如果线程A调用了一次或多次unpark,他也只有一张许可,他没有被阻塞,可以运行。而他只要调用一次park,这张许可就被拿走了,线程就进入阻塞了。这里要注意,park导致的阻塞,是不会释放锁的,跟wait不一样。所以park算是忙碌等待。所以线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException。如果调用park,导致阻塞了,那再调用一次unpark,某个线程又拿到许可,就又可以运行了。注意,决不允许多次连续调用park,会导致永远阻塞。举个例子:

public class JUCTest {
	public static void main(String[] args) throws InterruptedException {
		parktest p=new parktest();
		mythread tMythread1=new mythread(p);
		mythread tMythread2=new mythread(p);
	    Thread t1=new Thread(tMythread1,"t1");
	    Thread t2=new Thread(tMythread1,"t2");
		t1.start();
		t2.start();
	}
}
class parktest {
	private int i=0;
	public void count() throws InterruptedException {
		while(true) {
			Thread.sleep(1000);
			synchronized (this) {
				System.out.println(Thread.currentThread().getName()+" "+i);
				i++;
				if(i==5) {
					LockSupport.park();//没有释放锁,虽然挂起了线程
				}
					
			}
		}
	}
}

运行结果

t1 0
t2 1
t2 2
t1 3

t1 4

可以发现,在之后就没办法继续进行下去了,因为park没有释放锁。而如果把LockSupport.park()换成this.wait(),可以发现执行如下:

t1 0
t2 1
t2 2
t1 3
t2 4
t1 5
t1 6
t1 7
t1 8

这里面就看出区别了。

三、ReentrantReadWriteLock

前面介绍的锁,都是独占锁,也就是同一时刻只有一个线程能占有这个锁。这个时候问题来了,其实在程序开发中,这样很不灵活。例如,对于一个文件,如果对他加锁,那当然我们会想到,对于读来说,无论多少个线程想读取他都无所谓,而只有对于写操作,我们才希望他是互斥的。所以,我们也需要一些特别的共享锁。我们来介绍一个读写锁ReentrantReadWriteLock。

ReadWriteLock,顾名思义,是读写锁。它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。

“读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。

“写入锁”用于写入操作,它是“独占锁”,写入锁只能被一个线程锁获取。

注意:不能同时存在读取锁和写入锁。ReadWriteLock是一个接口,ReentrantReadWriteLock是它的实现类,ReentrantReadWriteLock包括子类ReadLock和WriteLock。readLock可多线程并发执行,writeLock只能单线程执行(类似于synchronized),线程获取writeLock锁之后可以继续获取readLock。而且读锁和写锁不能同时出现。只有写锁支持Condition。

四、Semaphore

Semaphore是一个计数信号量,它的本质是一个”共享锁”。信号量维护了一个信号量许可集。线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。一个信号量的许可证个数可以在new的时候自己决定。

五、线程池

一个大型的程序中,创建的线程多了,就需要一个线程池对线程进行统一的管理。要创建线程池,要通过Executors这个静态工厂类。

	public static void main(String[] args) throws InterruptedException {
		parktest p=new parktest();
		mythread tMythread1=new mythread(p);
		mythread tMythread2=new mythread(p);
	    Thread t1=new Thread(tMythread1,"t1");
	    Thread t2=new Thread(tMythread1,"t2");
		ExecutorService pool=Executors.newFixedThreadPool(2);
		pool.execute(t1);
		pool.execute(t2);
		pool.shutdown();//关闭线程池
	}

对于线程池的关闭:

终止线程池主要有两个方法:shutdown() 和 shutdownNow()。

shutdown()后线程池将变成shutdown状态,此时不接收新任务,但会处理完正在运行的 和 在阻塞队列中等待处理的任务。

shutdownNow()后线程池将变成stop状态,此时不接收新任务,不再处理在阻塞队列中等待的任务,还会尝试中断正在处理中的工作线程。

    原文作者:JUC
    原文地址: https://blog.csdn.net/sinat_27908213/article/details/80641163
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞