2020重新出发,JAVA基础,多线程编程

多线程编程

如果一次只完成一件事情,很容易实现。但是现实生活中很多事情都是同时进行的,所以在 Java中为了模拟这种状态,引入了线程机制。简单地说,当程序同时完成多件事情时,就是所谓的多线程程序。多线程的应用相当广泛,使用多线程可以创建窗口程序、网络程序等

世间万物都可以同时完成很多工作。例如,一台计算机既可以听歌,也可以编写文档和发送邮件,而这些活动的完成可以同时进行。这种同时执行多个操作的“思想”在 Java 中被称为并发,而将并发完成的每一件事称为线程

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

左图是单线程环境下任务 1 和任务 2 的执行模式。任务 1 和任务 2 是两个完全独立、互不相关的任务,任务 1 是在等待远程服务器返回数据,以便进行后期的处理,这时 CPU 一直处于等待状态,一直在“空运行”。如果任务 2 是在 5 秒之后被运行,虽然执行任务 2 用的时间非常短,仅仅是 1 秒,但必须在任务1运行结束后才可以运行任务 2。由于运行在单任务环境中,所以任务 2 有非常长的等待时间,系统运行效率大幅降低。

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

img

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

什么是线程

线程可以理解成是在进程中独立运行的子任务。比如,QQ.exe 运行时就有很多的子任务在同时运行。像好友视频、下载文件、传输数据、发送表情等,这些不同的任务或者说功能都可以同时运行,其中每一项任务完全可以理解成是“线程”在工作,传文件、听音乐、发送图片表情等功能都有对应的线程在后台默默地运行。

多线程的实现方式

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

继承 Thread 类

Thread 类的结构,如下:

public class Thread implements Runnable

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

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

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 异常。

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

public class MyThread extends Thread{    
    @Override    
    public void run(){        
        super.run();        
        System.out.println("这是线程类 MyThread");    
    }
}

接下来编写启动 MyThread 线程的主方法,代码如下:

public static void main(String[] args){    
    MyThread mythread=new MyThread();    //创建一个线程类    
    mythread.start();    //开启线程    
    System.out.println("运行结束!");    //在主线程中输出一个字符串
}

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

运行结束!
这是线程类 MyThread

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

例 :上面介绍了线程的调用具有随机性,为了更好地理解这种随机性这里编写了一个案例进行演示。

(1) 首先创建自定义的线程类 MyThread01,代码如下:

package ch14;
public class MyThread01 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("当前线程名称="+Thread.currentThread().getName()); 
            } 
        }
        catch(InterruptedException e)
        { 
            e.printStackTrace(); 
        } 
    } 
}

(2) 接下来编写主线程代码,在这里除了启动上面的 MyThread01 线程外,还实现了 MyThread01 线程相同的功能。主线程的代码如下:

package ch14;
public class Test02
{
    public static void main(String[] args)
    { 
        try
        { 
            MyThread01 thread=new MyThread01(); 
            thread.setName("myThread"); 
            thread.start(); 
            for (int i=0;i<10;i++)
            { 
                int time=(int)(Math.random()*1000); 
                Thread.sleep(time); 
                System.out.println("主线程名称="+Thread.currentThread().getName()); 
            } 
        }
        catch(InterruptedException e)
        { 
            e.printStackTrace(); 
        }
    }
}

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

MyThread01 类中的 start() 方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的 run() 方法。这个过程其实就是让系统安排一个时间来调用 Thread 中的 run() 方法,也就是使线程得到运行,启动线程,具有异步执行的效果。

如果调用代码 thread.run() 就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由 main 主线程来调用 run() 方法,也就是必须等 run() 方法中的代码执行完后才可以执行后面的代码。

这种采用随机数延时调用线程的方法又称为异步调用,程序运行的效果如下所示。

当前线程名称=myThread
主线程名称=main
当前线程名称=myThread
当前线程名称=myThread
当前线程名称=myThread
主线程名称=main
当前线程名称=myThread
当前线程名称=myThread
主线程名称=main
当前线程名称=myThread
主线程名称=main
当前线程名称=myThread
当前线程名称=myThread
当前线程名称=myThread
主线程名称=main
主线程名称=main
主线程名称=main
主线程名称=main
主线程名称=main
主线程名称=main

除了异步调用之外,同步执行线程 start() 方法的顺序不代表线程启动的顺序。下面创建一个案例演示同步线程的调用。

(1) 首先创建自定义的线程类 MyThread02,代码如下:

package ch14;
public class MyThread02 extends Thread
{
    private int i; 
    public MyThread02(int i)
    { 
        super(); 
        this.i=i; 
    } 
    @Override 
    public void run()
    { 
        System.out.println("当前数字:"+i); 
    }
}

(2) 接下来编写主线程代码,在这里创建 10 个线程类 MyThread02,并按顺序依次调用它们的 start() 方法。主线程的代码如下:

package ch14;
public class Test03
{
    public static void main(String[] args)
    { 
        MyThread02 t11=new MyThread02(1); 
        MyThread02 t12=new MyThread02(2); 
        MyThread02 t13=new MyThread02(3); 
        MyThread02 t14=new MyThread02(4); 
        MyThread02 t15=new MyThread02(5); 
        MyThread02 t16=new MyThread02(6); 
        MyThread02 t17=new MyThread02(7); 
        MyThread02 t18=new MyThread02(8); 
        MyThread02 t19=new MyThread02(9); 
        MyThread02 t110=new MyThread02(10); 
        t11.start(); 
        t12.start(); 
        t13.start(); 
        t14.start(); 
        t15.start(); 
        t16.start(); 
        t17.start(); 
        t18.start(); 
        t19.start(); 
        t110.start(); 
    }
}

程序运行后的结果如下所示,从运行结果中可以看到,虽然调用时数字是有序的,但是由于线程执行的随机性,导致输出的数字是无序的,而且每次顺序都不一样。

当前数字:1
当前数字:3
当前数字:5
当前数字:7
当前数字:6
当前数字:2
当前数字:4
当前数字:8
当前数字:10
当前数字:9

实现 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() 方法启动线程,如图所示。

img

例案例演示如何实现 Runnable 接口,以及如何启动线程。

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

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

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

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

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

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

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

两种方法的比较

虽然 Thread 类和 Runnable 接口都可以创建线程,但是它们也都有各自的优缺点。

继承 Thread 类的优缺点

当一个 run() 方法体现在继承 Thread 类中时,可以用 this 指向实际控制运行的 Thread 实例。因此,代码不需要使用以下控制语句:

Thread.currenThread().sleep();

不再使用上面的控制语句,而是可以简单地使用 Threadsleep() 方法,继承 Thread 类的方式使代码变得简单易读。

实现 Runnable 接口的优缺点

从面向对象的角度来看,Thread 类是一个虚拟处理机严格的封装,因此只有当处理机模型修改或扩展时,才应该继承该类。由于 Java 只允许单一继承,因此如果已经继承了 Thread 类,就不能再继承其他任何类,这会使用户只能采用实现 Runnable 接口的方式创建线程。

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

线程也具有生命周期,主要包括 7 种状态,分别是出生状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态和死亡状态

线程生命周期图

下面对线程生命周期中的 7 种状态做说明。

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

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

总结出使线程处于就绪状态有如下几种方法。

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

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

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

多线程之间访问实例变量

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

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

线程之间不共享数据实例图
线程间共享数据示例图

例如图 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

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

示例一下数据共享情况。首先对例 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 里面的代码。如果不能拿到锁,那么这个线程就会不断地尝试拿锁,直到能够拿到为止,而且有多个线程同时去争抢这把锁。

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

非线程安全是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。

本案例模拟了多线程下的用户登录验证功能。首先编写一个类实现验证功能, 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

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 对象的锁而不会相互影响,因此这段程序不会起到同步作用。如果同步的是类的属性,情况就不同了。

例在前面几节中,使用了 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

线程方法

curentThread()

curentThread() 方法可返回代码段正在被哪个线程调用的信息。

isAlive()

isAlive() 方法的作用是判断当前的线程是否处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。

sleep()

sleep() 方法的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指 this.currentThread() 返回的线程。

getId()

getId() 方法的作用非常简单,就是取得正在运行线程的唯一标识。

yieId()

yieId() 方法的作用是放弃当前的 CPU 资源,将它让给其他的任务去占用 CPU 执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片。

停止(终止)线程详解

停止线程是在多线程开发中很重要的技术点,掌握此技术可以对线程的停止进行有效的处理。停止线程在 Java语言中并不像 break 语句那样干脆,需要一些技巧性的处理。

使用 Java 内置支持多线程的类设计多线程应用是很常见的事情,然而,多线程给开发人员带来了一些新的挑战,如果处理不好就会导致超出预期的行为并且难以定位错误。

停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作。虽然这看起来非常简单,但是必须做好防范措施,以便达到预期的效果。

停止一个线程可以使用 Threadstop() 方法,但最好不用它。虽然它确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且已被弃用作废了,在将来的 Java 版本中,这个方法将不可用或不被支持。

大多数停止一个线程的操作使用 Thread.interrupt() 方法,尽管方法的名称是“停止,中止”的意思,但这个方法不会终止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。

在 Java 中有以下 3 种方法可以终止正在运行的线程:

  1. 使用退出标识,使线程正常退出,也就是当 run() 方法完成后线程终止。
  2. 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,因为 stop() 和 suspend() 及 resume() 一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
  3. 使用 interrupt() 方法中断线程。

停止不了的线程

interrupt() 方法的作用是用来停止线程,但 intermpt() 方法的使用效果并不像循环结构中 break 语句那样,可以马上停止循环。调用 intermpt() 方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。

例通过一个案例演示 interrupt() 方法停止线程的用法。案例用到的线程非常简单,仅仅是实现输出从 1~10000 的整数,代码如下:

package ch14;
public class MyThread13 extends Thread
{
    @Override 
    public void run()
    { 
        super.run(); 
        for (int i=0;i<10000;i++)
        { 
            System.out.println("i="+(i+1)); 
        } 
    } 
}

在调用 intermpt() 方法停止 MyThread13 线程之前,首先进行了一个 100 毫秒的休眠。主线程的代码如下:

package ch14;
public class Test17
{
    public static void main(String[] args)
    { 
        try
        { 
            MyThread13 thread=new MyThread13();      //创建MyThread13线程类实例
            thread.start();    //启动线程
            Thread.sleep(100);    //延时100毫秒
            thread.interrupt();    //停止线程
        }
        catch(InterruptedException e)
        { 
            System.out.println("main catch"); 
            e.printStackTrace(); 
        } 
    }
}

主线程的运行结果如下所示。从中可以看到,虽然在延时 100 毫秒后调用 intermpt() 方法停止了 thread 线程,但是该线程仍然执行完成输出 10000 行信息。

i=1
i=2
...
i=9999
i=10000

判断线程是不是停止状态

如何判断线程的状态是不是停止》在 Java 的 SDK 中,Thread.java 类里提供了两种方法。

  1. this.interrupted():测试当前线程是否已经中断。
  2. this.islnterrupted():测试线程是否已经中断。

那么这两个方法有什么区别呢?先来看看 this.intermpted() 方法的解释:测试当前线程是否已经中断,当前线程是指运行 this.interrupted() 方法的线程。为了对此方法有更深入的了解,下面通过一个案例进行说明。

例 假设 MyThread14 线程类的代码如下:

package ch14;
public class MyThread14 extends Thread
{
    @Override 
    public void run()
    { 
        super.run(); 
        for(int i=0;i<10000;i++)
        { 
            System.out.println("i="+(i+1)); 
        } 
    } 
}

主线程的代码如下:

package ch14;
public class Test18
{
    public static void main(String[] args)
    {
        try
        {
            MyThread14 thread=new MyThread14();
            thread.start();    //启动线程
            Thread.sleep(100);    //延时100毫秒
            thread.interrupt();    //停止线程
            //Thread.currentThread().interrupt();
            System.out.println("是否停止1?="+thread.interrupted());
            System.out.println("是否停止2?="+thread.interrupted());
        }
        catch(InterruptedException e)
        {
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end!");
    }
}

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

i=1
i=2
...
i=9999
i=10000
是否停止1?=false
是否停止2?=false
end!

在主线程中是在 thread 对象上调用以下代码来停止 thread 对象所代表的线程。

thread.interrupt();

后面又使用以下代码来判断 thread 对象所代表的线程是否停止。

System.out.println("是否停止 1 ? ="+thread.interrupted());
System.out.println("是否停止 2 ? ="+thread.interrupted());

从控制台打印的结果来看,线程并未停止,这也就证明了 interrupted() 方法的解释:测试当前线程是否已经中断。这个“当前线程”是 main,它从未中断过,所以打印的结果是两个 false。

那么如何使 main 线程产生中断效果呢?再来看一下如下的代码:

public static void main(String[] args)
{
    Thread.currentThread().interrupt();
    System.out.println(" 是否停止 1 ? ="+Thread.interrupted());
    System.out.println(" 是否停止 2 ? ="+Thread.interrupted());
    System.out.println("end!");
}

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

是否停止 1 ? =true
是否停止 2 ? =false 
end!

从上述的结果来看,intermpted() 方法的确用来判断出当前线程是不是停止状态。但为什么第二个布尔值是 false 呢?查看一下官方帮助文档中对 interrupted() 方法的解释如下(斜体显示):
测试当前线程是否已经中断。线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

文档已经解释得很详细,intermpted() 方法具有清除状态的功能,所以第二次调用 interrupted() 方法返回的值是 false。

介绍完 interrupted() 方法后再来看一下 isInterrupted() 方法。isInterrupted() 方法的声明如下:

public boolean isInterrupted()

从声明中可以看出 isIntermpted() 方法不是 static 的。仍然以 MyThread14 线程为例,这里使用 isInterrupted() 方法来判断线程是否停止,具体代码如下:

package ch14;
public class Test18
{
    public static void main(String[] args)
    {
        try
        {
            MyThread14 thread=new MyThread14();
            thread.start();
            Thread.sleep(100);
            thread.interrupt();
            System.out.println("是否停止1?="+thread.isInterrupted());
            System.out.println("是否停止2?="+thread.isInterrupted());
        }
        catch(InterruptedException e)
        {
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end!");
    }
}

程序运行结果如下所示。

i=498
是否停止1?=true
i=499
是否俜止2?=true
i=500
end!
i=501
i=502

从程序的运行结果中可以看到,isInterrupted() 方法并未清除状态标识,所以打印了两个 true。

经过上面示例的验证总结一下这两个方法。

  1. this.interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标识清除为 false 的功能。
  2. this.islnterrupted():测试线程 Thread 对象是否已经是中断状态,但不清除状态标识。

异常法停止线程

有了前面学习过的知识,就可在线程中用 for 语句来判断线程是否为停止状态,如果是停止状态,则后面的代码不再运行。

例下面的线程类 MyThread15 演示了在线程中使用 for 循环,并在循环中调用 intermpted() 方法判断线程是否停止。

package ch14;
public class MyThread15 extends Thread
{
    @Override
    public void run()
    {
        super.run();
        for(int i=0;i<500000;i++)
        {
            if(this.interrupted())
            {    //如果当前线程处于停止状态
                System.out.println("已经是停止状态了!我要退出了!");
                break;
            }
            System.out.println("i="+(i+1));
        }
    }
}

接下来编写启动 MyThread15 线程的代码,主线程代码如下:

package ch14;
public class Test19
{
    public static void main(String[] args)
    {
        try
        { 
            MyThread15 thread=new MyThread15(); 
            thread.start();    //启动线程
            Thread.sleep(2000);    //延时2000毫秒
            thread.interrupt();    //停止线程
        }
        catch(InterruptedException e)
        {    //捕捉线程停止异常
            System.out.println("main catch"); 
            e.printStackTrace(); 
        } 
        System.out.println("end!");    //主线程结束时输出
    }
}

上述代码启动 MyThread15 线程后延时 2000 毫秒,之后将线程停止。为避免主线程崩溃使用 catch 捕捉了 InterruptedException 异常,此时会输出“main catch”。在主线程执行结束后会输出“end!”。程序执行的输出结果如下所示。

......
i=271597
i=271598
已经是停止状态了!我要退出了!
end!

从程序执行的结果可以看到,在示例中虽然停止了线程,但如果 for 语句下面还有语句,还是会继续运行的。

下面对 MyThread15 线程进行修改,如下所示是 run() 方法的代码:

public void run()
{ 
    super.run(); 
    for(int i=0;i<500000;i++)
    { 
        if(this.interrupted())
        { 
            System.out.println("已经是停止状态了!我要退出了!"); 
            break; 
        } 
        System.out.println("i="+(i+1)); 
    } 
    System.out.println("我被输出,如果此代码是for又继续运行,线程并未停止!"); 
}

此时的运行效果如下所示,说明线程仍然在继续运行。

......
i=233702
i=233703
end!
已经是停止状态了!我要退出了!
我被输出,如果此代码是for又继续运行,线程并未停止!

那该如何解决线程停止后,语句仍然继续运行的问题呢?解决的办法是在线程中捕捉线程停止异常,如下为修改后的 run() 方法代码。

public void run()
{ 
    super.run(); 
    try
    { 
        for(int i=0;i<500000;i++)
        { 
            if(this.interrupted())
            { 
                System.out.println("已经是停止状态了!我要退出了!"); 
                throw new InterruptedException(); 
            } 
            System.out.println("i=" + (i + 1)); 
        } 
        System.out.println("我在for下面"); 
    }
    catch(InterruptedException e)
    { 
        System.out.println("进MyThread15.java类run方法中的catch了!"); 
        e.printStackTrace(); 
    } 
}

再次运行程序,当线程处于停止状态后,如果 for 循环中的代码继续执行将会拋出 InterruptedException 异常,运行结果如下所示。

......
i=251711
i=251712
i=251713
已经是停止状态了!我要退出了!
end!
进MyThread15.java类run方法中的catch了!
java.lang.InterruptedException
    at text.MyThread.run(MyThread.java:16)

在休眠中停止

如果线程在 sleep() 状态下停止,会是什么效果呢?

例 下面通过一个案例来演示这种情况。如下所示为案例中使用的 MyThread16 线程类代码。

package ch14;
public class MyThread16 extends Thread
{
    @Override
    public void run()
    {
        super.run();
        try
        {
            System.out.println("run begin");
            Thread.sleep(200000);
            System.out.println("run end");
        }
        catch(InterruptedException e)
        {
            System.out.println("在休眠中被停止!进入catch!"+this.isInterrupted());
            e.printStackTrace();
        }
    }
}

调用 MyThread16 线程的主线程代码如下:

package ch14;
public class Test20
{
    public static void main(String[] args)
    {
        try
        {
            MyThread16 thread=new MyThread16();
            thread.start();
            Thread.sleep(200);
            thread.interrupt();
        }
        catch(InterruptedException e)
        {
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end!");
    }
}

在上述代码中启动 MyThread16 线程后休眠了 200 毫秒,之后调用 interrupt() 方法停止线程,运行结果如下所示。

run begin
end!
在休眠中被停止!进入catch!false
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at text.MyThread.run(MyThread.java:12)

从运行结果来看,如果在休眠状态下停止某一线程则会拋出进入 InterruptedException 异常,所以会进入 catch 语句块清除停止状态值,使之变成 false。

例 这个示例是先休眠再停止线程,下面再编写一个案例来演示先停止再休眠线程的情况。案例使用的 MyThread17 线程类代码如下:

package ch14;
public class MyThread17 extends Thread
{
    @Override
    public void run()
    {
        super.run();
        try
        {
            for(int i=0;i<1000;i++)
            {
                System.out.println("i="+(i+1));
            }
            System.out.println("run begin");
            Thread.sleep(200);
            System.out.println("run end");
        }
        catch(InterruptedException e)
        {
            System.out.println("先停止,再遇到了sleep!进入catch!");
            e.printStackTrace();
        }
    }
}

使用 MyThread17 线程的主线程代码如下:

package ch14;
public class Test21
{
    public static void main(String[] args)
    {
        MyThread17 thread=new MyThread17();
        thread.start();
        thread.interrupt();
        System.out.println("end!");
    }
}

在上述代码中启动 MyThread17 线程后没有进行延时,马上调用 interrupt() 方法进行停止线程,但是在 MyThread17 线程中有一个 200 毫秒的延时。运行程序后,首先会看到下所示的输出,说明主线程执行完毕。

end!
i=1
i=2
i=3
i=4
i=5
i=6
......

稍等片刻后,将会看到如下所示的异常,说明线程停止了。

......
i=999
i=1000
run begin
先停止,再遇到了sleep!进入catch!
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at text.MyThread.run(MyThread.java:16)

强制停止线程

调用 stop() 方法可以在任意情况下强制停止一个线程。下面通过一个案例来演示 stop() 停止线程的方法。

package ch14;
public class MyThread18 extends Thread
{
    private int i=0;
    @Override
    public void run()
    {
        try
        {
            while (true)
            {
                i++;
                System.out.println("i=" + i);
                Thread.sleep(1000);
            }
        }
        catch(InterruptedException e)
        {
            //TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

如上述代码所示,MyThread18 线程中包含一个死循环,该循环每隔 1000 毫秒执行一次,每次将 i 的值递增 1 并输出。

调用 MyThread18 线程的主线程代码如下:

package ch14;
public class Test22
{
    @SuppressWarnings("deprecation")
    public static void main(String[] args)
    {
        try
        {
            MyThread18 thread=new MyThread18();
            thread.start();
            Thread.sleep(8000);
            thread.stop();
        }
        catch(InterruptedException e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

如上述代码所示,MyThread18 线程在启动后有一个 8000 毫秒的延时,在这段时间内会循环 9 次,之后 stop() 方法被执行从而线程停止。运行后的输出如下所示。

i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9

注意:调用 stop() 方法时会抛出 java.lang.ThreadDeath 异常,但在通常情况下,此异常不需要显式地捕捉。

释放锁的不良后果

从 JDK 1.6 以后 stop() 方法已经被作废,因为如果强制让线程停止则有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了“解锁”,导致数据得不到同步的处理,出现数据不一致的问题。

使用 stop() 释放锁将会给数据造成不一致性的结果。如果出现这样的情况,程序处理的数据就有可能遭到破坏,最终导致程序执行的流程错误,一定要特别注意。

例 下面通过一个案例来演示这种情况。案例使用了一个名为 SynchronizedObject 的实体类,该类代码如下:

package ch14;
public class SynchronizedObject
{
    private String username="root";
    private String password="root";
    public String getUsername()
    {
        return username;
    }
    public void setUsername(String username)
    {
        this.username=username;
    }
    public String getPassword()
    {
        return password;
    }
    public void setPassword(String password)
    {
        this.password=password;
    }
    synchronized public void printString(String username,String password)
    {
        try
        {
            this.username=username;
            Thread.sleep(100000);
            this.password=password;
        }
        catch(InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

如上述代码所示,SynchronizedObject 类包含用户名和密码两个成员,printString() 方法用于对这两个成员进行赋值,但是在设置密码之前有一个休眠时间。

下面编写一个线程来对 SynchronizedObject 类进行实例化,并调用 printString() 方法。线程代码如下:

package ch14;
public class MyThread19 extends Thread
{
    private SynchronizedObject object;
    public MyThread19(SynchronizedObject object)
    {
        super();
        this.object=object;
    }
    @Override
    public void run()
    {
        object.printString("admin","123456");
    }
}

接下来编写主线程代码如下:

package ch14;
public class Test23
{
    public static void main(String[] args)
    { 
        try
        { 
            SynchronizedObject object=new SynchronizedObject(); 
            MyThread19 thread=new MyThread19(object); 
            thread.start(); 
            Thread.sleep(500); 
            thread.stop(); 
            System.out.println("用户名:"+object.getUsername());
            System.out.println("密码:"+object.getPassword()); 
        }
        catch(InterruptedException e)
        { 
            e.printStackTrace();
        } 
    } 
}

在上述代码中创建一个 SynchronizedObject 类实例,并将该实例作为参数传递给 MyThread19 线程。MyThread19 线程启动后将立即调用 object.printString('fadminn,"123456") 方法,而在 printString() 方法内有一个较长时间的休眠。该休眠时间大于主线程的休眠时间,所以主线程会继续往下执行,当执行到 stop() 方法时线程被强制停止。

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

用户名:admin
密码:root

由于 stop() 方法已经在中被标明是“作废/过期”的方法,显然它在功能上具有缺陷,所以不建议在程序中使用 stop() 方法。

使用 return 停止线程

除了上面介绍的方法外,还可以将 intermpt() 方法与 return 结合使用来实现停止线程的效果

例通过一个案例来演示这种情况。如下所示为案例中使用 MyThread20 线程类的代码。

package ch14;
public class MyThread20 extends Thread
{
    @Override
    public void run()
    {
        while (true)
        {
            if (this.isInterrupted())
            {
                System.out.println("停止了!");
                return;
            }
            System.out.println("timer="+System.currentTimeMillis());
        }
    }
}

调用 MyThread20 线程的主线程代码如下:

package ch14;
public class Test24
{
    public static void main(String[] args) throws InterruptedException
    {
        MyThread20 t=new MyThread20();
        t.start();
        Thread.sleep(2000);
        t.interrupt();
    }
}

程序执行后的结果如下所示。

......
timer=1540977194784
timer=1540977194784
timer=1540977194784
timer=1540977194784
timer=1540977194784
停止了!

从程序的执行结果中可以看到成功停止了线程,不过还是建议使用“拋异常”的方法来实现线程的停止,因为在 catch 块中还可以将异常向上拋,使线程停止的事件得以传播。

暂停/挂起线程

暂停线程意味着此线程还可以恢复运行。在 Java 多线程中,可以使用 suspend() 方法暂停线程,使用 resume() 方法恢复线程的执行。

suspend() 与 resume() 方法

本节通过一个案例来介绍 suspend() 与 resume() 方法的用法。首先来看一下案例中使用到的 MyThread21 线程,代码如下所示。

package ch14;
public class MyThread21 extends Thread
{
    private long i=0;
    public long getI()
    {
        return i;
    }
    public void setI(long i)
    {
        this.i=i;
    }
    @Override
    public void run()
    {
        while(true)
        {
            i++;
        }
    }
}

MyThread21 线程中有一个成员 i,其中 setI() 方法和 getI() 方法分别用于设置和获取 i 的值,run() 方法则是一个从i开始递增的死循环。

下面编写主线程的代码,具体如下所示。

package ch14;
public class Test25
{
    public static void main(String[] args)
    {
        try
        {
            MyThread21 thread=new MyThread21();
            thread.start();
            Thread.sleep(5000);
            //A段
            thread.suspend();
            System.out.println("A= "+System.currentTimeMillis()+" i= "+thread.getI());
            Thread.sleep(5000);
            System.out.println("A= "+System.currentTimeMillis()+" i= "+thread.getI());
            //B段
            thread.resume();
            Thread.sleep(5000);
            //C段
            thread.suspend();
            System.out.println("B= "+System.currentTimeMillis()+" i= "+thread.getI());
            Thread.sleep(5000);
            System.out.println("B= "+System.currentTimeMillis()+" i= "+thread.getI());
        }
        catch(InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

最终运行结果如下所示。

A= 1540978346179 i= 2680986095
A= 1540978351179 i= 2680986095
B= 1540978356179 i= 5348657508
B= 1540978361179 i= 5348657508

从输出结果的时间来看,调用 suspend() 方法确实可以暂停线程,而在调用 resume() 方法后线程恢复运行状态。

独占问题

使用 suspend() 方法与 resume() 方法时,如果使用不当极易造成公共的同步对象被独占,从而使得其他线程无法访问公共同步对象。

例通过一个案例来演示这种情况。如下所示是案例中使用的公共对象的代码。

package ch14;
public class SynchronizedObject1
{
    synchronized public void printString()
    {
        System.out.println("begin");
        if (Thread.currentThread().getName().equals("a"))
        {
            System.out.println("a线程永远 suspend了!");
            Thread.currentThread().suspend();
        }
        System.out.println("end");
    }
}

SynchronizedObject1 类在 printString() 方法开始时输出“begin”,在该方法结束时输出“end”,方法体中的if判断如果当前线程的名称是 a 则调用 suspend() 方法暂停线程。

接下来编写主线程代码,在主线程中创建一个 SynchronizedObject1 类对象并在线程中调用 printString() 方法,具体代码如下所示。

package ch14;
public class Test26
{
    public static void main(String[] args)
    {
        try
        {
            final SynchronizedObject1 object=new SynchronizedObject1();
            Thread thread1=new Thread()
            {
                @Override
                public void run()
                {
                    object.printString();
                }
            };
            thread1.setName("a");
            thread1.start();
            Thread.sleep(1000);
            Thread thread2=new Thread()
            {
                @Override
                public void run()
                {
                    System.out.println("thread2启动了,但进入不了printString()方法!所以只会打印1个begin!");
                    System.out.println("因为printString()方法被a线程锁定并且永远暂停了!");
                    object.printString();
                }
            };
            thread2.start();
        }
        catch(InterruptedException e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

上述代码比较简单这里就不再解释,运行后的结果如下所示。从中可以看到由于线程被永久暂停,所以只会输出一个 begin。

begin
a线程永远 suspend了!
thread2启动了,但进入不了printString()方法!所以只会打印1个begin!
因为printString()方法被a线程锁定并且永远暂停了!

例还有另外一种独占锁的情况也要格外注意,稍有不慎就会掉进“坑”里。创建测试用的 MyThread22 线程,具体代码如下:

package ch14;
public class MyThread22 extends Thread
{
    private long i=0;
    @Override
    public void run()
    {
        while (true)
        {
            i++;
        }
    }
}

再来看主线程的代码,如下所示。

package ch14;
public class Test27
{
    public static void main(String[] args)
    {
        try
        {
            MyThread22 thread=new MyThread22();
            thread.start();
            Thread.sleep(1000);
            thread.suspend();
            System.out.println("main end!");
        }
        catch(InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

程序执行后将看到预料不到的结果,如下所示。

main end!

如果将 MyThread22 线程类的代码更改如下:

package ch14;
public class MyThread22 extends Thread
{   
    private long i=0; 
    @Override 
    public void run()
    { 
        while(true)
        { 
            i++; 
            System.out.println(i); 
        } 
    }
}

再次运行程序,控制台将不打印 main end,运行结果如下所示。

......
130862
130863
130864
130865
130866
130867

出现这种情况的原因是,当程序运行到 println() 方法内部停止时,同步锁未被释放。这导致当前 PrintStream 对象的 println() 方法一直呈“暂停”状态,并且“锁未释放”,而 main() 方法中的代码“System.out. println(Mmain end!'1);”迟迟不能执行打印。

提示:虽然 suspend() 方法是过期作废的方法,但还是有必要研究它过期作废的原因,这是很有意义的

不同步问题

在使用 suspend() 方法与 resume() 方法时也容易出现因为线程的暂停而导致数据不同步的情况。

例下面通过一个案例来演示这种情况。如下所示是案例中使用的公共对象的代码。

package ch14;
public class MyObject
{
    private String username="1";
    private String password="11";
    public void setValue(String u,String p)
    {
        this.username=u;
        if(Thread.currentThread().getName().equals("a"))
        {
            System.out.println("停止a线程!");
            Thread.currentThread().suspend();
        }
        this.password=p;
    }
    public void printUsernamePassword()
    {
        System.out.println(username+" "+password);
    }
}

如上述代码所示,MyObject 类的 setValue() 方法会在线程名称是 a 时执行停止线程操作。 如下所示是主线程代码。

package ch14;
public class Test28
{
    public static void main(String[] args) throws InterruptedException
    {
        final MyObject myobject=new MyObject();
        Thread thread1=new Thread()
        {
            public void run()
            {
                myobject.setValue("a","aa");
            };
        };
        thread1.setName("a");
        thread1.start();
        Thread.sleep(500);
        Thread thread2=new Thread()
        {
            public void run()
            {
                myobject.printUsernamePassword();
            };
        };
        thread2.start();
    }
}

程序运行结果如下所示。

停止a线程!
a 11

从程序运行的结果可以看到,出现了值不同步的情况,所以在程序中使用 suspend() 方法要格外注意。

线程的优先级和执行顺序

在学习运算符时,读者知道各个运算符之间有优先级,了解运算符的优先级对程序幵发有很好的作用。线程也是如此,每个线程都具有优先级,Java 虚拟机根据线程的优先级决定线程的执行顺序,这样使多线程合理共享 CPU 资源而不会产生冲突。

优先级概述

在 Java 语言中,线程的优先级范围是 1~10,值必须在 1~10,否则会出现异常;优先级的默认值为 5。优先级较高的线程会被优先执行,当执行完毕,才会轮到优先级较低的线程执行。如果优先级相同,那么就采用轮流执行的方式。

可以使用 Thread 类中的 setPriority() 方法来设置线程的优先级。语法如下:

public final void setPriority(int newPriority);

如果要获取当前线程的优先级,可以直接调用 getPriority() 方法。语法如下:

public final int getPriority();

使用优先级

简单了解过优先级之后,下面通过一个简单的例子来演示如何使用优先级。分别使用 Thread 类和 Runnable 接口创建线程,并为它们指定优先级。

(1) 创建继承自 Thread 类的 FirstThreadInput 类,重写该类的 run() 方法。代码如下:

package ch14;
public class FirstThreadInput extends Thread
{
    public void run()
    {
        System.out.println("调用FirstThreadInput类的run()重写方法");    //输出字符串
        for(int i=0;i<5;i++)
        {
            System.out.println("FirstThreadInput线程中i="+i);    //输出信息
            try
            {
                Thread.sleep((int) Math.random()*100);    //线程休眠
            }
            catch(Exception e){}
        }
    }
}

(2) 创建实现 Runnable 接口的 SecondThreadInput 类,实现 run() 方法。代码如下:

package ch14;
public class SecondThreadInput implements Runnable
{
    public void run()
    {
        System.out.println("调用SecondThreadInput类的run()重写方法");    //输出字符串
        for(int i=0;i<5;i++)
        {
            System.out.println("SecondThreadInput线程中i="+i);    //输出信息
            try
            {
                Thread.sleep((int) Math.random()*100);    //线程休眠
            }
            catch(Exception e){}
        }
    }
}

(3) 创建 TestThreadInput 测试类,分别使用 Thread 类的子类和 Runnable 接口的对象创建线程,然后调用 setPriority() 方法将这两个线程的优先级设置为 4,最后启动线程。代码如下:

package ch14;
public class TestThreadInput
{
    public static void main(String[] args)
    {
        FirstThreadInput fti=new FirstThreadInput();
        Thread sti=new Thread(new SecondThreadInput());
        fti.setPriority(4);
        sti.setPriority(4);
        fti.start();
        sti.start();
    }
}

(4) 运行上述代码,运行结果如下所示。

调用FirstThreadInput类的run()重写方法
调用SecondThreadInput类的run()重写方法
FirstThreadInput线程中i=0
SecondThreadInput线程中i=0
FirstThreadInput线程中i=1
FirstThreadInput线程中i=2
SecondThreadInput线程中i=1
FirstThreadInput线程中i=3
SecondThreadInput线程中i=2
FirstThreadInput线程中i=4
SecondThreadInput线程中i=3
SecondThreadInput线程中i=4

由于该例子将两个线程的优先级都设置为 4,因此它们交互占用 CPU ,宏观上处于并行运行状态。

重新更改 ThreadInput 类的代码、设置优先级。代码如下:

纯文本复制
fti.setPriority(1);sti.setPriority(10);

重新运行上述代码,如下所示。

调用FirstThreadInput类的run()重写方法
调用SecondThreadInput类的run()重写方法
FirstThreadInput线程中i=0
SecondThreadInput线程中i=0
SecondThreadInput线程中i=1
SecondThreadInput线程中i=2
SecondThreadInput线程中i=3
SecondThreadInput线程中i=4
FirstThreadInput线程中i=1
FirstThreadInput线程中i=2
FirstThreadInput线程中i=3
FirstThreadInput线程中i=4

你可能感兴趣的:(2020重新出发,JAVA基础,多线程编程)