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
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞