天天看点

黑马程序员--java基础复习之多线程及线程间通信

------ Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

进程: 进程:是一个正在执行的程序 每个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元

线程:

线程就是进程中的一个独立的控制单元,线程在控制着进程的执行。一个进程中至少有一个线程。

多线程:  在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。

 该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。

 JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。 像种在一个进程中有多个线程执行的方式,就叫做多线程。

多线程的好处:  多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。

就我们常见的,在电脑上,即能在记事本上打字,同时也能听音乐。这个就是多线程的应用。又比如360安全卫士,当我们在清理垃圾的时候,同时也能扫描系统漏洞。这同事是多线程的应用有。

1、如何在自定义的代码中,自定义一个线程呢?

通过对API的查找,java已经提供了对线程这类事物的描述。就是Thread类。

创建线程的第一种方式:继承Thread类

步骤:

1、定义类继承Thread。

2、复写Thread类中的run 方法

目的:将自定义代码存储在run方法中,让线程运行。

3、调用线程的start方法。该方法有两个作用:启动线程,调用run方法

如下:

class Test extends Thread
{		
	//复写父类中的run方法
	public  void run()
	{
		for(int i=0;i<30;i++)
		{			
			System.out.println(Thread.currentThread().getName()+" run .."+i);
		}
	}
}
class ThreadDemo
{
	public static void  main(String[] agrs)
	{
		Test t1=new Test();
		t1.start();//开启线程,并调用run方法
		Test t2=new Test();
		t2.start();
		for(int i=0;i<60;i++)
		{
			System.out.println("main run::"+i);
		}
		
		
	}
}
           

运行多次后,发现运行结果每一次都不同。因为多个线程都在获取CPU的执行权。CPU执行到谁,谁就运行。

明确一点,在某一个时刻,只能有一个程序在运行。(多核除外) cpu在做着快速的切换,以达到看上去是同时运行的效果。 我们可以形象地把多线程的运行行为看作在互相抢夺CPU的执行权。

这就是多线程的一个特性:随机性。轮到谁,谁执行。

为什么要覆盖run方法呢?

Thread 类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。

也就是说Thread类中的run 方法,用于存储线程要运行的代码。 我们要利用线程运行我们自己的代码,所以我们也要将代码存放在我们自己的run方法中,所以可以直接调用父类中的run方法,并将其复写。

线程都有自己默认的名称。

Thread-编号  该编号从0开始

static Thread currentThread():获取当前线程对象

getName():获取线程名称

设置线程名称:setName或者构造函数

创建线程的第二种方式:实现 Runnable接口

步骤:

1、定义类实现Runnable接口 2、覆盖Runnable接口中的run方法。 将线程要运行的代码存放在run方法中 3、通过Thread类建立线程对象。 4、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。 为什么要将Runnable接口的子类对象传递给Thread的构造函数。 因为,自定义的run方法所属的对象是Runnable接口 的子类对象。 所以要让线程去执行指定对象的run方法。 就必须明确该run方法所属对象。 5、调用Thread类的start方法开户线程并调用Runnable接口子类的Run方法

下面用一个卖票的小程序来说明:

//卖票线程
class Ticket implements Runnable
{
    int  num=100;
	Object obj=new Object();
	public void run()
	{
		while(true)
		{			
			if(num>0)
			{				
				System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
			}
			
		}
	}
	
	
}

class ThreadTest
{
	public static void main(String[] args)
	{
		Ticket t=new Ticket();//创建一个对象,然后再开始四个线程,模拟四个窗口卖票
		Thread th1=new Thread(t);//这里是用到了多态 ,传递的是Runnable类型或其子类的对象
		Thread th2=new Thread(t);
		Thread th3=new Thread(t);
		Thread th4=new Thread(t);
		th1.start();
		th2.start();
		th3.start();
		th4.start();
	}
}
           

通过实现Runnable接口方式创建线程的好处: 避免了单继承的局限性。(当一个类本身就有一个父类,则不能再继承Thread类)

在定义线程时,建议使用实现方式。

区别:

继承Thread:线程代码存放在Thread子类的run方法中。

实现Runnable,线程代码存放在接口的子类的run方法中。

多线程中的安全问题

还是那个卖票程序,再看代码:

<span style="font-size:14px;">//卖票线程
class Ticket implements Runnable
{
    int  num=100;
	Object obj=new Object();
	public void run()
	{
		while(true)
		{
				if(num>0)
				{
					//在此处sleep() 10毫秒,模拟CPU运行到此处的时候,将执行权交给其他进程
					try{Thread.sleep(10);} catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
				}
		}
	}	
}

class ThreadTest
{
	public static void main(String[] args)
	{
		Ticket t=new Ticket();
		Thread th1=new Thread(t);
		Thread th2=new Thread(t);
		Thread th3=new Thread(t);
		Thread th4=new Thread(t);
		th1.start();
		th2.start();
		th3.start();
		th4.start();
	}
}</span>
           

结果:

黑马程序员--java基础复习之多线程及线程间通信

    从结果中可以看到,票号出现了负数和0,这明显是不符合常理的。 造成这情况,是因为多线程的运行出现了安全问题

问题原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执 行,导致共享数据的错误。

解决办法:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。

1、java 对于多线程的安全问题提供了专业的解决方式,就是同步代码块

synchronized(对象)

{

需要被同步的代码

}

对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取了CPU的执行权,也进不去,因为没有锁。

同步的前提:

1、必须要有两个或两个以上的线程

2、必须是多个线程使用同一个锁

必须保证同步中至少有一个线程在运行。

好处:解决了多线程的安全问题

弊端:多个线程都需要判断锁,较为消耗资源。

同步代码块的使用,如下:

class Ticket implements Runnable
{
    int  num=100;
	Object obj=new Object();
	public void run()
	{
		while(true)
		{
			
			//同步
			synchronized(obj)
			{
				if(num>0)
				{
					try{Thread.sleep(10);} catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
				}
			}
			
		}
	}
		
}
           

如上,同步代码块,关键字synchronized,这里的锁为任一对象。

2、解决线程安全问题的第二种方式。同步函数

格式:在函数上加上synchronized修饰符

那么,同步函数用的是哪一个锁呢? 函数需要被对象调用。那么函数都有一个所属对象引用。就是this。 所以同步函数使用的锁是this

如上面的代码片段可以直接用同步代码块的方式完成,如下:

//卖票线程
class Ticket implements Runnable
{
    int  num=100;
	Object obj=new Object();
	public void run()
	{
		while(true)
		{
			this.Sale();
			/*
			//同步代码块
			synchronized(obj)
			{
				if(num>0)
				{
					try{Thread.sleep(10);} catch(Exception e){}
					System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
				}
			}
			*/
		}
	}
	
	public synchronized void Sale()
	{
		if(num>0)
		{
			try{Thread.sleep(10);} catch(Exception e){}
			System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
		}
	}
}
           

那么问题来了,如何确认哪些代码需要被同步呢?如何寻找多线程中的安全问题呢?         a,明确哪些代码是多线程运行代码。         b,明确共享数据。

        c,明确多线程运行代码中哪些语句是操作共享数据的。

静态同步函数

如果同步函数被静态修饰后,使用的锁是什么呢?

通过验证,发现不再是this。因为静态方法中也不可以定义this。

静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。

类名.class   该对象的类型是 Class

验证静态同步函数的锁为 类名.class

静态的同步函数,使用的锁是该方法所在类的字节码文件对象。 类名.class

说到多线程的安全问题,想到之前的单例设计模式。 单例设计模式的特点就是对象的唯一性。当多线程访问单例设计模式中的懒汉式的时候。因为懒汉式中有一个判断:if(s==null) s=new Single(); 这里是两行代码,当多线程运行到第一句发生CPU转移执行权的时候,会发生安全问题,这样,对象就可能不是一个,而是多个了,所以根据多线程的安全问题,我们可以对懒汉式进行同步操作,以解决懒汉式中的安全问题。如下:

class Single
{
	// 构造函数私有化,就不能创建类的对象
	private Single(){}
	private static Single s=null;
	//每次都要判断锁,比较低效
	public static synchronized Single getInstance()
	{
		if(s==null)		
			s=new Single();		
		return s;
	}
}
           

但是,每次线程访问getInstance()方法时,都有一个判断锁的动作,这样做效率比较低。所以又用同步代码块对懒汉式进行了优化:

class Single
{
	// 构造函数私有化,就不能创建类的对象
	private Single(){}
	private static Single s=null;
	public static Single getInstance()
	{
		//使用双重判断
		if(s==null)
		{
			//锁为该类所属字节码文件对象
			synchronized(Single.class)
			{
				if(s==null)		
					s=new Single();		
				return s;
			}
		}
	}
}
           

这里使用了双重判断,以免每次访问该函数时,在函数中要进行判断锁的操作,影响效率,使后面的访问只需要判断s是否为空即可。

死锁 死锁是什么?死锁即是同步中嵌套同步。如下例:

class Test implements Runnable
{
	boolean flag;
	Test(boolean flag)
	{
		this.flag=flag;
	}
	public void run()
	{
        if(flag)
		{
			synchronized(MyLock.obja)
			{
				System.out.println(" if obja");
				synchronized(MyLock.objb)
				{
					System.out.println(" if objb");
				}
			}
		}
		else
		{
			synchronized(MyLock.objb)
			{
				System.out.println(" else objb");
				synchronized(MyLock.obja)
				{
					System.out.println(" else obja");
				}
			}
		}	
	}
}

class MyLock
{
	static Object obja=new Object();
	static Object objb=new Object();
}

class DeadLockTest
{
	public static void main(String[] args)
	{
		Test te1=new Test(true);
		Test te2=new Test(false);
		
		Thread t1=new Thread(te1);
		Thread t2=new Thread(te2);
		t1.start();
		t2.start();
	}
}
           

结果:

黑马程序员--java基础复习之多线程及线程间通信

线程间通信

线程间通信,即是多个线程操作同一资源,但操作的内容不同。 举例,A和B去银行办个人业务,其中A是存钱,B是取钱。操作的都是银行的金库。

如下例,两个线程操作一个共同资源,一个向资源内赋值,一个从资源中取值。希望两个线程交互,存一个,取一个

<span style="font-size:14px;">//共同资源
class Res
{   
	boolean flag=false;
	String name;
	String sex;
}

//输入类,实现Runnable接口
class Input implements Runnable
{
	Res r;
	Input(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		int x=0;
		while(true)
		{
			synchronized(r)
			{
				if(r.flag==false)
				{
					if(x==0)
					{
						r.name="zhangsa";
						r.sex="man";
					}
					else
					{
						r.name="李四";
						r.sex="女";
					}
					r.flag=true;
				}
				
				
			}
			x=(x+1)%2;
			
		}
	}
}

//输出类
class Output implements Runnable
{
	Res r;
	Output(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		while(true)
		{
			synchronized(r)
			{
				if(r.flag)
				{
					System.out.println(r.name+"::::"+r.sex);
					r.flag=false;
				}
				
			}
			
		}
	}
}

class InputOutputDemo
{
	public static void main(String[] args)
	{
		//创建一个资源对象
		Res r=new Res();
		
		Input in=new Input(r);
		Output out=new Output(r);
		
		Thread t1=new Thread(in);
		Thread t2=new Thread(out);
		
		t1.start();
		t2.start();
		
	}
}</span>
           

结果:

黑马程序员--java基础复习之多线程及线程间通信

上述代码使用线程等待唤醒机制来优化:

//资源
class Res
{   
	boolean flag=false; //标识,用于判断当前线程是应做存取操作还是等待
	private String name;
	private String sex;
	
	//存入操作
	public synchronized void set(String name,String sex)
	{
		if(flag) //若已存入资源,则线程等待
			try{this.wait();}catch(Exception e){}  //线程等待。
		this.name=name;
		this.sex=sex;
		this.flag=true;
		//唤醒线程池中的其他等待线程
		this.notify();
	}
	//取出操作
	public synchronized void out()
	{		
		if(!flag)
			try{this.wait();} catch(Exception e){}		
		System.out.println(name+":::"+sex);
		this.flag=false;
		this.notify();
	}
}

//输入类,实现Runnable接口
class Input implements Runnable
{
	Res r;
	Input(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		int x=0;
		while(true)
		{
			
			if(x==0)					
				r.set("zhangsan","man");										
			else					
				r.set("李四","女");										
			x=(x+1)%2;
									
							
		}
	}
}

//输出类
class Output implements Runnable
{
	private Res r;
	Output(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		while(true)
		{
			r.out();			
		}
	}
}

class InputOutputDemo1
{
	public static void main(String[] args)
	{
		//创建一个资源对象
		Res r=new Res();
		
		new Thread(new Input(r)).start();
		new Thread(new Output(r)).start();
				
	}
}
           

wait(): notify()

notifyAll();

都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁

为什么这些操作线程的方法要定义在Objcect类中呢?

1、因为这些方法在操作同步中线程时,都必须要标识它们所操作线程持有的锁。

只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程唤醒。也就是说,等待和唤醒必须是同一个锁。

2、锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中

JDK1.5中提供了多线程升级解决方法

将同步synchronized替换成显式的Lock操作。 将Object中的wait,notify  notifyAll,替换成Condition对象。 该对象可以通过Lock锁进行获取.lock.newConditiom

通过下面一个代码来实现,看看与synchronized有什么不同:

import java.util.concurrent.locks.*;
class Res
{
	private String name;
	private int num=1;
	boolean flag=false;
	//创建Lock接口的子接口,父类引用指向子类对象
	private Lock lock=new ReentrantLock();
	//创建Condition对象,用来控制生产者的锁,等待和唤醒本锁上的线程
	private Condition condition_pro =lock.newCondition();
	//创建Condition对象,用来控制消费者的锁,等待和唤醒本锁上的线程
	private Condition condition_con =lock.newCondition();
	public  void set(String name) throws InterruptedException
	{
		lock.lock();
		
		//因为使用Conditiom的await方法,会抛出异常
			try
			{
				while(flag)
				condition_pro.await();//线程等待,抛出异常
				 this.name=name+(num++);
				System.out.println("生产者::::::"+this.name);
				condition_con.signal();//消费者线程唤醒
				flag=true;
			}
			//一定执行的操作:解开锁
			finally
			{
				lock.unlock();
			}
			
		
	}
	
	public synchronized void out()throws InterruptedException
	{
		//锁上锁
		lock.lock();
		try
		{
			while(!flag)
				condition_con.await(); //消费者线程等待
		System.out.println("消费者:  "+name);
		condition_pro.signal();//生产者线程唤醒
		flag=false;
		}
		finally{
			lock.unlock(); //解开锁
		}
		
	}
}

class Producter implements Runnable
{
	Res r;
	Producter(Res r)
	{
		this.r=r;
	}
	public void run() 
	{
		while(true)
		{
			try
			{
				r.set("商品");
			}
			catch(InterruptedException e)
			{
				
			}
			
		}
	}
}
class Consumer implements Runnable
{
	Res r;
	Consumer(Res r)
	{
		this.r=r;
	}
	public void run()
	{
		while(true)
			try
			{
				r.out();
			}
			catch(InterruptedException e)
			{}
			
	}
}

class ProConLockTest
{
	public static void main(String[] args)
	{
		Res r=new Res();
	
		Producter pro=new Producter(r);
		Consumer con=new Consumer(r);

		Thread t1=new Thread(pro);
		Thread t2=new Thread(con);
		Thread t3=new Thread(pro);
		Thread t4=new Thread(con);

		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
	
}
           

结果:

黑马程序员--java基础复习之多线程及线程间通信

停止线程

两种方式: 1、定义循环结束标记  因为线程运行代码一般都是循环,只要控制了循环即可。如下:

class StopThread implements Runnable
{
	private boolean flag=true;
	public  void run()
	{
		while(flag)
		{			
			System.out.println("aaaaa");
		}
	}
	//改变标志的值
	public void chageFlag()
	{
		flag=false;
	}
}

class StopThreadDemo
{
	public static void main(String[] args)
	{
		StopThread st=new StopThread();
		
		Thread t1=new Thread(st);
		t1.start();
		
		int num=0;
		while(true)
		{
			if(num++==60)
			{
				st.chageFlag();
				break;
			}
			System.out.println("num="+num);
		}
	}
}
           

特殊情况:

当线程处于了冻结状态,就不会读取到标记,那么线程就不会结束。当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结状态进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。

2、使用interrupt(中断)方法  (stop方法已过时)

该方法中结束线程的冻结状态,使线程回到运行状态中来。

//Thread 类提供该方法 interrupt();
*/

class StopThread implements Runnable
{
	private boolean flag=true;
	public  void run()
	{
		while(flag)
		{			
			try
			{
				wait();
			}
			catch(InterruptedException e)
			{
				flag=false;
			}
			System.out.println(Thread.currentThread().getName()+"....");
		}
	}
}

class StopThreadDemo
{
	public static void main(String[] args)
	{
		StopThread st=new StopThread();
		
		Thread t1=new Thread(st);
		t1.start();
		
		Thread t2=new Thread(st);
		t2.start();
		
		int num=0;
		while(true)
		{
			if(num++==60)
			{
				t1.interrupt();
				t2.interrupt();
				break;
			}
			System.out.println("num="+num);
		}
		System.out.println("over");
	}
}
           

多线程什么时候用? 当某些代码需要同时被执行时,就用单独的线程进行封装。

如下:

class UseMulThread
{
	public static void main(String[] args)
	{
		/*使用继承方式(匿名内部类)*/
		
		new Thread(){
			public void run()
			{
				for(int i=0;i<50;i++)
				{
					System.out.println("我是线程一");
				}
			}
		}.start();
		
		
		/******使用实现Runnable方式(匿名内部类)*********/
		
		Runnable ra= new Runnable()
		{
			public void run()
			{
				for(int j=0;j<40;j++)
				{
					System.out.println("我是线程二");
				}
			}
		};
		
		new Thread(ra).start();
		
		
		/***************主线程*********************/
		for(int k=0;k<40;k++)
		{
			System.out.println("我是主线程");
		}
		
	}
}
           

join方法

        当A线程执行到了b线程的.join()方法时,A线程就会等待,等B线程都执行完,A线程才会执行。(此时A和其他线程交替运行。)join可以用来临时加入线程执行。

  setPriority()方法用来设置优先级

        MAX_PRIORITY 最高优先级10

        MIN_PRIORITY   最低优先级1

        NORM_PRIORITY 分配给线程的默认优先级5

优先级高是获取CPU概率大一些,优先级高并不一定就先执行。 yield()方法可以暂停当前线程,让其他线程执行。

继续阅读