并发学习笔记(2)

避免代码块受到并发访问的干扰

java语言提供了两种机制实现这种功能

  • Synchonized 关键字(调用对象内部的锁)
    synchronized关键字自动提供一个锁以及相关的条件
  • 引入了ReentrantLock类。(显示锁)
  • 更好: JUC框架为这些基础机制提供了独立的类: 线程池,或者高级一点专门做并发的工具的支持

ReentrantLock类 - 锁

Lock 与synchronized 区别

Lock 不是Java语言内置(compared to synchronized),Lock是一个类,通过这个类可以实现同步访问;

Lock 和 synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让现场释放对锁的占用,而Lock必须要用户手动释放,如果没有主动释放锁,将会产生死锁。 + Lock优缺点

Lock优缺点(compared to Synchronized)

Lock 能完成synchronized所实现的所有功能,而且比synchronized更好的性能。而且没有synchronized简洁。
但是:
1. 如果希望当获取锁时,有一个等待时间,不会无限期等待下去。
2. 希望当获取不到锁时,能够响应中断
3. 当读多,写少的应用时,希望提高性能
4. 获取不到锁时,立即返回 false。获取到锁时返回 true。

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

myLock.lock();
try{
   critical section
}
finally{
    myLock.unlock(); // 把解锁语句放在finally子句内是至关重要的。如果在临界区的代码抛异常,锁必须被释放。否则,其他线程将永远阻塞。
}

用锁来保护Bank类的transfer方法

public class Bank
{
    private Lock bankLock = new ReentrantLock();
    public void transfer(int from, int to, int amount){
        bankLock.lock();
        try{
            accounts[from] -= amount;
            account[to] += amount; 
        }
        finally{
            bankLock.unlock();
        }
    }
}

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

JUC包关于Lock
java.util.concurrent.locks.Lock
- void lock()
获取这个锁;如果锁同时被另一个线程拥有则发生阻塞。
- void unlock()
释放这个锁
java.util.concurrent.locks.ReentrantLock
- ReentrantLock()
构建一个可以被用来保护临界区的可重入锁
- ReentrantLock(boolean fair)
构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的锁,但是公平的保证会导致大大降低性能。

条件对象

通常线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是不能做拥有工作的线程。
比如银行的模拟程序。我们避免没有足够资金的账户作为转出账户. 如下的代码是不可以的,代码有可能在transfer方法之前被中断,在线程在此运行前,账户余额可能已经低于提款金额了。

java if(bank.getBalance(from) >= amount)
    // thread might be deactivated at this point
    bank.transfer(from,to,amount);

所以必须确保没有其他线程再检查余额和转账活动之间修改金额。通过锁来保护检查与转账动作的原子性,来做到这一点:

javapublic void transfer(int from, int to, int amount){
    backLock.lock();
    try{
        while(accounts[from] < amount){
            // wait
        }
        // transfer funds;
    }
    finally{
        bankLock.unlock();
    }
}

当账户没有足够的余额的时候,应该做什么?当前线程陷入wait until 另一个线程向账户注入了资金。但是锁的排他性导致其他线程没有进行存款操作的机会。这就是为什么需要调节对象的原因。

一个锁对象可以有一个或者多个相关的条件对象。可以用newCondition方法获得一个条件对象。

java class Bank{
    private Condition sufficientFunds;
    public Bank(){
        sufficientFunds = bankLock.newCondition();
    }
 }

如果transfer方法发现余额不足,就可以调用sufficientFunds.await() 当前线程被阻塞,并放弃了锁;一旦一个线程调用了await(),它进入了该条件的等待集(进入等待状态)。当锁可用时,该线程不能马上解除阻塞,相反,它仍然处于阻塞状态,也就是自己不能激活自己,需要另一个线程调用同一条件的signalAll方法为止。而signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检查该条件。
所以正确的代码是:

javapublic void transfer(int from, int to, int amount){
    backLock.lock();
    try{
        while(accounts[from] < amount)
           sufficientFunds.await();
        // transfer funds;
        ...
       sufficientFunds.signalAll();
    }
    finally{
        bankLock.unlock();
    }
}
  • Condition newCondition()
    返回一个与该锁相关的条件对象。
    java.util.concurrent.locks.Condition
  • void await()
    将该线程放到条件的等待集中
  • void signalAll()
    解除该条件的等待集中的所有线程的阻塞状态
  • void signal()
    从该条件的等待集中随机地选择一个线程,解除其阻塞状态

总结一下有关锁(外部锁)和条件的关键之处:
- 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
- 锁可以管理试图进入被保护代码段的线程
- 锁可以拥有一个或多个相关的条件对象
- 每个条件对象管理那些已经进入被保护代码段但还不能运行的先。


Java Synchronized keywords

synchronized 是java的关键字,也就是说是java语言内置的特性, 是托管给JVM执行的。
java的每一个对象都有一个内部锁。如果一个方法用synchronized声明,那么对象的锁将保护整个方法。namely,要调用该方法线程必须获得内部的对象锁。通过使用synchonized 块可以避免竞争条件;synchonized 修饰的同步代码块确保了一次只能一个线程执行同步的代码块。所有其它试图进入同步块的线程都会阻塞,直到同步块里面的线程退出这个块。

用Synchronized保护代码块的基本结构如下:

public synchronized void method(){
...
}

synchronized锁定的是调用这个同步方法的对象。 namely 当一个对象P1在不同的线程中执行这个同步方法时,不同的线程会形成互斥,达到同步的效果。但是这个对象所属的类的另一个对象P2却能调用这个被加了synchonized的方法。
上述代码等同于

public void method(){
synchronized(this){...}
}

this 指的是调用这个方法的对象,可见同步方法实质上是将synchronized作用于object reference -- 拿到了P1对象锁的线程,才能调用调用P1的同步方法。

javaclass Bank{
    private double[] accounts;
    public synchronized void tranfer(int from, int to, int amount) throws InterruptedException{
        while(accounts[from] < amount)
            wait();// wait on intrinsic object lock's single condition
        accounts[from] -= amount;
        accounts[to] += amount;
        notifyAll(); // notify all threads waiting on the condition
    }
}

可以看到synchronized关键字来编写代码要简洁的多。要理解这一代码,再一次重申:每一个对象有一个内部锁,并且该锁有一个内部条件。 由内部锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程
java的 synchronized 关键字能够作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块.

而无论 synchronized 关键字是加在了方法上还是对象上,他取得的锁都是对象,而不是把一段代码或者函数当做锁; 每个对象只有一个锁(lock)与之关联。 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以要尽量不免无谓的同步控制。

相关条件

内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。调用wait or notifyall等价于

intrinsicCondition.await()
intrinsicCondition.signalAll()
    public synchronized void transfer(int from, int to, double amount) throws InterruptedException{
        while(accounts[from] < amount){
            wait();
        }
        System.out.print(Thread.currentThread());
        accounts[from] -= amount;
        accounts[to] += amount;
        notifyAll();
    }

java.lang.Object
- void notifyAll()
解除那些在该对象上调用wait方法的线程的阻塞状态
java.util.concurrent.locks.Condition
- void await()
将该线程放到条件的等待集中
- void signalAll()
解除该条件的等待集中的所有线程的阻塞状态
- void signal()
从该条件的等待集中随机地选择一个线程,解除其阻塞状态

synchorinzed(缺陷)

- 不能中断一个正在试图获得锁的线程; 

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2)线程执行发生异常,此时 JVM 会让线程自动释放锁。

  那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。

- 试图获得锁时不能设定超时;

- 每个锁仅有单一的条件,可能不够的;

总结

在代码中应该使用哪一种呢? Lock 和 Condition对象还是同步方法?
下面是Core java的一些建议:

  • 最好既不是用Lock/Condition 也不是用synchonized关键字。 在许多情况下可以使用JUC包中的一种机制,它会为你处理所有加锁.
  • 如果synchronized关键字适合你的程序,那么尽量使用它,这样可以减少编写的代码数量,减少出错几率。
  • 如果特别需要Lock/Condition结果提供的独有特性,才使用Lock/Condition

5.18

阻塞队列

对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。比如生产者线程向队列插入元素,消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。

阻塞队列是一种比lock, synchonized更高级的管理同步的方法。

具体实现:
先定义一个BlockingQueue,队列放共享的资源,然后多个线程取或者存然后直接调用他的函数放入和取出元素就行了.

阅读 2.5k

推荐阅读
我们俩
用户专栏

学习笔记 ,分享与反思。互联网搬砖工

86 人关注
74 篇文章
专栏主页