Java 多线程详解(三)

一、线程的同步与死锁

1、线程同步问题的引出

所谓的同步问题指的是多个线程操作同一资源时所带来的安全性问题。例如,下面模拟一个简单的卖票程序,要求有5个线程,卖6张票。

package com.wz.threaddemo;

class MyThread implements Runnable {
    private int ticket = 6;

    @Override
    public void run() {
        for (int x = 0; x < 10; x++) {
            if (this.ticket > 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket--);
            }
        }
    }
}

public class TestDemo {
    public static void main(String[] args) throws Exception {
        MyThread mt = new MyThread();
        new Thread(mt, "卖票线程A").start();
        new Thread(mt, "卖票线程B").start();
        new Thread(mt, "卖票线程C").start();
        new Thread(mt, "卖票线程D").start();
        new Thread(mt, "卖票线程E").start();

    }
}

运行结果:

卖票线程B卖票,ticket = 6
卖票线程E卖票,ticket = 3
卖票线程A卖票,ticket = 6
卖票线程D卖票,ticket = 4
卖票线程C卖票,ticket = 5
卖票线程B卖票,ticket = 2
卖票线程E卖票,ticket = 1
卖票线程D卖票,ticket = 0
卖票线程A卖票,ticket = 1
卖票线程C卖票,ticket = -1
卖票线程B卖票,ticket = -2

这时我们发现,操作的结果出现了负数,这个就可以理解为不同步问题。那么,到底是如何造成这种不同步的呢?

整个卖票的过程分为两个步骤:
第一步,判断是否还有剩余的票数;
第二步,票数减一。
但是在上面的操作代码中,两个步骤之间加了延迟操作,那么一个线程可能在还没有票数减一之前,其他线程就已经把票数减一了,于是,这就产生了负数。

2、线程同步问题的解决

如果想要解决这样的问题。就必须使用同步。所谓同步,就是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等这个线程执行完成才可以继续执行。

想解决资源共享的同步操作问题,可以使用同步代码块和同步方法两种方式完成。

方式一:同步代码块,使用synchronized关键字定义的代码块就称为同步代码块。同步代码块格式:

synchronized(同步对象){
     需要同步的代码
}

在进行同步的操作时必须设置一个要同步的对象,而这个对象应该理解为当前对象:this。

package com.wz.threaddemo;

class MyThread implements Runnable {
    private int ticket = 6;

    @Override
    public void run() { 
        for (int x = 0; x < 10; x++) {
            synchronized (this) {//同步代码块,当前操作每次只允许一个对象进入
                if (this.ticket > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket--);
                }
            }

        }
    }
}

public class TestDemo {
    public static void main(String[] args) throws Exception {
        MyThread mt = new MyThread();
        new Thread(mt, "卖票线程A").start();
        new Thread(mt, "卖票线程B").start();
        new Thread(mt, "卖票线程C").start();
        new Thread(mt, "卖票线程D").start();
        new Thread(mt, "卖票线程E").start();

    }
}

运行结果:

卖票线程A卖票,ticket = 6
卖票线程C卖票,ticket = 5
卖票线程E卖票,ticket = 4
卖票线程D卖票,ticket = 3
卖票线程B卖票,ticket = 2
卖票线程D卖票,ticket = 1

方式二:同步方法。除了可以将需要的代码设置成同步代码块之外,也可以使用synchronized关键字将一个方法声明成同步方法。定义格式:

synchronized 方法返回值 方法名称(参数列表){   }
package com.wz.threaddemo;

class MyThread implements Runnable { 
    private int ticket = 6;

    @Override
    public void run() { 
        for (int x = 0; x < 10; x++) {
            this.sale();
        }
    }

    public synchronized void sale() {//同步方法
        if (this.ticket > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖票,ticket = " + this.ticket--);
        }
    }
}

public class TestDemo {
    public static void main(String[] args) throws Exception {
        MyThread mt = new MyThread();
        new Thread(mt, "卖票线程A").start();
        new Thread(mt, "卖票线程B").start();
        new Thread(mt, "卖票线程C").start();
        new Thread(mt, "卖票线程D").start();
        new Thread(mt, "卖票线程E").start();

    }
}

运行结果:

卖票线程A卖票,ticket = 6
卖票线程A卖票,ticket = 5
卖票线程E卖票,ticket = 4
卖票线程D卖票,ticket = 3
卖票线程C卖票,ticket = 2
卖票线程B卖票,ticket = 1

同步操作与异步操作相比,异步操作的速度要明显高于同步操作,但同步操作是数据的安全性较高,属于安全的线程操作。

3、死锁

通过分析可以发现,所谓的同步就是指一个线程对象等待另外一个线程对象执行完成后再执行。但是过多的同步会导致线程死锁

所谓死锁, 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

那么为什么会产生死锁呢?
(1)因为系统资源不足。
(2)进程运行推进的顺序不合适。
(3)资源分配不当。

学过操作系统的都知道,产生死锁的条件有四个:
(1)互斥条件:所谓互斥就是进程在某一时间内独占资源。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何解决死锁?
可以从死锁的四个条件出发去解决,只要破坏一个必要条件,那么我们的死锁就解决了。
在Java中使用多线程的时候一定要考虑是否有死锁的问题。

二、线程间通信—生产者/消费者模型

对于多线程程序来说,不管任何编程语言,生产者和消费者模型都是最经典的。就像学习每一门编程语言一样,Hello World!都是最经典的例子。

1、生产者/消费者问题的引出

生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将消息放入缓冲区;另外一个是消费者,用于从缓冲区中取出消息。问题出现在当缓冲区已经满了,而此时生产者还想向其中放入一个新的数据项的情形,其解决方法是让生产者此时进行休眠,等待消费者从缓冲区中取走了一个或者多个数据后再去唤醒它。同样地,当缓冲区已经空了,而消费者还想去取消息,此时也可以让消费者进行休眠,等待生产者放入一个或者多个数据时再唤醒它。

2、生产者/消费者模型的实现

(1)首先定义公共资源类,其中的变量number是保存的公共数据。并且定义两个方法,增加number的值和减少number的值(由于多线程的原因,必须加上synchronized关键字,注意while判断的条件)。

package com.wz.threaddemo;
/**
 * 公共资源类
 */
public class PublicResource {
    private int number = 0;
    private int maxSize = 10;

    /**
     * 增加公共资源
     */
    public synchronized void increace() {
        while (number == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number++;
        System.out.println("增加一个资源,现在资源数为:"+number);
        notify();
    }

    /**
     * 减少公共资源
     */
    public synchronized void decreace() {
        while (number == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        number--;
        System.out.println("减少一个资源,现在资源数为:"+number);
        notify();
    }
}

(2)分别定义生产者线程和消费者线程,并模拟多次生产和消费,即增加和减少公共资源的number值。

package com.wz.threaddemo;

/**
 * 生产者线程,负责生产公共资源
 */
public class ProducerThread implements Runnable {
    private PublicResource resource;

    public ProducerThread(PublicResource resource) {
        this.resource = resource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep((long) (Math.random() * 100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resource.increace();
        }
    }
}
package com.wz.threaddemo;

/** 
 * 消费者线程,负责消费公共资源 
 */  
public class ConsumerThread implements Runnable {  
    private PublicResource resource;  

    public ConsumerThread(PublicResource resource) {  
        this.resource = resource;  
    }  

    @Override  
    public void run() {  
        for (int i = 0; i < 10; i++) {  
            try {  
                Thread.sleep((long) (Math.random() * 100));  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            resource.decreace();  
        }  
    }  
}  

(3)模拟多个生产者和消费者操作公共资源的情形,结果须保证是在允许的范围内。

package com.wz.threaddemo;

public class TestDemo {
    public static void main(String[] args) throws Exception {
        PublicResource resource = new PublicResource();
        new Thread(new ProducerThread(resource)).start();
        new Thread(new ConsumerThread(resource)).start();
        new Thread(new ProducerThread(resource)).start();
        new Thread(new ConsumerThread(resource)).start();
        new Thread(new ProducerThread(resource)).start();
        new Thread(new ConsumerThread(resource)).start();
    }

}

运行结果:

增加一个资源,现在资源数为:1
减少一个资源,现在资源数为:0
增加一个资源,现在资源数为:1
减少一个资源,现在资源数为:0
增加一个资源,现在资源数为:1
增加一个资源,现在资源数为:2
增加一个资源,现在资源数为:3
减少一个资源,现在资源数为:2
减少一个资源,现在资源数为:1
增加一个资源,现在资源数为:2
减少一个资源,现在资源数为:1
增加一个资源,现在资源数为:2
... ...//后面还有

下面是生产者/消费者模型的一些优点:
(1)它简化了开发,你可以独立地或并发的编写消费者和生产者,它仅仅只需知道共享对象是谁;
(2)生产者不需要知道谁是消费者或者有多少消费者,对消费者来说也是一样;
(3)生产者和消费者可以以不同的速度执行;
(4)分离的消费者和生产者在功能上能写出更简洁、可读、易维护的代码。

一个小问题:解释sleep()和wait()的区别?
(1)sleep()是Thread类定义的方法,而wait()方法时Object定义的方法;
(2)sleep()可以设置休眠时间,时间一到自动唤醒,而wait()方法需要使用notify()方法进行唤醒。

3、使用阻塞队列实现生产者/消费者模式

在上面的实现中,使用变量number来保存公共数据,并且使用wait()和notify()方法在生产者和消费者线程中调配资源,这样,是不是略显复杂了呢?有没有更简单的方式来实现呢?

有,使用阻塞队列实现生产者/消费者模式,它提供开箱即用支持阻塞的方法put()和take(),开发者不需要写困惑的wait-nofity代码去实现通信了。实现代码如下:

生产者线程:

package com.wz.threaddemo;

import java.util.concurrent.BlockingQueue;

/**
 * 生产者线程,负责生产资源
 */
public class ProducerThread implements Runnable {
    private BlockingQueue<Integer> sharedQueue;

    public ProducerThread(BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        for(int i=0; i<10; i++){
            try {

                System.out.println(Thread.currentThread().getName()+" Produced:" + i);
                sharedQueue.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

消费者线程:

package com.wz.threaddemo;

import java.util.concurrent.BlockingQueue;

/**
 * 消费者线程,负责消费公共资源
 */
public class ConsumerThread implements Runnable {
    private BlockingQueue sharedQueue;

    public ConsumerThread(BlockingQueue sharedQueue) {
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                System.out.println(Thread.currentThread().getName()+" Consumed:" + sharedQueue.take());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

模拟生产者和消费者操作公共资源:

package com.wz.threaddemo;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class TestDemo {
    public static void main(String[] args) throws Exception {

        BlockingQueue sharedQueue = new LinkedBlockingQueue();

        Thread prodThread = new Thread(new ProducerThread(sharedQueue), "生产线程");
        Thread consThread = new Thread(new ConsumerThread(sharedQueue), "消费线程");

        prodThread.start();
        consThread.start();

    }

}

运行结果:

生产线程 Produced:0
生产线程 Produced:1
生产线程 Produced:2
消费线程 Consumed:0
生产线程 Produced:3
消费线程 Consumed:1
生产线程 Produced:4
消费线程 Consumed:2
生产线程 Produced:5
消费线程 Consumed:3
生产线程 Produced:6
消费线程 Consumed:4
生产线程 Produced:7
消费线程 Consumed:5
生产线程 Produced:8
消费线程 Consumed:6
生产线程 Produced:9
消费线程 Consumed:7
消费线程 Consumed:8
消费线程 Consumed:9

可见,阻塞队列实现生产者消费者模式,对比传统的wait、nofity代码,它更易于理解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章