同步——锁对象和条件对象

同步

在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如果每一个线程都调用修改了该数据,可能会产生错误,这种情况叫做竞争条件。为了避免多线程对共享数据的讹误,就需要学习如何同步存取。

锁对象

有两种机制防止代码块受并发访问的干扰:

1)synchronized关键字

2)ReentrantLock类

用ReentrantLock保护代码块的基本机构如下:

        Lock lock=new ReentrantLock();
        lock.lock();
        try {
            System.out.println("进入lock锁");
            account--;
        } finally {
            lock.unlock();
        }

这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。

注意:把解锁操作放在finally子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。

每一个实例对象都有自己的ReentrantLock对象。假设现在有Bank类,如果两个线程试图访问同一个Bank对象,那么锁以串行方式提供服务。但是,如果连个线程访问不同的Bank对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞。本该如此,因为线程在操纵不同的Bank实例的时候,线程之间不会相互影响

锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。

public class LockDemo {

    private static Lock lock=new ReentrantLock();

    public static void main(String[] args) {
        new Thread(()-> {lockTest();}).start();
        new Thread(){
            @Override
            public void run() {
                lockTest();
            }
        }.start();
    }
    private static void lockTest(){
        lock.lock();
        try {
            System.out.println("进入lock锁1");
            lock.lock();
            try {
                System.out.println("进入lock锁2");
            }finally {
                lock.unlock();
            }
            System.out.println("出锁2");
        } finally {
            lock.unlock();
        }
        System.out.println("出锁1");
        System.out.println("===============");
    }
}

对于lock加锁和释放锁的问题,首先开启了两个线程进行打印(注意需要使用一个lock对象)。下图是正常打印的结果:

public class LockDemo {
    private static Lock lock=new ReentrantLock();

    public static void main(String[] args) {
        new Thread(()-> {lockTest();}).start();
        new Thread(){
            @Override
            public void run() {
                lockTest();
            }
        }.start();
    }
    private static void lockTest(){
        System.out.println(Thread.currentThread().getName());
        lock.lock();
        try {
            System.out.println("进入lock锁1");
            lock.lock();
            try {
                System.out.println("进入lock锁2");
            }finally {
                // 1
                lock.unlock();
            }
            System.out.println("出锁2");
        } finally {
              // 2
            //lock.unlock();
        }
        System.out.println("出锁1");
        System.out.println("===============");
    }
}

注释掉1或者2处的lock.unlock(),此时释放锁的次数不等于加锁的次数。

结果显示,已经发生了死锁。

ReentrantLock():构建一个可以被用来保护临界区的可重入锁。

ReentrantLock(boolean fair):构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。但是,这一公平的保证将大大降低性能。所以,默认情况下,锁没有被强制为公平的。

条件对象

通常,线程进入临界区,却发现在某一个条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。

先看一个简单的例子:
 

public class ConditionDemo {
    private static Lock lock=new ReentrantLock();
    private static int i=10;
    private static Condition condition=lock.newCondition();

    public static void main(String[] args){
       new Thread(()->conditionTest()).start();
       new Thread(()-> {lockTest();}).start();
    }
    private static void lockTest(){
        lock.lock();
        try {
            System.out.println("进入lock锁1");
            i++;
        } finally {
            System.out.println("释放锁1");
            lock.unlock();
        }
    }

    //测试条件对象
    private static void conditionTest(){
        lock.lock();
        try{
            System.out.println("获得锁");
            while(i<=10){
                System.out.println("进入条件对象中");
            }
            System.out.println("第二次获得锁");
        }finally {
            lock.unlock();
        }
    }
}

上面的程序跑起来的结果就是无限制的循环打印——“进入条件对象中”。如果正常执行第二个线程lockTest的i++,会得到正确的输出。但是,while的判断条件会让这一线程一直持有锁,并不给其他线程操作的机会。此时我们可以主动让当前线程阻塞,并放弃锁,这就是为什么使用条件对象的原因。

public class ConditionDemo {
    private static Lock lock=new ReentrantLock();
    private static int i=10;
    private static Condition condition=lock.newCondition();

    public static void main(String[] args){
       new Thread(()->conditionTest()).start();
       new Thread(()-> {lockTest();}).start();
    }
    private static void lockTest(){
        lock.lock();
        try {
            System.out.println("进入lock锁1");
            i++;
            //唤醒等待集中的所有线程
            condition.signalAll();
        } finally {
            System.out.println("释放锁1");
            lock.unlock();
        }
    }
    //测试条件对象
    private static void conditionTest(){
        lock.lock();
        try{
            System.out.println("获得锁");
            while(i<=10){
                System.out.println("进入条件对象中");
                //一旦一个线程调用await方法,它进入该条件的等待集,当锁可用时,该线程不能马上解除                
                //阻塞.相反,它处于阻塞状态,知道另一个线程调用同一条件上的signalAll方法时为止.
                condition.await();
                System.out.println("被另一线程唤醒");
            }
            System.out.println("第二次获得锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

正确是执行结果:

signalAll的调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。

此时,线程应该再次测试该条件。由于无法确保该条件被满足——signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再去检测该条件。

至关重要的是最终需要某个其他的线程调用signalAll方法。当一个线程调用await时,它没有办法重新激活自身。如果没有其他线程重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁(deadlock)现象

比如,去掉代码中的 condition.signalAll(); 最终运行结果如下:

即使锁已经被释放,但是没有其他线程的唤醒,该阻塞线程也无法自唤醒重新获取锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值