10、线程

1、想要让对象具有多线程功能,只要继承java.lang.Thread类或是实现java.lang.Runnable接口。

2、继承Thread
   首先继承java.lang.Thread类,并重新定义run()方法,之后可以实例化自定义的Thread类,接着使用start()方法。
   但是这种方法有缺陷!
   如果使用继承的方法来定义线程类,就意味着定义的类是一个Thread类型,且由于在Java中
   一次只能继承一个类,如果一个类已经要继承某个类,那么就不能继承Thread类!
   
   下面这个范例,将定义一个文字模式下的密码屏蔽线程,这个线程会不断地重复返回上一个字符位置并显示屏蔽字符;
   我们可以在截取使用者密码输入前启动这个线程,仿真密码屏蔽功能,以避免使用者输入的密码被窥视。
public class EraserThread extends Thread
{
	private boolean active;
	private String mask;


	public EraserThread()
	{
		this('*');
	}


	public EraserThread(char maskChar)
	{
		active = true;
		mask = "\010" + maskChar;
	}


	//停止线程时设定为false
	public void setActive(boolean active)
	{
		this.active = active;
	}


	public boolean isActive()
	{
		return active;
	}


	//重新定义run()方法
	public void run()
	{
		while(isActive())
		{
			System.out.print(mask);
			try
			{
				//暂停目前的线程50ms
				//如果用户没有输入的话,这个线程会一直刷新当前字符为'#'
				Thread.currentThread().sleep(50);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}
}
   在文字模式下输出\010表示返回一个字符位置
   
import java.util.Scanner;


public class EraserThreadDemo
{
	public static void main(String[] args)
	{
		Scanner scanner = new Scanner(System.in);


		while(true)
		{
			System.out.print("输入名称:");
			String name = scanner.next();


			System.out.print("输入密码:");
			//启动EraserThread线程
			EraserThread eraserThread = new EraserThread('#');
			eraserThread.start();
			String password = scanner.next();
			eraserThread.setActive(false);


			if("Justice".equals(name) && "123456".equals(password))
			{
				System.out.println("欢迎Justice!");
				break;
			}
			else
			{
				System.out.printf("%s, 名称或密码错误,请重新输入!%n", name);
			}
		}
	}
}

3、实现Runnable接口
   可以实现java.lang.Runnable接口来定义具有线程功能的类,Runnable接口中定义了一个
   run()方法要实现,在实例化一个Thread对象时,可以传入一个实现Runnable接口的对象作为变量。
   Thread对象会调用Runnable对象的run方法,进而执行其中所定义的流程。
   
   下面范例改写密码屏蔽程序:
public class Eraser implements Runnable
{
	private boolean active;
	private String mask;


	public Eraser()
	{
		this('*');
	}


	public Eraser(char maskChar)
	{
		active = true;
		mask = "\010" + maskChar;
	}


	//停止线程时设定为false
	public void setActive(boolean active)
	{
		this.active = active;
	}


	public boolean isActive()
	{
		return active;
	}


	//重新定义run()方法
	public void run()
	{
		while(isActive())
		{
			System.out.print(mask);
			try
			{
				//暂停目前的线程50ms
				//如果用户没有输入的话,这个线程会一直刷新当前字符为'#'
				Thread.currentThread().sleep(50);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}
}

import java.util.Scanner;


public class EraserDemo
{
	public static void main(String[] args)
	{
		Scanner scanner = new Scanner(System.in);


		while(true)
		{
			System.out.print("输入名称:");
			String name = scanner.next();


			System.out.print("输入密码:");
			
			//EraserThread实现Runnable接口
			Eraser eraser = new Eraser('#');
			//传入实现Runnable接口的对象,启动Eraser线程
			Thread eraserThread = new Thread(eraser);
			eraserThread.start();
			
			String password = scanner.next();
			eraser.setActive(false);


			if("Justice".equals(name) && "123456".equals(password))
			{
				System.out.println("欢迎Justice!");
				break;
			}
			else
			{
				System.out.printf("%s, 名称或密码错误,请重新输入!%n", name);
			}
		}
	}
}
   基本上建议以实现Runnable的方式让对象具有线程功能,保留日后修改的弹性。
   
4、Daemon线程
   设计一个程序,除了主线程之外,还运用了一个线程在背景中进行相关运算工作。程序可能这样:
public class SimpleThread
{
	public static void main(String[] args)
	{
		Thread thread = new Thread(new Runnable()
		{
			public void run()
			{
				while(true)
				{
					System.out.print("T");
				}
			}
		});
		thread.start();


		//主线程继续其他工作...
		//现在主线程执行到这里,工作应该结束了
	}
}

   运行main()的线程执行到最后一个语句了,工作该结束了,但程序并没有终止,因为还有一个线程在执行。
   System.exit()虽然可以停止它,但这是强迫程序结束,并且这个方法不是随时可以使用。
   
   “Daemon”线程的作用是在程序的运行期间于后台提供一种“常规”服务,但它并不属于程序的一个基本部分。
   因此,一旦所有非Daemon线程完成,程序也会中止运行。相反,假若有任何非Daemon线程仍在运行
  (比如还有一个正在运行main()的线程),则程序的运行不会中止。  
   修改程序如下:
public class DaemonThread
{
	public static void main(String[] args)
	{
		//匿名类写法!!!
		Thread thread = new Thread(new Runnable()
		{
			public void run()
			{
				while(true)
				{
					System.out.print("T");
				}
			}
		});


		//设定为Daemon线程
		thread.setDaemon(true);
		thread.start();
	}
}   

   可以使用setDaemon()方法来设定一个线程是否为Daemon线程;
   使用isDaemon()方法可以判断一个线程是否为Daemon线程。
   
   Java默认所有从Daemon线程产生的线程也是Daemon线程,因为基本上由一个背景服务线程
   衍生出来的线程,也应该是为了在背景服务而产生的,所以在产生它的线程终止时,它也应该一并跟着终止。

5、线程有优先权,由1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY),默认是5(Thread.NORM_PRIORITY)。
   可以使用Thread的SetPriority()方法来设定优先权,设定必须在1到10之间。
   
   绝大多数操作系统都支持Timeslicing,即为每个线程分配一段CPU;
   在不支持Timeslicing的操作系统中,如果想要让目前线程礼让一下其他线程,
   让它们获得CPU时间,可以调用线程执行的yield()方法。   
   
6、线程的4个主要周期状态是创建线程、可执行(Runnable)、非可执行和终止(Dead)。
   当实例化一个Thread并执行Start()之后,线程进入Runnable状态并开始执行run()方法;
   通过调用sleep(),wait()等方法线程由Runnable状态转为Not Runnable状态;
   当run()方法执行完毕后,线程进入Dead状态。
   
7、有几种状况会让线程进入Not Runnable状态(或是blocked状态)
   1)调用sleep()方法  
   2)调用wait()方法    
   3)等待输入/输出完成
   
   以下几种情况可以让线程回到Runnable状态
   1)调用notify()
   2)调用notifyAll()
   3)调用interrupt()
   4)输入/输出结束
   
   最后线程执行结束,进入Dead状态,可以调用isAlive()方法检测线程是否存活。
   
8、当使用Thread.sleep()让线程暂停执行进入Not Runnable状态时,可以使用interrupt()让它
   离开Not Runnable状态,且会丢出java.lang.InterruptedException异常对象。
   范例如下:
public class InterruptDemo
{
	public static void main(String[] args)
	{
		Thread thread = new Thread(new Runnable()
		{
			public void run()
			{
				try
				{
					//暂停99999ms
					Thread.sleep(99999);
				}
				catch (InterruptedException e)
				{
					System.out.println("I'm interrupted!");
				}
			}
		});


		thread.start();
		thread.interrupt(); //立即中断它
	}
}

9、如果要暂停线程,但暂停时间未知,使用sleep()不是个好办法;
   可以使用wait(),然后让别的线程用notify()或notifyAll()来通知Not Runnable的线程回到Runnable状态。

10、线程的加入(join)
    当线程使用join()加入至另一个线程时,另一个线程会等待这个被加入的线程工作完毕,然后再继续未完成的任务;
    例如:A线程正在运行,然后插入B线程,并且B先执行完毕后,A在继续。
    join()表示将线程加入成为另一个线程的流程之一。
    范例如下:
public class ThreadA
{
	public static void main(String[] args)
	{
		//当前线程为A线程
		System.out.println("线程A执行");


		//声明B线程
		Thread threadB = new Thread(new Runnable()
		{
			public void run()
			{
				try
				{
					System.out.println("线程B开始...");

					for(int i = 0; i < 5; i++)
					{
						Thread.sleep(1000);
						System.out.println("线程B执行...");
					}

					System.out.println("线程B即将结束...");
				}
				catch (InterruptedException e)
				{
					e.printStackTrace();
				}
			}
		});


		//线程B执行后才加入到A
		threadB.start();
		try
		{
			//threadB 加入 threadA
			threadB.join();
		}
		catch (InterruptedException e)
		{
			e.printStackTrace();
		}


		System.out.println("Thread A 执行");
	}
}

    有时候加入的线程不能处理太久,可以在join()方法上指定时间,如join(5000);

11、线程的停止
    查询API会发现Thread的stop()方法已经被标示为deprecated,不建议使用stop()停止一个线程的运行。
【@deprecated】 
这是Java 1.1的新特性。该标记用于指出一些旧功能已由改进过的新功能取代。该标记的作用是建议用户不
    必再使用一种特定的功能,因为未来改版时可能摒弃这一功能。若将一个方法标记为@deprecated,则使用该
    方法时会收到编译器的警告。

如果要停止一个线程,最好自行实现,即执行完run()方法。
1)如果线程的run()方法执行的是一个重复执行的循环,可以提供一个标记(flag)来控制循环;
2)如果线程因为执行sleep()或是wait()而进入Not Runnable状态,而我们想要停止它,则可以
 使用interrupt(),而程序会丢出InterruptedException异常,使得线程离开run()方法。
 范例如下:
public class SomeThread implements Runnable
{
	public void run()
	{
		System.out.println("sleep...至 not runnable状态");
		try
		{
			Thread.sleep(9999);
		}
		catch (InterruptedException e)
		{
			System.out.println("I'm interrupted...");
		}
	}


	public static void main(String[] args)
	{
		Thread thread = new Thread(new SomeThread());
		thread.start();
		thread.interrupt();
	}
}

如果程序因为输入/输出的装置等待而停滞(进入Not Runnable状态),基本上必须等待输入/输出动作完成才能离开。
无法使用interrupt()方法来使得线程离开run()方法,要提供替代的方法,基本上也是引发一个异常,
而这个异常如何引发,要看所使用的输入/输出而定。

例如使用readLine()在等待网络上的一个信息,此时线程进入Not Runnable直到读到一个信息,
让它离开run()的方法就是使用close()关闭它的流,这时会引发一个IOException异常而使得线程离开run()方法。例如:
	public class Client implements Runnable
	{
	   private Socket skt;
	   //...
	   public void terminate()
	   {
	      skt.close();
	   }
	   
	   public void run()
	   {
	      //...
		  try
		  {
		     BufferedReader buf = new BufferedReader(new InputStreamReader(skt.getInputStream()));
			 
			 //读取客户端信息
			 //执行readLine()会进入Not Runnable状态直到读到客户端信息
			 while((userMessage = buf.readLine()) != null)
			 {
			    //...
			 }
		  }
		  catch(IOException e)
		  {
		     System.out.println("线程被终止...");
		  }
	   }
	}

12、ThreadGroup
    在Java中每个线程都属于某个线程组(Thread Group)管理的一员,可以使用下面的指令取得当前进程所属的线程组名称:
Thread.currentThread().getThreadGroup().getName();

每一个线程产生的时候,都会被归入某个线程组,这视该线程是在哪个组中产生而定。
如果没有指定,则归入产生该子线程的线程组中;也可以自行指定线程组,线程一旦归入某个组,就无法更换组。

java.lang.ThreadGroup类可以统一管理整个组中的线程。


13、同步化
    只要继承Thread类或是实现Runnable接口,就可以让对象具有多线程的功能;
但如果多个线程共享某个数据,数据的同步就要特别注意!
范例如下,设计了PersonalInfo类:
public class PersonalInfo
{
	private String name;
	private String id;
	private int count;


	public PersonalInfo()
	{
		name = "nobody";
		id = "N/A";
	}


	public void setNameAndID(String name, String id)
	{
		this.name = name;
		this.id = id;


		if(!checkNameAndIDEqual())
		{
			System.out.println(count + ") illegal name or ID...");
		}
		count++;
	}


	private boolean checkNameAndIDEqual()
	{
		return (name.charAt(0) == id.charAt(0)) ? true : false;
	}
}

测试程序如下:
public class PersonalInfoTest
{
	public static void main(String[] args)
	{
		final PersonalInfo person = new PersonalInfo();


		//假设会有两个线程可能更新person对象
		Thread thread1 = new Thread(new Runnable()
		{
			public void run()
			{
				while(true)
				{
					person.setNameAndID("Justin Lin", "J.L");
				}
			}
		});


		Thread thread2 = new Thread(new Runnable()
		{
			public void run()
			{
				while(true)
				{
					person.setNameAndID("Shang Hwang", "S.H");
				}
			}
		});

		System.out.println("开始测试...");

		thread1.start();
		thread2.start();
	}
}
   
   虽然传给setNameAndID()的变量没有问题,在某个时间点时,thread1设定了Justin、J.L给name和id,
   在进行if测试的前一刻,thread2可能此时刚好调用setNameAndID("Shang Hwang", "S.H")。
   在name被设定为Shang Hwang时,checkNameAndIDEqual()开始执行,此时name等于Shang Hwang,而id还是J.L,
   所以checkNameAndIDEqual()就会返回false,结果就显示错误信息。
   
14、使用synchronized可以进行同步化操作。
    它有下面几种用法:
1)一种使用方式是用于方法上,让方法的范围内都成为被同步化区域。例如:
   
public synchronized void setNameAndID(String name, String id)
	   {
		  this.name = name;
		  this.id = id;


		  if(!checkNameAndIDEqual())
		  {
			  System.out.println(count + ") illegal name or ID...");
		  }
		  count++;
	   }

  被标示为synchronized的方法就成为被同步化区域的一员,当线程执行某个对象的被同步化方法时,
  线程会在对象上得到一个锁定,锁定所有同样被标示为synchronized的区域,不让其他的线程执行这些区域。
  
2)也可以用于限定某个程序区块为被同步化区域。例如:
   public void setNameAndID(String name, String id)
       {
		   synchronized(this)
		   {
			   this.name = name;
		       this.id = id;


	    	   if(!checkNameAndIDEqual())
	    	   {
	     		   System.out.println(count + ") illegal name or ID...");
	    	   }
    		   count++;
   		   }
	   }

3)也可以标示某个对象要求同步化。
  例如在多线程存取同一个ArrayList对象时,由于ArrayList并没有实现数据存取时的同步化,所以当它
  使用于多线程环境时,必须注意多个线程存取同一个ArrayList时,有可能发生两个以上的线程将数据
  存入ArrayList的同一个位置,造成数据的相互覆盖。
  为了确保数据存入时的正确性,可以在存取ArrayList对象时要求同步化。例如:
  //arrayList参考至一个ArrayList的一个实例
  synchronized(arrayList)
  {
     arrayList.add(new SomeClass());
  }
  
15、wait()和notify()
    wait()、notify()和notifyAll()是由Object类所提供的方法,因为java中所有的对象的最顶层
都继承自Object,所以在定义自己的类时会继承这些方法,它们都被声明为final,所以无法重新定义它们。
通过这3个方法可以要求线程进入等待,或是通知线程回到Runnable状态。

一定要搞清楚对象的方法和调用它的线程之间的关系!!! 如下:

必须在被同步化的方法或区块中调用wait()方法,当对象的wait()方法被调用时,当前的线程
会被放入对象的等待集合(Wait Set)中,线程会解除对对象的锁定,其他的线程可以竞争被同步化区块。
被放在等待集中的线程将不参与线程的排队,wait()可以指定等待的时间,如果指定时间,则时间到之后
线程会再度加入排队,如果指定时间为0或不指定,则线程会持续等待,知道被中断,或是被告知参与排队。

当对象的notify()被调用时,它会从当前对象的等待集中通知一个线程加入排队,被通知的线程是随机的,
被通知的线程会与其他正在执行的线程共同竞争对对象的锁定。
如果调用notifyAll(),则所有在等待集中的线程都会被通知加入排队,这些线程会与其他正在执行的线程共同竞争对对象的锁定。

简单地说,当线程调用wait()方法时,表示它要先让出对象的被同步区使用权并等待通知,或是等待
一段指定的时间,直到被通知或时间到时再与其他线程竞争,如果可以执行时就从等待点开始执行!

说明wait()、notify()和notifyAll()最常见的例子,就是生产者和消费者。
范例如下:生产者每次生产一个int整数交给店员,而消费者从店员处取走整数,店员一次只能持有一个整数。
【Clerk.java】
public class Producer implements Runnable
{
	private Clerk clerk;


	public Producer(Clerk clerk)
	{
		this.clerk = clerk;
	}


	public void run()
	{
		System.out.println("生产者开始生产整数...");


		//生产1到10的整数
		for(int product = 1; product <= 10; product++)
		{
			try
			{
				//暂停随机时间
				Thread.sleep((int)Math.random() * 3000);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}

			//将产品交个店员
			clerk.setProduct(product);
		}
	}
}
【Producer.java】
public class Producer implements Runnable
{
	private Clerk clerk;


	public Producer(Clerk clerk)
	{
		this.clerk = clerk;
	}


	public void run()
	{
		System.out.println("生产者开始生产整数...");


		//生产1到10的整数
		for(int product = 1; product <= 10; product++)
		{
			try
			{
				//暂停随机时间
				Thread.sleep((int)Math.random() * 3000);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}


			//将产品交个店员
			clerk.setProduct(product);
		}
	}
}

  【Consumer.java】
public class Consumer implements Runnable
{
	private Clerk clerk;


	public Consumer(Clerk clerk)
	{
		this.clerk = clerk;
	}


	public void run()
	{
		System.out.println("消费者开始消耗整数...");
		//消耗10个整数
		for(int i = 1; i <= 10; i++)
		{
			try
			{
				//等待随机时间
				Thread.sleep((int)Math.random() * 3000);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}


			//从店员处取走整数
			clerk.getProduct();
		}
	}
}

    【ProductTest.java】
public class ProductTest
{
	public static void main(String[] args)
	{
		Clerk clerk = new Clerk();


		//生产者线程
		Thread producerThread = new Thread(new Producer(clerk));
		//消费者线程
		Thread consumerThread = new Thread(new Consumer(clerk));


		producerThread.start();
		consumerThread.start();
	}
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值