【java 多线程】多线程并发同步问题及生产者、消费者问题

一、线程并发同步概念

线程同步其核心就在于一个“同”。所谓“同”就是协同、协助、配合,“同步”就是协同步调昨,也就是按照预定的先后顺序进行运行,即“你先,我等, 你做完,我再做”。

线程同步,就是当线程发出一个功能调用时,在没有得到结果之前,该调用就不会返回,其他线程也不能调用该方法。

就一般而言,我们在说同步、异步的时候,特指那些需要其他组件来配合或者需要一定时间来完成的任务。在多线程编程里面,一些较为敏感的数据时不允许被多个线程同时访问的,使用线程同步技术,确保数据在任何时刻最多只有一个线程访问,保证数据的完整性。

二、线程同步中可能存在安全隐患

用生活中的场景来举例:小生去银行开个银行账户,银行给 me 一张银行卡和一张存折,小生用银行卡和存折来搞事情:

银行卡疯狂存钱,存完一次就看一下余额;同时用存折子不停地取钱,取一次钱就看一下余额;

具体代码实现如下:

先弄一个银行账户对象,封装了存取插钱的方法:

package com.test.threadDemo2;

/**
 * 银行账户
 * @author Administrator
 *
 */
public class Acount {
    private int count=0;
    
    /**
     * 存钱
     * @param money
     */
    public void addAcount(String name,int money) {
        17             // 存钱
            count += money;
            System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
            SelectAcount(name);
22     }
    
    /**
     * 取钱
     * @param money
     */
    public void subAcount(String name,int money) {
        30             // 先判断账户现在的余额是否够取钱金额
            if(count-money < 0){  
                System.out.println("账户余额不足!"); 
                return;  
            } 
            // 取钱
            count -= money;
            System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
            SelectAcount(name);
40     }
    
    /**
     * 查询余额
     */
    public void SelectAcount(String name) {
        System.out.println(name+"...余额:"+count);
    }
}

编写银行卡对象:

package com.test.threadDemo2;
/**
 * 银行卡负责存钱
 * @author Administrator
 *
 */
public class Card implements Runnable{
    private String name;
    private Account account = new Account();
    
    public Card(String name,Account account) {
        this.account = account;
        this.name = name;
    }
    
    @Override
    public void run() {
        
        while(true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.addAccount(name,100);26         }
    }
    
}

编写存折对象(和银行卡方法几乎一模一样,就是名字不同而已):

package com.test.threadDemo2;
/**
 * 存折负责取钱
 * @author Administrator
 *
 */
public class Paper implements Runnable{
    private String name;
    private Account account = new Account();
    
    public Paper(String name,Account account) {
        this.account = account;
        this.name = name;
    }
    
    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.subAccount(name,50);
        }
        
    }

}

主方法测试,演示银行卡疯狂存钱,存折疯狂取钱:

package com.test.threadDemo2;

public class ThreadDemo2 {
    public static void main(String[] args) {
        
        // 开个银行帐号
        Account account = new Account();
        // 开银行帐号之后银行给张银行卡
        Card card = new Card("card",account);
        // 开银行帐号之后银行给张存折
        Paper paper = new Paper("存折",account);
        
        Thread thread1 = new Thread(card);
        Thread thread2 = new Thread(paper);
        
        thread1.start();
        thread2.start();            
    }
}

结果显示:从中可以看出 bug

【java 多线程】多线程并发同步问题及生产者、消费者问题_第1张图片


 

从上面的例子里就可以看出,银行卡存钱和存折取钱的过程中使用了 sleep() 方法,这只不过是小生模拟“系统卡顿”现象:银行卡存钱之后,还没来得及查余额,存折就在取钱,刚取完钱,银行卡这边“卡顿”又好了,查询一下余额,发现钱存的数量不对!当然还有“卡顿”时间比较长,存折在卡顿的过程中,把钱全取了,等银行卡这边“卡顿”好了,一查发现钱全没了的情况可能。

因此多个线程一起访问共享的数据的时候,就会可能出现数据不同步的问题,本来一个存钱的时候不允许别人打断我(当然实际中可以存在刚存就被取了,有交易记录在,无论怎么动这个帐号,都是自己的银行卡和存折在动钱。小生这个例子里,要求的是存钱和查钱是一个完整过程,不可以拆分开),但从结果来看,并没有实现小生想要出现的效果,这破坏了线程“原子性”。

三、线程同步中可能存在安全隐患的解决方法

  从上面的例子中可以看出线程同步中存在安全隐患,我们必须不能忽略,所以要引入“锁”(术语叫监听器)的概念:

3.1 同步代码块:

  使用 synchronized() 对需要完整执行的语句进行“包裹”,synchronized(Obj obj) 构造方法里是可以传入任何类的对象,

  但是既然是监听器就传一个唯一的对象来保证“锁”的唯一性,因此一般使用共享资源的对象来作为 obj 传入 synchronized(Obj obj) 里:

  只需要锁 Account 类中的存钱取钱方法就行了:

package com.test.threadDemo2;

/**
 * 银行账户
 * @author Administrator
 *
 */
public class Acount {
    private int count=0;
    
    /**
     * 存钱
     * @param money
     */
    public void addAcount(String name,int money) {
        synchronized(this) {
            // 存钱
            count += money;
            System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
            SelectAcount(name);
        }
    }
    
    /**
     * 取钱
     * @param money
     */
    public void subAcount(String name,int money) {
        synchronized(this) {
            // 先判断账户现在的余额是否够取钱金额
            if(count-money < 0){  
                System.out.println("账户余额不足!"); 
                return;  
            } 
            // 取钱
            count -= money;
            System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
            SelectAcount(name);
        }
    }
    
    /**
     * 查询余额
     */
    public void SelectAcount(String name) {
        System.out.println(name+"...余额:"+count);
    }
}

3.2 同步方法

者在方法的申明里申明 synchronized 即可:

package com.test.threadDemo2;
/**
 * 银行账户
 * @author Administrator
 *
 */
public class Acount {
    private int count;
    
    /**
     * 存钱
     * @param money
     */
    public synchronized void addAcount(String name,int money) {
            // 存钱
            count += money;
            System.out.println(name+"...存入:"+money);
    }
    
    /**
     * 取钱
     * @param money
     */
    public synchronized void subAcount(String name,int money) {
            // 先判断账户现在的余额是否够取钱金额
            if(count-money < 0){  
                System.out.println("账户余额不足!");  
                return;  
            } 
            // 取钱
            count -= money;
            System.out.println(name+"...取出:"+money);
    }
    
    /**
     * 查询余额
     */
    public void SelectAcount(String name) {
        System.out.println(name+"...余额:"+count);
    }
}


运行效果:

【java 多线程】多线程并发同步问题及生产者、消费者问题_第2张图片

3.3 使用同步锁:

account 类创建私有的 ReetrantLock 对象,调用 lock() 方法,同步执行体执行完毕之后,需要用 unlock() 释放锁。

package com.test.threadDemo2;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 银行账户
 * @author Administrator
 *
 */
public class Acount {
    private int count;
    private ReentrantLock lock = new ReentrantLock();
    
    /**
     * 存钱
     * @param money
     */
    public void addAcount(String name,int money) {
        lock.lock();
        try{
            // 存钱
            count += money;
            System.out.println(name+"...存入:"+money);
        }finally {
            lock.unlock();
        }
    }
    
    /**
     * 取钱
     * @param money
     */
    public void subAcount(String name,int money) {
        lock.lock();
        try{
            // 先判断账户现在的余额是否够取钱金额
            if(count-money < 0){  
                System.out.println("账户余额不足!");  
                return;  
            } 
            // 取钱
            count -= money;
            System.out.println(name+"...取出:"+money);
        }finally {
            lock.unlock();
        }
    }
    
    /**
     * 查询余额
     */
    public void SelectAcount(String name) {
        System.out.println(name+"...余额:"+count);
    }
}

运行效果: 

【java 多线程】多线程并发同步问题及生产者、消费者问题_第3张图片

四、死锁

当线程需要同时持有多个锁时,有可能产生死锁。考虑如下情形:

线程 A 当前持有互斥所锁 lock1,线程 B 当前持有互斥锁 lock2。

接下来,当线程 A 仍然持有 lock1 时,它试图获取 lock2,因为线程 B 正持有 lock2,因此线程 A 会阻塞等待线程 B 对 lock2 的释放。

如果此时线程 B 在持有 lock2 的时候,也在试图获取 lock1,因为线程 A 正持有 lock1,因此线程 B 会阻塞等待 A 对 lock1 的释放。

二者都在等待对方所持有锁的释放,而二者却又都没释放自己所持有的锁,这时二者便会一直阻塞下去。这种情形称为死锁。

package com.testDeadLockDemo;

public class LockA {
    
    private LockA(){}
    
    public static final LockA lockA = new LockA();
}

package com.testDeadLockDemo;

public class LockB {
    private LockB(){}
    
    public static final LockB lockB = new LockB();
}
package com.testDeadLockDemo;

public class DeadLock implements Runnable{
    private int i=0;
    
    @Override
    public void run() {
        while(true) {
            if(i%2==0){
                synchronized(LockA.lockA) {
                    System.out.println("if...lockA");
                    synchronized(LockB.lockB) {
                        System.out.println("if...lockB");
                    }
                }
            }else {
                synchronized(LockB.lockB) {
                    System.out.println("else...lockB");
                    synchronized(LockA.lockA) {
                        System.out.println("else...lockA");
                    }
                }
            }
            i++;
        }
        
    }
}

测试:

package com.testDeadLockDemo;

public class Test {
    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();
        
        Thread t1 = new Thread(deadLock);
        Thread t2 = new Thread(deadLock);
        t1.start();
        t2.start();
        
    }
}


运行结果:

【java 多线程】多线程并发同步问题及生产者、消费者问题_第4张图片

五、线程通信(生产者、消费者问题)

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

在共享资源中增加镖旗,当镖旗为真的时候才可以存钱,存完了就把镖旗设置成假,当取款的时候发现镖旗为假的时候,可以取款,取完款就把镖旗设置为真。

只需修改 Account 类 和 测试类 即可

package com.test.threadDemo2;

/**
 * 银行账户
 * @author Administrator
 *
 */
public class Acount {
    private boolean flag=false;    // 默认flag 为false,要求必须先存款再取款
    private int count=0;
    
    /**
     * 存钱
     * @param money
     */
    public void addAcount(String name,int money) {
        synchronized(this) {
            // flag 为true 表示可以存款,否则不可以存款
            if(flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }else {
                // 存钱
                count += money;
                System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
                SelectAcount(name);
                flag = true;
                this.notifyAll();
            }
        }
    }
    
    /**
     * 取钱
     * @param money
     */
    public void subAcount(String name,int money) {
        synchronized(this) {
            if(!flag) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
            // 先判断账户现在的余额是否够取钱金额
            if(count-money < 0){  
                System.out.println("账户余额不足!"); 
                return;  
            } 
                // 取钱
                count -= money;
                System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
                SelectAcount(name);
                flag = false;
                this.notifyAll();
            }
        }
    }
    
    /**
     * 查询余额
     */
    public void SelectAcount(String name) {
        System.out.println(name+"...余额:"+count);
    }
}

package com.test.threadDemo2;

public class ThreadDemo2 {
    public static void main(String[] args) {
        
        // 开个银行帐号
        Acount acount = new Acount();
        
        // 开银行帐号之后银行给张银行卡
        Card card1 = new Card("card1",acount);
        Card card2 = new Card("card2",acount);
        Card card3 = new Card("card3",acount);
        
        // 开银行帐号之后银行给张存折
        Paper paper1 = new Paper("paper1",acount);
        Paper paper2 = new Paper("paper2",acount);
        
        // 创建三个银行卡
        Thread thread1 = new Thread(card1,"card1");
        Thread thread2 = new Thread(card2,"card2");
        Thread thread3 = new Thread(card3,"card3");
        // 创建两个存折
        Thread thread4 = new Thread(paper1,"paper1");
        Thread thread5 = new Thread(paper2,"paper2");
        
        thread1.start();
        thread2.start();
        thread3.start();
        
        thread4.start();
        thread5.start();
    }
}

运行结果:

【java 多线程】多线程并发同步问题及生产者、消费者问题_第5张图片

使用同步锁也可以达到相同的目的:

package com.test.threadDemo2;

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

/**
 * 银行账户
 * @author Administrator
 *
 */
public class Acount2 {
    private boolean flag=false;    // 默认flag 为false,要求必须先存款再取款
    private int count=0;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    
    /**
     * 存钱
     * @param money
     */
    public void addAcount(String name,int money) {
        lock.lock();
        try {
            // flag 为true 表示可以存款,否则不可以存款
            if(flag) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }else {
                // 存钱
                count += money;
                System.out.println(name+"...存入:"+money+"..."+Thread.currentThread().getName());
                SelectAcount(name);
                flag = true;
                condition.signalAll();
            }
        }finally {
            lock.unlock();
        }
    }
    
    /**
     * 取钱
     * @param money
     */
    public void subAcount(String name,int money) {
        lock.lock();
        try {
            if(!flag) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
            // 先判断账户现在的余额是否够取钱金额
            if(count-money < 0){  
                System.out.println("账户余额不足!"); 
                return;  
            } 
                // 取钱
                count -= money;
                System.out.println(name+"...取出:"+money+"..."+Thread.currentThread().getName());
                SelectAcount(name);
                flag = false;
                condition.signalAll();
            }
        }finally {
            lock.unlock();
        }
    }
    
    /**
     * 查询余额
     */
    public void SelectAcount(String name) {
        System.out.println(name+"...余额:"+count);
    }
}

你可能感兴趣的:(java学习笔记,多线程,线程同步,生产者消费者问题)