java线程详解

来源:互联网 发布:梦龙即时通讯软件 编辑:程序博客网 时间:2024/06/08 04:17

线程与进程的区别

(1)程序是一段静态的代码,进程是程序的一次动态执行过程,它是操作系统资源调度的基本单位。线程是比进程更小的执行单位,一个进程在其执行过程中,可以产生多个线程,所以又称线程为“轻型进程”。虽然说可以并发运行多个线程,但在任何时刻cpu只运行一个线程,只是宏观上看好像是同时运行,其实微观上它们只是快速交替执行的。这就是java中的多线程机制。
(2)不同进程的代码、内部数据和状态都是完全独立的,而一个程序内的多线程是共享同一块内存空间和同一组系统资源的,有可能互相影响。
(3)线程切换比进程切换的负担要小。

线程的创建

java提供了类java.lang.Thread来支持多线程编程,创建线程主要有两种方法:

(1)继承Thread类

Thread类中的run 方法是空的,直接通过 Thread类实例化的线程对象不能完成任何事,所以可以通过继承Thread 类,重写run 方法,实现具有各种不同功能的线程类。
run()又称为线程体,不能直接调用run(),而是通过调用start(),让线程自动调用run(),因为start()会首先进行与多线程相关的初始化(即让start()做准备工作)。

class ThreadType extends Thread{        public void run(){   //重写Thread类中的run 方法         ……        }   }  

(2)实现Runnable接口

java只允许单继承,如果类已经继承了其他类,就不能再继承Thread类了,所以提供了实现Runnable接口来创建线程的方式。
该接口只定义了一个run方法,在新类中实现它即可。Runnable接口并没有任何对线程的支持,还必须通过创建Thread类的实例,将Rnnable接口对象作为Thread类构造方法的参数传递进去,从而创建一个线程。如:

 class ThreadDemo3 implements Runnable {          // 重载run函数          public void run() {              for (int count = 1, row = 1; row < 10; row++, count++){ // 循环计算输出的*数目                  for (int i = 0; i < count; i++){ // 循环输出指定的count数目的*                      System.out.print('*');                   }                  System.out.println();               }          }          public static void main(String argv[]) {              Runnable rb = new ThreadDemo3(); // 创建,并初始化ThreadDemo3对象rb              Thread td = new Thread(rb); // 通过Thread创建线程              td.start(); // 启动线程td          }      }  

注意:如果当前线程是通过继承Thread类创建的,则访问当前线程可以直接使用this,如果当前线程是通过实现Runnable接口创建的,则通过调用Thread.currentThread()方法来获取当前线程。

线程的生命周期

按照线程体在计算机系统内存中状态的不同,可以将线程分为以下5种状态:
(1)创建状态
新建一个线程对象,仅仅作为一个实例存在,JVM没有为其分配运行资源。
(2)就绪状态
创建状态的线程调用start方法后,转换为就绪状态,此时线程已得到除CPU时间之外的其他系统资源,一旦获得CPU,就进入运行状态。注意的是,线程没有结束run()方法之前,不能再调用start()方法,否则将发生IllegalThreadStateException异常,即启动的线程不能再启动。
(3)运行状态
就绪状态的线程获取了CPU,执行程序代码。
(4)阻塞状态
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种: (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。 (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。 (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(5)死亡状态
线程死亡的原因有两个:一是执行完了线程体(run方法),二是因为异常run方法被强制性终止。如果线程进入死亡状态,JVM会收回线程占用的资源(释放分配给线程对象的内存)。
注意:调用stop()可以使线程立即进入死亡状态,不过该方法现在已经不推荐使用了,线程的退出通常采用自然终止的方法,不建议人工强制停止,容易引起“死锁”。

转换图如下:

这里写图片描述

从图中,可以看出,比较复杂的是就绪状态和阻塞状态转换的过程,java提供了大量的方法来支持阻塞,下面一 一说明:
sleep():可以以毫秒为单位,指定休眠一段时间(作为参数),时间一过,又进入就绪状态。
wait()和notify():wait使得线程进入阻塞状态,它有两种形式,一种是允许指定以毫秒为单位的一段时间作为参数的,另一种是无参数的。前者当对应的notify方法被调用或超出指定时间时线程重新进入就绪状态,后者则必须调用notify方法才能重新进入就绪状态。
注意:此外,还有suspend方法(对应的恢复则用resume方法)也能使线程进入阻塞状态,不过这个方法现在已经不提倡使用了,会引起“死锁”,因为调用该方法会释放占用的所有资源,由JVM调度转入临时存储空间。

线程调度和优先级

java采用抢占式调度,即优先级高线程的先运行,优先级相同的交替运行
java将线程的优先级分为10个等级,1-10,数字越大表明线程的级别超高,可以通过setPriority方法设置线程优先级。
在java中有一个比较特殊的线程称为守护线程,它具有最低的优先级,用于为系统中的其他线程对象提供服务。典型的就是JVM中的系统资源自动回收线程。

线程互斥(银行取款问题)

线程互斥是什么?什么时候要用到线程互斥呢?

发现问题

举个例子,假设你的银行账户有100元,并且你和你的妻子两人都知道账户密码,如果某一天,你去取100元,银行系统会先查看你的账户够不够100元,明显你是满足条件的,但是,如果此时你的妻子也需要去取100元,并且你的取钱线程刚好因为某些状况被打断了(这时系统还来不及修改你的账户余额),所以你的妻子去取钱时也满足条件,所以她完成了取钱动作,而你取钱线程恢复之后,你也将完成取钱动作。大家可以发现共享数据(账户余额)的完整性被破坏了,两人都从银行里取出了一百元,而账户明明只有一百元,如果现实中真发生这种情况,估计银行就要哭晕在厕所了。代码及运行结果如下:

//Account.javapublic class Acount{  double balance;  public Acount(double money){    balance = money;    System.out.println("Totle Money: "+balance);  }}//AccountThread.javaclass Account{    double balance;    public Account(double money)    {        balance = money;        System.out.println("Totle Money: " + balance);    }}public class AccountThread extends Thread{    Account Account;    int delay;    public AccountThread(Account Account, int delay)    {        this.Account = Account;        this.delay = delay;    }    public void run()    {        if (Account.balance >= 100) {            try {                sleep(delay);                Account.balance = Account.balance - 100;                System.out.println("withdraw  100 successful!");            } catch (InterruptedException e) {            }        } else            System.out.println("withdraw failed!");    }    public static void main(String[] args)    {        Account Account = new Account(100);        AccountThread AccountThread1 = new AccountThread(Account, 1000);        AccountThread AccountThread2 = new AccountThread(Account, 0);        AccountThread1.start();        AccountThread2.start();    }}

这里写图片描述

解决问题

为了解决这个问题,java提供了线程互斥,通过synchronized关键字为共享的资源或数据加锁,避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。在java语言中,每一个对象都有一把内置锁。线程进入同步代码块或方法的时候会通过synchronized关键字自动获取该对象上的内置锁,其他需要获取该锁的线程,必须等待当前拥有该锁的线程将其释放,从而保证任一时刻,只有一个线程访问共享资源。
为了接下来更好地理解synchronized用法的一些区别,我们先引入两个概念:对象锁类锁
java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。 但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的

synchronized详解

synchronized的用法:修饰方法和修饰代码块。
下面分析synchronized这两种用法在对象锁和类锁上有什么区别

(1)对象锁——synchronized修饰方法和代码块

    public class TestSynchronized       {            public void test1()           {            /*       synchronized修饰代码块。传入的对象实例是this,表明是当前对象,当然,如果需要同步其他对象实例,也可传入其他对象的实例        */             synchronized(this)               {                      int i = 5;                      while( i-- > 0)                     {                           System.out.println(Thread.currentThread().getName() + " : " + i);                           try                          {                                Thread.sleep(500);                           }                          catch (InterruptedException ie)                          {                           }                      }                 }            }              /*          synchronized修饰方法。因为前面同步代码块中传入参数是this,所以两个公共资源代码所需要获得的对象锁都是同一个对象锁          */        public synchronized void test2()           {                 int i = 5;                 while( i-- > 0)                {                      System.out.println(Thread.currentThread().getName() + " : " + i);                      try                     {                           Thread.sleep(500);                      }                     catch (InterruptedException ie)                     {                      }                 }            }            public static void main(String[] args)           {                 final TestSynchronized myt2 = new TestSynchronized();               /*             main方法中分别开启两个线程(这两个线程的run()方法分别调用test1和test2方法),因为两个公共资源代码所需要获得的对象锁都是同一个对象锁,所以当有一个线程获得锁时,另一个线程必须等待。上面也给出了运行的结果可以看到:直到test1线程执行完毕,释放掉锁,test2线程才开始执行。             */               Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );                 Thread test2 = new Thread(  new Runnable() {  public void run() { myt2.test2();   }  }, "test2"  );                 test1.start();;                 test2.start();        //         TestRunnable tr=new TestRunnable();      //         Thread test3=new Thread(tr);      //         test3.start();          }       }  

运行结果:

这里写图片描述

如果我们把test2方法的synchronized关键字去掉,执行结果会如何呢?

这里写图片描述

我们可以看到,结果输出是交替着进行输出的,这是因为,虽然某个线程得到了对象的内置锁(即可以访问同步的方法或代码),但是另一个线程还是可以访问该对象的,即访问没有进行加锁的方法或者代码,所以加锁方法和没加锁方法之间是互不影响的。
(这里说一个题外话,代码里面明明是先开启test1线程,为什么先执行的是test2呢?这是因为java编译器在编译成字节码的时候,会根据实际情况对代码进行一个重排序,编译前代码写在前面,在编译后的字节码不一定排在前面,所以这种运行结果是正常的)

(2)类锁——synchronized修饰(静态)方法和代码块:

    public class TestSynchronized       {            public void test1()           {                 synchronized(TestSynchronized.class)                {                      int i = 5;                      while( i-- > 0)                     {                           System.out.println(Thread.currentThread().getName() + " : " + i);                           try                          {                                Thread.sleep(500);                           }                          catch (InterruptedException ie)                          {                           }                      }                 }            }            public static synchronized void test2()           {                 int i = 5;                 while( i-- > 0)                {                      System.out.println(Thread.currentThread().getName() + " : " + i);                      try                     {                           Thread.sleep(500);                      }                     catch (InterruptedException ie)                     {                      }                 }            }            public static void main(String[] args)           {                 final TestSynchronized myt2 = new TestSynchronized();                 Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );                 Thread test2 = new Thread(  new Runnable() {  public void run() { TestSynchronized.test2();   }  }, "test2"  );                 test1.start();                 test2.start();        //         TestRunnable tr=new TestRunnable();      //         Thread test3=new Thread(tr);      //         test3.start();          }       }  

执行结果如下:

这里写图片描述

从中可以看出,两个同步代码所需要获得的对象锁都是同一个对象锁,即synchronized修饰静态方法所对应的锁为类锁(即TestSynchronized.class),注意喔,类锁只是我们为了方便区别静态方法的特点而抽象出来的一个概念,因为静态方法是所有对象实例共用的,所以对应着synchronized修饰的静态方法的锁也是唯一的,所以抽象出来个类锁。
为了更好地这证明类锁和对象锁是两个不一样的锁,我们同时用synchronized修饰静态方法和普通的方法,看看运行结果如何

    public class TestSynchronized       {            public synchronized void test1()   //修饰普通方法        {                      int i = 5;                      while( i-- > 0)                     {                           System.out.println(Thread.currentThread().getName() + " : " + i);                           try                          {                                Thread.sleep(500);                           }                          catch (InterruptedException ie)                          {                           }                      }            }            public static synchronized void test2()  //修饰静态方法         {                 int i = 5;                 while( i-- > 0)                {                      System.out.println(Thread.currentThread().getName() + " : " + i);                      try                     {                           Thread.sleep(500);                      }                     catch (InterruptedException ie)                     {                      }                 }            }            public static void main(String[] args)           {                 final TestSynchronized myt2 = new TestSynchronized();                 Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );                 Thread test2 = new Thread(  new Runnable() {  public void run() { TestSynchronized.test2();   }  }, "test2"  );                 test1.start();                 test2.start();        //         TestRunnable tr=new TestRunnable();      //         Thread test3=new Thread(tr);      //         test3.start();          }       }  

运行结果:

这里写图片描述

可见,线程是交替执行的,这就验证了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。而且,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。

总结:
1、无论是同步代码块还是同步方法,必须获得对象锁才能够进入同步代码块或者同步方法进行操作。
2、同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
3、如果采用方法级别的同步,对象锁为方法所在的对象;如果是静态同步方法,对象锁为方法所在的类(唯一)。
4、对于代码块,对象锁即指synchronized(object)中的object。

此处参考了博客:http://langgufu.iteye.com/blog/2152608

线程同步(生产-消费者模型)

线程互斥和线程同步都是指,某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。不同的是,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问(有序交替执行),而线程互斥无法限制访问者对资源的访问顺序,即访问是无序的(一个线程释放锁之后,不能保证什么时候再次获得锁)。
一言蔽之,同步是一种更复杂的互斥
一个典型的线程同步的应用是生产-消费者模型。其约束条件为:
(1)生产者生产产品,并将其保存到仓库中。
(2)消费者从仓库中取得产品。
(3)由于库房容量有限,因此只有当库房还有空间时,生产者才可以将产品放入库房;否则只能等待。
(4)只有库房中存在满足数量的产品时,消费者才能取走产品,否则只能等待。
实际应用中,很多例子都可以归结为该模型。这里举个例子,还是之前存款和取款的问题。假设存在一个账户对象(仓库)及两个线程:存款线程(生产者)和取款线程(消费者),并对其进行如下的限制:

  • 只有当账户上的余额balance=0时,存款线程才可以存进100元;否则只能等待。
  • 只有当账户上的余额balance=100时,取款线程才可以取走100元;否则只能等待。

    根据生产-消费者模型,应该得到一个交替执行的运行序列:存款100元、取款100元、存款100元、取款100元……很明显,使用前面的互斥对象是无法完成这两个线程的同步问题的。为了实现线程同步,java为互斥对象提供了两个方法:一个是wait();另一个是notify()。(可见,同步确实是在互斥的基础上加上某些机制实现次序访问的)
    要注意的是,这两个方法是作为互斥对象的方法来实现的,而不是作为Thread类的方法实现,并且,必须将这两个方法放在临界代码段中(synchronized修饰的代码),也就是说执行该方法的线程必须已获得了互斥对象的互斥锁,因为这两个方法实际上也是在操作互斥对象的互斥锁。
    wait():阻塞线程,释放互斥对象的互斥锁。(而sleep方法阻塞线程后,并不释放互斥锁)
    notify():当另一个线程调用互斥对象的notify()方法时,该互斥对象等待队列中的第一个线程才能进入就绪状态。
    例子代码及运行结果如下:

//Account4.javapublic class Account4 {  double balance;  public Account4(){     balance = 0;     System.out.println("Totle Money: "+balance);   }   /*   取款   */   public synchronized void withdraw(double money){       if(balance == 0)         try{              wait();  //使取款线程进入阻塞状态,并释放互斥对象的互斥锁             }catch(InterruptedException e){        }         balance = balance - money;         System.out.println("withdraw 100 success");         notify();     //使存款线程进入就绪状态       }   /*   存款   */   public synchronized void deposite(double money){     if (balance != 0)       try {         wait();      //使存款线程进入阻塞状态,并释放互斥对象的互斥锁           }       catch (InterruptedException e) {       }     balance = balance + money;     System.out.println("deposite 100 success");     notify();       //使取款线程进入就绪状态   }}//WithdrawThread.javapublic class WithdrawThread extends Thread{    Account4 account;    public WithdrawThread(Account4 acount)    {        this.account = acount;    }    public void run()    {        for (int i = 0; i < 5; i++)            account.withdraw(100);    }}//DepositeThread.javaclass DepositeThread extends Thread {  Account4 acount;  public DepositeThread(Account4 acount) {  this.acount = acount;  }  public void run(){    for(int i=0;i<5;i++)      acount.deposite(100);  }}//TestProCon.javapublic class TestProCon{    public static void main(String[] args)    {        Account4 acount = new Account4();        WithdrawThread withdraw = new WithdrawThread(acount);        DepositeThread deposite = new DepositeThread(acount);        withdraw.start();        deposite.start();    }}

运行结果:

这里写图片描述

线程通信

线程通信是指线程之间相互传递信息。线程之间有好几种通信方式,如数据共享、管道等。这里,我们主要讲解线程间通过管道来进行通信的方式。管道通信具有如下特点:
(1)管道是单向的。如果需要建立双向通信,可以通过建立多个管道来解决。
(2)管道通信是面向连接的。发送线程建立管道的发送端,接收线程建立与发送管道的连接。
(3)管道中的信息是严格按照发送的顺序进行传送的。收到的数据和发送方在顺序上完全一致。
java语言管道看作是一种特殊的I/O流,并提供了两对相应的基本类来支持管道通信。这些类都位于java.io包中。一对是PipedOutStream和PipedInputStream,用于建立基于字节的通信;另一对是PipedWriter和PipedReader,用于建立基于字符的管道通信。
下面这个例子建立的就是字符管道。

//SenderThread.javaimport java.io.*;class SenderThread extends Thread{  PipedWriter pipedWriter;  public SenderThread( ){  pipedWriter = new PipedWriter( );  }  public PipedWriter getPipedWriter( ){    return pipedWriter;  }  public void run( ){    for (int  i =0; i<5;i++){     try{      pipedWriter.write(i);      }catch(IOException e){        }     System.out.println("Send: "+i);    }  }}//ReceiverThread.javaimport java.io.*;class ReceiverThread extends Thread{  PipedReader pipedReader;  public ReceiverThread( SenderThread senderThread) throws IOException{  pipedReader = new PipedReader(senderThread.getPipedWriter( ));  }  public void run( ){    int i=0;    while(true){      try{      i = pipedReader.read();      System.out.println("Received: "+i);      }catch(IOException e){              }     if(i == 4)          break;         }  }}//ThreadComm.javaimport java.io.*;public class ThreadComm{    public static void main(String[] args) throws Exception    {        SenderThread sender = new SenderThread();        ReceiverThread receiver = new ReceiverThread(sender);        sender.start();        receiver.start();    }}

运行结果:

这里写图片描述

线程死锁(哲学家用餐问题)

线程死锁是并发程序设计中可能遇到的问题之一,它是指程序运行中,多个线程竞争共享资源时可能出现的一种系统状态。该问题可以形象地描述为哲学家用餐问题(此处对其进行了简化):5个哲学家围坐在一圆桌旁,每人的两边放着一筷子,共5支筷子。并规定如下条件:
(1)每个人只有拿起位于自己两边的筷子,合成一双才可以用餐。
(2)用餐后每人必须将两只筷子放回原处。
如果每个哲学家都彬彬有礼,轮流吃饭,则这种融洽的气氛可以长久地保持下去,但是如果每个人都拿起自己左手边的筷子,并想要去拿自己右手边的筷子(这支在另一个哲学家手中),这样就会处于僵持状态,这就是相当于线程死锁。
要注意的是,死锁不是一定会发生的,相反它出现的可能性很小,简单的测试往往无法发现,只有在程序设计中尽量避免这种情况的发生。
示例代码如下:

//ChopStick.javapublic class ChopStick{    private String name;    public ChopStick(String name)    {        this.name = name;    }    public String getNumber()    {        return name;    }}//Philosopher.javaimport java.util.*;public class Philosopher extends Thread{    private ChopStick leftChopStick;    private ChopStick rightChopStick;    private String name;    private static Random random = new Random();    public Philosopher(String name, ChopStick leftChopStick,            ChopStick rightChopStick)    {        this.name = name;        this.leftChopStick = leftChopStick;        this.rightChopStick = rightChopStick;    }    public String getNumber()    {        return name;    }    public void run()    {        try {            sleep(random.nextInt(10));        } catch (InterruptedException e) {        }        synchronized (leftChopStick) {            System.out.println(this.getNumber() + " has "                    + leftChopStick.getNumber() + " and wait for "                    + rightChopStick.getNumber());            synchronized (rightChopStick) {                System.out.println(this.getNumber() + " eating");            }        }    }    public static void main(String args[])    {        // 建立三个筷子对象        ChopStick chopStick1 = new ChopStick("ChopStick1");        ChopStick chopStick2 = new ChopStick("ChopStick2");        ChopStick chopStick3 = new ChopStick("ChopStick3");        // 建立哲学家对象,并在其两边摆放筷子。        Philosopher philosopher1 = new Philosopher("philosopher1", chopStick1,                chopStick2);        Philosopher philosopher2 = new Philosopher("philosopher2", chopStick2,                chopStick3);        Philosopher philosopher3 = new Philosopher("philosopher3", chopStick3,                chopStick2);        // 启动三个线程        philosopher1.start();        philosopher2.start();        philosopher3.start();    }}

运行结果一:

这里写图片描述

运行结果二:

这里写图片描述

运行结果一发生了死锁,结果二没发生死锁。可见,线程死锁存在偶然性,不是一定会发生的,并且发生概率一般比较小,不过我们还是要尽可能地避免它,这样才算是优雅的代码。

线程池

创建和清除线程垃圾都会大量占用CPU等系统资源,所以java中用线程池来解决这一问题。基本思想是:在系统中开辟一块区域,用来存放一些待命的线程,这个区域就叫线程池,如果需要执行任务,则从线程池中取一个待命的线程来执行指定的任务,到任务结束再将其放回,这样可以避免重复创建线程。
常用的两种线程池为:
固定尺寸线程池,待命线程数量一定;
可变尺寸线程池,待命线程数量是根据任务负载的需要动态变化的。
之前在探索资料的时候,发现有一篇详细介绍线程池的博客,讲得挺好的,可以学习下:http://blog.csdn.net/hsuxu/article/details/8985931

6 0
原创粉丝点击