黑马程序员-java多线程

---------------------- ASP.Net+Unity开发.Net培训、期待与您交流! ----------------------

        操作系统在运行一个程序的时候,至少会创建一个进程,每个进程至少会有一个运行中的线程(Thread)。线程是通过程序执行代码的一条路劲,每个线程都有自己的 局部变量,程序计数器(当前执行指令的指针),以及生存周期。大部分操作系统允许在一个进程内并发运行多个线程。

进程和线程的关系:
进程是一个正在执行的程序。每一个进程都有一个执行顺序,该顺序是一个执行路劲,或者叫一个控制单元,而线程就是一个进程中一个独立的控制单元,线程在控制着进程的执行。

java主线程:JV启动的时候会有一个进程java.exe,该进程中至少有一个线程负责java程序的运行,而且这个线程运行的代码存在于main方法中,该线程就称之为主线程。


创建线程:

java创建线程的第一种方法(多实例-多线程,继承Thread类):
继承Thread,覆盖Thread类中的run方法,再调用线程的start方法启动线程,该线程执行的是run方法中的代码。

创建线程的例子:

<span style="font-family:KaiTi_GB2312;font-size:18px;">class ThreadDemo extends Thread
{

	public void run() 
	{
		for(int i=0; i<100; i++)
		{
			System.out.println("demo run........."+i);
		}
	}
	
}

public class test 
{
	public static void main(String[] args)
	{
		ThreadDemo t = new ThreadDemo();
		
		t.start();
		
		for(int i=0; i<100; i++)
		{
			System.out.println("mainrun........."+i);
		}
		
	}
}</span>

运行程序后可以看到控制太交替输出‘demo run.........’和‘mainrun.........’。



线程的状态:

1,创建状态:当利用new关键字创建一个线程对象实例后,它仅仅作为一个对象实例存在,JVM并没有为其分配cpu资源时间片等运行资源。


2,就绪状态:当创建完的线程调用start方法时,线程将转换为就绪状态,这时,线程已经得到cpu时间片之外的其他资源,等JVM的线程调度器按照线程的优先级对线程进行调度从而使该线程获得运行的机会。

3,运行状态:JVM的线程调度器选中处于就绪状态的线程,使其获得cpu时间片。

4,休眠状态:在线程运行过程中可以调用线程的sleep方法,并在方法中指定要休眠的时间,在休眠时间内,线程在不释放占用资源的情况下停止运行。时间到达后,线程重新进入运行状态。

5,挂起状态:通过Thread类的suspend()方法将线程的状态转为挂起状态。这时,线程将释放占用的所有资源,由JVM调度转入临时的存储空间,直至应用程序调用Thread类的resume()方法恢复线程的运行。

6,死亡状态:调用Thread类的stop()方法可以停止线程的运行并回收其线程占用的所有资源。但此方法存在严重bug已经禁用。


通过Thread.currentThread()获取当前运行的线程,Thread.currentThread().getName()获取当前运行线程的名称。

一个售票的例子(假设共有十张票,启动四个线程去出售):

<span style="font-family:KaiTi_GB2312;font-size:18px;">class Ticket extends Thread
{
	private int ticket = 10;
	
	public void run()
	{
		while(true)
		{
			if(ticket > 0)
			{
				System.out.println(Thread.currentThread().getName()+"........"+ticket);
				ticket--;
			}
		}
	}
	
}

public class  test
{
	public static void main(String[] args)
	{
		Ticket t1 = new Ticket();
		Ticket t2 = new Ticket();
		Ticket t3 = new Ticket();
		Ticket t4 = new Ticket();
		
		t1.start();		
		t2.start();
		t3.start();
		t4.start();
	}
}
</span>

发现共出售了40张票,原因是每个线程都有自己的ticket成员变量,解决这个问题的方法是定义ticket为static变量,让四个线程共享ticket变量。但由于static变量生命周期太长,这种方法不可取!!

于是引入了第二种创建线程的方式(单实例-多线程,实现Runnable接口):
1,定义类实现Runnable接口。

2,覆盖Runnable接口中的run方法,将线程要运行的代码存在run方法中。

3,通过Thread类建立线程对象。

4,将Runnable接口的子类对象传递给Thread的构造函数。

5,调用Thread类的start方法启动线程。

这种方式创建的多线程可以让多个线程去运行指定对象的run方法,同时也带来了由于多线程并发访问所带来的共享变量安全的问题。

实现Runnable接口的售票的例子:

<span style="font-family:KaiTi_GB2312;font-size:18px;">class Ticket implements Runnable
{
	private int ticket = 10;
	
	public void run()
	{
		while(true)
		{
	
			if(ticket > 0)
			{					
				System.out.println("code....................."+ticket);
				ticket--;
			}
			
		}

		
	}
}


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


程序运行后,在控制台上打印出的数据是混乱的,有时会售出几个相同的票,还可能出现0号票,产生这个结果的原因是当一个线程打印出ticket之后,还没来得急将ticket减去1,就被其他线程抢到了执行权,并将ticket打印出,这是就会出现同号票和零号票,解决这个问题的方法是对多个线程操作享数据的语句,只能让一个线程执行,在执行过程中,其他线程不可以参与。

java提供了对于多线程安全问题的专业解决方法(同步代码块):

synchronized(对象)
{
 
}

其中的对象相当于一把锁,对于运行在同一个对象上的多线程,当有一个线程进入同步代码块之后,它就持有了这把锁,此时,其他线程不能进入这个代码块,只有当此线程执行完同步代码块中的代码并释放锁之后,其他线程才可以进入这个代码块。

用同步代码块实现的售票例子:

<span style="font-family:KaiTi_GB2312;font-size:18px;">class Ticket1 implements Runnable
{
	private int ticket = 10;
	
	public void run()
	{
		while(true)
		{
			synchronized (this) 
			{
				if(ticket > 0)
				{					
					System.out.println("code....................."+ticket);
					ticket--;
				}
			}		
		}		
	}
}


public class test
{
	public static void main(String[] args)
	{
		Ticket1 t = new Ticket1();
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
		
	}
}</span>


在控制台上运行结果是正确的。另外要注意的是,也可以将函数前面加上synchronized关键字,此时同步函数持有的锁是this,即本类对象。
售票例子也可以改写如下:

<span style="font-family:KaiTi_GB2312;font-size:18px;">class Ticket1 implements Runnable
{
	private int ticket = 10;
	
	public void run()
	{
		while(true)
		{
			sale();
		}		
	}
	
	public synchronized void sale()
	{		
		if(ticket > 0)
		{					
			System.out.println("code....................."+ticket);
			ticket--;
		}
	}
}

public class test
{
	public static void main(String[] args)
	{
		Ticket1 t = new Ticket1();
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
		new Thread(t).start();
		
	}
}</span>



多线程的单例设计模式:

饿汉式:

<span style="font-family:KaiTi_GB2312;font-size:18px;">class Singleton
{
      private static Singleton instance = new  Singleton();
      public static  Singleton getInstance()
      {
            return instance;
       }
}</span>


饿汉式不存在多线程并发访问的问题。

懒汉式:
先写一个简单的懒汉式:

<span style="font-family:KaiTi_GB2312;font-size:18px;">public class Singleton
{
	private static Singleton instance = null;
	private Singleton() {}

	public static synchronized Singleton getInstance()
	{
		if(instance == null)
		{
			instance = new Singleton();
		}
		return instance;
	}
}</span>


显然在多线程下肯定是不安全的,故加上synchronized,这样做封锁的是整个对象,性能会降低,因为每次调用getInstance()时都要对对象上锁,事实上只要在第一次调用时会创建对象,以后就不用创建了。
改进如下:

<span style="font-family:KaiTi_GB2312;font-size:18px;">public class Singleton
{
	private static Singleton instance = null;
	private Singleton() {}

	public static Singleton getInstance()
	{
		if(instance == null)
		{
			synchronized(this)
			{
				if(instance == null)
				{
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}</span>


似乎解决了之前的问题,性能得到提升,但是还是有问题,原因在于在JAVA指令中创建对象和赋值是分开操作的,
也就是instance = new Singleton();语句是分开操作执行的,但是JVM并不保证这两个操作的顺序是先后执行的,
它可能为新的Singleton实例分配空间,然后直接复制给instance成员
然后再去初始化Singleton实例,假设有线程A和B
1,A,B同时进入第一个if判断
2,A首先进入synchronized模块,发现instance为null,开始执行instance = new Singleton();
3,由于JVM的一些优化操作,JVM先划出了一些空白内存给Singleton的实例instance,但并未初始化instance,A便离开了synchronized模块
4,B发现instance不为null便直接离开了
5,B打算使用instance实例发现它没有初始化,编译器出错!!!
最保险的方式就是用饿汉式,在成员上就创建完Singleton的实例!!!


线程间通信:

线程的等待唤醒机制:Thread类提供了一些丰富的控制线程运行的函数。
1,void wait(): 使线程陷入一直等待状态,除非被唤醒。

2,void wait(long timeout): 线程呆呆timeout后若还未被唤醒,则重新被线程调度器调度。

3,void notify(): 唤醒在此对象上等待一个线程,如果在此对象上有多个线程在等待,在唤醒其中任何一个。

4,void notifyAll():唤醒在此对象上等待的所有线程。


一个经典的例子是生产者-消费者问题:生产者生产一个物品,消费者便将物品取走,如果消费者没有取走上次生产的物品,则生产者不生产物品。

<span style="font-family:KaiTi_GB2312;font-size:18px;">//资源
class Res
{
	private int goods = 0;
	boolean flag = true;  //睡眠判断标记
	
	public synchronized void setGoods()
	{
		while(true)
		{
			//如果消费者还没有取走物品,睡眠等待
			if(!flag) 
				try {this.wait();}catch (Exception e) {}
			goods++;
			System.out.println("produce.........."+goods);
			//置flag为假表示物品已经生产完
			flag = false;
			//唤醒等待生产者生产物品的消费者取走物品
			this.notify();
		
		}
	}
	
	public synchronized void getGoods()
	{
		while(true)
		{
			//如果生产者还没有生产物品,睡眠等待
			if(flag) 
				try {this.wait();}catch (Exception e) {}
			System.out.println("consumer.........."+goods);
			//置flag为真表示物品已经取走
			flag = true;
			//唤醒等待消费者取走物品的生产者继续生产
			this.notify();
		
		}
	}
}

//生产者
class Producer implements Runnable
{
	private Res r = null;
	
	public Producer(Res r)
	{
		this.r = r;
	}
	
	public void run()
	{
		r.setGoods();
	}	
}


//消费者
class Consumer implements Runnable
{
	private Res r = null;
	
	public Consumer(Res r)
	{
		this.r = r;
	}
	
	public void run()
	{
		r.getGoods();
	}	
}

public class test
{
	public static void main(String[] args)
	{
		Res r = new Res();
		
		new Thread(new Producer(r)).start();
		new Thread(new Consumer(r)).start();
	}
}</span>



要知道,在现实生活中,生产者和消费者都不止一个,为此将上例中的main函数改为如下的代码:

<span style="font-family:KaiTi_GB2312;font-size:18px;">public class test
{
	public static void main(String[] args)
	{
		Res r = new Res();
		
		new Thread(new Producer(r)).start();
		new Thread(new Producer(r)).start();
		new Thread(new Consumer(r)).start();
		new Thread(new Consumer(r)).start();
	}
}</span>

即有两个生产者和两个消费者,但此时在控制台上打印的数据是错误的,并不是按照生产一个便消费一个的规则打印。部分打印结果为:
produce..........8906
consumer..........8906
consumer..........8906
produce..........8907
consumer..........8907
consumer..........8907
produce..........8908
consumer..........8908
consumer..........8908
produce..........8909

问题在于当一个消费者线程进入睡眠状态(即等待生产者生产物品),生产者生产了一个物品之后,另外一个消费者线程进入,将物品取走,而当这个睡眠的消费者线程被唤醒之后,它并没有再次判断flag标记,而是直接就执行下面的代码将上一次取走的物品再了取一次,这就出现了上面的打印结果。


解决方法是将if判断语句改成while判断。同时唤醒所有睡眠中的进程(this.notifyAll())。

修正后代码如下:

<span style="font-family:KaiTi_GB2312;font-size:18px;">class Res
{
	private int goods = 0;
	boolean flag = true;  //睡眠判断标记
	
	public synchronized void setGoods()
	{
		while(true)
		{
			//如果消费者还没有取走物品,睡眠等待
			while(!flag) 
				try {this.wait();}catch (Exception e) {}
			goods++;
			System.out.println("produce.........."+goods);
			//置flag为假表示物品已经生产完
			flag = false;
			//唤醒等待生产者生产物品的消费者取走物品
			this.notifyAll();
		
		}
	}
	
	public synchronized void getGoods()
	{
		while(true)
		{
			//如果生产者还没有生产物品,睡眠等待
			while(flag) 
				try {this.wait();}catch (Exception e) {}
			System.out.println("consumer.........."+goods);
			//置flag为真表示物品已经取走
			flag = true;
			//唤醒等待消费者取走物品的生产者继续生产
			this.notifyAll();
		
		}
	}
}

//生产者
class Producer implements Runnable
{
	private Res r = null;
	
	public Producer(Res r)
	{
		this.r = r;
	}
	
	public void run()
	{
		r.setGoods();
	}	
}


//消费者
class Consumer implements Runnable
{
	private Res r = null;
	
	public Consumer(Res r)
	{
		this.r = r;
	}
	
	public void run()
	{
		r.getGoods();
	}	
}

public class test
{
	public static void main(String[] args)
	{
		Res r = new Res();
		
		new Thread(new Producer(r)).start();
		new Thread(new Producer(r)).start();
		new Thread(new Consumer(r)).start();
		new Thread(new Consumer(r)).start();
	}
}</span>

这样可以创建任意多个生产者和消费者线程,同时保证生产一个就消费一个。但有一个问题是,在执this.notifyAll()语句的时候,不管是生产者还是消费者,都有可能唤醒本方的另一个线程,而现实情况是我们只需要唤醒对方的一个睡眠线程就可以了,JDK5.0的Lock类给了我们解决这个问题的方法。

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

其中Condition将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过
将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

在资源(Res)里面创建一把lock,并根据这把锁分别创建生产者和消费者Condition对象,这样,当生产者要睡眠时,我们调用生产者Condition对象的await方法,当生产者生产完物品后,调用消费者Conditioin对象的signal方法唤醒由于调用消费者Conditioin对象的await方法而睡眠的任意一个消费者线程。
记住:必须将释放所的代码放在finally块中!!

代码如下:

class Res
{
	private int goods = 0;
	boolean flag = true;
	private Lock lock = new ReentrantLock();
	
	private Condition pro_condition = lock.newCondition();
	
	private Condition con_condition = lock.newCondition();
		
	public void setGoods()
	{
		while(true)
		{
			//上锁
			lock.lock();
			try 
			{
				while(!flag) //如果物品还没被取走就睡眠
					pro_condition.await();
				goods++;
				System.out.println("produce.........."+goods);
				flag = false;//物品已经生产,置标记为假
				//唤醒在con_condition对象上等待的一个消费者线程
				con_condition.signal();
			}
			catch (Exception e) 
			{
				e.printStackTrace();
			}
			finally
			{
				lock.unlock(); //解锁
			}
		}
	}
	
	public  void getGoods()
	{
		while(true)
		{
			lock.lock(); //上锁
			try 
			{
				while(flag) //如果物品还没被生产出就睡眠
					con_condition.await();
				System.out.println("consumer.........."+goods);
				flag = true;//物品已经生产,置标记为真
				//唤醒在pro_condition对象上等待的一个生产者线程
				pro_condition.signal();
			} 
			catch (Exception e) 
			{
				e.printStackTrace();
			}
			finally
			{
				lock.unlock(); //解锁
			}
		
		}
	}
}

class Producer implements Runnable
{
	private Res r = null;
	
	public Producer(Res r)
	{
		this.r = r;
	}
	
	public void run()
	{
		r.setGoods();
	}	
}

class Consumer implements Runnable
{
	private Res r = null;
	
	public Consumer(Res r)
	{
		this.r = r;
	}
	
	public void run()
	{
		r.getGoods();
	}	
}

public class test
{
	public static void main(String[] args)
	{
		Res r = new Res();
		
		new Thread(new Producer1(r)).start();
		new Thread(new Producer1(r)).start();
		new Thread(new Consumer1(r)).start();
		new Thread(new Consumer1(r)).start();
	}
}



非常漂亮地以生产一个就消费一个的规则输出在控制台上。


停止线程:

前面提到Thread类中停止线程的stop方法存在严重bug,已经禁用。因此为了做到停止线程,也即让线程结束run方法的执行,必须手动控制run方法中的循环来停止线程。

停止线程的例子:

<span style="font-family:KaiTi_GB2312;font-size:18px;">class StopThread implements Runnable
{
	private boolean flag = true;
	public synchronized void run()
	{
		while(flag)
		{
			System.out.println(Thread.currentThread().getName()+"......Run");
		}
	}
	
	public void changeFlag()
	{
		flag = false;
	}
}


public class test
{
	public static void main(String[] args)
	{
		StopThread st = new StopThread();
	    	new Thread(st).start();
	    	new Thread(st).start();
		int num = 0;
		while(true)
		{
			if(num++ == 60)
			{
				st.changeFlag(); //改变标记,让线程执行完run方法
				break;
			}
			System.out.println(Thread.currentThread().getName()+"........main"+num);
		}
		
		System.out.println("over");
	}
}</span>


特殊情况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。
解决方法:当调用线程的interrupt方法中断线程时,会抛出InterruptedException异常,利用这个特性在catch语句中设置标记状态来结束线程的run方法。


<span style="font-family:KaiTi_GB2312;font-size:18px;">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;
	}
}


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



守护线程:
守护线程是一种“在后台提供通用性支持”的线程,它并不属于程序本体。当调用线程对象的 Daemon(true)方法是,该线程将会设置为守护线程。


线程join方法:

Thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在主线程中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行主线程。
 
但要注意的一点就是:只有线程被启动之后,也就是调用start方法之后,调用join方法才有效,否则,调用join
方法是没有任何作用的,程序也不会抛出异常,而是继续向下往下执行。
   

调用join的例子:

<span style="font-family:KaiTi_GB2312;font-size:18px;">class Demo implements Runnable
{

	public void run() 
	{
		for(int i=0; i<100; i++)
			System.out.println(Thread.currentThread().getName()+"..........."+i);
	}
	
}

public class test 
{
	public static void main(String[] args) throws Exception
	{
		Demo d = new Demo();
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		t1.start();
		t1.join();
		t2.start();
		for(int i=0; i<100; i++)
			System.out.println(Thread.currentThread().getName()+"......"+i);	
		System.out.println("over");
	}
}</span>


控制台上首先一直在运行t1这个线程,知道它运行完之后,才交替运行主线程和t2线程。

多线程的优先级:
每个线程都有自己的线程优先级,java线程的优先Java线程的优先级是一个整数,
其取值范围是1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

设置优先级:调用Thread对象中的setPriority方法可以设置该线程的优先级,优先级高的更有资格先抢到cpu执行权。

---------------------- ASP.Net+Unity开发.Net培训、期待与您交流! ----------------------详细请查看:www.itheima.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值