黑马程序员 多线程

------- android培训java培训、java学习型技术博客、期待与您交流! ----------

java之多线程详解

线程和进程

进程:进程是一个正在执行中的程序。,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元

线程:线程就是进程中的一个独立的控制单元,线程在控制着进程的执行。

 

进程和线程的区别:

一个进程中至少有一个线程。

 

Java VM 启动的时候会有一个进程java.exe

 

该进程中至少有一个线程负责java程序的执行。

而且这个线程运行的代码存在于main方法中,该线程称之为主线程

 

扩展:其实更细节说明JVM,jvm启动不止一个线程,还有负责垃圾回收机制的线程

多线程的几种状态

创建:即创建一个线程

运行:调用start()方法开始(启动)一个线程,并运行

冻结:调用了sleep()或者是wait()进入冻结状态,即放弃了执行资格

临时(阻塞):即当冻结状态sleep()时间到了,或者被唤醒notify(),就进入了阻塞状态,即具备了执行资格,没有执行权,要等待cpu去执行

消亡状态:即一个线程运行完或者调用了stop()方法,线程结束,run方法结束。

 

常用的线程的几种方法:

a)      public final String getName():返回该线程的名称。线程都有自己的默认名称,即Thread-编号,该编号从零开始

b)      public static ThreadcurrentThread():返回对当前正在执行的线程对象的引用。

c)      public final voidsetName(String name):改变线程名称,我们也可以通过构造方法来设置名称

线程的创建和启动

如何在自定义的代码中华,自定义一个线程?

 

我们通过对api的查找,java已经提供了对线程这类事物的描述,就是Thread类

 

创建线程总共有两种方式:

第一种方式:继承Thread类。

步骤:

1.      定义类继承Thread

2.      复写Thread类中的run()方法

目的:将自定义代码存储在run方法,让线程运行。

3.      调用线程的start()方法,该方法有两个作用:

a)  启动线程

b)  调用run方法

例如以下代码:

class ThreadTest

{

         publicstatic void main(String[] args)

         {

        ThreadDemo td=new ThreadDemo();//创建了一个线程

                    td.start();//开启线程并执行该线程的run方法。

      //d.run();仅仅是对象调用方法。而线程创建了,并没有运行

 

      for(int i=0;i<60;i++){

                   System.out.println("主线程"+i);

            }

         }

}

class ThreadDemo extends Thread   //继承Thread

{

         publicvoid run(){

         for(inti=0;i<60;i++){

           System.out.println("自定义线程"+i);

         }

         }

}

 

通过运行上面代码,发现运行结果每一次都不同。因为多个线程都在获取cpu的执行权。cpu执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。也就是说cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。

 

以上说的就是多线程的一个特性:随机性。谁抢到谁执行;至于执行多长,cpu说的算。

 

为什么要覆盖run()方法?

 

Thread类用于描述线程。该类就定义了一个功能,用于存储线程要执行的代码。该存储功能就是run方法。也就是说Thread类中的run()方法,用于存储线程要运行的代码。

 

第二种方式:实现Runnable接口

步骤:

1.      定义类并实现Runnable接口

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

3.      通过Thread类建立线程对象

4.      将Runable接口的子类对象作为实际参数传递给Thread类的构造方法

为什么要将Runnable接口的子类对象传递给Thread的构造方法。

因为,自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程去指定对象的run方法。就必须明确该run方法所属对象

5.      调用Thread类的start方法开启线程并调用Runnable接口子类的run方法

例如以下买票的例子:

class TicketDemo

{

         publicstatic void main(String[] args)

         {

                   Tickett=new Ticket();

                   Threadt1=new Thread(t);

                   Threadt2=new Thread(t);

                   Threadt3=new Thread(t);

                   Threadt4=new Thread(t);

                                      t1.start();

                                      t2.start();

                                       t3.start();

                                        t4.start();

         }

}

 

class Ticket implements Runnable

{

 

         privatestatic int  ticket=100;

 public void run(){

    while(true){

              if(ticket>0){

                      System.out.println(Thread.currentThread().getName()+"卖出"+ticket--);

                    }

          }

  }

}

 

第一种继承方式和第二种实现方式有什么区别?

a) 继承Thread:线程代码存放在Thread子类的run方法中。

b) 实现Runnable,线程代码存放在接口的子类的run方法。实现方式好处:避免了单继承的局限性,在定义线程时,建议使用实现方式

线程同步

线程安全

在以上代码中,会出现线程的安全性问题,我们使用sleep方法测试下:

class TicketDemo

{

         publicstatic void main(String[] args)

         {

                   Tickett=new Ticket();

                   Threadt1=new Thread(t);

                   Threadt2=new Thread(t);

                   Threadt3=new Thread(t);

                   Threadt4=new Thread(t);

                                      t1.start();

                                      t2.start();

                                       t3.start();

                                        t4.start();

         }

}

 

class Ticket implements Runnable

{

 

         private  int ticket=100;

 public void run(){

    while(true){

              if(ticket>0){

                             try{         //使用try-catch语句,记住不能抛出

                                     Thread.sleep(10);//睡眠三毫秒

                             }catch(Exception e){

                             }

                       System.out.println(Thread.currentThread().getName()+"卖出"+ticket--);

                    }

          }

  }

}

 

通过打印,发现,打印出0,-1,-2等错票。多线程的运行出现了安全问题

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

解决办法:对多台操作共享数据的语句,只能让一个线程都执行完,其他线程不可以参与执行

 

对于此,Java对于多线程的安全问题提供了专业的解决方式,那就是同步代码块。

非静态线程同步

线程同步有两种格式:

第一种同步代码块的格式:

synchronized(对象){

需要被同步的代码

所以对于以上代码,我们使用了同步代码块后,就不会出现安全问题。看如下代码:

class TicketDemo

{

         publicstatic void main(String[] args)

         {

                   Tickett=new Ticket();

                   Threadt1=new Thread(t);

                   Threadt2=new Thread(t);

                   Threadt3=new Thread(t);

                   Threadt4=new Thread(t);

                                      t1.start();

                                      t2.start();

                                       t3.start();

                                        t4.start();

         }

}

 

class Ticket implements Runnable

{

 

         private  int ticket=100;

         Objectobj=new Object();  //同步代码块要传入的对象,任何对象都可以

 public void run(){

    while(true){

                   synchronized(obj){   //同步代码块,obj就是锁

              if(ticket>0){

                             try{         

                                     Thread.sleep(10);

                             }catch(Exception e){

                             }

                      System.out.println(Thread.currentThread().getName()+"卖出"+ticket--);

                    }

                    }

          }

  }

}

以上打印结果是一个有序的数字。

 

我们可以认为对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。

 

我们不是任何情况下都使用同步的,使用同步有两个前提:

1.      必须要有两个或者两个以上线程

2.      必须是多个线程使用同一个锁

 

我们要必须保证同步中只能有一个线程在运行

 

同步有好处也有弊端,好处是解决了多线程的安全问题,弊端是多个线程需要判断锁,较为消耗资源

 

第二种同步函数的格式:

修饰符synchronized 返回类型方法名(){

   需要同步的代码;

 

我们注意synchronized不能写在线程run方法里。

 

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

函数需要被对象调用,那么函数都有一个所属对象引用,就是this。

所以同步函数使用的锁是this。

以下代码进行验证:

class TicketDemo

{

         publicstatic void main(String[] args)

         {

                   Tickett=new Ticket();

                   Threadt1=new Thread(t);

                   Threadt2=new Thread(t);

                  

                  

                                      t1.start();

 

                                      try{

                                        Thread.sleep(10);

                                      }catch(Exception e){

                                      }

                                      t.tag=false; //切换标记

 

                                      t2.start();

                                      

         }

}

 

class Ticket implements Runnable

{

  Object obj=new Object();

         private  int ticket=100;

         booleantag=true;//标记

        

        

         publicvoid run(){

                   if(tag){

                   synchronized(this){

                            while(ticket>0){

                   if(ticket>0){

                             try{         

                                     Thread.sleep(10);

                             }catch(Exception e){

                             }

                      System.out.println(Thread.currentThread().getName()+"卖出....code...."+ticket--);

                    }

          }

                   }

 

                   }else{

                            while(ticket>0){

                            this.show();

                            }

                   }

         }

 public synchronized void show(){

       

                            if(ticket>0){

                             try{         

                                     Thread.sleep(20);

                             }catch(Exception e){

                             }

                       System.out.println(Thread.currentThread().getName()+"卖出...show."+ticket--);

                    }

  }

         }

 

静态线程同步

如果我们把这句代码private  int ticket=100;

加上static修饰符,我们会发现出现不一样的结果,如果同步方法被静态修饰后,使用的锁又是什么呢?

通过验证,发现不再是this。因为静态方法中也不可以this。当静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象,它就是类名.class,该对象的类型是Class。

所以静态的同步方法,使用的锁是该方法所在类的字节码文件对象。类名.class

 

我们现在举一下单例模式的懒汉模式(重要):

class SingleTest

{

         publicstatic void main(String[] args)

         {

                   Singles1=Single.getInstance();

                   Singles2=Single.getInstance();

       System.out.println(s1==s2);

         }

}

class Single

{

         publicstatic Single s=null;//这里不能加上final

         privateSingle(){

         }

         publicstatic Single getInstance(){

            if(s==null){   //双判断可以增加效率

              synchronized(Single.class){  //静态的对象必须这样写,不能写this

                        if(s==null){

                                  s=new Single();

                             }

                   }

            }

             return s;

         }

}

死锁

线程中还有一种死锁的概念,就是同步中嵌套同步,以后我们在写程序的时候避免写死锁,现在我们看看死锁的程序:

class LockThreadTest

{

         publicstatic void main(String[] args)

         {

                   Threadt1=new Thread(new LockThread(true));

                   Threadt2=new Thread(new LockThread(false));

                   t1.start();

                   t2.start();

         }

}

class LockThread implements Runnable

{

         booleantag;

         LockThread(booleantag){

            this.tag=tag;

         }

  public void run(){

         if(tag){

         synchronized(synObject.locka){

                   System.out.println("iflocka");

           synchronized(synObject.lockb){

                      System.out.println("iflockb");

           }

          

         }

         }else{

        

         synchronized(synObject.lockb){

                            System.out.println("elselockb");

           synchronized(synObject.locka){

                System.out.println("elselocka");

           }

        

         }

         }

         }

}

class synObject

{

         staticObject locka=new Object();

         staticObject lockb=new Object();

 

}

线程通信

什么是线程间通信,其实就是多个线程在操作同一个资源,但是操作的动作不同

线程通信安全:线程通信安全就需要用到线程同步来操作,而且每个线程的操作数据都需要加上同步,且同步中的锁必须是同一个对象。

线程等到唤醒机制

线程等待唤醒机制需要以下三种常用的方法:

a)  public final void notify():唤醒在此对象监视器上(锁)等待的单个线程

b)  public final void notifyAll():唤醒在此对象监视器上等待的所有线程

c)  public final void wait()throws InterruptedException:在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待

以上三种方法都使用在同步中,因为要对持有监视器(锁)的线程操作。之所以要使用在同步中,是因为只有同步中才具有锁。

 

那么,为什么这些操作线程的方法要定义在Object类中呢?

因为这些方法在操作同步中线程时,都必须要标识它们所操作线程持有的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。也就是说,等待和唤醒都必须是同一个锁。

而锁是任意一个对象,所以可以被任意对象调用的方法定义Object类中。

 

下面,我们看看如下代码就知道了:

class Res

{

        

         privateString name;  //姓名

         privateString sex;   //性别

         privateboolean tag=false;  //切换标记

 

         publicsynchronized void set(String name,String sex){

     if(this.tag)

           try{this.wait();}catch(Exception e){};            //线程A等待

     

           this.name=name;

           this.sex=sex;

 

                 this.tag=true;

                     this.notify();   //线程B唤醒

 

         }

 

         publicsynchronized void  get(){   

                    if(!this.tag) 

                   try{this.wait();}catch(Exceptione){};  //线程B等待

                    

            System.out.println(name+"-----"+sex);

 

                   this.tag=false;

                   this.notify();  //线程A唤醒

 

         }

        

}

class Input implements Runnable   //输入

{

         privateRes r;

 

         Input(Resr){

           this.r=r;

         }

         intx=0;

 

     public void run(){

                     while(true){

                            if(x==0)

                                      r.set("Tom","男");

                             else

                                     r.set("Lucy","女");

                                     x=(x+1)%2;

                     }

                    }

}

                    

class Output implements Runnable  //输出

{

         privateRes r;

         Output(Resr){

           this.r=r;

         }

  public void run(){

 

            while(true){

                             r.get();  

          }

}

 

}

class OutputInputDemo

{

         publicstatic void main(String[] args)

         {

                   Resr=new Res();

                   newThread(new Input(r)).start();

                   newThread(new Output(r)).start();

         }

}

总结:对于多个生产者和消费者,为什么要定义while判断标记,因为让被唤醒的线程再一次判断标记。又为什么定义notifyAll?因为需要唤醒对方的线程,如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都在等待。

 

在jdk1.5中提供了多线程升级方案,将同步Synchronized替换成Lock操作。将Object中的wait,notify,notifyAll,替换了Condition对象,该对象可以Lock锁,进行获取,该实例中,实现了本方只唤醒对方操作。

 

线程控制

线程停止

线程的stop()方法,在jdk中已经停止使用了,那么如何去停止线程呢?只有一种,那就是run方法结束,当我们开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。

也有特殊的情况,当线程处于冻结状态,就不会读到标记,那么线程就不会结束。当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除,强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。Thread类提供方法叫interrupt();

例如如下代码,演示而来如何使用:

class StopThreadDemo

{

         publicstatic void main(String[] args)

         {

                   StopThreadst=new StopThread();

                   Threadt1=new Thread(st);

                   Threadt2=new Thread(st);

                   t1.start();

                   t2.start();

 

                   intnum=0;

 

           while (true)

           {

                            if(60==num++){

                              // st.changF();

                              t1.interrupt();   //清除线程1

                              t2.interrupt();    //清除线程2

                               break;

                            }

                            System.out.println(Thread.currentThread().getName()+"......"+num);

           }

 

                            System.out.println("over");

         }

}

class StopThread implements Runnable

{

         booleanflag=true;//标记

   public synchronized void run(){

         while(flag)

         {

                   try{

              wait();

                   }catch(InterruptedExceptione){   //捕捉清除错误

                              flag=false;

                            System.out.println(Thread.currentThread().getName()+"............Exception");

                   }

     System.out.println(Thread.currentThread().getName()+"............run");

         }

         }

 

         publicvoid changF(){

           flag=false;

      线程守护,join,让步

当线程出现无线循环或者线程全部等待的时候,可以使用线程守护。线程守护就是类似于后台,当前台完毕以后,后台也结束。总之线程守护是总会让线程结束,不会一直处于等待或者循环。线程守护使用setDaemon方法

例如如下代码:

class ProtectThread

{

         publicstatic void main(String[] args)

         {

                   ThreadDemotd=new ThreadDemo();

                    Thread t1=new Thread(td);

                    Thread t2=new Thread(td);

                    t1.setDaemon(true);

                    t2.setDaemon(true);

        t1.start();

        t2.start();

         }

}

 

class ThreadDemo implements Runnable

{

         Objectobj=new Object();

          int i=0;

     public  void run(){

             while(true){   //线程处于死循环

                       i++;

                            System.out.println(Thread.currentThread().getName()+"----"+i);

                   }

           }

}

 

 

join:线程还有一种join()方法,当A线程执行到而来B线程的.join方法时,A就会等待,等B线程都执行完,A才会执行。join可以用来临时加入线程执行。

class JoinDemo

{

         publicstatic void main(String[] args) throws InterruptedException

         {

               Join j=new Join();

                     Thread t1=new Thread(j);

                     Thread t2=new Thread(j);

                     t1.start();

                     t2.setPriority(10);

                     t2.start();

 

 

                      for(int i=0;i<60;i++){

                               if(i==30){

                                  t1.join();    //join,main方法停止,t1线程执行完

                               }

                                     System.out.println(Thread.currentThread().getName()+"-----"+i);

                              }

         }

}

class Join implements Runnable

{

   public void run(){

           for(int i=0;i<60;i++){

            System.out.println(Thread.currentThread().getName()+"...."+i);

           }

         }

}

 

yield方法是让此段线程出现频率比较高,我们把上面代码join()换成yeild()就可以发现当main到30时,t1线程会出现频率比较高。此处不再举例。


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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值