java 多线程

来源:互联网 发布:centos升级kde 编辑:程序博客网 时间:2024/06/06 10:02

线程概述

多任务操作系统,即能够同时执行多个应用程序,最常见的有 Windows、Linux、UNIX等。

在一个操作系统中,每个独立运行的程序都可以称为一个进程,即“正在运行的程序”。

在多任务操作系统中,例如可以一边听音乐一边聊天,但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由 CPU 执行的,对于一个 CPU 而言,在某个时间点只能运行一个程序,即只能执行一个进程。由于 CPU 运行速度很快,能在极短的时间内,在不同的进程之间进行切换,所以给人以同时运行多个程序的感觉。

每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序运行的一条条线索,被称为线程。

操作系统中的每一个进程中都至少存在一个线程。当一个 Java 程序启动时,就会产生一个进程,该进程会默认创建一个线程,在这个线程上会运行 main() 方法中的代码。

单线程程序:代码都是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果。

多线程程序:多段程序代码交替运行的效果。

多线程和进程一样,也是由 CPU 轮流执行的,只不过 CPU 运行速度很快,所以给人同时执行的感觉。


线程的创建

第一种方式:继承 java.lang 包下的 Thread 类,覆写 Thread 类的 run() 方法,在 run() 方法中实现运行在线程上的代码

示例代码如下:

class MyThread extends Thread {    public void run() {        while(true) {            System.out.println("MyThread类的 run() 方法在运行");        }    }}public class Example {    public static void main(String[] args) {        MyThread myThread = new MyThread();        myThread.start();        while() {            System.out.println("main() 方法在运行");        }    }}

第二种方式:实现 java.lang.Runnable 接口,同样是在 run() 方法中实现运行在线程上的代码

class MyThread implements Runnable {    public void run() {  //线程的代码段,当调用 start() 方法时,线程从此处开始执行        while(true) {            System.out.println("main() 方法在运行");        }    }}public class Example {    public static void main(String[] args) {        MyThread myThread = new MyThread();   //创建 MyThread 的实例对象        Thread thread = new Thread(myThread); //创建线程对象        thread.start();  //开启线程,执行线程中的 run() 方法        while(true) {            System.out.println("main() 方法正在运行");        }    }}

实现 Runnable 接口相对于继承 Thread 类来说,有以下优点:

  • 适合多个相同程序代码的线程去处理同一个资源的情况,把线程同程序代码、数据有效的分离,很好地体现了面向对象的设计思想。
  • 可以避免由于 Java 的单继承带来的局限性。在开发中经常碰到这样一种情况,就是使用一个已经继承了某一个类的子类创建线程,由于一个类不能同时有两个父类,所以不能用继承 Thread 类的方式,只能采用实现 Runnable 接口的方式
  • 事实上,大部分的应用程序都会采用第二种方式来创建多线程,即实现 Runnable 接口。

前台线程与后台线程

前台线程:新创建的线程默认都是前台线程。
后台线程:若某个线程对象在启动之前(即调用 start() 方法之前)调用了 setDaemon(true) 语句,这个线程就是一个后台线程。

注意:要将某个线程设置为后台线程,必须在该线程启动之前,也就是说 setDaemon() 方法必须在 start() 方法之前,否则会引发 IllegalThreadStateException 异常。

对 Java 程序来说,只要还有一个前台线程在运行,这个进程就不会结束。若一个进程中只有后台线程运行,这个进程就会结束。

示例代码如下:
演示当程序只有后台线程时就会结束的情况

class DamonThread implements Runnable {    public void run() {        while(true) {            System.out.println(Thread.currentThread().getName() + "-----is running.");        }    }}public class Example {    Public static void main(String[] args) {        System.out.println("main 线程就是后台线程么?" + Thread.currentThread().isDaemon());        DamonThread dt = new DamonThread();  //创建一个 DamonThread 对象 dt        Thread t = new Thread(dt, "后台线程"); //创建线程 t 共享 dt 资源        System.out.println("t 线程默认是后台线程么?" + t.isDaemon());  //判断是否为后台线程        t.setDaemon(true);        t.start();        for(int i=0; i<10; i++) {            System.out.println(i);        }    } }

该程序说明,当开启线程 t 后,会执行死循环中的打印语句,但我们将线程 t 设置为后台线程后,当前台线程死亡后,JVM 会通知后台线程。由于后台线程从接受指令,到做出响应,需要一定的时间。因此,打印了几次 “后台线程—is running” 语句后,后台也结束了。所以当只有后台线程运行时,进程就会结束。


线程的调度

线程的调度:程序中的多个线程是并发执行的,某个线程若想被执行必须要得到 CPU 的使用权,Java 虚拟机会按照特定的机制为程序中的每个线程分配 CPU 的使用权,这种机制被称作线程的调度。

在计算机中,线程调度有两种模型,即分时调度模型和抢占式调度模型。Java 虚拟机默认采用抢占式调度模型。

分时调度模型:是指让所有的线程轮流获得 CPU 的使用权,并且平均分配每个线程占用的 CPU 的时间片。
抢占式调度模型:是指让可运行池中优先级高的线程优先占用 CPU,而对于优先级相同的线程,随机选择一个线程使其占用 CPU,当它失去了 CPU 的使用权后,再随机选择其他线程获取 CPU 使用权。

线程的优先级

在应用程序中,若要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得 CPU 执行的机会越大。线程的优先级用 1~10 之间的整数来表示,数字越大优先级越高。除了数字表示线程的优先级,还可以使用 Thread 类中提供的三个静态常量表示线程的优先级,如下表所示:

Thread 类的静态常量 功能描述 static int MAX_PRIORITY 表示线程的最高优先级,相当于值 10 static int MIN_PRIORITY 表示线程的最低优先级,相当于值 1 static int NORM_PRIORITY 表示线程的普通优先级,相当于值 5

示例代码如下:

//定义类 MaxPriority 实现 Runnable 接口class MaxPriority implements Runnable {    public void run() {        for(int i=0; i<10; i++){        System.out.println(Thread.currentThread().getName() + "正在输出:" + i);        }    }}//定义类 MinPriority 实现 Runnable 接口class MinPriority implements Runnable {    public void run(){        for(int i=0; i<10; i++){            System.out.println(Thread.currentThread().getName() + "正在输出:" + i);        }    }}public class Example {    public static void main(String[] args){        //创建两个线程        Thread minPriority = new Thread(new MinPriority, "优先级较低的线程");        Thread maxPriority = new Thread(new MaxPriority, "优先级较高的线程");        minPriority.setPriority(Thread.MIN_PRIORITY);  //设置线程的优先级为 1        maxPriority.setPriority(10);  //设置线程的优先级为 10        //开启两个线程        maxPriority.start();        minPriority.start();    }}

注意:虽然 Java 中提供了 10 个线程优先级,但不同的操作系统对优先级的支持是不一样的,不能很好地和 Java 中线程优先级一一对应。因此,在设计多线程应用程序时,其功能的实现一定不能依赖于线程的优先级,只能把线程优先级作为一种提高程序效率的手段


线程休眠

若要人为地控制线程,使正在执行的线程暂停,将 CPU 让给别的线程,可以使用静态方法 sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用 sleep(long millis) 方法后,在指定时间内是不会执行的,这样其他的线程就可以得到执行的机会了。

注意:第一点:sleep(long millis) 方法声明抛出 InterruptedException 异常,因此在调用该方法时,应该捕获异常,或者声明抛出该异常。
第二点:sleep() 是静态方法,只能控制当前正在运行的线程休眠,而不能控制其他线程休眠。当休眠时间结束后,线程就会返回到就绪状态,而不是立即开始运行。

示例代码如下:

class SleepThread implements Runnable{    public void run() {        for(int i =1; i<10; i++){            if(i==3){                try{                    Thread.sleep(2000);  //当前线程休眠 2 秒                }catch(InterruptedException e){                    e.printStackTrace();                }            }            System.out.println("线程一直在输出:" + i);            try{                Thread.sleep(500);        //当前线程休眠 500 毫秒            }catch(Exception e){                e.printStackTrace();            }        }    }}public class Example {    public static void main(String[] args) throws Exception{    //创建一个线程    new Thread(new SleepThread()).start();    for(int i=1; i<=10; i++){        if(i==5){            Thread.sleep(20000);  //当前线程休眠 2 秒        }        System.out.println("主线程正在输出:" + i);        Thread.sleep(500);        //当前线程休眠 500 毫秒    }    }}

线程让步

在校园中,经常会看到同学互相抢篮球,当某个同学抢到篮球后就可以拍一会,之后他会把篮球让出来,大家重新开始抢篮球,这个过程就相当于 Java 程序中的线程让步。

线程让步可以通过 yield() 方法来实现,该方法和 sleep() 方法有点相似,都可以让当前正在运行的线程暂停,区别在于 yield() 方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用 yield() 方法后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

示例代码如下:

//定义 YieldThread 类继承 Thread 类class YieldThread extends Threads {    //定义一个有参的构造方法    public YieldThread(String name){        super(name);  //调用父类的构造方法    }    public void run(){        for(int i=0; i<5; i++){            System.out.println(Thread.currentThread().getName() + "---" + i);            if(i==3){                System.out.print("线程让步:");                Thread.yield();  //线程运行到此,做出让步            }        }    }}public class Example{    public static void main(String[] args){        //创建两个线程        Thread t1 = new YieldThread("线程 A");        Thread t2 = new YieldThread("线程 B");        //开启两个线程        t1.start();        t2.start();    }}

线程插队

现实生活中经常碰到“插队”的情况,同样,在 Thread 类中也提供了一个 join() 方法来实现这个“功能”。

当在某个线程中调用其他线程的 join() 方法时,调用的线程将被阻塞,知道被 join() 方法加入的线程执行完成后,它才会继续运行。

示例代码如下:

class EmergencyThread implements Runnable{    public void run(){        for(int i=1; i<6; i++){            System.out.println(Thread.currentThread().getName() + "输入:" + i);            try{                Thread.sleep(500);  //线程休眠 500 毫秒            }catch(InterruptedException e){                e.printStackTrace();            }        }    }}public class Example{    public static void main(String[] args) throws Exception{        Thread t = new Thread(new EmergencyThread(), "线程一");        t.start();  //开启线程        for(int i=1; i<6; i++){            System.out.println(Thread.currentThread().getName() + "输入:" + i);            if(i==2){                t.join();       //调用 join() 方法            }            Thread.sleep(500);  //线程休眠 500 毫秒        }    }}

多线程同步

线程安全问题是由多个线程同时处理共享资源所导致的。因此必须能保证共享资源在任何时刻只能有一个线程访问。
为了实现这种限制,Java 中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用 synchronized 关键字来修饰,被称作同步代码块,其语法结构如下:

synchronized(lock){    操作共享资源代码块}

其中,lock 是一个锁对象,它是同步代码块的关键。当线程执行同步代码块时,首先会检查所对象的标志位,默认情况下标志位为 1,此时线程会执行同步代码块,同时将锁对象的标志位置为 0。当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为 0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为 1,新线程才能进入同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程类似于一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。

示例代码如下:

//定义 Ticket 类实现 Runnable 接口class Ticket implements Runnable{    private int ticket = 10;     //定义变量 tickets,并赋值 10    object lock = new Object();  //定义任意一个对象,用作同步代码块的锁    public void run(){        while(true){            synchronized(lock){       //定义同步代码块                try{                    Thread.sleep(10); //经过的线程休眠 10 毫秒                }catch(InterruptedException e){                    e.printStackTrace();                }                if(tickets>0){                System.out.println(Thread.currentThread().getName()                 + "---卖出的票" + tickets--);                }else{                    break;                }            }        }    }}public class Example{    public static void main(String[] args){        Ticket ticket = new Ticket();  //创建 Ticket 对象        //创建并开启四个线程        new Thread(ticket, "线程一").start();        new Thread(ticket, "线程二").start();        new Thread(ticket, "线程三").start();        new Thread(ticket, "线程四").start();    }}

注意:同步代码块中的锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是唯一的。还有一点,锁对象的创建代码不能放到 run() 方法中,否则每个线程运行到 run() 方法都会创建一个新对象,这样每个线程都有一个不同的锁,每个锁都有自己的标志位。线程之间便不能产生同步的效果。

同步方法
在方法前,使用 synchronized 关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:

synchronized 返回值类型 方法名 ([参数1, 参数2, ...]){}

注意:被 synchronized 修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法。

示例代码如下:

//定义 Ticket 类实现 Runnable 接口class Ticket implements Runnable{    private int ticket = 10;    public void run(){        while(true){            saleTicket();            if(ticket<=0){  //调用售票方法                break;            }        }    }    //定义一个同步方法 saleTicket()    if(ticket>0){        try{            Thread.sleep(10);  //经过的线程休眠 10 毫秒        }catch(InterruptedException e){            e.printStackTrace();        }        System.out.println(            Thread.currentThread().getName()            + "---卖出的票" + tickets--        );    }}public class Example{    public static void main(String[] args){        Ticket ticket = new Ticket();   //创建 Ticket 对象        //创建并开启四个线程        new Thread(ticket, "线程一").start();        new Thread(ticket, "线程二").start();        new Thread(ticket, "线程三").start();        new Thread(ticket, "线程四").start();    }}

多线程同步小结:
同步代码块的锁是自己定义的任意类型的对象
同步方法的锁是当前调用该方法的对象,也就是 this 指向的对象。


多线程通信
为了更好地理解线程间通信,可以模拟这样一种应用场景,假设有两个线程同时去操作同一个存储空间,其中一个线程负责向存储空间中存入数据,另一个线程负责取出数据。

示例代码如下:

class Storage{    private int[] cells = new int[10];   //数据存储数组    private int[] inPos, outPos;         //inPos存入时数组下标,outPos取出时数组下标    private int count;                   //存入或者取出数据的数量    public synchronized void put(int num){        try{            //如果放入数据等于 cells 的长度,此线程等待            while(count==cells.length){                this.wait();            }            cells[inPos] = num;   //向数组中放入数据            System.out.println("在 cells["+inPos+"]中放入数据---" + cells[inPos]);            inPos++;              //存完元素让位置加 1            this.notify();        }catch(Exception e){            e.printStackTrace();        }    }    public synchronized void get(){        try{            while(count==0){           //如果 count 为 0,此线程等待                this.wait();            }            int data = cells[outPos];  //从数组中取出数据            System.out.println("从 cells["+outPos+"] 中取出数据" + data);            cells[outPos] = 0;         //取出后,当前位置的数据置 0            outPos++;                  //取完元素让位置加 1            if(outPos==cells.length){   //当从 cells[9] 取完数据后再从 cells[0] 开始                outPos = 0;            count--;            this.notify();        }catch(Exception e){            e.printStackTrace();        }    }}

//讲解待续

原创粉丝点击