Java中的多线程(线程间通信)

/学习笔记/

线程间通信:

多个线程在处理同一资源,但是任务却不同。
先看一个例子,采用两个线程执行进行输入和输出任务:

      //资源
        class Resource
        {
        	String name;
        	String sex;
        }
    
    
    //输入
    class Input implements Runnable
    {
    	Resource r ;
    //	Object obj = new Object();
    	Input(Resource r)
    	{
    		this.r = r;
    	}
    	public void run()
    	{
    		int x = 0;
    		while(true)
    		{
    	
    				if(x==0)
    				{
    					r.name = "mike";
    					r.sex = "nan";
    				}
    				else
    				{
    					r.name = "丽丽";
    					r.sex = "女女女女女女";
    				}

    			x = (x+1)%2;
    
    		}
    	}
    }
    //输出
    class Output implements Runnable
    {
    
    	Resource r;

    	Output(Resource r)
    	{
    		this.r = r;
    	}
    
    	public void run()
    	{
    		while(true)
    		{
    		

    				System.out.println(r.name+"....."+r.sex);

    		}
    	}
    }
    
    
    
    class  ResourceDemo
    {
    	public static void main(String[] args) 
    	{
    		//创建资源。
    		Resource r = new Resource();
    		//创建任务。
    		Input in = new Input(r);
    		Output out = new Output(r);
    		//创建线程,执行路径。
    		Thread t1 = new Thread(in);
    		Thread t2 = new Thread(out);
    		//开启线程
    		t1.start();
    		t2.start();
    	}
    }

运行结果如下图所示,产生了多线程的安全问题,因为有多个线程在操作同一个共享数据(name,sex)。当输入方法还未执行完毕,输出方法就抢先将还未赋值正确的(name,sex)进行输出了。
在这里插入图片描述
改进办法:加入同步锁(监视器)
在这里插入图片描述
在这里插入图片描述
由于使用的都是r对象这同一个锁,可以保证输入方法执行完毕之后,另一个线程再执行输出方法。
在这里插入图片描述
上图中,输出方法拿到执行权后进行了反复输出,能否做到只输出一次,就让输入方法进行修改的效果呢?下面的机制可以解决。

等待/唤醒机制

1,wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。
如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。

2,notify():唤醒线程池中一个线程(任意)。如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。

3,notifyAll():唤醒线程池中的所有线程。如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。

这些方法都必须定义在同步中。
因为这些方法是用于操作线程状态的方法。
必须要明确到底操作的是哪个锁上的线程。

使用wait方法和使用synchornized来分配cpu时间是有本质区别的。wait会释放锁,synchornized不释放锁。

以下例子对上面的代码进行了优化,使用wait方法和notify方法,使两个线程交替进行。

    class Resource
    {
    	private String name;
    	private String sex;
    	private boolean flag = false;
    
    	public synchronized void set(String name,String sex)
    	{
    		if(flag)
    			try{this.wait();}catch(InterruptedException e){}  //同步函数的锁是this
    		this.name = name;
    		this.sex = sex;
    		flag = true;
    		this.notify();
    	}
    
    	public synchronized void out()
    	{
    		if(!flag)
    			try{this.wait();}catch(InterruptedException e){}
    		System.out.println(name+"...+...."+sex);
    		flag = false;
    		notify();
    	}
    }
    
    
    //输入
    class Input implements Runnable
    {
    	Resource r ;

    	Input(Resource r)
    	{
    		this.r = r;
    	}
    	public void run()
    	{
    		int x = 0;
    		while(true)
    		{
    			if(x==0)
    			{
    				r.set("mike","nan");
    			}
    			else
    			{
    				r.set("丽丽","女女女女女女");
    			}
    			x = (x+1)%2;
    		}
    	}
    }
    //输出
    class Output implements Runnable
    {
    
    	Resource r;

    	Output(Resource r)
    	{
    		this.r = r;
    	}
    
    	public void run()
    	{
    		while(true)
    		{
    			r.out();
    		}
    	}
    }
    
    
    
    class  ResourceDemo3
    {
    	public static void main(String[] args) 
    	{
    		//创建资源。
    		Resource r = new Resource();
    		//创建任务。
    		Input in = new Input(r);
    		Output out = new Output(r);
    		//创建线程,执行路径。
    		Thread t1 = new Thread(in);
    		Thread t2 = new Thread(out);
    		//开启线程
    		t1.start();
    		t2.start();
    	}
    }

在这里插入图片描述

多生产者+多消费者问题

这是一个经典例子,代码如下:

    class Resource
    {
    	private String name;
    	private int count = 1;
    	private boolean flag = false;
    	public synchronized void set(String name)//  t0   t1
    	{
    		while(flag)
    			try{this.wait();}catch(InterruptedException e){} //若线程在此处被唤醒,需要回头重新判断标记
    		
    		this.name = name + count;//烤鸭1  烤鸭2  烤鸭3
    		count++;//2 3 4
    		System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);//生产烤鸭1 生产烤鸭2 生产烤鸭3
    		flag = true;
    		notify();
    	}
    
    	public synchronized void out()
    	{
    		while(!flag)
    			try{this.wait();}catch(InterruptedException e){}	//t2  t3
    		System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);//消费烤鸭1
    		flag = false;
    		notify();
    	}
    }
    
    class Producer implements Runnable
    {
    	private Resource r;
    	Producer(Resource r)
    	{
    		this.r = r;
    	}
    	public void run()
    	{
    		while(true)
    		{
    			r.set("烤鸭");
    		}
    	}
    }
    
    class Consumer implements Runnable
    {
    	private Resource r;
    	Consumer(Resource r)
    	{
    		this.r = r;
    	}
    	public void run()
    	{
    		while(true)
    		{
    			r.out();
    		}
    	}
    }
    
    
    
    class  ProducerConsumerDemo
    {
    	public static void main(String[] args) 
    	{
    		Resource r = new Resource();
    		Producer pro = new Producer(r);
    		Consumer con = new Consumer(r);
    
    		Thread t0 = new Thread(pro);
    		Thread t1 = new Thread(pro);
    		Thread t2 = new Thread(con);
    		Thread t3 = new Thread(con);
    		t0.start();
    		t1.start();
    		t2.start();
    		t3.start();
    
    	}
    }

while判断标记,解决了线程获取执行权后,是否要运行的判断问题
notify:只能唤醒一个线程,如果本方唤醒了本方,没有意义。而且while判断标记+notify会导致死锁(如下图)。因为所有线程都被冻结无人唤醒。
在这里插入图片描述

notifyAll达到了这样的效果:本方线程一定会唤醒对方线程。
——小结:while+notifyAll可以解决此类问题。
那么问题来了,能否有更好的方法呢?

接口 Lock

jdk1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。
以下引用自API文档:

Lock 实现提供了比使用 synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。

锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问, ReadWriteLock 的读取锁。

synchronized方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。

虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用
“hand-over-hand” 或 “chain locking”:获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。

Lock接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。

随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:

     Lock l = ...; 
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }

Lock接口: 出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成现实锁操作。
同时更为灵活。可以一个锁上加上多组监视器。
lock():获取锁。
unlock():释放锁,通常需要定义finally代码块中。

Condition接口:出现替代了Object中的wait notify notifyAll方法。
将这些监视器方法单独进行了封装,变成Condition监视器对象。
可以任意锁进行组合。
await();
signal();
signalAll();
将其应用到上面提到的生产者消费者问题中:

    import java.util.concurrent.locks.*;

    class Resource
    {
    	private String name;
    	private int count = 1;
    	private boolean flag = false;
    
    //	创建一个锁对象。
    	Lock lock = new ReentrantLock();
    
    	//通过已有的锁获取该锁上的监视器对象。
    //	Condition con = lock.newCondition();
    
    	//通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
    	Condition producer_con = lock.newCondition();
    	Condition consumer_con = lock.newCondition();
    
    	
    	public  void set(String name)//  t0 t1
    	{
    		lock.lock();
    		try
    		{
    			while(flag)
    //			try{lock.wait();}catch(InterruptedException e){}//   对比,这是之前的写法
    			try{producer_con.await();}catch(InterruptedException e){}
    		
    			this.name = name + count;//烤鸭1  烤鸭2  烤鸭3
    			count++;//2 3 4
    			System.out.println(Thread.currentThread().getName()+"...生产者5.0..."+this.name);//生产烤鸭1 生产烤鸭2 生产烤鸭3
    			flag = true;

    			consumer_con.signal(); //唤醒对方线程
    		}
    		finally
    		{
    			lock.unlock(); //释放锁
    		}
    		
    	}
    
    	public  void out()// t2 t3
    	{
    		lock.lock();
    		try
    		{
    			while(!flag)
    //			try{this.wait();}catch(InterruptedException e){}	//t2  t3
    			try{cousumer_con.await();}catch(InterruptedException e){}	//t2  t3
    			System.out.println(Thread.currentThread().getName()+"...消费者.5.0......."+this.name);//消费烤鸭1
    			flag = false;

    			producer_con.signal();
    		}
    		finally
    		{
    			lock.unlock();
    		}
    		
    	}
    }
    
    class Producer implements Runnable
    {
    	private Resource r;
    	Producer(Resource r)
    	{
    		this.r = r;
    	}
    	public void run()
    	{
    		while(true)
    		{
    			r.set("烤鸭");
    		}
    	}
    }
    
    class Consumer implements Runnable
    {
    	private Resource r;
    	Consumer(Resource r)
    	{
    		this.r = r;
    	}
    	public void run()
    	{
    		while(true)
    		{
    			r.out();
    		}
    	}
    }
    
    
    
    class  ProducerConsumerDemo2
    {
    	public static void main(String[] args) 
    	{
    		Resource r = new Resource();
    		Producer pro = new Producer(r);
    		Consumer con = new Consumer(r);
    
    		Thread t0 = new Thread(pro);
    		Thread t1 = new Thread(pro);
    		Thread t2 = new Thread(con);
    		Thread t3 = new Thread(con);
    		t0.start();
    		t1.start();
    		t2.start();
    		t3.start();
    
    	}
    }

wait 和 sleep 的区别

1,wait可以指定时间也可以不指定。
sleep必须指定时间。

2,在同步中时,对cpu的执行权和锁的处理不同。
wait:释放执行权,释放锁。
sleep:释放执行权,不释放锁。

停止线程

停止线程:
1,stop方法。

2,run方法结束。

怎么控制线程的任务结束呢?
任务中都会有循环结构,只要控制住循环就可以结束任务。
控制循环通常就用定义标记来完成。

但是如果线程处于了冻结状态,无法读取标记。如何结束呢?
可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备cpu的执行资格。
但是强制动作会发生了InterruptedException,记得要处理。

线程类的其他方法

守护线程:setDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。
join

    public final void join()
                    throws InterruptedException

等待当前线程终止(就加入)。 临时加入一个线程运算时可以使用join方法。

多线程总结

1,进程和线程的概念。
|–进程:
|–线程:

2,jvm中的多线程体现。
|–主线程,垃圾回收线程,自定义线程。以及他们运行的代码的位置。

3,什么时候使用多线程,多线程的好处是什么?创建线程的目的?
|–当需要多部分代码同时执行的时候,可以使用。

4,创建线程的两种方式。★★★★★
|–继承Thread
|–步骤
|–实现Runnable
|–步骤
|–两种方式的区别?

5,线程的5种状态。
对于执行资格和执行权在状态中的具体特点。
|–被创建:
|–运行:
|–冻结:
|–临时阻塞:
|–消亡:

6,线程的安全问题。★★★★★
|–安全问题的原因:
|–解决的思想:
|–解决的体现:synchronized
|–同步的前提:但是加上同步还出现安全问题,就需要用前提来思考。
|–同步的两种表现方法和区别:
|–同步的好处和弊端:
|–单例的懒汉式。
|–死锁。

7,线程间的通信。等待/唤醒机制。
|–概念:多个线程,不同任务,处理同一资源。
|–等待唤醒机制。使用了锁上的 wait notify notifyAll. ★★★★★
|–生产者/消费者的问题。并多生产和多消费的问题。 while判断标记。用notifyAll唤醒对方。 ★★★★★
|–JDK1.5以后出现了更好的方案,★★★
Lock接口替代了synchronized
Condition接口替代了Object中的监视方法,并将监视器方法封装成了Condition
和以前不同的是,以前一个锁上只能有一组监视器方法。现在,一个Lock锁上可以多组监视器方法对象。
可以实现一组负责生产者,一组负责消费者。
|–wait和sleep的区别。★★★★★

8,停止线程的方式。
|–原理:
|–表现:–中断。

9,线程常见的一些方法。
|–setDaemon()
|–join();
|–优先级
|–yield();
|–在开发时,可以使用匿名内部类来完成局部的路径开辟。

猜你喜欢

转载自blog.csdn.net/z714405489/article/details/82814133