Java多线程

来源:互联网 发布:freebsd vs ubuntu 编辑:程序博客网 时间:2024/06/07 02:55


多线程

一、多线程概述

1、进程:正在进行中的程序。其实进程就是一个应用程序运行时的内存分配空间。

2、线程:其实就是进程中一个程序执行控制单元,一条执行路径。进程负责的是应用程序的空间的标示。线程负责的是应用程序的执行顺序。

3、多线程

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

一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序,每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量。

jvm在启动的时,首先有一个主线程,负责程序的执行,调用的是main函数。主线程执行的代码都在main方法中。

当产生垃圾时,收垃圾的动作,是不需要主线程来完成,因为这样,会出现主线程中的代码执行会停止,会去运行垃圾回收器代码,效率较低,所以由单独一个线程来负责垃圾回收。 

4、随机性的原理:因为cpu的快速切换造成,哪个线程获取到了cpu的执行权,哪个线程就执行。而cpu执行哪个程序,是毫无规律性的。这也是多线程的一个特性:随机性。哪个线程被cpu执行,或者说抢到了cpu的执行权,哪个线程就执行。而cpu不会只执行一个,当执行一个一会后,又会去执行另一个,或者说另一个抢走了cpu的执行权。至于究竟是怎么样执行的,只能由cpu决定。

二、创建线程的方式

创建线程共有两种方式:继承方式和实现方式。

线程的名称是由:Thread-编号定义的。编号从0开始。

线程要运行的代码都统一存放在了run方法中。

返回当前线程的名称:Thread.currentThread().getName()

线程要运行必须要通过类中指定的方法开启。start方法。

start方法:

1、启动了线程;

2、让jvm调用了run方法。

 

创建线程的第一种方式:继承Thread ,由子类复写run方法。

步骤:

1,定义类继承Thread类;

2,目的是复写run方法,将要让线程运行的代码都存储到run方法中;

3,通过创建Thread类的子类对象,创建线程对象;

4,调用线程的start方法,开启线程,并执行run方法。

 

创建线程的第二种方式:实现一个接口Runnable。

步骤:

1,定义类实现Runnable接口。

2,覆盖接口中的run方法(用于封装线程要运行的代码)。

3,通过Thread类创建线程对象;

4,将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数。

为什么要传递呢?因为要让线程对象明确要运行的run方法所属的对象。

5,调用Thread对象的start方法。开启线程,并运行Runnable接口子类中的run方法。

用实现方式创建线程示例如下:

/*需求:简单的卖票程序。多个窗口卖票。*/class Ticket implements Runnable//extends Thread{private  int tick = 100;public void run(){while(true){if(tick>0){//显示线程名及余票数System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);}}}}  class  TicketDemo{public static void main(String[] args) {//创建Runnable接口子类的实例对象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();}} 

为什么要有Runnable接口的出现?

1:通过继承Thread类的方式,可以完成多线程的建立。但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承Thread类,因为java单继承的局限性。

可是该类中的还有部分代码需要被多个线程同时执行。这时怎么办呢?

只有对该类进行额外的功能扩展,java就提供了一个接口Runnable。这个接口中定义了run方法,其实run方法的定义就是为了存储多线程要运行的代码。

所以,通常创建线程都用第二种方式。

因为实现Runnable接口可以避免单继承的局限性。

2:其实是将不同类中需要被多线程执行的代码进行抽取。将多线程要运行的代码的位置单独定义到接口中。为其他类进行功能扩展提供了前提。

所以Thread类在描述线程时,内部定义的run方法,也来自于Runnable接口。

实现Runnable接口可以避免单继承的局限性。而且,继承Thread,是可以对Thread类中的方法,进行子类复写的。但是不需要做这个复写动作的话,只为定义线程代码存放位置,实现Runnable接口更方便一些。所以Runnable接口将线程要执行的任务封装成了对象。

 

三、线程的几种状态:

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

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

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

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

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

图解如下:


 

四、线程安全问题

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

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

2、涉及到两个因素:

1,多个线程在操作共享数据。

2,有多条语句对共享数据进行运算。

原因:这多条语句,在某一个时刻被一个线程执行时,还没有执行完,就被其他线程执行了。


3、解决安全问题的原理:

只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他线程不能进来执行就可以解决这个问题。

如何进行多句操作共享数据代码的封装呢?

java中提供了一个解决方式:就是同步代码块

格式:

synchronized(对象) {  // 任意对象都可以。这个对象就是锁。

需要被同步的代码;

}


五、同步

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

弊端:相对降低性能,因为判断锁需要消耗资源,产生了死锁。


定义同步的前提

1,必须要有两个或者两个以上的线程,才需要同步。

2,多个线程必须保证使用的是同一个锁。


同步的第二种表现形式:

同步函数:其实就是将同步关键字定义在函数上,让函数具备了同步性。

格式:

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

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

通过验证,函数都有自己所属的对象this,所以同步函数所使用的锁就是this锁。

示例如下:

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


六、静态函数的同步方式

当同步函数被static修饰时,这时的同步用的是哪个锁呢?

静态函数在加载时所属于类,这时有可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经被封装成了对象,这个对象就是该类的字节码文件对象。

所以静态加载时,只有一个对象存在,那么静态同步函数就使用的这个对象。

这个对象就是 类名.class

典型示例如下:

 /*加同步的单例设计模式————懒汉式*/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;}} 

同步代码块和同步函数的区别

同步代码块使用的锁可以是任意对象。

同步函数使用的锁是this,静态同步函数的锁是该类的字节码文件对象。

在一个类中只有一个同步,可以使用同步函数。如果有多同步,必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些。

七、死锁

同步死锁:通常只要将同步进行嵌套,就可以看到现象。同步函数中有同步代码块,同步代码块中还有同步函数。

八、线程间通信

线程间通信:多个线程在操作同一个资源,但是操作的动作却不一样。

1:将资源封装成对象。

2:将线程执行的任务(任务其实就是run方法。)也封装成对象。

等待唤醒机制:

涉及的方法:

wait:将同步中的线程处于冻结状态。释放了执行权,释放了资格。同时将线程对象存储到线程池中。

notify:唤醒线程池中某一个等待线程。

notifyAll:唤醒的是线程池中的所有线程。

注意:

1:这些方法都需要定义在同步中。 

2:因为这些方法必须要标示所属的锁。

你要知道 A锁上的线程被wait了,那这个线程就相当于处于A锁的线程池中,只能A锁的notify唤醒。

3:这三个方法都定义在Object类中。为什么操作线程的方法定义在Object类中?

因为这三个方法都需要定义同步内,并标示所属的同步锁,既然被锁调用,而锁又可以是任意对象,那么能被任意对象调用的方法一定定义在Object类中。

生产者、消费者示例:

public class ProducerConsumerDemo {      /**多生产者消费者的情况      * 当出现多个线程时,要用while标识和notifyAll      * @param args      */      public static void main(String[] args) {          Resource r=new Resource();          Produce pro=new Produce(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)//用了while出现了全部等待      try {          wait();      } catch (InterruptedException e) {          // TODO Auto-generated catch block          e.printStackTrace();      }       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 (InterruptedException e) {              // TODO Auto-generated catch block              e.printStackTrace();          }       System.out.println(Thread.currentThread().getName()+"...消费者.."+this.name);     flag=false;     this.notifyAll();   }  }  class Produce implements Runnable  {   private Resource res;   Produce(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();           }  }  }    

wait和sleep区别: 分析这两个方法:从执行权和锁上来分析:

wait:可以指定时间也可以不指定时间。不指定时间,只能由对应的notify或者notifyAll来唤醒。

sleep:必须指定时间,时间到自动从冻结状态转成运行状态(临时阻塞状态)。

wait:线程会释放执行权,而且线程会释放锁。

Sleep:线程会释放执行权,但不是不释放锁。

 

线程的停止:通过stop方法就可以停止线程。但是这个方式过时了。

停止线程:原理就是:让线程运行的代码结束,也就是结束run方法。

怎么结束run方法?一般run方法里肯定定义循环。所以只要结束循环即可。

第一种方式:定义循环的结束标记。

第二种方式:如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过Thread类中的interrupt方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标记,并结束。

 

JDK1.5新特性标准

JdK1.5提供了多线程升级解决方案,将同步synchronized替换成现实LOCK操作

wait,notify,notifyAll替换了condition对象

线程停止的Stop方法已经过时,新的停止方法有两种

1,定义循环结束标记

2,使用interrupt(中断)方法,该方法是结束线程的冻结状态使线程回到运行状态中

运用LOCK操作代替同步,之前的wait,notify,notifyall方式用lock中的await,signal(),signAll()

 示例如下:

class BoundedBuffer {   final Lock lock = new ReentrantLock();//生成lock实例对象   final Condition notFull  = lock.newCondition();   final Condition notEmpty = lock.newCondition();   final Object[] items = new Object[100];   int putptr, takeptr, count;   public void put(Object x) throws InterruptedException {     lock.lock();//加锁     try {       while (count == items.length)         notFull.await();//等待       items[putptr] = x;       if (++putptr == items.length) putptr = 0;       ++count;       notEmpty.signal();//唤醒     }    finally {       lock.unlock();//解锁     }   }   public Object take() throws InterruptedException {     lock.lock();//加锁     try {       while (count == 0)         notEmpty.await();//等待       Object x = items[takeptr];       if (++takeptr == items.length) takeptr = 0;       --count;       notFull.signal();//唤醒       return x;     }finally {       lock.unlock();//解锁     }   } } 


知识扩展:

interrupt():中断线程。

setPriority(int newPriority):更改线程的优先级。

getPriority():返回线程的优先级。

toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。

Thread.yield():暂停当前正在执行的线程对象,并执行其他线程。

setDaemon(true):将该线程标记为守护线程或用户线程。将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。

join:临时加入一个线程的时候可以使用join方法。

当A线程执行到了B线程的join方式。A线程处于冻结状态,释放了执行权,B开始执行。A什么时候执行呢?只有当B线程运行结束后,A才从冻结状态恢复运行状态执行。


0 0