欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

多线程(四)--Lock

程序员文章站 2024-01-08 10:58:46
...

在上篇博客中通过notifyAll()的方式将所有线程唤醒来避免了死锁的情况。这种解决方法会降低效率,因为它也会唤醒本方的线程,本方线程有可能获得执行权,然后判断标志后又进入冻结状态。如果能有一种方式可以使本方线程只唤醒对方的线程,那么效率就会得到提高。
在jdk1.4版本以及此版本之前并没有这样的解决办法。jdk1.5版本以后(这些版本从新命名为jdk5.0、jdk6.0……)提供了一个Lock接口,实现了本方线程只唤醒对方线程的设想。

Lock接口

jdk1.5以后将同步和锁封装成了对象。并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
Lock接口:出现替代了同步代码块或者同步函数。将同步的隐式操作变成了显式锁操作。同时更为灵活,可以一个锁上加多组监视器。它有两个主要方法:
lock():获取锁
unlock():释放锁,通常需要定义在finally代码块,避免前面的代码发生异常而导致锁不能被释放的情况。
Lock实现提供了比synchronized方法和语句可获得更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。
Lock所有已知的实现类有:ReentrantLock,ReentrantReadWriteLock.ReadLock,ReentrantReadWriteLock.WriteLock
ReentrantLock:一个可重入的互斥锁Lock,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义。
使用Lock接口的步骤:
1, 创建锁对象:Lock lock=new ReentrantLock();
2,获得锁:lock.lock();
3,释放锁:lock.unlock();
这样将上篇博客的多生产者多消费者的例子用Lock接口实现代替synchronized方法和语句后的代码为:

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

class Resource
{
    private String name;
    private int count=1;
    private boolean flag=false;
    Lock lock=new ReentrantLock();
    public void set(String name)
    {
        lock.lock();
        try{
            while(flag)//用while可以每次唤醒一个线程后都进行判断,避免了有时候生产的没有被消费到的情况,或者生产一个被多次消费的情况。
                try{this.wait();}catch(InterruptedException e){}
            this.name=name+count;
            count++;
            System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
            flag=true;
            notifyAll();//这样可以解决死锁问题(全部线程进入等待状态,没有线程来唤醒)
        }
        finally{
            lock.unlock();
        }
    }
    public void out()
    {
        lock.lock();
        try{
            while(!flag)
                try{this.wait();}catch(InterruptedException e){}
            System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
            flag=false;
            notifyAll();
        }
        finally
        {
            lock.unlock();
        }
    }
}
class Producer implements Runnable
{
    private Resource r;
    Producer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
        {
            r.set("duck");
        }
    }
}

class Consumer implements Runnable
{
    private Resource r;
    Consumer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
            r.out();
    }
}

class ThreadDemo
{
    public static void main(String[] args)
    {
        Resource r=new Resource();
        Producer pro=new Producer(r);
        Consumer con=new Consumer(r);

        Thread t0=new Thread(pro);
        Thread t1=new Thread(pro);
        Thread t2=new Thread(con);
        Thread t3=new Thread(con);

        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

运行上面的代码会抛出java.lang.IllegalMonitorStateException
异常,这是因为代码中的notifyAll(),wait()方法都是Object对象的方法,在这里是this这个对象锁的方法。但是我们已经把锁改成了Lock接口实现的lock对象,所以当前线程不是对象锁的持有者。应该将方法上的对象锁改变,但是使用Lock接口对象锁后这些监视器方法也发生了改变:

Condition接口

Condition接口:它的出现替代了Object的wait,notify,notifyAll方法。它将这些监视器方法单独进行了封装,变成了Condition监视器对象。可以与任意锁进行组合。
Condition将Object监视器方法(wait,notify,notifyAll)分解成截然不同的对象,以使这些对象与任意Lock实现组合使用,为每个对象提供多个等待set。其中,Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。其中await(),signal(),signalAll()分别替代了wait(),notify(),notifyAll()方法。
Condition实例实质上被绑定到一个锁上,要为特定的Lock实例获得Condition实例,使用其newCondition()方法。
简单来说,用Condition接口就可以实现一个Lock对象上有多组监视器。可以用一个锁上的不同监视器来对不同的任务进行监视器方法的操作。
接着将多生产者多消费者的例子进行改写:可以用Lock接口实现一个互斥的对象锁,这样当set方法得到对象锁lock的时候out方法得不到对象锁lock,反之亦然。然后再这个lock对象锁上绑定两个Condition实例,分别用于监控set方法和out方法,这样便可以实现在set方法中只唤醒out方法中的线程,在out方法在中只唤醒set方法中的线程。代码如下:

import java.util.concurrent.locks.*;

class Resource
{
    private String name;
    private int count=1;
    private boolean flag=false;
    //创建锁对象
    Lock lock=new ReentrantLock();
    //通过已有的锁获取该锁上的监视器对象
    //Condition con=lock.newCondition();
    //通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者
    Condition pro_con=lock.newCondition();
    Condition con_con=lock.newCondition();
    public void set(String name)
    {
        lock.lock();
        try
        {
            while(flag)//用while可以每次唤醒一个线程后都进行判断,避免了有时候生产的没有被消费到的情况,或者生产一个被多次消费的情况。
                try{pro_con.await();}catch(InterruptedException e){}
            this.name=name+count;
            count++;
            System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
            flag=true;
            //con.signalAll();
            con_con.signal();
            //notifyAll();//这样可以解决死锁问题(全部线程进入等待状态,没有线程来唤醒)
        }
        finally
        {
            lock.unlock();
        }
    }
    public void out()
    {
        lock.lock();
        try
        {
            while(!flag)
                try{con_con.await();}catch(InterruptedException e){}
            System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
            flag=false;
            pro_con.signalAll();
            //notifyAll();
        }
        finally
        {
            lock.unlock();
        }
    }
}
class Producer implements Runnable
{
    private Resource r;
    Producer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
        {
            r.set("duck");
        }
    }
}

class Consumer implements Runnable
{
    private Resource r;
    Consumer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
            r.out();
    }
}

class ProducerConsumerDemo
{
    public static void main(String[] args)
    {
        Resource r=new Resource();
        Producer pro=new Producer(r);
        Consumer con=new Consumer(r);

        Thread t0=new Thread(pro);
        Thread t1=new Thread(pro);
        Thread t2=new Thread(con);
        Thread t3=new Thread(con);

        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

接着来看一下JDK1.5版本的API中的一个范例,该范例中有一个缓冲区,它支持put和take方法。如果试图在空的缓冲区上执行take操作,则在某一项变得可用之前,线程一直阻塞;如果试图在满的缓冲区中执行put操作,则在有空间变得可用之前,线程将一直阻塞。
这个例子与上面的例子不同之处在于,上面那个例子的一次操作的资源只有一个变量,而这个例子中的资源是一个数组,数组中的元素是一个对象。数组中有对象元素时,可以take,有空间时,可以put,如果数组满了就不能put,如果数组空了就不能take。这种情况比较符合实际开发。代码如下:

class BoundedBuffer{
    final Lock lock=new ReentrantLock();
    final Condition notFull=lock.newCondition();
    final Condition notEmpty=lock.newCondition();

    final Object[] items=new Object[100];
    int putptr,takeptr,count;

    public void put(Object x) throws InterruptedException{
        lock.lock();
        try{
            while (count==items.length)
                notFull.await();
            items[putptr]=x;
            if(++putptr==items.length)
                putptr=0;
            ++count;
            notEmpty.signal();
        }finally{
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException{
        lock.lock();
        try{
            while(count==0)
                notEmpty.await();
            Object x=items[takeptr];
            if(++takeptr==items.length)
                takeptr=0;
            --count;
            notFull.signal();
            return x;
        }finally{
            lock.unlock();
        }
    }
}

小练习
1.下面代码是否有错误?如果有,发生在哪一行?

class Test implements Runnable
{
    public void run(Thread t)
    {}
}

有错,因为Runnable接口的run方法是没有参数的,所以这个Test类里的run方法不是复写Runnable接口里的run方法,而是Test类中特有的普通方法。但是由于Test类继承了Runnable接口,而没有复写接口中的run方法,所以这个类只能是抽象类,应该在class 之前加上关键字abstract。

2.下面代码输出的结果是什么?

class ThreadTest
{
    public static void main(String[] args)
    {
        new Thread(new Runnable()
        {
            public void run()
            {
                System.out.println("Runnable run");
            }
        })
        {
            public void run()
            {
                System.out.println("subThread run");
            }
        }.start();

输出的结果是:subThread run
运行的应该是子类的run方法;如果子类没有复写run方法,应该运行线程中自定义任务的run方法;如果没有写任务的run方法,则运行Thread类默认的run方法。

相关标签: 多线程