Java 多线程编程

java学习血泪史

  • 多线程编程
    • 继承 Thread 类
      • Thread 类构造方法
      • 例 1
    • 实现 Runnable 接口
      • 例 2
    • Java线程的生命周期及线程的几种状态
    • Java多线程之间访问实例变量
      • 例 1
      • 例 2
    • Java非线程安全问题的解决方法
    • Java多线程的同步机制:synchronized
      • 例 1

多线程编程

之前学习的程序都是单线程的,即一个程序只有一条从头到尾的执行线索。

然而很多程序有很多过程需要多条线索同时运行的特性。

在 Java 中,并发机制非常重要,但并不是所有程序语言都支持线程。在以往的程序中,多以一个任务完成以后再进行下一个任务的模式进行,这样下一个任务的开始必须等待前一个任务的结束。Java 语言提供了并发机制,允许开发人员在程序中执行多个线程,每个线程完成一个功能,并与其他线程并发执行。这种机制被称为多线程。

系统可以分配给每个进程一段有限的执行 CPU 的时间(也称为 CPU 时间片),CPU 在这段时间中执行某个进程,然后下一个时间段又跳到另一个进程中去执行。由于 CPU 切换的速度非常快,给使用者的感受就是这些任务似乎在同时运行,所以使用多线程技术后,可以在同一时间内运行更多不同种类的任务。

单任务的特点就是排队执行,也就是同步,就像在 cmd 中输入一条命令后,必须等待这条命令执行完才可以执行下一条命令一样。这就是单任务环境的缺点,即 CPU 利用率大幅降低。

Java 多线程编程_第1张图片
图2 单线程和多线程执行模式

图 2 的右侧则是多线程环境下的执行模式。从中可以发现,CPU 完全可以在任务 1 和任务 2 之间来回切换,使任务 2 不必等到 5 秒再运行,系统的运行效率大大得到提升。

JVM加载代码时,发现main方法之后会自动启动一个线程,这个线程称为主线程,该线程负责执行main放法。那么,在main方法中创建的线程称为这个程序中的其他线程。当程序中所有的线程全部结束后JVM才会结束java程序

在 Java 的 JDK 开发包中,已经自带了对多线程技术的支持,可以方便地进行多线程编程。实现多线程编程的方式主要有两种:一种是继承 Thread 类,另一种是实现 Runnable 接口。下面详细介绍这两种具体实现方式。

继承 Thread 类

在学习如何实现多线程前,先来看看 Thread 类的结构,如下:

public class Thread implements Runnable

从上面的源代码可以发现,Thread 类实现了 Runnable 接口,它们之间具有多态关系。

其实,使用继承 Thread 类的方式实现多线程,最大的局限就是不支持多继承,因为 Java 语言的特点就是单根继承,所以为了支持多继承,完全可以实现 Runnable 接口的方式,一边实现一边继承。但用这两种方式创建的线程在工作时的性质是一样的,没有本质的区别。

Thread 类构造方法

Thread 类有如下两个常用构造方法:

  1. public Thread(String threadName)
  2. public Thread()

继承 Thread 类实现线程的语法格式如下:

public class NewThreadName extends Thread
{    //NewThreadName 类继承自 Thread 类
    public void run()
    {
        //线程的执行代码在这里
    }
}

线程实现的业务代码需要放到 run() 方法中。当一个类继承 Thread 类后,就可以在该类中覆盖 run() 方法,将实现线程功能的代码写入 run() 方法中,然后同时调用 Thread 类的 start() 方法执行线程,也就是调用 run() 方法。

Thread 对象需要一个任务来执行,任务是指线程在启动时执行的工作,该工作的功能代码被写在 run() 方法中。当执行一个线程程序时,就会自动产生一个线程,主方法正是在这个线程上运行的。当不再启动其他线程时,该程序就为单线程程序。主方法线程启动由 Java 虚拟机负责,开发人员负责启动自己的线程。

如下代码演示了如何启动一个线程:

new NewThreadName().start();    //NewThreadName 为继承自 Thread 的子类

注意:如果 start() 方法调用一个已经启动的线程,系统将会抛出 IllegalThreadStateException异常。

例 1

编写一个 Java 程序演示线程的基本使用方法。这里创建的自定义线程类为 MyThread,此类继承自 Thread,并在重写的 run() 中输出一行字符串。

MyThread 类代码如下:

package try02;

public class MyThread extends Thread{
	 @Override
	    public void run()
	    {
	        super.run();
	        System.out.println("这是线程类 MyThread");
	    }
	 
	 //接下来编写启动 MyThread 线程的主方法
	 public static void main(String[] args) throws InterruptedException
	 {
	     MyThread mythread=new MyThread();    //创建一个线程类
	     mythread.start();    				//开启线程
	     //Thread.sleep(1);
	     System.out.println("运行结束!");    //在主线程中输出一个字符串
	 }
}

运行上面的程序将看到如下所示的运行效果。

运行结束!
这是线程类 MyThread

如果加上Thread.sleep(1);

这是线程类 MyThread
运行结束!

从上面的运行结果来看,MyThread 类中 run() 方法执行的时间要比主线程晚。这也说明在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。同时也验证了线程是一个子任务,CPU 以不确定的方式,或者说以随机的时间来调用线程中的 run() 方法,所以就会出现先打印“运行结束!”,后输出“这是线程类yThread”这样的结果了。

代码的运行结果与代码执行顺序或调用顺序是无关的

同步执行线程 start() 方法的顺序不代表线程启动的顺序

实现 Runnable 接口

如果要创建的线程类已经有一个父类,这时就不能再继承 Thread 类,

因为 Java 不支持多继承,所以需要实现 Runnable 接口来应对这样的情况。

实现 Runnable 接口的语法格式如下:

public class thread extends Object implements Runnable

提示:从 JDK 的 API 中可以发现,实质上 Thread 类实现了 Runnable 接口,其中的 run() 方法正是对 Runnable 接口中 run() 方法的具体实现。

实现 Runnable 接口的程序会创建一个 Thread 对象,并将 Runnable 对象与 Thread 对象相关联。Thread 类有如下两个与 Runnable 有关的构造方法:

  1. public Thread(Runnable r);
  2. public Thread(Runnable r,String name);

使用上述两种构造方法之一均可以将 Runnable 对象与 Thread 实例相关联。使用 Runnable 接口启动线程的基本步骤如下。

  1. 创建一个 Runnable 对象。
  2. 使用参数带 Runnable 对象的构造方法创建 Thread 实例。
  3. 调用 start() 方法启动线程。

通过实现 Runnable 接口创建线程时开发人员首先需要编写一个实现 Runnable 接口的类,然后实例化该类的对象,这样就创建了 Runnable 对象。接下来使用相应的构造方法创建 Thread 实例。最后使用该实例调用 Thread 类的 start() 方法启动线程,如图 1 所示。

img
图1 使用Runnable接口启动线程流程

例 2

编写一个简单的案例演示如何实现 Runnable 接口,以及如何启动线程。

(1) 首先创建一个自定义的 MyRmmable 类,让该类实现 Runnable 接口,并在 run() 方法中输出一个字符串。代码如下:

package try01;
public class MyRunnable implements Runnable
{
    @Override 
    public void run()
    { 
        System.out.println("MyRunnable运行中!"); 
    }
}

(2) 接下来在主线程中编写代码,创建一个 MyRunnable 类实例,并将该实例作为参数传递给 Thread 类的构造方法,最后调用 Thread 类的 start() 方法启动线程。具体实现代码如下:

package try01;
public class Test04
{
    public static void main(String[] args)
    {
        Runnable runnable=new MyRunnable();
        Thread thread=new Thread(runnable);
        thread.start();
        System.out.println("主线程运行结束!");
    }
}

如上述代码所示,启动线程的方法非常简单。运行结s果如下所示,同样验证了线程执行的随机性。

主线程运行结束!
MyRunnable运行中!

注意:要启动一个新的线程,不是直接调用 Thread 子类对象的 run() 方法,而是调用 Thread 子类的 start() 方法。Thread 类的 start() 方法会产生一个新的线程,该线程用于执行 Thread 子类的 run() 方法。

Java线程的生命周期及线程的几种状态

Java 多线程编程_第2张图片

  1. 出生状态:用户在创建线程时所处的状态,在用户使用该线程实例调用 start() 方法之前,线程都处于出生状态。
  2. 就绪状态:也称可执行状态,当用户调用 start() 方法之后,线程处于就绪状态。
  3. 运行状态:当线程得到系统资源后进入运行状态。
  4. 等待状态:当处于运行状态下的线程调用 Thread 类的 wait() 方法时,该线程就会进入等待状态。进入等待状态的线程必须调用 Thread 类的 notify() 方法才能被唤醒。notifyAll() 方法是将所有处于等待状态下的线程唤醒。
  5. 休眠状态:当线程调用 Thread 类中的 sleep() 方法时,则会进入休眠状态。
  6. 阻塞状态:如果一个线程在运行状态下发出输入/输出请求,该线程将进入阻塞状态,在其等待输入/输出结束时,线程进入就绪状态。对阻塞的线程来说,即使系统资源关闭,线程依然不能回到运行状态。
  7. 死亡状态:当线程的 run() 方法执行完毕,线程进入死亡状态。

提示:一旦线程进入可执行状态,它会在就绪状态与运行状态下辗转,同时也可能进入等待状态、休眠状态、阻塞状态或死亡状态。

根据图 1 所示,可以总结出使线程处于就绪状态有如下几种方法。

  • 调用 sleep() 方法。
  • 调用 wait() 方法。
  • 等待输入和输出完成。

当线程处于就绪状态后,可以用如下几种方法使线程再次进入运行状态。

  • 线程调用 notify() 方法。
  • 线程调用 notifyAll() 方法。
  • 线程调用 intermpt() 方法。吵醒线程
  • 线程的休眠时间结束。
  • 输入或者输出结束。

Java多线程之间访问实例变量

自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间进行交互时是很重要的一个技术点。

图 1 所示为不共享数据的示例,图 2 所示为共享数据的示例。

Java 多线程编程_第3张图片
图1 线程之间不共享数据实例图

Java 多线程编程_第4张图片
图2 线程间共享数据示例图

例 1

如图 1 所示,在不共享数据时每个线程都拥有自己作用域的变量,且多个线程之间相同变量名的值也不相同。下面创建一个示例演示这种特性。

首先创建自定义的线程类 MyThread03, 代码如下:

package ch14;
public class MyThread03  extends Thread
{
    private int count=5; 
    public MyThread03(String name)
    { 
        super(); 
        this.setName(name);//设置线程名称 
    } 
    @Override 
    public void run()
    { 
        super.run(); 
        while (count>0)
        { 
            count--; 
            System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count); 
        } 
    }
}

如上述代码所示,MyThread03 线程的代码非常简单。下面编写代码在主线程中创建 3 个 MyThread03 线程,并启动这些线程。具体代码如下:

package ch14;
public class Test05
{
    public static void main(String[] args)
    { 
        MyThread03 a=new MyThread03("A"); 
        MyThread03 b=new MyThread03("B"); 
        MyThread03 c=new MyThread03("C"); 
        a.start(); 
        b.start(); 
        c.start(); 
    }
}

运行主线程将看到图 1 所示效果。从如下所示的运行结果可以看出,程序一共创建了 3 个线程,每个线程都有各自的 count 变量,自己减少自己的 count 变量的值。这样的情况就是变量不共享,此实例并不存在多个线程访问同一个实例变量的情况。

由 B 计算,count=4
由 B 计算,count=3
由 B 计算,count=2
由 B 计算,count=1
由 B 计算,count=0
由 C 计算,count=4
由 C 计算,count=3
由 C 计算,count=2
由 C 计算,count=1
由 C 计算,count=0
由 A 计算,count=4
由 A 计算,count=3
由 A 计算,count=2
由 A 计算,count=1
由 A 计算,count=0

例 2

如果想实现多个线程共同对一个变量进行操作的目的,该如何设计代码呢?这时就必须使用共享数据的方案。共享数据的情况就是多个线程可以访问同一个变量,比如在实现投票功能的软件时,多个线程可以同时处理同一个人的票数。

下面通过一个示例来看一下数据共享情况。首先对例 1 的 MyThread03 类进行修改,这里将新线程类命名为 MyThread04。

package ch14;
public class MyThread04  extends Thread
{
    private int count=5; 
    @Override 
    public void run()
    { 
        super.run(); 
        count--; 
        //此示例不要用for语句,因为使用同步后其他线程就得不到运行的机会了, 
        //一直由一个线程进行减法运算 
        System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count); 
    }
}

编写代码在主线程中创建 5 个 MyThread04 线程,并启动这些线程。具体代码如下:

package ch14;
public class Test06
{
    public static void main(String[] args)
    { 
        MyThread04 mythread=new MyThread04(); 
        Thread a=new Thread(mythread,"A"); 
        Thread b=new Thread(mythread,"B"); 
        Thread c=new Thread(mythread,"C"); 
        Thread d=new Thread(mythread,"D"); 
        Thread e=new Thread(mythread,"E"); 
        a.start(); 
        b.start(); 
        c.start(); 
        d.start(); 
        e.start();
    }
}

运行主线程将看到如下所示的效果。从运行结果中可以看到,线程 A 和 B 打印出的 count 值都是 3,说明 A 和 B 同时对 count 进行处理,产生了“非线程安全”问题,但我们想要得到的打印结果却不是重复的,而是依次递减的。

由 A 计算,count=4
由 B 计算,count=3
由 C 计算,count=1
由 E 计算,count=1
由 D 计算,count=0

在某些 JVM 中,i-- 的操作要分成如下3步:

  • 取得原有 i 值。
  • 计算 i-1。
  • 对 i 进行赋值。

在这 3 个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。

其实这个示例就是典型的销售场景:5 名销售员,每名销售员卖出一件货品后不可以得出相同的剩余数量,必须在每一名销售员卖完一件货品后其他销售员才可以在新的剩余物品数上继续减1操作。这时就需要使多个线程之间进行同步,也就是用按顺序排队的方式进行减1操作。更改代码如下:

package ch14;
public class MyThread04  extends Thread
{
    private int count=5; 
    @Override 
    synchronized  public void run()
    { 
        super.run(); 
        count--; 
        //此示例不要用for语句,因为使用同步后其他线程就得不到运行的机会了, 
        //一直由一个线程进行减法运算 
        System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count); 
    }
}

再次运行程序,就不会出现值一样的情况了,如下所示。

由 A 计算,count=4
由 B 计算,count=3
由 C 计算,count=2
由 D 计算,count=1
由 E 计算,count=0

通过在 run() 方法前加 synchronized 关键字,使多个线程在执行 run() 方法时,以排队的方式进行处理。当一个线程调用 run() 前,先判断 run() 方法有没有被上锁,如果上锁,说明有其他线程正在调用 run()方法,必须等其他线程对 run() 方法调用结束后才可以执行 run()方法。这样也就实现了排队调用 run() 方法的目的,达到了按顺序对 count 变量减 1 的效果。synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区” 或“临界区”。

当一个线程想要执行同步方法里面的代码时,线程首先尝试去拿这把锁,如果能够拿到锁,那么这个线程就可以执行 synchronize 里面的代码。如果不能拿到锁,那么这个线程就会不断地尝试拿锁,直到能够拿到为止,而且有多个线程同时去争抢这把锁。

Java非线程安全问题的解决方法

非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。下面用一个示例来学习一下如何解决非线程安全问题。

本案例模拟了多线程下的用户登录验证功能。首先编写一个类实现验证功能, LoginCheck 类的代码如下:

package ch14;
public class LoginCheck
{
    private static String username; 
    private static String password; 
    public static void doPost(String _username,String _password)
    { 
        try
        { 
            username=_username; 
            if (username.equals("admin"))
            { 
                Thread.sleep(5000); 
            } 
            password=_password; 
            System.out.println("username="+username+"password="+password); 
        }
        catch(InterruptedException e)
        { 
            // TODO Auto-generated catch block 
            e.printStackTrace(); 
        } 
    } 

接下来创建线程类 LoginThreadA 和 LoginThreadB,这两个线程都调用 LoginCheck 类进行登录信息。其中 LoginThreadA 类的代码如下:

package ch14;
public class LoginThreadA extends Thread
{
    public void run()
    { 
        LoginCheck.doPost("admin","admin"); 
    }
}

LoginThreadB 类的代码如下:

package ch14;
public class LoginThreadB extends Thread
{ 
    public void run()
    { 
        LoginCheck.doPost("root","root"); 
    }
}

现在编写主线程程序,分别创建 LoginThreadA 线程实例和 LoginThreadB 线程实现,然后启动这两个线程。主线程的代码如下:

package ch14;
public class Test07
{
    public static void main(String[] args)
    {
        LoginThreadA a=new LoginThreadA();
        a.run();    //启动线程LoginThreadA
        LoginThreadB b=new LoginThreadB();
        b.run();    //启动线程LoginThreadB
    }
}

程序运行后的结果如下所示:

username=root password=admin
username=root password=root

从运行结果中可以看到用户名 root 出现了这两次,这是由于多个线程同时修改 username,导致值不一致的情况。

仔细查看代码可以发现问题出现在两个线程都会调用 doPost() 方法上。解决这个非线程安全问题的方法是

使用 synchronized 关键字修饰 doPost() 方法即不允许多个线程同时修改 doPost() 方法中的变量

更改代码如下:

package ch14;
public class LoginCheck
{
    private static String username; 
    private static String password; 
    synchronized public static void doPost(String _username,String _password)
    { 
        try
        { 
            username=_username; 
            if (username.equals("admin"))
            { 
                Thread.sleep(5000); 
            } 
            password=_password; 
            System.out.println("username="+username+"password="+password); 
        }
        catch (InterruptedException e)
        { 
            // TODO Auto-generated catch block 
            e.printStackTrace(); 
        } 
    } 
}

再次运行主线程,此时将看到如下所示的结果,说明不存在“非线程安全”问题了。

username=admin password=admin
username=root password=root

Java多线程的同步机制:synchronized

如果程序是单线程的,就不必担心此线程在执行时被其他线程“打扰”,就像在现实世界中,在一段时间内如果只能完成一件事情,不用担心做这件事情被其他事情打扰。但是,如果程序中同时使用多线程,好比现实中的“两个人同时通过一扇门”,这时就需要控制,否则容易引起阻塞。

为了处理这种共享资源竞争,可以使用同步机制。所谓同步机制,指的是两个线程同时作用在一个对象上,应该保持对象数据的统一性和整体性。Java 提供 synchronized 关键字,为防止资源冲突提供了内置支持。共享资源一般是文件、输入/输出端口或打印机。

在一个类中,用 synchronized 关键字声明的方法为同步方法。格式如下:

class类名
{
    public synchronized 类型名称 方法名称()
    {
        //代码
    }
}

Java 有一个专门负责管理线程对象中同步方法访问的工具——同步模型监视器,它的原理是为每个具有同步代码的对象准备唯一的一把“锁”。当多个线程访问对象时,只有取得锁的线程才能进入同步方法,其他访问共享对象的线程停留在对象中等待。

synchronized 不仅可以用到同步方法,也可以用到同步块。对于同步块,synchronized 获取的是参数中的对象锁。格式如下:

synchronized(obj)
{
    //代码
}

当线程执行到这里的同步块时,它必须获取 obj 这个对象的锁才能执行同步块,否则线程只能等待获得锁。必须注意的是,Obj 对象的作用范围不同,控制情况也不尽相同。如下代码为简单的一种使用:

public void method()
{
    Object obj=new Object();
    synchronized(obj)
    {
        //代码
    }
}

上述代码创建局部对象 Obj,由于每一个线程执行到 Object obj=new Object() 时都会产生一个 obj 对象,每一个线程都可以获得新创建的 obj 对象的锁而不会相互影响,因此这段程序不会起到同步作用。如果同步的是类的属性,情况就不同了。

例 1

在前面几节中,使用了 synchronized 关键字同步方法来解决非线程安全的问题。下面通过一个案例演示 println() 方法与 i-- 联合使用时“有可能”出现的另外一种异常情况,并说明其中的原因。

(1) 首先创建线程类 MyThread05,该类的代码很简单,如下所示:

package ch14;
public class MyThread05 extends Thread
{
    private int i=5;
    @Override
    public void run()
    {
        System.out.println("当前线程名称="+Thread.currentThread().getName()+",i="+(i--));
        //注意:代码i--由前面项目中单独一行运行改成在当前项目中在println()方法中直接进行打印
    }
}

(2) 编写主线程代码,首先创建一个 MyThread05 线程类,再启动 5 个相同的线程。具体代码如下:

package ch14;
public class Test08
{
    public static void main(String[] args)
    {
        MyThread05 run=new MyThread05(); 
        Thread t1=new Thread(run); 
        Thread t2=new Thread(run); 
        Thread t3=new Thread(run); 
        Thread t4=new Thread(run); 
        Thread t5=new Thread(run); 
        t1.start(); 
        t2.start(); 
        t3.start(); 
        t4.start(); 
        t5.start();
    }
}

从如下所示的运行效果可以看出,i 的值并不是从 5 递减 1。这是因为虽然 println() 方法在内部是同步的,但 i-- 操作却是在进入 println() 之前发生的,所以有发生非线程安全问题的概率。

当前线程名称=Thread-2,i=5
当前线程名称=Thread-3,i=2
当前线程名称=Thread-4,i=3
当前线程名称=Thread-1,i=4
当前线程名称=Thread-5,i=1

(3) 为了防止发生非线程安全问题,应继续使用同步方法。在这里使用同步块完成,修改后的代码如下:

package ch14;
public class MyThread05 extends Thread
{
    private int i=5;
    @Override
    public void run()
    {
        synchronized (this)
        {
            System.out.println("当前线程名称="+Thread.currentThread().getName()+",i="+(i--));
            //注意:代码i--由前面项目中单独一行运行改成在当前项目中在println()方法中直接进行打印
        }
    }
}

(4) 再次运行将看到如下所示的正常的运行效果。

当前线程名称=Thread-1,i=5
当前线程名称=Thread-2,i=4
当前线程名称=Thread-3,i=3
当前线程名称=Thread-4,i=2
当前线程名称=Thread-5,i=1

你可能感兴趣的:(Java)