从生产者消费者开始讲线程
先说多线程的好处:
- 多线程技术使程序的响应速度更快,用户界面可以在进行其它工作的同时一直处于活动状态;
- 当前没有进行处理的任务时可以将处理器时间让给其它任务;
- 占用大量处理时间的任务可以定期将处理器时间让给其它任务;
- 可以随时停止任务;
- 可以分别设置各个任务的优先级以优化性能。
创建线程主要的方法有两个:
1、继承Thread
1)定义一个类A 继承于Java.lang.Thread类
2)在A类中覆盖Thread类中的run方法。在run方法中我们编写需要执行的操作,run方法里的是线程执行体。
3)在main方法中创建线程对象,并启动线程的
A a = new A(); a.start();
可以看出,这个方法中,线程和线程所要执行的任务是绑定在一起的,无法共享资源。
2、实现Runable接口
1)定义一个类A实现java.lang.Runable接口。类A只是一个普通的类。
2)在A类中覆盖Runable接口中的run方法,编写需要执行的操作。
3)在main方法中,创建线程对象,并启动线程。
Thread t = new Thread(new A()); t.start()
在这个方法中,一个任务可以开辟多个线程(即一个类被传进多个线程实例),这些线程执行的是同一个任务,即资源共享。
生产者消费者进程
很明显,生产者和消费者是两个不同的进程,但是它们共享一个资源,所以要用第二种方法来实现。
作为被共享的资源,资源类要先被创建。
///资源类
public class ShareResource {
String name;
int kg;
//生产者要用到的push方法
public void push(String name, int kg) {
this.name = name;
Thread.sleep(10);
this.kg = kg;
}
public void pop() {
System.out.println("食物名称:" + name + "重量" + kg);
}
}
```
接着Resource的实例作为参数传递进Producer类和Consumer类。
//消费者进程
public class Consumer implements Runnable {
private ShareResource resource = null;
Consumer(ShareResource resource) {
this.resource = resource;
} //重载一个带参数的构造器,将共享的资源传进来
public void run() {
for (int i = 0; i < 50; i++) {
resource.pop();
}
}
}
//生产者进程
public class Producer implements Runnable {
private ShareResource resource = null;
Producer(ShareResource resource) {
this.resource = resource;
} //重载一个带参数的构造器,将共享的资源传进来
public void run() {
for (int i = 0; i < 50; i++) {
if (i % 2 == 1) {
resource.push("prok", 5);
} else {
resource.push("rice", 3);
}
}
}
}
最后是一个测试方法
public class Producer_Consumer {
public static void main(String[] args) {
ShareResource resource = new ShareResource();
Thread producer = new Thread(new Producer(resource));
Thread consumer = new Thread(new Consumer(resource));
producer.start();
consumer.start();
}
}
这样一个基本的生产者消费者框架就出来了,但是即使运行了,也存在线程安全问题,下面要讲线程的同步。
#####线程的同步
线程的同步就是,虽然不能控制线程的执行顺序,但是我们可以控制哪些代码必须在一起执行,比如生产者进程要用的push()方法中,物品的名称和重量的赋值行为必须绑定在一起,如果不绑定在一起,中间断开了会出错。比如:原resource的值为猪肉 10kg,现在有一个新的值 羊肉 5kg 要被传入,当羊肉赋值给name之后,5kg还没来得及赋值,消费者线程已经开始执行pop操作,这时消费者取走的就时羊肉 10kg。 这就产生了错误。所以要引入synchronized 修饰词。
**三种机制的模式都为 “加锁--修改公共变量--释放锁”**
1.同步代码块 当要执行的代码被synchronized{}包含在内之后,里面的代码就称为一次原子操作。 代码会一次执行完name 和kg的赋值后自动释放锁。
public void push(String name, int kg) {
synchronized(this){
this.name = name;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.kg = kg;
}
}
2.同步方法 顾名思义,在方法前面加上synchronized修饰符。也是执行完毕之后自动释放锁。但是,千万不能用synchronized来修饰run方法,如下面是消费者进程的run方法,这样,一旦消费者线程开始执行,就会一次性取出50次,失去了线程存在的意义。
public synchronized void run() {
for (int i = 0; i < 50; i++) {
resource.pop();
}
}
3.lock机制
其中最常用的是RetrantLock可重入锁. RetrantLock是Lock接口的一个实现类。可重入锁由最近成功获得锁,并且还没有释放该锁的线程所拥有。
官方推荐我们的最佳用法如下,因为Lock机制不会自动解锁,所以必须在用完之后手动解锁,我们将要运行的代码写进try{ }中。
class X {
private final ReentrantLock lock = new ReentrantLock();//此处为创建一个锁对象
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
但是加入锁机制也不能使得程序按照我们的想法运行起来。因为生产者消费者还需要进行通信,生产者生产出一个产品之后通知消费者来取,生产一个消费一个的顺序。
**线程的通信**
线程的通信有两种方式,一种方式基于synchronized同步方法,另一种基于锁机制。
1. 基于synchronized同步方法
这里主要用的是wait() 和notify()方法。这两个方法都只能被同步锁对象来调用,所以必须使用synchronized来修饰,并定义了一个boolean的isEmpty变量,来判断是否要进入该方法。
public class Resource {
String name;
int kg;
private final ReentrantLock lock = new ReentrantLock();
boolean isEmpty = true;
public void push(String name, int kg) {
synchronized (this) {
try {
while (!isEmpty) {
this.wait();
}
this.name = name;
Thread.sleep(10);
this.kg = kg;
isEmpty = false;
this.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized public void pop() {
try {
while (isEmpty) {
this.wait();
}
System.out.println("食物名称:" + name + "重量" + kg);
isEmpty=true;
this.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
第二种方法用到Lock机制。Lock能完成Synchronized所实现的所有功能。但是Lock机制要与Condition绑定在一起使用。此时用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。
public class Resource {
String name;
int kg;
private final ReentrantLock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
boolean isEmpty = true;
public void push(String name, int kg) {
try {
while (!isEmpty) {
condition.await();
}
lock.lock();
this.name = name;
Thread.sleep(10);
this.kg = kg;
isEmpty = false;
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
public void pop() {
try {
while (isEmpty) {
condition.await();
}
lock.lock();
System.out.println("食物名称:" + name + "重量" + kg);
isEmpty = true;
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
运行结果如下:
食物名称:rice重量3
食物名称:prok重量5
食物名称:rice重量3
食物名称:prok重量5
食物名称:rice重量3
食物名称:prok重量5
食物名称:rice重量3
食物名称:prok重量5
食物名称:rice重量3
食物名称:prok重量5
食物名称:rice重量3
在这个生产者消费者的例子中,Lock机制相比于Synchronized的优势不明显,因为这里只有两个线程,一个阻塞之后唤醒的必然是另外一个。所以只需要new出一个condition对象。但是线程中,不管是signal()还是nofify()方法都是从线程池中随机挑选出一个线程来唤醒,如果存在多个生产者和消费者,那么生产者唤醒的就未必是消费者了,也有可能是另一个生产者。这个时候只需要new两个condition对象。
如下 muxProducer 和muxConsumer,这样当生产者阻塞了唤醒的一定是消费者,消费者阻塞了唤醒的一定是生产者。
public class Resource {
String name;
int kg;
private final ReentrantLock lock = new ReentrantLock();
public Condition muxProducer = lock.newCondition();
public Condition muxConsumer = lock.newCondition();
boolean isEmpty = true;
public void push(String name, int kg) {
lock.lock();
try {
while (!isEmpty) {
muxProducer.await();
}
this.name = name;
Thread.sleep(10);
this.kg = kg;
isEmpty = false;
muxConsumer.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
public void pop() {
lock.lock();
try {
while (isEmpty) {
muxConsumer.await();
}
System.out.println(Thread.currentThread().getName()+"取得食物:" + name + "重量" + kg);
isEmpty = true;
muxProducer.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
**这里一定要注意, lock.lock();一定要写在muxProducer.await();
之前。不然就要报错。因为任务如果要执行等待或者进行唤醒,前提是必须拥有锁。**
运行结果为:
Thread-0生产出米饭3kg
Thread-2取得食物:rice重量3
Thread-1生产出米饭3kg
Thread-2取得食物:rice重量3
Thread-1生产出猪肉5kg
Thread-3取得食物:prok重量5
Thread-1生产出米饭3kg
Thread-2取得食物:rice重量3
Thread-1生产出猪肉5kg
Thread-3取得食物:prok重量5