----------------------ASP.Net+Unity开发、.Net培训、期待与您交流! ----------------------
一.多线程的概念
以往开发的程序大多是单线程的,即一个程序只有一条从头至尾的执行线索。然而现实世界中的很多过程都具有多条线索同时动作的特性:例如,我们可以一边看电视,一边活动胳膊,如果不容许这样做,我们会感觉很难受。再如一个网络服务器可能需要同时处理多个客户机的请求等。
Java的一大特性点就是内置对多线程的支持。多线程是指同时存在几个执行体,按几条不同的执行线索共同工作的情况,它使得编程人员可以很方便地开发出具有多线程功能,能同时处理多个任务的功能强大的应用程序。虽然执行线程给人一种几个事件同时发生的感觉,但这只是一种错觉,因为计算机在任何给定的时刻只能执行那些线程中的一个。为了建立这些线程正在同步执行的感觉,Java快速地把控制从一个线程切换到另一个线程。
观察下列代码:
- <span style="font-size:12px;"> public static void main(String[] args) {
- // TODO Auto-generated method stub
- while(true) {
- System.out.println("JAVA");
- }
- while(true) {
- System.out.println("C++");
- }
- }</span>
上述代码是有问题的,因为第二个while语句是永远没有机会执行的代码。如果能在程序中创建两个线程,每个线程分别执行一个while循环,那么两个循环就都有机会执行,即一个线程中的while语句执行一段时间后,就会轮到另一个线程中的while语句执行一段时间,这是因为Java虚拟机(JVM)负责管理这些线程,这CPU将被轮流执行,使得每个线程有机会使用CPU资源.
1.线程与进程
进程是程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也是进程本身从产生、发展至消亡的过程。线程是比进程更小的执行单位。一个进程在其执行过程中,可以产生多个线程,形成多条执行线索,每条线索,即每个线程也有它自身的产生、存在和消亡的过程,也是一个动态的概念。每个进程都有一段专用的内存区域,与此不同的是,线程间可以共享相同的内存单元(包括代码与数据),并利用这些共享单元来实现数据交换、实时通信与必要的同步操作。多线程的程序能更好地表达和解决现实世界的具体问题,是计算机应用开发和程序设计的一个必然发展趋势。
每个Java程序都有一个默认的主线程。Java应用程序总是从主类的main方法开始执行。当JVM加载代码,发现main方法之后,就会启动一个线程,这个线程称作“主线程”,该线程负责执行main方法。在main方法的执行中再创建的线程,就称为程序中的其他线程。如果main方法中没有创建其他的线程,那么当main方法执行完最后一个语句,即main方法返回时,JVM就会结束Java应用程序。如果main方法中又创建了其他线程,那么JVM就要在主线程和其他线程之间轮流切换,保证每个线程都有机会使用CPU资源,main方法即使执行完最后的语句,JVM也不会结束程序,JVM一直要等到程序中的所有线程都结束之后,才结束Java应用程序。
2.线程的状态与生命周期
Java使用Thread类及其子类的对象来表示线程,新建的线程在它的一个完整的生命周期中通常要经历如下的4种状态。
(1)新建
当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。此时它已经有了相应的内存空间和其他资源。
(2)运行
线程创建之后就具备了运行的条件,一旦轮到它来享用CPU资源时,即JVM将CPU的使用权切换给该线程时,此线程就可以脱离创建它的主线程,独立开始自己的生命周期了。
线程创建后仅仅是占有了内存资源,在JVM管理的线程中还没有这个线程,此线程必须调用startO方法(从父类继承的方法)通知JVM,这样JVM就会知道又有一个新线程捧队等候切换了。
当JVM将CPU的使用权切换给线程时,如果线程是Thread的子类创建的,该类中run方法就立刻执行。所以必须在子类中重写父类的run方法,Thread类中的run方法没有具体内容,程序要在Thread类的子类中重写run方法来覆盖父类的run方法,run方法规定了该线程的具体使命。在线程没有结束run0方法之前,不要让线程再调用start0方法,否则将发生ILLegalThreadStateException异常。
(3)中断
有4种原因的中断:
1)JVM将CPU资源从当前线程切换给其他线程,使本线程让出CPU的使用权处于中断状态。
2)线程使用CPU资源期间,执行了sleep(int millsecond)方法,使当前线程进入休眠状态。slecp(int millsecond)方法是Thread类中的一个类方法,线程一旦执行了slecp(int millsecond)方法,就立刻让出CPU的使用权,使当前线程处于中断状态。经过参数millsecond指定的毫秒数之后,该线程就重新进到线程队列中捧队等待CPU资源,以便从中断处继续运行。
3)线程使用CPU资源期间,执行了wait0方法,使得当前线程进入等待状态。等待状态的线程不会主动进到线程队列中排队等待CPU资源,必须由其他线程调用notify0方法通知它,使得它重新进入到线程队列中排队等待CPU资源,以便从中断处继续运行。有关wait()、noftify()和notifyall()方法将在本博客后面详细讨论。
4)线程使用CPU资源期间,执行某个操作进入阻塞状态,比如执行读/写操作引起阻塞。进入阻塞状态时线程不能进入排队队列,只有当引起阻塞的原因消除后,线程才重新进入到线程队列中排队等待CPU资源,以便从原来中断处继续运行。
(4)死亡
处于死亡状态的线程不具有继续运行的能力。线程死亡的原因有二个:一个是正常运行的线程完成了它的全部工作,即执行完run方法中的全部语句,结束了run方法:Thread原因是线程被提前强制性地终止,即强制run方法结束。所谓死亡状态就是线程释放了实体,即释放分配给线程对象的内存。现在,看一个完整的例子,通过分析运行结果阐述线程的4种状态。该例子中用Thread的子类创建了两个线程,模拟猫狗线程。
- <span style="font-size:12px;">public class ThreadDemo2 {
- /**一个多线程例子的展示
- * @黑马ZWF
- */
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- Cat c = new Cat(); //为猫类创建对象
- Dog d = new Dog(); //为狗类创建对象
- c.start(); //启动多线程
- d.start(); //启动多线程
- for(int x = 0; x < 8; x++) {
- System.out.println("我是主人");
- }
- }
- }
- class Cat extends Thread {
- public void run() { //重写run方法
- for(int x = 0; x < 8; x++) {
- System.out.println("我是猫");
- }
- }
- }
- class Dog extends Thread {
- public void run() { //重写run方法
- for(int x = 0; x < 8; x++) {
- System.out.println("我是狗");
- }
- }
- }</span>
二. 线程创建的两种方式:继承Thread类和使用Runnable接口
1.利用Thread类的子类创建线程
其实上面的ThreadDemo2就是继承Thread类来进行线程的创建。下面我们来具体介绍如何用Thread类的子类来创建对象。
在编写Thread类的子类时,需要重写父类的run()方法,其目的是规定线程的具体操作,否则线程就什么也不做,因为父类的run()方法中没有任何操作语句。
下面例子中除主线程外还有两个线程,这两个线程共享一个对象,两个线程在运行期间修改这个对象的成员变量。为了使结果尽量不依赖于当前CPU资源的使用情况。应当让线程主动调用sleep方法让出CPU的使用权进入中断状态,sleep方法是Thread类的静查布法,线程在占用CPU资源期间,通过调用sleep方法来使自己最弃CPU资源,休眠一段时间。代码示例如下:
- public class ThreadDemo3 {
- /**用Thread类的子类创建进程
- * @黑马ZWF
- */
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- ComputerSum sum = new ComputerSum();
- People teacher = new People("老师", 200, sum);
- People student = new People("学生", 200, sum);
- teacher.start() ;
- student.start() ;
- }
- }
- class ComputerSum {
- int sum;
- public void setSum(int n) {
- sum = n;
- }
- public int getSum() {
- return sum;
- }
- }
- class People extends Thread {
- int timeLength; //线程休眠的时间长度
- ComputerSum sum;
- People(String name, int timeLength, ComputerSum sum) {
- setName(name); //调用Thread类的方法setName为线程起个名字
- this.timeLength = timeLength;
- this.sum = sum;
- }
- public void run() {
- for(int i = 1; i <= 5; i++) {
- int m = sum.getSum();
- sum.setSum(m + 1);
- System.out.println("我是" + getName() +",现在的和:" + sum.getSum());
- try {
- sleep(timeLength);
- }
- catch(InterruptedException e){
- System.out.println("发生InterruptedException异常");
- }
- }
- }
- }
运行结果如下:
2.使用Runnable接口创建进程(推荐)
使用Thread子类创建线程的优点是:可以在子类中增加新的成员变量,使线程具有某种属性,也可以在子类中新增加方法,使线程具有某种功能。但是,Java不支持多继承,Thread类的子类不能再扩展其他的类。但是使用Runnable接口的话,就可以实现扩展,提高代码的灵活性。
1)Runnable接口与目标对象
创建线程的另一个途径就是用Thread类直接创建线程对象。使用Thread创建线程对象时,通常使用的构造方法是:
thread(Runnable target)
该构造方法中的参数是一个Runnable类型的接口,因此,在创建线程对象时必须向构造方法的参数传递一个实现Runnable接口类的实例,该实例对象称作所创线程的目标对象,当线程调用start()方法后,一旦轮到它来享用CPU资源,目标对象就会自动调用接口中的run()方法(接口回调),这一过程是自动实现的,用户程序只需要让线程调用start()方法即可,也就是说,当线程被调度并转入运行状态时,所执行的就是run()方法中所规定的操作。
线程间可以共享相同的内存单元(包括代码与数据),并利用这些共享单元来实现数据交换、实时通信与必要的同步操作。对于Thread(Runnable target)杓造方法创建的线程,轮到它来享用CPU资源时,目标对象就会自动调用接口中的run()方法,因此,对于使用同一目标对象的线程,目标对象的成员变量自然就是这些线程共亨的数据单元。另外,创建目标对象类在必要时还可以是某个特定类的子类,因此,使用Runnable接口比使用Thread类的子类更具有灵活性.下面代码示例是Runnable接口创建多线程的展示,改动自ThreadDemo2.
- public class ThreadDemo4 {
- /**Runnable接口创建多线程的展示,改动自ThreadDemo2
- * @黑马ZWF
- */
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- Cat c = new Cat();
- Dog d = new Dog();
- Thread t1 = new Thread(c); //使用Runnable接口的创建方法
- Thread t2 = new Thread(d);
- t1.start(); //启动多线程t1
- t2.start(); //启动多线程t2
- }
- }
- class Cat implements Runnable {
- public void run() { //重写run方法
- for(int x = 0; x < 8; x++) {
- System.out.println("我是猫");
- }
- }
- }
- class Dog implements Runnable {
- public void run() { //重写run方法
- for(int x = 0; x < 8; x++) {
- System.out.println("我是狗");
- }
- }
- }
2)关于run()方法中的局部变量
对于具有相同目标对象的线程,当其中一个线程享用CPU资源时,目标对象自动调用接口中的run()方法,这时,run()方法中的局部变量被分配内存空间,当轮到另一个线程享用CPU资源时,目标对象会再次调用接口中的run()方法.那么,run0方法中的局部变量会再次分配内存空间。也就是说run()方法已经启动运行了两次,分别运行在不同的线程中,即运行在不同的时间片内。不同线程的run()方法中的局部变量互不十扰,一个线程改变了自己的run()方法中局部变量的值不会影响其他线程的run()方法中的局部变量的值。
- public class ThreadDemo5 {
- /**展示多线程run方法局部变量的互补干扰性
- * @黑马ZWF
- */
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- Move move=new Move();
- move.zhangsan.start();
- move.lisi.start();
- }
- }
- class Move implements Runnable{
- Thread zhangsan,lisi;
- Move() {
- zhangsan=new Thread(this);
- zhangsan.setName("张三");
- lisi=new Thread(this);
- lisi.setName("李四");
- }
- public void run() {
- int i=0;
- while(i<=5) {
- if(Thread.currentThread()==zhangsan) {
- i=i+1;
- System.out.println(zhangsan.getName()+"线程的局部变量i="+i);
- }
- else if(Thread.currentThread()==lisi) {
- i=i+1;
- System.out.println(lisi.getName()+"线程的局部变量i="+i);
- }
- try {
- Thread.sleep(800);
- }
- catch(InterruptedException e){}
- }
- }
- }
输出结果如下:
三.线程的同步和锁
1.锁的原理
Java中每个对象都有一个内置锁。当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。释放锁是指持锁线程退出了synchronized同步方法或代码块。
关于锁和同步,有一下几个要点:
1)只能同步方法,而不能同步变量和类;
2)每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
3)不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4)如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5)如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。
6)线程睡眠时,它所持的任何锁都不会释放。
7)线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
8)同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9)在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。例如:
- public int fix(int y) {
- synchronized (this) {
- x = x - y;
- }
- return x;
- }
当然,同步方法也可以改写为非同步方法,但功能完全一样的,例如:
- public synchronized int getX() {
- return x++;
- }
和
- public int getX() {
- synchronized (this) {
- return x;
- }
- }
效果是完全一样的。
2.线程安全类
当一个类已经很好的同步以保护它的数据时,这个类就称为“线程安全的”。
即使是线程安全类,也应该特别小心,因为操作的线程是间仍然不一定安全。
举个形象的例子,比如一个集合是线程安全的,有两个线程在操作同一个集合对象,当第一个线程查询集合非空后,删除集合中所有元素的时候。第二个线程也来执行与第一个线程相同的操作,也许在第一个线程查询后,第二个线程也查询出集合非空,但是当第一个执行清除后,第二个再执行删除显然是不对的,因为此时集合已经为空了。
代码示例:
- public class ThreadDemo6 {
- public static void main(String[] args) {
- final NameList nl = new NameList();
- nl.add("aaa");
- class NameDropper extends Thread{
- public void run(){
- String name = nl.removeFirst();
- System.out.println(name);
- }
- }
- Thread t1 = new NameDropper();
- Thread t2 = new NameDropper();
- t1.start();
- t2.start();
- }
- }
- public class NameList {
- private List nameList = Collections.synchronizedList(new LinkedList());
- public void add(String name) {
- nameList.add(name);
- }
- public String removeFirst() {
- if (nameList.size() > 0) {
- return (String) nameList.remove(0);
- } else {
- return null;
- }
- }
- }
虽然集合对象
private List nameList = Collections.synchronizedList(new LinkedList());
是同步的,但是程序还不是线程安全的。
出现这种事件的原因是,上例中一个线程操作列表过程中无法阻止另外一个线程对列表的其他操作。解决上面问题的办法是,在操作集合对象的NameList上面做一个同步。改写后的代码如下:
- public class NameList {
- private List nameList = Collections.synchronizedList(new LinkedList());
- public synchronized void add(String name) {
- nameList.add(name);
- }
- public synchronized String removeFirst() {
- if (nameList.size() > 0) {
- return (String) nameList.remove(0);
- } else {
- return null;
- }
- }
- }
这样,当一个线程访问其中一个同步方法时,其他线程只有等待。
四.在同步方法中使用wait()、notify()和notifyAII()方法
在上面的介绍中已经知道,当一个线程正在使用一个同步方法(用synchronized修饰的方法)时,其他线程就不能使用这个同步方法。对于同步方法,有时涉及某些特殊情况,比
如当一个人在售票窗口排队购买电影票时,如果给售票员的钱不是零钱,而售票员又没有零钱找时,那么就必须等待,并允许后面的人买票,以便售票员获得零钱后找零。如果第二个人仍没有零钱,那么两人必须等待,并允许后面的人买票。
当一个线程使用的同步方法中用到某个变量,而此变量又需要其他线程修改后才能符合本线程的需要,那么可以在同步方法中使用wait0方法。使用wait()方法可以中断方法的执行,使本线程等待,暂时让出CPU的使用权,并允许其他线程使用这个同步方法。其他线程如果在使用这个同步方法时不需要等待,那么它使用完这个同步方法的同时,应当用notifyAIIO方法通知所有的由于使用这个同步方法而处于等待的线程结束等待。曾中断的线程就会从刚才的中断处继续执行这个同步方法,并遵循“先中断先继续”的原则。如果使用notify()方法,那么只是通知处于等待中的线程的某一个结束等待。
wait()、notify()和notifyAll()都是Object粪中的final方法,被所有的类继承、且不允许重写的方法。
- public class ThreadDemo7 {
- /**生产者,消费者案例。多线程不同方向操作数据解决这个问题的办法:
- 1. 线程只要被唤醒,就必须判断标记
- 2. 唤醒全部的线程
- * @黑马ZWF
- */
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- Rescourc r = new Rescourc();//资源产品
- Pro p = new Pro(r);//生产者
- Cus c = new Cus(r);//消费者
- Thread t1 = new Thread(p);
- Thread t2 = new Thread(c);
- Thread t3 = new Thread(p);
- Thread t4 = new Thread(c);
- t1.start();
- t2.start();
- t3.start();
- t4.start();
- }
- }
- class Rescourc { //定义产品
- private String name ; //产品的名字
- private int count = 0 ; //产品的计数器
- private boolean flag = false;
- public synchronized void set(String name) { //提供一个赋值的方法,生产产品
- while(flag==true) { //生产完了,没消费呢,不能生产了
- try{ this.wait();}catch(Exception e){}
- }
- this.name = name + count;
- count++;
- System.out.println(Thread.currentThread().getName()+
- "生产---"+this.name);
- flag = true; //标记改成true
- this.notifyAll();
- }
- public synchronized void get() { //提供一个获取值的方法,消费产品
- while(flag==false){ //消费完了,没生产了,不能再消费了
- try{ this.wait();}catch(Exception e){}
- }
- System.out.println(Thread.currentThread().getName()+
- "消费======="+this.name);
- flag = false;
- this.notifyAll();
- }
- }
- class Pro implements Runnable { //定义生产者类,实现接口,覆盖run方法
- private Rescourc r ;
- Pro(Rescourc r){ this.r = r;}
- public void run(){
- while(true){
- r.set("鼠标");
- }
- }
- }
- class Cus implements Runnable { //定义消费者类,实现接口,覆盖run方法
- private Rescourc r ;
- Cus(Rescourc r){ this.r = r;}
- public void run(){
- while(true){
- r.get();
- }
- }
- }