Java 线程同步 锁 条件变量

1. 死锁的产生条件

计算机系统中同时具备下面四个必要条件时,那么会发生死锁</br>

  1. 互斥条件。即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。这种独占资源如CD-ROM驱动器,打印机等等,必须在占有该资源的进程主动释放它之后,其它进程才能占有该资源。这是由资源本身的属性所决定的。</br>
  2. 不可抢占条件。进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。</br>
  3. 占有且申请条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。</br>
  4. 循环等待条件。存在一个进程等待序列{P1,P2,...,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,......,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。</br>

当程序存在竞争条件时,需要同步,避免出现不合预期的运行结果。同步实现的两个工具:锁和条件状态。</br>
以银行存取款为例,如果没有采取同步操作</br>

Code1
public class Bank {
    private final double[] accounts;

    public Bank(int n, double initialBalance){
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++){
            accounts[i] = initialBalance;
        }
    }

    public void transfer(int from, int to , double amount){
        if (accounts[from] < amount ) {
            return;
        }
        System.out.println(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf(" 10.2f from %d to %d", amount, from, to);
        accounts[to]  += amount;
        System.out.printf(" Total Balance: %10.2f%n", getTotalBalance();
    }

    public double getTotalBalance(){
        double sum = 0;
        for (double a : accounts){
                sum += a;
            }
            return sum;
        } 
    }

    public int size(){
        return accounts.length;
    }
}

如果存在两个线程同时执行指令

accounts[to]  += amount;

由于指令不是原子操作,该指令可能被处理为

  1. 将accounts[to]加载到寄存器</br>
  2. 增加amount </br>
  3. 将结果写回account[to]</br>
    在线程1执行完步骤1,2还没有执行步骤3的时候,即只是在寄存器中增加了amount,线程1被剥夺了运行权限,处理器将运行权限交给了线程2,线程2执行步骤1,2,还没有执行步骤3,即线程2获取和线程1拥有一样的初始值,并且只是在寄存器中增加了amount值,这时候处理器又将时间片给了线程1,线程1将计算后的值写入内存,而当时间片继续转给线程2的时候,仍然是在和线程一样的初始值上增加amount,这种情况下,则擦去了线程2所做的更新。

2. ReentrantLock可重入锁

可重入锁:是一种特殊的互斥锁,可以被同一个线程多次获取,而不会产生死锁。具有两个特点:</br>
1.是互斥的,任意时刻,只有一个线程锁,假设A线程已经获取了锁,在A线程释放这个锁之前,B线程无法获取到。</br>
2.它可以被同一线程多次持有,即假设A线程已经获取了这个锁,如果A线程在释放这个锁前又一次请求获取这个锁,能够获取成功</br>
锁持有一个计数器,来跟踪lock方法的嵌套调用。如下代码,transfer调用getTotalBalance方法,也会封锁bankLock对象,此时bankLock对象的持有计数为2。当getTotalBalance方法退出时,持有计数变回1。当transfer退出时,持有计数变为0。线程锁释放。</br>

Code2
public class Bank {
    private final double[] accounts;
    private Lock bankLock = new ReentrantLock();

    public Bank(int n, double initialBalance){
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++){
            accounts[i] = initialBalance;
        }
    }

    public void transfer(int from, int to , double amount){
        bankLock.lock();
        try {
             if (accounts[from] < amount ) {
                    return;
            }
            System.out.println(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" 10.2f from %d to %d", amount, from, to);
            accounts[to]  += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
        } finally {
            bankLock.unlock();
        }
    }

    public double getTotalBalance(){
        bankLock.lock();
        try {
            double sum = 0;

            for (double a : accounts){
                sum += a;
            }
            return sum;
        } finally {
            bankLock.unlock();
        }
    }

    public int size(){
        return accounts.length;
    }
}
package java.util.concurrent.locks.Lock
//获取这个锁,如果锁同时被另一个线程拥有则发生阻塞
void lock():
//释放这个锁
void unlock();
package java.util.concurrent.locks.ReentrantLock
//构建一个可以被用来保护临界区的可重入锁
ReentrantLock();
//构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。但是,这一公平的保证将大大降低性能,所以默认情况下,锁没有被强制为公平的。
ReentrantLock(boolean fair);

3 条件对象(条件变量)

使用场景:线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象对象来管理那些已经获得了一个锁却不能做有用工作的线程。</br>
银行账户需要转账,账户内只有500元却需要转600元,即账户中没有足够的余额,应该怎么办呢?现实情况下,银行柜员会告诉你账户余额不足,无法办理,直接退出。或者,我们可以等待另一个线程账户注入100元及以上的金额。</br>
当transfer方法写成如下

Code3
public void transfer(int from, int to, int amount){
    banklock.lock();
    try{
        while(accounts[from] < amount){
            //wait... 这里采取等待,而不是立即返回
        }
        //transfer funds...
    }finally{
        banklock.unlock();
    }
}

可以看出这个线程刚刚获得了对banklock的排他性访问,因此别的线程没有进行存取操作的机会。所以这是需要条件对象的原因。</br>
一个锁对象可以有一个或多个相关的条件对象,可以用newCondition方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。</br>

Code4
public class Bank {
    private final double[] accounts;
    private Lock bankLock;
    private Condition sufficientFunds;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = initialBalance;
        }
        //一个bank对象拥有一个ReentrantLock
        bankLock = new ReentrantLock();
        sufficientFunds = bankLock.newCondition();
    }

    public void transfer(int from, int to, double amount) {
        bankLock.lock();

        try {
            while (accounts[from] < amount) {
                //当前线程被阻塞,并且放弃了锁,并且该线程进入该条件的等待集
                sufficientFunds.await();
            }
            System.out.println(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" 10.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
            //重新激活因为这一条件而等待的所有线程。这些线程中等待集中移出时,它们再次成为可运行的,调度器将再次激活它们
            //同时,它们将试图重新进入该对象
            //一旦锁成为可用的,它们中的某一个将从await调用返回,获得该锁并且从被阻塞的地方继续执行
            //采用循环while表明此时线程应该再次检测该条件。由于无法确保该条件被满足,signalAll方法仅仅是通知正在等待的线程
            //siganlAll语义可以理解为:此时有可能已经满足条件,值得再次去检测条件
            sufficientFunds.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bankLock.unlock();
        }

    }

    public double getTotalBalance() {
        bankLock.lock();
        try {
            double sum = 0;
            for (double a : accounts) {
                sum += a;
            }
            return sum;
        } finally {
            bankLock.unlock();
        }
    }

    public int size() {
        return accounts.length;
    }
}

Condition.signal():是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的等待状态效率要高,但是存在危险。如果随机选择的线程发现自己仍然不能运行,那么它再次被阻塞。如果没有其他线程再次调用signal,那么系统就死锁了。

package java.util.concurrent.locks.Lock
//返回一个与该锁相关的条件对象
Condition new Condition();
e.g. Condition sufficientFunds = bankLock.newCondition();
package java.util.concurrent.locks.Condition
//将线程放到条件的等待集合中
void await();
//解除该条件的等待集中的所有线程的阻塞状态
void signalAll();
//从该条件的等待集中随机地选择一个线程,解除其阻塞状态
void Signal();

4 锁与条件对象的关键之处

*. 锁可以用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。</br>
*. 锁可以管理试图进入被保护代码段的线程
*. 锁可以拥有一个或多个相关的条件对象</br>
*. 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程

5 synchronized 关键字

每一对象有一个内部锁,并且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。</br>

Code5
public class Bank {
    private double[] accounts;
   
    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = initialBalance;
        }
    }

    public synchronized void transfer(int from, int to, double amount) {

        try {
            while (accounts[from] < amount) {
                //将线程添加到一个线程等待集中,该方法只能在一个同步方法中调用方法中调用
                wait();
            }
            System.out.println(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" 10.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
            //notifyAll/notify方法解除等待线程的阻塞状态,该方法只能在同步方法或者同步块中调用
            notifyAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized double getTotalBalance() {
        double sum = 0;
        for (double a : accounts) {
            sum += a;
        }
        return sum;
    }

    public int size() {
        return accounts.length;
    }
}

将静态方法声明为synchronized也是合法的,如果调用这种方法,该方法获得相关的类对象的内部锁。如果Bank类有一个静态同步的方法,那么当该方法被调用时,Bank.class对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或者任何其他的同步静态方法。

pulic class Bank
{
    private double[] accounts;
    private Object lock = new Object();
}

6 锁和条件对象的局限

*. 不能中断一个正在试图获得锁的线程</br>
*. 试图获得锁时不能设定超时
*. 每个锁仅有单一的条件,可能是不够的</br>
*. 最好既不是用Locl/Condition也不使用synchronized关键字
*. 如果synchronized关键字适合程序,那么请尽量使用它,这样可以减少编写的代码数量,减少出错的几率。</br>
*. 如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition</br>

7 Volatile域

有时,仅仅为了读写一个或两个实例域就使用同步,开销过大。可以采用volatile关键字声明域,该修饰词告诉编译器和虚拟机该域是可能被另一个线程并发更新的。它为实例域的同步访问提供了一个种免锁机制。

8 final变量

将域声明为final,可以安全的访问一个共享域。

final Map<String, Double> accounts = new HashMap<>();

其他线程会在构造函数完成构造之后才看到这个accounts变量。如果不适用final,就不能保证其他线程看到的是account更新后的值,他们可能都只是看到null,而不是新构造的HashMap。当然,对这个映射表的操作不是线程安全的,如果多个线程在读写这个映射表,仍然需要进行的。

学习资料:《Java核心技术卷一》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,943评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,426评论 1 15
  • 该文章转自:http://blog.csdn.net/evankaka/article/details/44153...
    加来依蓝阅读 7,322评论 3 87
  • 写在前面的话: 这篇博客是我从这里“转载”的,为什么转载两个字加“”呢?因为这绝不是简单的复制粘贴,我花了五六个小...
    SmartSean阅读 4,707评论 12 45
  • Java-Review-Note——4.多线程 标签: JavaStudy PS:本来是分开三篇的,后来想想还是整...
    coder_pig阅读 1,610评论 2 17