成都计算机培训 网络报名 Java软件开发培训课程 朗沃成都软件Java培训中心师资介绍 成都软件培训中心开班信息 朗沃成都Java软件开发培训中心学员就业情况 Java朗沃成都软件培训中心在线咨询
成都朗沃教育课程升级啦!
□ 您现在的位置:首页> 学员天地> java学习> 正文

Java中的多线程(二)

朗沃成都软件培训学校在线咨询         朗沃成都软件培训学校在线报名

关键词:java

  在上一篇文章中我们理解了多线程的概念和基本用法,那么在这一篇文章中我们将继续对多线程展开更深入的剖析。

    之前我们解释过,线程是由CPU分配时间片来运行的,并且执行的顺序也是不确定的,如果多个线程同时访问某一个资源,那么就会出现竞争的问题。举个简单的例子,比如在你的银行账户上余额有1000块钱,现在你要再存1000块钱到账户上,而与此同时你的妻子在外地要取1000块钱,同一时间又要存钱又要取钱,那么就会出现问题。下面用代码来模拟一下这个过程:


public class ThreadSyn {
    
    int money = 1000;
    public void save(int money){
        System.out.println("
存入:" + money);
        this.money += money;
        System.out.println("
当前余额:" + this.money);
    }
    public void get(int money){
        System.out.println("
取走:" + money);
        this.money -= money;
        System.out.println("
当前余额:" + this.money);
    }
    public static void main(String [] args) {
        ThreadSyn syn = new ThreadSyn();
        SaveMoney save = new SaveMoney(syn);
        getMoney get = new getMoney(syn);
    }
}

class SaveMoney extends Thread{
    ThreadSyn syn;
    public SaveMoney(ThreadSyn syn) {
        this.syn = syn;
        start();
    }
    public void run(){
        syn.save(1000);
    }
}

class getMoney extends Thread{
    ThreadSyn syn;
    public getMoney(ThreadSyn syn) {
        this.syn = syn;
        start();
    }
    public void run(){
        syn.get(1000);
    }
}
现在我们来看上面这段程序有什么问题。首先两个线程分别对同一个money对象进行操作。一个是存钱,一个是取钱。在银行里存钱和取钱一般都是这样一过程,首先获得账户上的余额,然后加上或者减去要存或者要取的金额,最后更新账户余额。如果他们不是同步处理,比如在存钱的时候还没到最后更新余额时就有人在其他地方将账户的钱取走了,最后更新余额并不会知道有人取走了钱,那么你的账户余额依然是你存入钱后的金额,取走的钱也就不会被计算在你的账户内了。象上面这段程序一样,如果SaveMoney这个线程在调用save方法时,money的值还处于不稳定状态,这时getMoney这个线程进来将money的值改变了,最后结果可能就难以预料。但是上面这段程序很小,CPU执行速度会很快,可能你永远也看不到这种竞争的情况,但是这个程序是存在竞争的隐患。要解决这个问题,我们就要让线程之间保持同步性。

1
、线程同步
什么叫做同步。同步这个词语不能按照字面意思来理解。因为这样很可能造成误解。打个比方,就象上面银行存钱和取钱这个例子。如果使用同步,那么只有当存钱完毕后才能进行取钱,这两个操作就是一种同步。如果是异步,那么就是说存钱和取钱可以在同一时刻进行。在java中提供了synchronized这个关键字来让线程之间保持同步。可以使用两种方法进行同步操作,方法同步和代码块同步。当在方法或者代码块中加上synchronized关键字,就表示某个线程执行到这里时,首先会获得该对象的锁,如果该对象没有被上锁,那么该线程就会为这个对象加上一把锁,并且继续执行。而其他线程执行到这里时,由于该对象已经被上锁了,那么这些线程就只有等待该对象上的锁释放。这样就能让执行到这里的线程保持同步性。同步的操作有两种形式:方法同步和代码块同步。
1)方法同步
在需要同步的方法前加上Synchronized关键字,就表示该对象上的此方法需要同步,下面我们可以修改一下上面的这个程序。

public class ThreadSyn {
    
    int money = 1000;
    synchronized public void save(int money){
        System.out.println("
存入:" + money);
        this.money += money;
        System.out.println("
当前余额:" + this.money);
    }
    synchronized public void get(int money){
        System.out.println("
取走:" + money);
        this.money -= money;
        System.out.println("
当前余额:" + this.money);
    }
    public static void main(String [] args) {
        ThreadSyn syn = new ThreadSyn();
        SaveMoney save = new SaveMoney(syn);
        getMoney get = new getMoney(syn);
    }
}

class SaveMoney extends Thread{
    ThreadSyn syn;
    public SaveMoney(ThreadSyn syn) {
        this.syn = syn;
        start();
    }
    public void run(){
        for (int i = 0; i < 5; i++)
        {
            syn.save(1000);
        }            
    }
}

class getMoney extends Thread{
    ThreadSyn syn;
    public getMoney(ThreadSyn syn) {
        this.syn = syn;
        start();
    }
    public void run(){
        for (int i = 0; i < 5; i++)
        {
            syn.get(1000);
        }        
    }
}
下面是运行结果::
存入:1000
当前余额:2000
存入:1000
当前余额:3000
存入:1000
当前余额:4000
存入:1000
当前余额:5000
存入:1000
当前余额:6000
取走:1000
当前余额:5000
取走:1000
当前余额:4000
取走:1000
当前余额:3000
取走:1000
当前余额:2000
取走:1000
当前余额:1000
由于CPU运行的太快,因此可能会产生一种误解,认为同步就一定会是1个线程执行完后另外一个线程才会执行。其实并不是这样的,当一个线程进入同步方法后,其他线程都在外面等待,而当该线程释放了对象锁后,下一个进入该方法的线程我们并不知道,这个也是不确定的。

2)代码块内同步
还有一种方法可以将同步仅限定在方法里的某个代码块中,这个也被称为是临界区。方法同步有个不好的地方就是只能为当前类的对象进行加锁,如果需要为其他的对象进行加锁那就无能为力了。而在代码块中同步,我们可以为某个特定的对象进行加锁,只有持有相同对象的线程才会被同步:比如
synchronized(Object object){}

2
、等待和通知
既然有了线程同步,那么我们也就需要线程之间的通讯,而这个通讯就需要使用wait()notify()这两个方法来进行。wait()的意思是将拥有该对象锁的线程挂起,并且让其释放锁,这样其他线程就可以进入同步的方法。wait()方法和sleep()方法区别就在于sleep不会释放对象上的锁,而wait()会释放锁。当使用了wait()方法后,就需要某个线程使用notify()来释放该线程。这个就是等待与通知。非常典型的例子就是生产者与消费者的问题。

生产者与消费者
下面我们简化一下这个例子。以Think in Java中的例子为原形。大致是这样的,我们将生产者看成是餐厅里的厨师,消费者看成是餐厅里的服务员。厨师会不停的做出食物,而服务员会将厨师做出的食物拿给顾客。刚开始服务员是处于等待阶段,直到厨师将食物做出来后,他会通知服务员将食物拿走。这时厨师会处于等待阶段,直到服务员将食物拿走后,他会通知厨师可以继续生产食物。下面是这个例子的代码:

public class WaitNotify {

    boolean isfull = false;
    int food;
    
    synchronized void put(int i){
        if(isfull)
        {
            try
            {
                wait();
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
        food = i;
        isfull = true;
        System.out.println(Thread.currentThread().getName() + food);
        notify();
    }
    synchronized void get(){
        if(!isfull)
        {
            try
            {
                wait();
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
        isfull = false;
        System.out.println(Thread.currentThread().getName() + food);
        notify();
    }
    class Produce extends Thread{
        Produce(){
            super("
厨师做出了食物 ");
            start();
        }
        public void run(){
            for (int i = 0; i < 10; i++)
            {
                put(new Random().nextInt(100) + 1);
            }            
        }
    }
    class Consume extends Thread{
        Consume(){
            super("
服务员取得了食物 ");
            start();
        }
        public void run(){
            for (int i = 0; i < 10; i++)
            {
                get();
            }            
        }
    }
    public static void main(String [] args) {

        WaitNotify wn = new WaitNotify();
        Produce produce = wn.new Produce();
        Consume consume = wn.new Consume();
    }
}
在这里例子中,首先创建了两个线程,ProduceConsumeProduce线程通过调用put方法,随机生产出食物,Consume线程调用get方法,取得刚才Produce生产的食物。这个食物是又一个变量food存放,这里还有个布尔变量isfull,当isfullfalse时,那么就表明当前没有任何食物被生产出来,生产者这个线程就生产出食物,并将isfull设置为true,同时通知消费者来取食物。如果isfulltrue时,就表明此时已经有食物,并且消费者还没来取食物,那么生产者这时就该被挂起,等待消费者取得食物后唤醒他。在消费者这个线程进入get方法后,如果isfullfalse时,表明此时生产者还没有生产出食物,那么消费者就会被挂起,等待生产者生产出食物后唤醒他。如果isfulltrue时,消费者就取得该食物,并将isfull设置为false,并唤醒生产者需要继续生产食物了。由于这里使用了线程同步,因此只可能有一个线程在执行方法里的操作,而其他的线程在外面进行等待。所以这种线程之间的通讯是安全的。

总结:wait()方法和notify()方法是属于Object的方法,因此所有的类都拥有这些方法。并且waitnotify方法必须放在同步方法或者同步块中,否则虽然能通过编译器,但运行时会抛出IllegalMonitorStateException的异常。表示该方法没有获得相应对象的锁。

3
、死锁
死锁在多线程同步中,是个非常让人头疼的问题。正是由于他的不确定性,因此变成了非常难以解决的问题。因为有可能永远也不会发生死锁,但是如果一旦发生了死锁你却根本知道下一次会在什么时候发生死锁,也就无从入手。首先我们来看看什么是死锁,当某个线程在等待另一个线程,而后者又等待别的线程,这样一直等待下去,直到这个链条上的线程又在等待第一个线程释放锁。这得到了一个线程之间互相等待的连续循环,没有哪个线程能继续,这就被称之为死锁。下面来看个死锁的例子:

public class DeadLock {

    String threadName;
    class ThreadLock extends Thread{
        DeadLock deadLock;
        ThreadLock(String name,DeadLock deadLock){
            threadName = name;
            this.deadLock = deadLock;
            start();
        }
        public void run(){
            threadIn(deadLock);
        }
    }
    synchronized void threadIn(DeadLock deadLock){
        try
        {
            Thread.sleep(100);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        deadLock.getNext();
    }
    synchronized void getNext(){
        System.out.println("
当前线程是" + Thread.currentThread().getName() + " " + threadName);
    }
    public static void main(String [] args) {
        DeadLock deadLock1 = new DeadLock();
        DeadLock deadLock2 = new DeadLock();
        DeadLock deadLock3 = new DeadLock();
        ThreadLock threadLock1 = deadLock1.new ThreadLock("
下一个线程是Thread-0",deadLock2);
        ThreadLock threadLock2 = deadLock2.new ThreadLock("
下一个线程是Thread-1",deadLock3);
        ThreadLock threadLock3 = deadLock3.new ThreadLock("
下一个线程是Thread-2",deadLock1);
    }

}

为了能让死锁更快的发生,因此在threadIn这个方法里加上的延迟时间。如果不加这个时间,也许永远也看不到死锁。当发生死锁后,在windows系统下按ctrl-c强制退出,或者ctrl-break打印出堆栈信息。在里面你就能看到发生死锁的具体信息。下面来分析一下上面的程序。
首先创建出三个线程,每个线程调用三个不同的DeadLock对象中的threadIn()方法,threadLock1线程调用deadLock1对象的threadIn()方法,并且在这个方法中又调用deadLock2 对象的getNext方法,而这个getNext方法是被threadLock2线程同步锁定的,如果此时threadLock2线程已经将deadLock2对象的同步方法锁定,那么线程threadLock1就只能等待threadLock2释放锁才能继续运行。同样threadLock2线程调用deadLock2对象的threadIn()方法后,也会调用deadLock3getNext方法,threadLock3线程调用deadLock3对象的threadIn()方法后,调用deadLock1getNext方法。这样就形成了一个循环调用的链,如果当这三个线程同时调用各自的threadIn()方法后就会产生死锁。因为他们都必须等待下一个线程释放对象的锁,这样他们才能调用getNext方法。因此我们在他们进入thread()方法后,加上休眠时间,这样就会立即发生死锁。当然实际的死锁情况远不没有这么简单。
要破坏死锁方法也很简单,只要破坏他的循环等待就行了。如果在上面的程序中,只需要破坏这个循环链就可以避免死锁,但是解决死锁问题最困难的地方是发现死锁。这就需要程序员非常谨慎的设计多线程,一定不要相信对多线程所做出的任何假设!

朗沃成都软件培训学校在线咨询         朗沃成都软件培训学校在线报名
作者: 朗沃IT教育 成都中心
原载:朗沃IT教育 成都中心 lovoinfo.com
版权所有,转载时必须以链接形式注明作者和原始出处及本声明
在线客服
在线客服系统