Java多线程编程核心技术
上QQ阅读APP看书,第一时间看更新

1.2 使用多线程

想学习一个技术就要“接近”它,所以在本节,首先用一个示例来接触一下线程。

一个进程正在运行时至少会有1个线程在运行,这种情况在Java中也是存在的。这些线程在后台默默地执行,比如调用public static void main()方法的线程就是这样的,而且它是由JVM创建的。

创建示例项目callMainMethodMainThread,创建Test.java类。代码如下:

package test;
public class Test {
public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}

程序运行后的效果如图1-5所示。

图1-5 主线程main

在控制台中输出的main其实就是一个名称叫作main的线程在执行main()方法中的代码。另外需要说明一下,在控制台输出的main和main方法没有任何的关系,仅仅是名字相同而已。

1.2.1 继承Thread类

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

但在学习如何创建新的线程前,先来看看Thread类的结构,如下:

public class Thread implements Runnable 

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

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

本节来看一下第一种方法。创建名称为t1的Java项目,创建一个自定义的线程类MyThread.java,此类继承自Thread,并且重写run方法。在run方法中,写线程要执行的任务的代码如下:

package com.mythread.www;
public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("MyThread");
    }
}

运行类代码如下:

package test;
import com.mythread.www.MyThread;
public class Run {
    public static void main(String[] args) {
        MyThread mythread = new MyThread();
        mythread.start();
        System.out.println("运行结束!");
    }
}

运行结果如图1-6所示。

图1-6 运行结果

从图1-6中的运行结果来看,MyThread.java类中的run方法执行的时间比较晚,这也说明在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。

线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法,所以就会出现先打印“运行结束!”后输出“MyThread”这样的结果了。

注意 如果多次调用start()方法,则会出现异常Exception in thread"main"java.lang.IllegalThreadStateException。

上面介绍了线程的调用的随机性,下面将在名称为randomThread的Java项目中演示线程的随机性。

创建自定义线程类MyThread.java,代码如下:

package mythread;
public class MyThread extends Thread {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                int time = (int) (Math.random() * 1000);
                Thread.sleep(time);
                System.out.println("run=" + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

再创建运行类Test.java,代码如下:

package test;
import mythread.MyThread;
public class Test {
    public static void main(String[] args) {
        try {
            MyThread thread = new MyThread();
            thread.setName("myThread");
            thread.start();
            for (int i = 0; i < 10; i++) {
                int time = (int) (Math.random() * 1000);
                Thread.sleep(time);
                System.out.println("main=" + Thread.currentThread().getName());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在代码中,为了展现出线程具有随机特性,所以使用随机数的形式来使线程得到挂起的效果,从而表现出CPU执行哪个线程具有不确定性。

Thread.java类中的start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完后才可以执行后面的代码。

以异步的方式运行的效果如图1-7所示。

图1-7 随机被执行的线程

另外还需要注意一下,执行start()方法的顺序不代表线程启动的顺序。创建测试用的项目名称为z,类MyThread.java代码如下:

package extthread;
public class MyThread extends Thread {
    private int i;
    public MyThread(int i) {
        super();
        this.i = i;
    }
    @Override
    public void run() {
        System.out.println(i);
    }
}

运行类Test.java代码如下:

package test;
import extthread.MyThread;
public class Test {
    public static void main(String[] args) {
        MyThread t11 = new MyThread(1);
        MyThread t12 = new MyThread(2);
        MyThread t13 = new MyThread(3);
        MyThread t14 = new MyThread(4);
        MyThread t15 = new MyThread(5);
        MyThread t16 = new MyThread(6);
        MyThread t17 = new MyThread(7);
        MyThread t18 = new MyThread(8);
        MyThread t19 = new MyThread(9);
        MyThread t110 = new MyThread(10);
        MyThread t111 = new MyThread(11);
        MyThread t112 = new MyThread(12);
        MyThread t113 = new MyThread(13);
        t11.start();
        t12.start();
        t13.start();
        t14.start();
        t15.start();
        t16.start();
        t17.start();
        t18.start();
        t19.start();
        t110.start();
        t111.start();
        t112.start();
        t113.start();
    }
}

程序运行后的结果如图1-8所示。

图1-8 线程启动顺序与start()执行顺序无关

1.2.2 实现Runnable接口

如果欲创建的线程类已经有一个父类了,这时就不能再继承自Thread类了,因为Java不支持多继承,所以就需要实现Runnable接口来应对这样的情况。

创建项目t2,继续创建一个实现Runnable接口的类MyRunnable,代码如下:

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

如何使用这个MyRunnable.java类呢?这就要看一下Thread.java的构造函数了,如图1-9所示。

图1-9 Thread构造函数

在Thread.java类的8个构造函数中,有两个构造函数Thread(Runnable target)和Thread(Runnable target,String name)可以传递Runnable接口,说明构造函数支持传入一个Runnable接口的对象。运行类代码如下:

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

运行结果如图1-10所示。

图1-10 运行结果

图1-10所示的打印结果没有什么特殊之处。

使用继承Thread类的方式来开发多线程应用程序在设计上是有局限性的,因为Java是单根继承,不支持多继承,所以为了改变这种限制,可以使用实现Runnable接口的方式来实现多线程技术。这也是上面的示例介绍的知识点。

另外需要说明的是,Thread.java类也实现了Runnable接口,如图1-11所示。

图1-11 类Thread实现Runnable接口

那也就意味着构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用。

1.2.3 实例变量与线程安全

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

(1)不共享数据的情况

不共享数据的情况如图1-12所示。

图1-12 不共享数据

下面通过一个示例来看下数据不共享情况。

创建实验用的Java项目,名称为t3,MyThread.java类代码如下:

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

运行类Run.java代码如下:

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

不共享数据运行结果如图1-13所示。

图1-13 不共享数据的运行结果

由图1-13可以看到,一共创建了3个线程,每个线程都有各自的count变量,自己减少自己的count变量的值。这样的情况就是变量不共享,此示例并不存在多个线程访问同一个实例变量的情况。

如果想实现3个线程共同对一个count变量进行减法操作的目的,该如何设计代码呢?

(2)共享数据的情况

共享数据的情况如图1-14所示。

图1-14 共享数据

共享数据的情况就是多个线程可以访问同一个变量,比如在实现投票功能的软件时,多个线程可以同时处理同一个人的票数。

下面通过一个示例来看下数据共享情况。

创建t4测试项目,MyThread.java类代码如下:

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

运行类Run.java代码如下:

public class Run {
    public static void main(String[] args) {
        MyThread mythread=new MyThread();
        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();
    }
}

运行结果如图1-15所示。

图1-15 共享数据运行结果

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

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

1)取得原有i值。

2)计算i-1。

3)对i进行赋值。

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

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

public class MyThread extends Thread {
    private int count=5;
    @Override
    synchronized public void run() {
        super.run();
            count--;
            System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count);
    }
}

重新运行程序,就不会出现值一样的情况了,如图1-16所示。

图1-16  方法调用被同步

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

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

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

创建t4_threadsafe项目,来实现一下非线程安全的环境。LoginServlet.java代码如下:

package controller;
//本类模拟成一个Servlet组件
public class LoginServlet {
    private static String usernameRef;
    private static String passwordRef;
    public static void doPost(String username, String password) {
        try {
            usernameRef = username;
            if (username.equals("a")) {
                Thread.sleep(5000);
            }
            passwordRef = password;
            System.out.println("username=" + usernameRef + " password="
                    + password);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

线程ALogin.java代码如下:

package extthread;
import controller.LoginServlet;
public class ALogin extends Thread {
    @Override
    public void run() {
        LoginServlet.doPost("a", "aa");
    }
}

线程BLogin.java代码如下:

package extthread;
import controller.LoginServlet;
public class BLogin extends Thread {
    @Override
    public void run() {
        LoginServlet.doPost("b", "bb");
    }
}

运行类Run.java代码如下:

public class Run {
    public static void main(String[] args) {
        ALogin a = new ALogin();
        a.start();
        BLogin b = new BLogin();
        b.start();
    }
}

程序运行后的效果如图1-17所示。

图1-17 非线程安全

解决这个“非线程安全”的方法也是使用synchronized关键字。更改代码如下:

    synchronized public static void doPost(String username, String password) {
        try {
            usernameRef = username;
            if (username.equals("a")) {
                Thread.sleep(5000);
            }
            passwordRef = password;
            System.out.println("username=" + usernameRef + " password="
                    + password);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

程序运行后效果如图1-18所示。

图1-18 排队进入方法

1.2.4 留意i--与System.out.println()的异常

在前面章节中,解决非线程安全问题使用的是synchronized关键字,本节将通过程序案例细化一下println()方法与i++联合使用时“有可能”出现的另外一种异常情况,并说明其中的原因。

创建名称为sameNum的项目,自定义线程MyThread.java代码如下:

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

运行类Run.java代码如下:

package test;
import extthread.MyThread;
public class Run {
    public static void main(String[] args) {
        MyThread run = new MyThread();
        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();
    }
}

程序运行后根据概率还是会出现非线程安全问题,如图1-19所示。

图1-19 出现非线程安全问题

本实验的测试目的是:虽然println()方法在内部是同步的,但i--的操作却是在进入println()之前发生的,所以有发生非线程安全问题的概率,如图1-20所示。

图1-20 println内部同步

所以,为了防止发生非线程安全问题,还是应继续使用同步方法。