《Java多线程编程核心技术》——1.2节使用多线程

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所示。

在控制台中输出的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中的运行结果来看,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所示。
另外还需要注意一下,执行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.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所示。

在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所示的打印结果没有什么特殊之处。
使用继承Thread类的方式来开发多线程应用程序在设计上是有局限性的,因为Java是单根继承,不支持多继承,所以为了改变这种限制,可以使用实现Runnable接口的方式来实现多线程技术。这也是上面的示例介绍的知识点。
另外需要说明的是,Thread.java类也实现了Runnable接口,如图1-11所示。

   

下面通过一个示例来看下数据不共享情况。
创建实验用的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可以看到,一共创建了3个线程,每个线程都有各自的count变量,自己减少自己的count变量的值。这样的情况就是变量不共享,此示例并不存在多个线程访问同一个实例变量的情况。
如果想实现3个线程共同对一个count变量进行减法操作的目的,该如何设计代码呢?
(2)共享数据的情况
共享数据的情况如图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中可以看到,线程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所示。
通过在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所示。
解决这个“非线程安全”的方法也是使用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.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所示。

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

时间: 2024-08-31 11:40:24

《Java多线程编程核心技术》——1.2节使用多线程的相关文章

读《Java多线程编程核心技术》一些问题

读<Java多线程编程核心技术>一些问题 1.suspend resume虽然不推荐使用,但是书中还是介绍了,实例中的1-48图 println()方法是同步锁的,当在线程中调用println()方法,线程被suspend()暂停的话, 那么println()的锁也被锁住了.

《Java多线程编程核心技术》——1.1节进程和多线程的概念及线程的优点

1.1 进程和多线程的概念及线程的优点 本节主要介绍在Java语言中使用多线程技术.但讲到多线程这个技术时不得不提及"进程"这个概念,"百度百科"里对"进程"的解释如图1-1所示. 初看这段文字会觉得十分的抽象,难以理解,但如果你看到图1-2所示的内容,那么你对进程还不能理解吗? 难道可以将一个正在操作系统中运行的exe程序理解成一个"进程"吗?没错! 通过查看"Windows任务管理器"中的列表,完全可以

《Java多线程编程核心技术》——第2章对象及变量的并发访问

第2章对象及变量的并发访问本章主要介绍Java多线程中的同步,也就是如何在Java语言中写出线程安全的程序,如何在Java语言中解决非线程安全的相关问题.多线程中的同步问题是学习多线程的重中之重,这个技术在其他的编程语言中也涉及,如C++或C#.本章应该着重掌握如下技术点:synchronized对象监视器为Object时的使用.synchronized对象监视器为Class时的使用.非线程安全是如何出现的.关键字volatile的主要作用.关键字volatile与synchronized的区别

《Java多线程编程核心技术》——导读

前 言为什么要写这本书早在几年前笔者就曾想过整理一份与Java多线程有关的稿件,因为市面上所有的Java书籍都是以一章或两章的篇幅介绍多线程技术,并没有完整地覆盖该技术的知识点,但可惜,苦于当时的时间及精力有限,一直没有达成所愿.也许是注定的安排,我目前所在的单位是集技术与教育为一体的软件类企业.我在工作中发现很多学员在学习完JavaSE/JavaEE之后想对更深入的技术进行探索,比如在对大数据.分布式.高并发类的专题进行攻克时,立即遇到针对java.lang包中Thread类的学习,但Thre

多线程编程之二——MFC中的多线程开发

五.MFC对多线程编程的支持 MFC中有两类线程,分别称之为工作者线程和用户界面线程.二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环. 工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等.用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等.但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务. 在MFC中,一般用全局函数Afx

多线程编程之二 ---MFC中的多线程开发

下载源代码 五.MFC对多线程编程的支持 MFC中有两类线程,分别称之为工作者线程和用户界面线程.二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环. 工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等.用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等.但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务. 在MFC中,一般用全

《Java多线程编程核心技术》——1.8节暂停线程

1.8 暂停线程 暂停线程意味着此线程还可以恢复运行.在Java多线程中,可以使用suspend()方法暂停线程,使用resume()方法恢复线程的执行. 1.8.1 suspend与resume方法的使用 本节将讲述如何使用suspend与resume方法. 创建测试用的项目suspend_resume_test,文件MyThread.java代码如下: package mythread; public class MyThread extends Thread { private long

《Java多线程编程核心技术》——1.7节停止线程

1.7 停止线程停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效的处理.停止线程在Java语言中并不像break语句那样干脆,需要一些技巧性的处理.使用Java内置支持多线程的类设计多线程应用是很常见的事情,然而,多线程给开发人员带来了一些新的挑战,如果处理不好就会导致超出预期的行为并且难以定位错误.本节将讨论如何更好地停止一个线程.停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作.虽然这看起来非常简单,但是必须做好防范措施,以便达到预期的效

《Java多线程编程核心技术》——2.1节synchronized同步方法

2.1 synchronized同步方法在第1章中已经接触"线程安全"与"非线程安全"相关的技术点,它们是学习多线程技术时一定会遇到的经典问题."非线程安全"其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是"脏读",也就是取到的数据其实是被更改过的.而"线程安全"就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象.此知识点在第1章也介绍,但本章将细化线程并发访问的内容,在