线程之间的同步互斥关系
1. 同步关系:
它是指为了完成某种任务而建立两个或多个线程,
这些线程要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。
例如卖烧鸡,生产者必须把烧鸡生产出来,然后消费者才可以去买。这就产生了次序问题。
2 互斥关系:
当一个线程进入临界区使用临界资源时,另一个线程就必须等待,
当占用临界资源的线程退出临界区后,另一个线程才允许去访问此临界资源。
例如一个车厢内的厕所,就是临界资源,当一个人正在使用时,
其他人是不能进去的,必须等待前一个人从厕所中出来,然后他才可以进去。
临界资源就是某一时刻,只允许一个线程进行访问的资源。
3 在java中有等待唤醒机制去解决这些同步互斥的问题。
在Object类中提供了对象监视器方法
wait:让线程处于冻结状态,被wait的线程会被存储到线程池中。
notify:唤醒线程池中的一个线程(任意)。
notifyAll:唤醒线程池中所有的线程。
这些方法必须定义在同步中,
因为这些方法是用于操作线程状态的方法,
4 一个生产者与一个消费者的例子
class Resource //临界缓冲区区,同一时刻内只能有一个线程访问。
{
private String commodity;//商品的名字
private int num;//商品的编号
private boolean flag = false;//代表资源是否存在,没有的话就false,有的话就true
public synchronized void setCommodity(String commodity)//加同步锁防止线程间对共享数据进行操作引发的安全问题
{
if(this.flag)//首先判断缓冲出中是否存在商品,
{
try {
this.wait();//如果有商品,生产者线程冻结
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.commodity = commodity+num;//生产者制作商品,放入缓冲区
System.out.println(Thread.currentThread().getName()+"生产者制作"+this.commodity);
num++;
this.flag = true;//修改缓冲区是否有商品的标志。
this.notify();//唤醒消费者线程,让消费者购买
}
public synchronized void out()
{
if(!this.flag)//判断缓冲区中是否有商品
{
try {
this.wait();//没有商品的话就冻结线程,等待生产者生产
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"消费者购买....."+this.commodity);
this.flag = false;//购买完商品后,修改缓冲区是否有商品的标志
this.notify();//唤醒生产者线程,让生产者生产
}
}
class product implements Runnable
{
private Resource r;//接收缓冲区资源对象,因为缓冲区内提供了生产和销售的方法只需要拿来调用即可
product(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.setCommodity("烧鸡");
}
}
}
class consumer implements Runnable
{
private Resource r;//接收缓冲区资源对象,用来访问缓冲区的一些资源。
consumer(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
public class ProducterConsumer {
public static void main(String[] args)
{
//创建资源对象
Resource r = new Resource();
//创建任务对象
//生产者和消费者对同一个临界资源进行访问
product p = new product(r);
consumer c = new consumer(r);
//创建线程任务
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
运行结果
4.2如果两个消费者和两个生产者对一个临界资源区进行访问的话,就会出现以下错误
public class ProducterConsumer {
public static void main(String[] args)
{
//创建资源对象
Resource r = new Resource();
//创建任务对象
product p = new product(r);
product p2 = new product(r);
consumer c = new consumer(r);
consumer c2 = new consumer(r);
//创建线程任务
Thread t1 = new Thread(p);
Thread t2 = new Thread(p2);
Thread t3 = new Thread(c);
Thread t4 = new Thread(c2);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
出现这个问题的原因是,消费者线程2购买完以后,把缓冲区标志修改,
然后唤醒线程池中的其他线程(唤醒后,如果消费者线程2时间片没有结束,唤醒的线程在就绪队列,等待消费者线程2释放CPU),
如果此时唤醒的是消费者线程3,因为它之前被wait,被wait之前它已经判断过了标志,
因为用的是if判断所以就直接往下执行,就会出现上图的错误(生产一个东西,消费多次)。
解决这个问题只需把if判断换成while判断,让唤醒后的线程,重新判断标志。
//生产者的
while(this.flag)//首先判断缓冲出中是否存在商品,
{
try {
this.wait();//如果有商品,生产者线程冻结
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费者的
while(!this.flag)//判断缓冲区中是否有商品
{
try {
this.wait();//没有商品的话就冻结线程,等待生产者生产
} catch (InterruptedException e) {
e.printStackTrace();
}
}
4.3但是这样做又会出现新的问题,如下图
出现了死锁,这种情况的出现是因为,唤醒是随机的,可以唤醒的是本方消费者的线程,又可以唤醒对方的线程,
如果唤醒的是对方的线程,就会和谐的运行下去,但是唤醒的是本方的线程就会出现锁,(假设此时只有一个生产者线程0在运行状态,其他线程都处于wait冻结状态)
生产者线程0走完自己的代码后,如果唤醒了生产者线程1,因为此时标志是true所以生产者线程1就会再次被wait冻结,此时4个线程都是冻结状态,程序就无法进行了,进入了死锁
解决方案:线程做唤醒动作时要唤醒对方线程。可以用notifyAll
this.notifyAll();虽然说这样可以解决死锁的问题但是造成了效率的降低,因为每次都要唤醒线程池全部的线程。
4.4 jdk1.5以后将同步和锁封装成了对象。并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
(1)Lock接口:
替代了同步代码块和同步函数,将同步的隐式锁操作变成了显示锁操作。
同时更为灵活,可以一个锁上绑定多个监视器。
lock():获取锁。
unlock():释放锁,通常需要定义在finally代码块中。
例子:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
(2)Condition接口:
替代了Object中的wait,notify,notifyAll方法。
将这些监视器方法单独进行了封装,变成Condition监视器对象。
提供了以下方法
await();
signal();
signalAll();
例子 Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
用这些方法可以解决效率的问题。
修改后的代码如下
class Resource //临界缓冲区区,同一时刻内只能有一个线程访问。
{
private String commodity;//商品的名字
private int num;//商品的编号
private boolean flag = false;//代表资源是否存在,没有的话就false,有的话就true
Lock lock = new ReentrantLock();//定义一个锁对象。
Condition product_con = lock.newCondition();//在锁上绑定生产者监视器
Condition consumer_con = lock.newCondition();//在锁上绑定消费者监视器
public void setCommodity(String commodity)//加同步锁防止线程间对共享数据进行操作引发的安全问题
{
lock.lock();
try {
while (this.flag)//首先判断缓冲出中是否存在商品,
{
try {
product_con.await();//如果有商品,生产者线程冻结
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.commodity = commodity + num;//生产者制作商品,放入缓冲区
System.out.println(Thread.currentThread().getName() + "生产者制作" + this.commodity);
num++;
this.flag = true;//修改缓冲区是否有商品的标志。
consumer_con.signal();//唤醒消费者线程,让消费者购买
}finally {
lock.unlock();
}
}
public void out()
{
lock.lock();
try {
while(!this.flag)//判断缓冲区中是否有商品
{
try {
consumer_con.await();//没有商品的话就冻结线程,等待生产者生产
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"消费者购买....."+this.commodity);
this.flag = false;//购买完商品后,修改缓冲区是否有商品的标志
}
finally {
product_con.signal();//唤醒生产者线程,让生产者生产
}
}
}
5.wait和sleep的区别?
1.wait可以指定时间也可以不指定时间。
sleep必须指定时间。
2.在同步中时对于cpu的执行权和锁的处理不同。
wait:释放执行权,释放锁。
sleep:释放执行权,不释放锁。
6.停止线程
1.stop方法。
2.run方法结束。
怎么控制线程的任务结束呢?
任务中都会有循环结构,只要控制住循环就可以结束任务。
控制循环通常就用定义标记来完成。
但是如果线程处于了冻结状态,无法读取标记。如何结束呢?
可以使用interrupt方法将线程从冻结状态强制恢复到运行状态中来,让线程具备CPU的执行资格。
但是强制动作会发生InterruptException,记得要处理。
设置守护线程(后台线程)setDaemon(true);虚拟机中的前台线程全部结束时,退出虚拟机。