黑马程序员——浅谈java中的多线程

一、多线程概述

        要理解多线程,就必须理解线程。而要理解线程,就必须知道进程。

1、 进程

        是一个正在执行的程序。

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

2、线程

         就是进程中的一个独立的控制单元。线程在控制着进程的执行。只要进程中有一个线程在执行,进程就不会结束。

        一个进程中至少有一个线程。

3、多线程

        java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。像种在一个进程中有多个线程执行的方式,就叫做多线程。

4、多线程存在的意义

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

         例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象,而同时对象调用完后,就成了垃圾。如果垃圾过多就有可能是堆内存出现内存不足的现象,只是如果只有一个线程工作的话,程序的执行将会很低效。而如果有另一个线程帮助处理的话,如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得更有效率。

5、计算机CPU的运行原理

         我们电脑上有很多的程序在同时进行,就好像cpu在同时处理这所以程序一样。但是,在一个时刻,单核的cpu只能运行一个程序。而我们看到的同时运行效果,只是cpu在多个进程间做着快速切换动作。

         cpu执行哪个程序,是毫无规律性的。这也是多线程的一个特性:随机性。哪个线程被cpu执行,或者说抢到了cpu的执行权,哪个线程就执行。而cpu不会只执行一个,当执行一个一会后,又会去执行另一个,或者说另一个抢走了cpu的执行权。至于究竟是怎么样执行的,只能由cpu决定。

 

二、创建线程的方式

        创建线程共有两种方式:继承方式和实现方式(简单的说)。

1、 继承方式

        通过查找java的帮助文档API,我们发现java中已经提供了对线程这类事物的描述的类——Thread类。这第一种方式就是通过继承Thread类,然后复写其run方法的方式来创建线程。

创建步骤:

        a,定义类继承Thread

        b,复写Thread中的run方法。

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

        c,创建定义类的实例对象。相当于创建一个线程。

        d,用该对象调用线程的start方法。该方法的作用是:启动线程,调用run方法。

注:如果对象直接调用run方法,等同于只有一个线程在执行,自定义的线程并没有启动。

覆盖run方法的原因:

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

执行是随机、交替执行的,每一次运行的结果都会不同。

       

2、 实现方式

        使用继承方式有一个弊端,那就是如果该类本来就继承了其他父类,那么就无法通过Thread类来创建线程了。这样就有了第二种创建线程的方式:实现Runnable接口,并复习其中run方法的方式。

创建步骤:

        a,定义类实现Runnable的接口。

        b,覆盖Runnable接口中的run方法。目的也是为了将线程要运行的代码存放在该run方法中。

        c,通过Thread类创建线程对象。

        d,将Runnable接口的子类对象作为实参传递给Thread类的构造方法。

       为什么要将Runnable接口的子类对象传递给Thread的构造函数?

        因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。

        e,调用Thread类中start方法启动线程。start方法会自动调用Runnable接口子类的run方法。

实现方式好处:避免了单继承的局限性。在定义线程时,建议使用实现方式。 

程序示例:

/*
//一、通过继承Thread创建线程
class Ticket extends Thread
{
	private static int tick = 100; 
	public void run()
	{
		while(true)
		{
			if(tick>0)
				System.out.println(Thread.currentThread().getName()+"..."+tick--); 
		}
	}
}
*/

//二、通过实现runnable接口
class Ticket implements Runnable//extends Thread
{
	private static int tick = 100; 

	Object obj = new Object(); 
	public void run()
	{
		while(true)
		{
			synchronized(obj)
			{
				if(tick>0)
				{
					try
					{
						Thread.sleep(10); 	
					}
					catch (Exception e)
					{
					}
					System.out.println(Thread.currentThread().getName()+"..."+tick--); 
				}
			}
		}
	}
}

class  TicketDemo
{
	public static void main(String[] args) 
	{
		Ticket t = new Ticket(); 

		Thread t1 =  new Thread(t); //创建一个线程;
		Thread t2 =  new Thread(t); //创建一个线程;
		Thread t3 =  new Thread(t); //创建一个线程;
		Thread t4 =  new Thread(t); //创建一个线程;
		
		t1.start(); 
		t2.start(); 
		t3.start(); 
		t4.start(); 

		/*
		Ticket t1 = new Ticket(); 
		Ticket t2 = new Ticket();
		Ticket t3 = new Ticket();
		Ticket t4 = new Ticket();

		t1.start(); 
		t2.start(); 
		t3.start(); 
		t4.start(); 
		*/

	}
}

三、两种方式的区别和线程的几种状态

1、两种创建方式的区别

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

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

2、几种状态

        被创建:等待启动,调用start启动。

         运行状态:具有执行资格和执行权。

         临时状态(阻塞):有执行资格,但是没有执行权。

         冻结状态:遇到sleeptime)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。

         消忙状态:stop()方法,或者run方法结束。

注:当已经从创建状态到了运行状态,再次调用start()方法时,就失去意义了,java运行时会提示线程状态异常。

图解:

   

四、线程安全问题

1、导致安全问题的出现的原因:

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

简单的说就两点:

        a、多个线程访问出现延迟。

        b、线程随机性    

注:线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。

2、解决办法——同步

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

        在java中对于多线程的安全问题提供了专业的解决方式——synchronized(同步)

        这里也有两种解决方式,一种是同步代码块,还有就是同步函数。都是利用关键字synchronized来实现。

         a、同步代码块

        用法:

                  synchronized(对象)

                  {需要被同步的代码}

        同步可以解决安全问题的根本原因就在那个对象上。其中对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。

示例:

/*	
给卖票程序示例加上同步代码块。
*/
class Ticket implements Runnable
{
	private int tick=100;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			//给程序加同步,即锁
			synchronized(obj)
			{
				if(tick>0)
				{
					try
					{	
						//使用线程中的sleep方法,模拟线程出现的安全问题
						//因为sleep方法有异常声明,所以这里要对其进行处理
						Thread.sleep(10);
					}
					catch (Exception e)
					{
					}
					//显示线程名及余票数
					System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
				}
			}	
		}
	}
}


/*	
给卖票程序示例加上同步代码块。
*/
class Ticket implements Runnable
{
	private int tick=100;
	Object obj = new Object();
	public void run()
	{
		while(true)
		{
			//给程序加同步,即锁
			synchronized(obj)
			{
				if(tick>0)
				{
					try
					{	
						//使用线程中的sleep方法,模拟线程出现的安全问题
						//因为sleep方法有异常声明,所以这里要对其进行处理
						Thread.sleep(10);
					}
					catch (Exception e)
					{
					}
					//显示线程名及余票数
					System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
				}
			}	
		}
	}
}

        b,同步函数

        格式:

                在函数上加上synchronized修饰符即可。

        那么同步函数用的是哪一个锁呢?

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

示例:

class Ticket1 implements Runnable//extends Thread
{
	private static int tick = 100; 
	Object obj = new Object();

	//用来标志是线程是进去同步代码块还是同步函数
	boolean flag = true; 

	public void run()
	{
		if(flag)
		{
			while(true)
			{
				synchronized(obj)
				{
					if(tick>0)
					{
						try{Thread.sleep(10);}catch (Exception e){}
						System.out.println(Thread.currentThread().getName()+"...code.."+tick--); 
					}
				}
			}
		}
		else
			while(true)
				show(); 

	}

	public synchronized void show()//this
	{
		if(tick>0)
		{
			try{Thread.sleep(10);}catch (Exception e){}
			System.out.println(Thread.currentThread().getName()+"...show.."+tick--); 
		}
	}
}

class  ThisLockDemo
{
	public static void main(String[] args) 
	{
		Ticket1 t = new Ticket1(); 

		Thread t1 =  new Thread(t); //创建一个线程;
		Thread t2 =  new Thread(t); //创建一个线程;
		
		//两个线程使用的不是同一个锁,t1使用的是obj对象的锁,t2使用的是this的
		t1.start(); 
		try{Thread.sleep(100);}catch (Exception e){}
		t.flag = false; 
		t2.start();
	}
}


3、同步的前提

        a,必须要有两个或者两个以上的线程。

        b,必须是多个线程使用同一个锁。

4、同步的利弊

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

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

5、如何寻找多线程中的安全问题

        a,明确哪些代码是多线程运行代码。

        b,明确共享数据。

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

 

五、静态函数的同步方式

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

        通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。如:

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

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

经典示例:

class Ticket1 implements Runnable//extends Thread
{
	private static int tick = 100; 
	Object obj = new Object();

	//用来标志是线程是进去同步代码块还是同步函数
	boolean flag = true; 

	public void run()
	{
		if(flag)
		{
			while(true)
			{
				synchronized(Ticket1.class)
				{
					if(tick>0)
					{
						try{Thread.sleep(10);}catch (Exception e){}
						System.out.println(Thread.currentThread().getName()+"...code.."+tick--); 
					}
				}
			}
		}
		else
			while(true)
				show(); 

	}

	public static synchronized void show()//静态同步函数的锁,是class对象
	{
		if(tick>0)
		{
			try{Thread.sleep(10);}catch (Exception e){}
			System.out.println(Thread.currentThread().getName()+"...show.."+tick--); 
		}
	}
}

class  StaticLockDemo
{
	public static void main(String[] args) 
	{
		Ticket1 t = new Ticket1(); 

		Thread t1 =  new Thread(t); //创建一个线程;
		Thread t2 =  new Thread(t); //创建一个线程;
		
		//两个线程使用的不是同一个锁,t1使用的是obj对象的锁,t2使用的是this的
		t1.start(); 
		try{Thread.sleep(100);}catch (Exception e){}
		t.flag = false; 
		t2.start(); 
	}
}


/*
加同步的单例设计模式————懒汉式
*/
class Single
{
	private static Single s = null;
	private Single(){}
	public static void getInstance()
	{
		if(s==null)
		{
			synchronized(Single.class)
			{
				if(s==null)
					s = new Single();
			}
		}
		return s;
	}
}

 

六、死锁

        当同步中嵌套同步时,就有可能出现死锁现象。

示例:

class Test implements Runnable
{
	private boolean flag; 
	Test(boolean flag)
	{
		this.flag = flag; 
	}
	public void run()
	{
		if(flag)
		{	
			while(true)
			{
				synchronized(MyLock.locka)
				{
					System.out.println("if locka");
					synchronized(MyLock.lockb)
					{
						System.out.println("if lockb"); 
					}
				}
			}
		}
		else
		{
			while(true)
			{
				synchronized(MyLock.lockb)
				{
					System.out.println("else lockb");
					synchronized(MyLock.locka)
					{
						System.out.println("else locka"); 
					}
				}
			}
		}
	}
}

class MyLock
{
	static Object locka = new Object(); 
	static Object lockb = new Object(); 
}

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


七、线程间通信

        其实就是多个线程在操作同一个资源,但是操作的动作不同。

代码示例:

/*
生产者消费者问题

一个生产者
一个消费者
共享一个缓存区
*/
class  ProducerConsumerDemo
{
	public static void main(String[] args) 
	{
		Resource r = new Resource(); 

		Producer pro = new Producer(r); 
		Consumer con = new Consumer(r); 

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

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


class Resource
{
	private String name; 
	private int count = 1; 
	private boolean flag = false; 
	public synchronized void set(String name)
	{
		if(flag)
			try{wait(); } catch(Exception e){}
		this.name = name+"--"+count++; 
		System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name); 
		flag = true; 
		this.notify(); 
	}

	public synchronized void out()
	{
		if(!flag)
			try{wait(); } catch(Exception e){}	//等待线程
		System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name); 
		flag = false; 
		this.notify(); //唤醒线程池中的一个线程
	}
}

class Producer implements Runnable
{
	private Resource res; 
	Producer(Resource res)
	{
		this.res = res; 
	}
	public void run()
	{
		while(true)
		{
			res.set("商品"); 
		}
	}
}

class Consumer implements Runnable 
{
	private Resource res; 
	Consumer(Resource res)
	{
		this.res = res; 
	}
	public void run()
	{
		while(true)
		{
			res.out(); 
		}
	}
}

/*
生产者消费者问题

多个生产者
多个消费者
共享一个缓存区

为什么要定义while判断标记?
原因:让被唤醒的线程再一次判断标记。

为什么定义notifyAll?
因为需要唤醒对方线程(要唤醒所有线程)
因为只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。
*/
class  ProducerConsumerDemo2
{
	public static void main(String[] args) 
	{
		Resource r = new Resource(); 

		Producer pro = new Producer(r); 
		Consumer con = new Consumer(r); 

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

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


class Resource
{
	private String name; 
	private int count = 1; 
	private boolean flag = false; 
	public synchronized void set(String name)
	{
		while(flag)
			try{wait(); } catch(Exception e){}
		this.name = name+"--"+count++; 
		System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name); 
		flag = true; 
		this.notifyAll(); 
	}

	public synchronized void out()
	{
		while(!flag)
			try{wait(); } catch(Exception e){}	//等待线程
		System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name); 
		flag = false; 
		this.notifyAll(); //唤醒线程池中的所有线程
	}
}

class Producer implements Runnable
{
	private Resource res; 
	Producer(Resource res)
	{
		this.res = res; 
	}
	public void run()
	{
		while(true)
		{
			res.set("商品"); 
		}
	}
}

class Consumer implements Runnable 
{
	private Resource res; 
	Consumer(Resource res)
	{
		this.res = res; 
	}
	public void run()
	{
		while(true)
		{
			res.out(); 
		}
	}
}


 几个小问题:

        1)wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?

                a,这些方法存在与同步中。

                b,使用这些方法时必须要标识所属的同步的锁。同一个锁上wait的线程,只可以被同一个锁上的notify唤醒。

                c,锁可以是任意对象,所以任意对象调用的方法一定定义Object类中。

        2)wait(),sleep()有什么区别?

              wait():释放cpu执行权,释放锁。

              sleep():释放cpu执行权,不释放锁。

        3)为甚么要定义notifyAll

        因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所以线程都等待。

2JDK1.5中提供了多线程升级解决方案。

   

/*
生产者消费者问题JDK1.5升级版

JDK1.5中提供了多线程的升级解决方案。
将同步synchronized替换成显示Lock操作。
将object中的wait,notify,notifyAll 替换成了condition对象。
该对象可以用Lock锁进行获取。
该示例中,实现了本方只唤醒对方操作。

*/
import java.util.concurrent.locks.*; 

class  ProducerConsumerDemo3
{
	public static void main(String[] args) 
	{
		Resource r = new Resource(); 

		Producer pro = new Producer(r); 
		Consumer con = new Consumer(r); 

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

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


class Resource
{
	private String name; 
	private int count = 1; 
	private boolean flag = false; 

	private Lock lock = new ReentrantLock(); //定义一个显示的锁
	
	private Condition con_pro = lock.newCondition(); 
	private Condition con_con = lock.newCondition(); 

	public  void set(String name) throws InterruptedException
	{
		lock.lock(); 
		try
		{
			while(flag)
				con_pro.await(); //生产者等待
			this.name = name+"--"+count++; 
			System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name); 
			flag = true; 
			con_con.signal(); //唤醒消费者 
		}
		finally
		{
			lock.unlock(); 
		}
	}

	public  void out()	throws InterruptedException
	{
		lock.lock();
		try
		{
			while(!flag)
				con_con.await();//消费者线程等待 
			System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name); 
			flag = false; 
			con_pro.signal(); //唤醒生产者线程
		}
		finally
		{
			lock.unlock(); 
		}
	}
}

class Producer implements Runnable
{
	private Resource res; 
	Producer(Resource res)
	{
		this.res = res; 
	}
	public void run()
	{
		while(true)
		{
			try
			{
				res.set("商品"); 
			}
			catch (InterruptedException e)
			{
			}
			
		}
	}
}

class Consumer implements Runnable 
{
	private Resource res; 
	Consumer(Resource res)
	{
		this.res = res; 
	}
	public void run()
	{
		while(true)
		{
			try
			{
				res.out(); 
			}
			catch (InterruptedException e)
			{
			} 
		}
	}
}


八、停止线程

        JDK 1.5版本之前,有stop停止线程的方法,但升级之后,此方法已经过时。

/*
线程结束

stop方法已经过时了,如何停止线程呢?
只有一种方法,run方法结束。
开启多线程运行,运行代码通常是循环结构。

只要控制住循环,就可以让run方法结束,也就是结束线程。


特殊情况:
当线程处于了冻结状态时
就不会读取到标记,那么线程就不会结束。

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

Thread类提供了该方法 interrupt(); 

*/

/*
class StopThread implements Runnable
{
	private boolean flag = true; 
	public void run()
	{
		while(flag)
			System.out.println(Thread.currentThread().getName()+"...run"); 

	}
	public void changeFlag()
	{
		flag = false; 
	}
}

*/

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

	}
	public void changeFlag()
	{
		flag = false; 
	}
}


class  StopThreadDemo 
{
	public static void main(String[] args) 
	{
		StopThread st = new StopThread(); 
		
		Thread t1 = new Thread(st);
		Thread t2 = new Thread(st);
		
		t1.start(); 
		t2.start(); 

		int num = 0; 
		while(true)
		{
		if(num++ == 100)
		{ 
			//st.changeFlag();	//设置标记,中断线程
			//t1.interrupt();	//线程中断
			//t2.interrupt();	//线程中断
			break; 
		}
		System.out.println(Thread.currentThread().getName()+"..."+num); 
		}
	}
}


扩展小知识:

1join方法

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

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

        MAX_PRIORITY 最高优先级10

        MIN_PRIORITY   最低优先级1

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

3yield()方法可以暂停当前线程,让其他线程执行。

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