通过前面的文章我们知道了,可以通过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状态,此时不接收新任务,不再处理在阻塞队列中等待的任务,还会尝试中断正在处理中的工作线程。