Java多线程编程

来源:互联网 发布:3306端口攻击 编辑:程序博客网 时间:2024/05/29 06:56

Java多线程编程


概述


对于一个初学者,可能问的最多的就是在我刚学Java的时候,没有用到线程,但是为什么我还能执行程序呢?一个Java程序最少会含有2个线程,怎么解释这个问题呢?首先,main方法是每一个程序都必须有的函数,此时JVM会为其创建一个线程,Java是一个面向对象的编程语言,并且Java提供了垃圾回收机制,此时垃圾回收也会占用一个线程,这样的话有两个线程也是可以理解的。在操作系统课程中,我们同样接触过线程的概念,在Java中线程和操作系统中线程的概念是一致的,在此不在重复说明概念,下面的文章中将会用编程语言实现Java的线程机制,每个知识点都是根据一个具体的需求实现的,便于理解。请注意:本文实际上全部是使用java.lang.Thread类的方法,请学会参考JDK

线程的两种创建方式


  • 继承Thread类,并重写run方法
    • 继承Thread
    • 重写run方法,将线程中将要执行的代码放到run方法中
    • 调用Thread类的start方法开启线程
  • 实现Runnable接口
    • 首先定义一个类实现Runnable接口
    • 重写Runnable接口的run方法
    • 创建Runnable实现类的对象
    • 创建Thread类的对象,并使用构造函数:Thread(Runnable target,String name)将第一步创建的Runnable实现类的对象传入到Thread构造函数中
    • Thread类的对象调用start方法,在这里有一个疑问,并没有重写Thread类的run方法,只是重写了Runnable接口的run方法,这个调用关系是怎样解释的?看下面的代码:
public void run() {    if (target != null) {        target.run(); //这的target实际上就是Runnable接口实现类的对象,实际上是Thread类的run方法调用了Runnable接口实现类的run方法    }}
  • 推荐使用实现Runnable类的接口,原因:在Java中继承是单继承,比如现实开发中我们必须去继承其他的类,比如项目经理或者以前的设计人员写的代码,但是由于你已经继承了Thread,所以就不能继承其他的类,这样直接导致我们需要重复的写一些已经被别人实现的类的方法,重复率很大,不科学。但是下文全部是以第一种方式去创建的线程,因为实现方式简单。

Java中的线程类


Java提供了一个类java.lang.Thread类提供给我们一种简单的定义,简单的代码如下:

class Test extends Thread{//继承Thread类,并重写run()方法    @Override    public void run(){//重写run()方法        for(int i = 0 ; i < 100 ; i++){            System.out.println("子线程"+ (i+1));        }    }    public static void main(String[] args){        Test test = new Test();        test.start();//开启线程必须调用start()方法,不能直接调用run()方法        for(int i = 0;i < 100 ; i++){            System.out.println("main线程"+(i+1));        }    }}

现实编程中,很多时候需要同时开启多个线程,Java支持吗?回答是肯定的,像下面的代码:

class VideoThread extends Thread{    @Override    public void run(){        while(true){            System.out.println("咱们聊天吧!");        }    }}class TalkThread extends Thread{    @Override    public void run(){        while(true){            System.out.println("视频好不好?");        }    }}class Test{    public static void main(){        TalkThread talkThread = new TalkThread();        talkThread.start();//开启聊天线程        VideoThread videoThread = new VideoThread();        videoThread.start();//开启视频线程    }}

Thread类常用方法


  • 构造函数
    在JDK 1.7中,Thread类给我们提供给了7种不同的构造函数,其中我个人觉得用的最多的是Thread(String name),该构造函数能帮助我们在创建线程的时候自定义线程的名字,但是我们不是直接使用该方法去构造线程的名字,因为在这之前我们采用的创建线程的方法是通过继承Thread类实现,所以我们使用super关键字调用父类构造函数,代码如下:
class MyThread extends Thread{    public MyThread(String name){        super(name);    }    @Override    public void run(){        System.out.println("自定义线程");    }}class TestThread{    public static void main(String[] args){         MyThread myThread = new MyThread("该线程的名字");         myThread.start();//开启线程,调用run方法    }}
  • 获取当前线程的信息
    • currentThread方法,该方法是一个静态方法,直接就能使用类名调用,并且返回的是执行该方法的进程,测试代码:
class MyThread extends Thread{    public MyThread(String name){        super(name);    }    @Override    public void run(){        System.out.println("自定义线程");    }}class TestThread{    public static void main(String[] args){         MyThread myThread = new MyThread("该线程的名字");         myThread.start();//开启线程,调用run方法         //获取当前进程的引用         Thread thread = myThread.currentThread();//请注意,返回的是执行该方法的进程,在这个例子中应该是main进程,尽管使用了myThread调用         System.outprintln(thread.toString());    }}
  • getId
  • getName
  • getPriority,获取当前进程的优先级
  • setName,设置线程的名字,但是我们一般会在初始化线程时使用构造函数创建,这个函数通常情况下供我们使用去修改线程的名字
  • setPriority,设置线程的优先级,优先级从1到10,默认情况下优先级为5
  • start,该函数开启线程去调用run方法
  • sleep函数,该函数也是静态函数,但是sleep方法会抛出一个InterruptedException异常,当sleep方法写在重载的run方法中时不能抛出异常只能捕获异常。
package thread;class MyThread extends Thread{    public MyThread(String name){        super(name);    }    @Override    public void run(){        Thread thread = MyThread.currentThread();        System.out.println(thread.toString());        System.out.println("自定义线程");    }}public class MainThread {    public static void main(String[] args)throws InterruptedException{        MyThread myThread = new MyThread("线程名字");        myThread.start();//开启线程,调用run方法         //获取当前进程的引用        Thread thread = myThread.currentThread();//请注意,返回的是执行该方法的进程,在这个例子中应该是main进程,尽管使用了myThread调用        System.out.println(thread.toString());        //try{            //Thread.currentThread().sleep(1000); //sleep函数必须捕获异常        //}catch(InterruptedException e){            //e.printStackTrace();        //}        Thread.currentThread().sleep(1000);    }}

在重载的run方法中使用sleep方法,代码如下:

package thread;class MyThread extends Thread{    public MyThread(String name){        super(name);    }    @Override    public void run(){        Thread thread = MyThread.currentThread();        System.out.println(thread.toString());        System.out.println("自定义线程");        try{             Thread.currentThread().sleep(1000);  //当在run方法中时只能捕获异常,抛出异常时错误的        }catch(InterruptedException e){            e.printStackTrace();        }    }}public class MainThread {    public static void main(String[] args)throws InterruptedException {        MyThread myThread = new MyThread("线程名字");        myThread.start();//开启线程,调用run方法         //获取当前进程的引用        Thread thread = myThread.currentThread();//请注意,返回的是执行该方法的进程,在这个例子中应该是main进程,尽管使用了myThread调用        System.out.println(thread.toString());//        try{//            Thread.currentThread().sleep(1000); //sleep函数必须捕获异常//        }catch(InterruptedException e){//            e.printStackTrace();//        }        Thread.currentThread().sleep(1000);    }}

为什么在run方法中只能捕获异常,但是在其他方法中使用时既可以抛出异常也可以捕获异常呢?我们看一下在Thread类中run方法的定义,这个问题就解决了。

public void run() {        if (target != null) {            target.run();        }    }

可以看出在Thread中父类方法没有抛出异常,根据方法重写的要求,子类抛出的异常应该比父类的异常要小,如果父类不抛出异常,那么子类也必定不能抛出异常。

同步代码块与线程安全问题


所谓线程安全问题,是指不同的线程之间争用同一个临界区时,由于CPU的调度,可能会使临界区(mutex)出现脏数据的情况,这在操作系统课程中已经学习过,在这里不再赘述,本文的重点是怎么解决线程安全问题,采用两种方案:同步代码块、同步函数,两种方法,下面先将同步代码块的使用。synchronized(同步),在同步的时候需要一个共享的锁,这个锁与临界区一样,必须是所有线程共享的,所以,临界区和锁被实现为static,但是对于锁有一个特例就是String,比如”锁”,这个字符串被创建,并且能够保证,当使用的是同一个字符串时,这种形式的字符串能够保证不会创建重复的副本,实际上也保证锁的一致性,那么在Java编程中,究竟什么能作为锁呢?回答是:任何的对象都能作为锁,但是该对象必须被修饰为static,那么为什么任何的对象都能作为锁呢?原因是:每一个对象内部都维护了一个state,Java同步机制便是利用了对象的状态作为锁的标识。简单的举一个现实生活中的例子,我们的需求是:在车站售票时,总是同时几个售票窗口在售票,我假设目前有3个售票窗口,并且只有50张票,怎么编码保证线程安全?

/*** @author:junpeng zhu* 功能:用同步代码块实现线程之间的同步,解决线程安全问题*/package thread;class SaleTickets extends Thread{    public SaleTickets(String name){        super(name);    }    static int tickets = 50 ;//50张票作为临界资源    @Override    public void run(){            while(true){                synchronized("锁"){                if(tickets > 0){                    System.out.println("售票窗口"+Thread.currentThread().getName()+"售出第"+tickets+"张票");                    tickets--;                }else{                    System.out.println("票卖完了");                    break;                }                }        }    }}public class MainThread {    public static void main(String[] args){        //创建三个售票窗口,也即是三个线程,并且分别赋值为窗口1、窗口2、窗口3        SaleTickets saleTickets1 = new SaleTickets("窗口1");        saleTickets1.start();        SaleTickets saleTickets2 = new SaleTickets("窗口2");        saleTickets2.start();        SaleTickets saleTickets3 = new SaleTickets("窗口3");        saleTickets3.start();        }}

同步函数与线程安全问题


同步函数就是使用synchronized去修饰一个函数,如果是一个非静态的同步函数的锁,锁对象是this对象,也就是当前调用该同步函数的对象,这样的话实际上锁不是多个线程共享的,这样的话根本不能达到同步的效果,所以只能使用静态函数,如果是静态同步函数的锁,锁对象是当前函数所属的类的字节码文件(class对象),所以同步函数的锁对象不能由自己指定,它是固定的。

package thread;class SaleTickets extends Thread{    public SaleTickets(String name){        super(name);    }    static int tickets = 50 ;//50张票作为临界资源    @Override    public  void run(){        getMoney();    }    public synchronized static void getMoney(){        while(true){            if(tickets > 0){                System.out.println("售票窗口"+Thread.currentThread().getName()+"售出第"+tickets+"张票");                tickets--;            }else{                System.out.println("票卖完了");                break;            }        }    }}public class MainThread {    public static void main(String[] args){        //创建三个售票窗口,也即是三个线程,并且分别赋值为窗口1、窗口2、窗口3        SaleTickets saleTickets1 = new SaleTickets("窗口1");        saleTickets1.start();        SaleTickets saleTickets2 = new SaleTickets("窗口2");        saleTickets2.start();        SaleTickets saleTickets3 = new SaleTickets("窗口3");        saleTickets3.start();        }}

在同步代码块和同步函数解决线程安全问题时,推荐使用同步代码块,因为同步代码块的控制很灵活,而使用同步函数的话,整个函数的所有代码都被同步,并且由于其锁对象不能由自己指定,缺乏灵活性。上面的代码提供给了一种罪常见的使用同步函数的例子,其结果一旦被一个对象掌握,就会一直执行结束,其它对象不能获得临界区。

死锁


同步解决了线程安全问题,但是却引起了另外一个更加重要的问题死锁,死锁也是在操作系统中存在的,详细问题,请看操作系统相关部分。死锁现象出现的根本原因是:互相等待对方的资源,也就是存在两个或者两个以上的共享资源。出现死锁怎么解决呢?没有方案,死锁只能尽量去避免,但是不能解决,在写代码的过程中要注意。

线程的通讯


一个线程完成自己的任务时,这时要通知另外一个线程去做另外的任务,官方给出的例子是生产者、消费者问题。首先定义需求:生产者生产一个产品,消费者接着就去消费一个产品,如果消费者没有消费那么生产者等待消费者消费,如果生产者没有生产,此时消费者应该等待生成。

线程通讯用到的三个方法

  • wait方法,等待,如果线程执行了wait方法,线程就会进入等待状态,此时必须要有另外的线程调用notify方法去唤醒这个处于等待状态的线程
  • notify方法,唤醒,唤醒等待的线程
  • notifyAll方法,唤醒线程池中所有等待的线程
  • 三个方法需要注意的事项
    • java.lang.Object中才有wait方法和notify方法、notifyAll方法
      • 为什么要把wait方法和notify方法放在Object类中?原因是锁对象可以是任意类型的对象,并且wait方法和notify方法只能用锁对象进行调用,出于这种原因只能利用Java的多态性才能实现。
    • wait方法和notify必须在同步代码块或者同步函数中才能使用
      • 因为这个两个方法必须由锁对象调用,而锁对象只能出现在同步函数或者同步代码块中。
    • 这个两个方法必须由锁对象调用,否则会报错
      • 因为设置为等待和唤醒的线程是用锁对象为标识放在线程池中的,换句话说就是以锁对象为标识创建的线程池。
生产者消费者实例
//产品类class Product{    String name;  //名字    double price;  //价格    boolean flag = false; //产品是否生产完毕的标识,默认情况是没有生产完成。}//生产者class Producer extends Thread{    Product  p ;      //产品    public Producer(Product p) {        this.p  = p ;    }    @Override    public void run() {        int i = 0 ;         while(true){         synchronized (p) {            if(p.flag==false){                 if(i%2==0){                     p.name = "苹果";                     p.price = 6.5;                 }else{                     p.name="香蕉";                     p.price = 2.0;                 }                 System.out.println("生产者生产出了:"+ p.name+" 价格是:"+ p.price);                 p.flag = true;                 i++;                 p.notifyAll(); //唤醒消费者去消费            }else{                //已经生产 完毕,等待消费者先去消费                try {                    p.wait();   //生产者等待                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }          }        }}//消费者class Customer extends Thread{    Product p;     public  Customer(Product p) {        this.p = p;    }    @Override    public void run() {        while(true){            synchronized (p) {                    if(p.flag==true){  //产品已经生产完毕                    System.out.println("消费者消费了"+p.name+" 价格:"+ p.price);                    p.flag = false;                     p.notifyAll(); // 唤醒生产者去生产                }else{                    //产品还没有生产,应该 等待生产者先生产。                    try {                        p.wait(); //消费者也等待了...                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        }        }}public class Demo5 {    public static void main(String[] args) {        Product p = new Product();  //产品        //创建生产对象        Producer producer = new Producer(p);        //创建消费者        Customer customer = new Customer(p);        //调用start方法开启线程        producer.start();        customer.start();    }}

线程知识杂烩


学习任何的编程语言,实际上首先应该明确的就是应该认真的学习编程语言自带的API,其实本文大部分都是对java.lang.Thread类的方法的使用,只是其中渗透了自己在编程中的一些经验,本文还属于Java SE部分,其中很多的功能都只是模拟,用输出语句代替现实功能,这样做的好处是可以隐藏功能本身带来的复杂性,只关注API本身的特性和功能,下面再讲三个在Java.lang.Thread类中常用的方法,均是以代码去呈现的。顺便说一个可能被大家忽略的问题:线程函数中通常都有循环,并且我们通常用的是死循环,如果需要,使用break停止线程

停止线程

在有些情况下我们需要去停止一个线程,这种停止线程的需求也是很常见的,怎么去停止线程呢?停止一个线程,我们通常会使用一个变量去控制,如果停止一个处于等待状态的线程,我们需要使用notify方法去控制或者使用interrupt方法,但是Interrupt方法会抛出一个异常。

package thread;class TestThread extends Thread{    public TestThread(String name){        super(name);    }    boolean flag = true;    @Override    public void run(){        int i= 0;        while(flag){            synchronized(this.currentThread()){                System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次");                i++;            }        }    }}public class StopThread {    public static void main(String[] args){        TestThread thread = new TestThread("张三");        thread.start();        //给一个循环,当循环到第80次时停止线程        for(int i = 0 ; i < 100 ; i++){            System.out.println("执行了main线程第"+i+"次");            if(i == 80){                thread.flag = false;                break;            }        }    }}

当线程处于等待状态时,怎么办呢?看下面的代码:

package thread;class TestThread extends Thread{    public TestThread(String name){        super(name);    }    boolean flag = true;    @Override    public  void run(){        int i= 0;        while(flag){                this.setPriority(10);                synchronized(this.currentThread()){                    try {                        this.wait();                    } catch (InterruptedException e) {                        // TODO Auto-generated catch block                        e.printStackTrace();                    }                    System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次");                    i++;                }        }    }//  public synchronized void run(){//      int i= 0;//      while(flag){//              this.setPriority(10);//              try {//                  this.wait();//              } catch (InterruptedException e) {//                  // TODO Auto-generated catch block//                  e.printStackTrace();//              }//              System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次");//              i++;//      }//  }}public class StopThread {    public static void main(String[] args){        TestThread thread = new TestThread("李四");        thread.start();        for(int i = 0 ; i < 100 ; i++){            System.out.println("执行了main线程第"+i+"次");            if(i == 80){                synchronized(thread){                    thread.notify();                }                thread.flag = false;                break;            }        }    }}

用interrupted方法终止线程:

package thread;class TestThread extends Thread{    public TestThread(String name){        super(name);    }    boolean flag = true;    @Override    public void run(){        int i= 0;        while(flag){                this.setPriority(10);                try {                    this.wait();                } catch (InterruptedException e) {                    System.out.println("线程异常终止了...");                }                synchronized(this.currentThread()){                    System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次");                    i++;                }           }    }//  public synchronized void run(){//      int i= 0;//      while(flag){//              this.setPriority(10);//              try {//                  this.wait();//              } catch (InterruptedException e) {//                  // TODO Auto-generated catch block//                  e.printStackTrace();//              }//              System.out.println("线程"+Thread.currentThread().getName()+"执行第"+i+"次");//              i++;//          //      }//  }}public class StopThread {    public static void main(String[] args){        TestThread thread = new TestThread("李四");        thread.start();        for(int i = 0 ; i < 100 ; i++){            System.out.println("执行了main线程第"+i+"次");            if(i == 80){                thread.flag = false;                thread.interrupted();                break;            }        }    }}
守护线程

所谓守护线程,其实就是在背后干一些偷鸡摸狗的事情,比如当你下载一个Java设计的游戏时,通常情况下会有很多的广告,但是你肯定是不会去点击那些广告的,这样的话程序的钱从哪里来,所以通常情况下,软件编写人员会在后台开启一个守护线程去点击广告,这是一个最贴近现实的例子。在一个进程中,如果只剩了守护线程,那么守护线程也会死亡。设置守护线程的方法是setDaemon(boolean on),当on为true时,设置为守护线程,当on为false时,设置为非守护线程。默认情况下,线程是非守护线程,我们可以使用isDaemon方法去判断一个线程是否是守护线程。需求提出:在使用QQ的时候,经常会发现在我们不注意的时候QQ在进行版本更新,当只有守护线程在运行时,守护线程必须结束执行,偷鸡摸狗现象最常见,下面的代码模拟这个功能。

package thread;class TestThread extends Thread{    public TestThread(String name){        super(name);    }    @Override    public void run(){        for(int i = 0 ; i < 100 ; i++){            if(i==99){                System.out.println("完成下载");                break;            }            System.out.println("已经下载"+(i+1)+"%");            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}public class StopThread {    public static void main(String[] args){        TestThread thread = new TestThread("王五");        thread.setDaemon(true);//将该进程设置为守护进程,该方法必须在线程启动之前调用(在JDK中有说明)        thread.start();        for(int i = 0 ; i < 10 ; i++){            System.out.println(Thread.currentThread()+"正在执行");        }    }}
线程让步

所谓的线程让步就是在执行一个线程的同时,调用了另外的一个线程,并且,必须等待这个被调用线程结束才能执行调用者线程,此时就需要使用线程让步方法join,该方法也出现在java.lang.Thread类中。需求提出:小时候,当妈妈做饭的时候,想要炒菜但是发现没有酱油了,于是妈妈叫我们去打酱油,同时她做饭也停止了,等打完酱油回来再继续做饭,这就是典型的线程让步,妈妈做饭是一个线程,我们打酱油是一个线程,当出现这种情况时,妈妈线程必须停下来等待打酱油线程结束。

package thread;class TestThreadMum extends Thread{    public TestThreadMum(String name){        super(name);    }    @Override    public void run(){        System.out.println("妈妈正在做菜");        System.out.println("但是没有酱油了");        TestThreadSun testthreadsun = new TestThreadSun("儿子线程");        testthreadsun.start();        try {            testthreadsun.join();  //必须执行完TestThreadSun线程,才能执行TestThreadMum线程        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("妈妈继续做菜");        System.out.println("全家人一起快乐的吃饭");    }}class TestThreadSun extends Thread{    public TestThreadSun(String name){        super(name);    }    @Override    public void run(){        System.out.println("我要替妈妈去打酱油啦!");        System.out.println("打完酱油回到家!");    }}public class StopThread {    public static void main(String[] args){        TestThreadMum testthreadmum = new TestThreadMum("妈妈进程");        testthreadmum.start();    }}

总结


这些方法都是在平时的线程编程中经常用到的,在java.lang.Thread中还有很多方法在本文没有涉及到,这些不常见,使用时可以查看JDK文档。

0 0
原创粉丝点击