ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Java多线程编程核心技术

2021-01-02 22:35:19  阅读:172  来源: 互联网

标签:Java Thread 编程 System myThread 线程 println 多线程 public


1 Java多线程技能

本章主要介绍线程和进程的相关概念,多线程的实现和停止,以及Thread类中的核心方法。

目录

1.1 进程和线程

1.进程

一个可并发执行的具有独立功能的程序关于某个数据集合的一次执行过程,也是操作系统进行资源分配和保护的基本单位。简单的说,进程就是一个程序的一次执行过程。

2.引入线程的动机和思路

操作系统采用进程机制使得多任务能够并发执行,提高了资源使用和系统效率。在早期操作系统中,进程是系统进行资源分配的基本单位,也是处理器调度的基本单位,进程在任一时刻只有一个执行控制流,这种结构称为单线程进程。单线程进程调度时存在进程时空开销大、进程通信代价大、进程并发粒度粗、不适合于并发计算等问题,操作系统引入线程机制来解决这些问题。线程机制的基本思路是,把进程的两项功能——独立分配资源和被调度分派执行分离开来,后一项任务交给线程实体完成。这样,进程作为系统资源分配与保护的独立单位,不需要频繁切换;线程作为系统调度和分派的基本单位会被频繁的调度和切换。

3.线程的定义

线程是操作系统进程中能够独立执行的实体,是处理器调度和分派的基本单位。线程是进程的组成部分,每个进程内允许包含多个并发执行的线程。同一个进程中所有的线程共享进程的主存空间和资源,但是不拥有资源。
线程就是进程中的一个负责程序执行的一个控制单元(执行路径)。一个进程中可以有多个执行路径,称之为多线程。

4 进程和线程的区别

  • 定义方面:进程是程序在某个数据集合上的一次执行过程;线程是进程中的一个执行路径。
  • 角色方面:在支持线程机制的系统中,进程是系统资源分配的单位,线程是系统调度的单位。
  • 资源共享方面:进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。同时线程还有自己的栈和栈指针,程序计数器等寄存器。
  • 独立性方面:进程有自己独立的地址空间,而线程没有,线程必须依赖于进程而存在。

1.2 多线程的实现

多线程的实现主要有四种

  • 继承Thread类
  • 实现Runnable接口
  • 使用Callable接口,需要采用FutureTask类实现中间传递功能,默认带返回值
  • 线程池实现
  1. 继承Thread类

    1)定义一个类继承Thread类。
    (2)覆盖Thread类中的run方法。(方法run称为线程体)
    (3)直接创建Thread类的子类对象创建线程。
    (4)调用start方法,开启线程并调用线程的任务run方法执行。
    注意:run()方法和start()方法的区别。start()方法来启动线程,run()方法当作普通方法的方式调用,程序还是顺序执行。

public class MyThread extend Thread{
    @Override
    public void run () {
        for (int i = 0; i <500000; i++) {
            if(this.isInterrupted()){
                System.out.println("我结束了");
                break;
            }
            System.out.println("i="+i+1);
        }
    }
}

2.实现Runnable接口

(1)定义类实现Runnable接口
(2)覆盖接口中的run方法,将线程的任务代码封装到run方法中。
(3)通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread构造函数的参数进行传递。线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务。
(4)调用线程对象的start方法开启线程。

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

3.使用Callable接口,需要采用FutureTask类实现中间传递功能,默认带返回值

 class MyThread  implements Callable<Integer>{ 
 @Override
    public Integer call () throws Exception {
        System.out.println("*****call in call method ");
        return 1024;
    }
}
public class Run{
    public static void main (String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer > futureTask = new FutureTask(new MyThread2());
        new Thread(futureTask, "A").start();
        Integer integer = futureTask.get();//获取返回值
        System.out.println(integer);
    }
}

4.线程池实现

1.3 多线程安全问题

1.3.1 Servlet技术造成的非线程安全问题

非线程安全问题主要指多个线程对同一对象中的同一个实例变量进行操作的时候会出现

值被更改的、值不同步的情况,进而影响程序执行流程。下面通过一个案例来学习如何

解决该问题

错误现场

创建t4_threadsafe项目,实现非线程安全的环境

package com.ybzn.thread01.t4_servlet;

public class LoginServlet {
    private static String usernameRef;
    private static String passwordRef;
    public static void doPost(String username,String password) throws InterruptedException {
        usernameRef=username;
        if("a".equals(username)){
            Thread.sleep(5000);
        }
        passwordRef=password;
        System.out.println("username="+usernameRef+"\npassword="+passwordRef);
    }
}

线程ALogin.java,代码如下:

package com.ybzn.thread01.t4_servlet;

public class ALogin  extends Thread{
    @Override
    public void run () {
        super.run();
        try {
            LoginServlet.doPost("a", "aa");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程BLogin.java,代码如下:

public class BLogin  extends Thread{
    @Override
    public void run () {
        try {
            LoginServlet.doPost("b","bb");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试运行代码:

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

执行结果如下

image-20210102165852621

原因分析:

运行结果是错误的,首先,两个线程的同一个对象的public static void doPost(String username,String password)方法传参数的时候,方法的参数是不会被覆盖的,方法的参数值会绑定到当前线程上的。(具体分析请看《Java多线程编程核心技术》P22)

解决方案

采用synchronized锁对象,更改LoginServlet.java代码如下:

public class LoginServlet {
    private static String usernameRef;
    private static String passwordRef;
//    public static void doPost(String username,String password) throws InterruptedException {
//        usernameRef=username;
//        if("a".equals(username)){
//            Thread.sleep(5000);
//        }
//        passwordRef=password;
//        System.out.println("username="+usernameRef+"\npassword="+passwordRef);
//    }

    /**
     * 升级版,添加synchronized锁对象
     */

    synchronized public static void doPost (String username, String password) throws InterruptedException {
        usernameRef = username;
        if ("a".equals(username)) {
            Thread.sleep(5000);
        }
        passwordRef = password;
        System.out.println("username=" + usernameRef + "\npassword=" + passwordRef);
    }
}

执行结果如下,注意,在Web开发中,Servlet本身就是单例的,所以为了不出现非线程安全问题,建议不要在Servlet中出现实例变量

image-20210102170520118

1.3.2 i- -与System.out.println()出现的非线程安全问题

错误现场

通过细化println()方法与i–-联合使用可能会出现另外一种异常,代码如下:

public class MyThread  extends  Thread{
    private int i =5;

    @Override
    public void run () {
        System.out.println("i="+(i--)+" threadName="+Thread.currentThread().getName());

        //注意:代码i单独一行运行
        //被改成当前项目中println()方法直接进行输出
    }
}

运行代码:

public class Run {
    public static void main (String[] args) {
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread);
        Thread thread2 = new Thread(myThread);
        Thread thread3 = new Thread(myThread);
        Thread thread4 = new Thread(myThread);
        Thread thread5 = new Thread(myThread);
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
    }
}

image-20210102172247049方法内部的同步的,但是和i - - 一同使用还是会出现非线程安全的问题

解决方案

采用同步方法,自定义一个方法

	public void println(String x){
        synchronized(this){
            print(x);
            newLine();//换行
        }
    }

1.4 currentThread

currentThread()方法能够返回代码段正在被那个线程所调用

通过一个项目来演示

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

运行结果如下图

image-20210102172729412

该结果说明main()方法被名为main线程调用

继续测试,创建一个MyThread.java类,代码如下

public class MyThread extends Thread {
    @Override
    public void run () {
        System.out.println("run 打印" + Thread.currentThread().getName());
    }

    MyThread () {
        System.out.println("构造方法打印: " + Thread.currentThread().getName());
    }
}

运行类代码Run2.java

public class Run2 {
    public static void main (String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

结果如图所示:

image-20210102172856664

将代码Run2.java修改如下

public class Run2 {
    public static void main (String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
         myThread.run();
    }
}

运行结果如下

image-20210102173022026

对比上面的,发现操作run()方法线程 变成了main,

执行run()和start()方法有两个区别:

  • my.run(): 立即执行run()方法,不启动新的线程,所以是main
  • my.start(): 执行run()方法的时机不确定,启动新的线程

再来测试一个比较复杂的情况,创建一个``CountOperate.java`,代码如下:

public class CountOperate extends Thread {
    public CountOperate(){
        System.out.println("countOperate ---begin");
        System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());
        System.out.println("this.getName="+this.getName());
        System.out.println("countOperate ---end");
    }

    @Override
    public void run () {
        System.out.println("run---begin");
        System.out.println("Thread.currentThread().getName()="+Thread.currentThread().getName());
        System.out.println("this.getName="+this.getName());
        System.out.println("run---end");
    }
}

启动类Run.java

public class Run {
    public static void main (String[] args) {
        CountOperate c= new CountOperate();
        Thread thread = new Thread(c);
        thread.setName("A");
        thread.start();
    }
}

运行结果如下:

image-20210102173355969

结果分析:

* 运行结果:
* countOperate ---begin
* Thread.currentThread().getName()=main 构造方法属于main对象
* this.getName=Thread-0  main方法的线程对象是Thread-0
* countOperate ---end
*
* run---begin
* Thread.currentThread().getName()=A run方法属于该线程A
* this.getName=Thread-0  main方法的线程对象任然是Thread-0 当前线程
* run---end

1.5 常用多线程API

  • isAlive()方法:判断当前线程是否处于活动状态。活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态。
  • sleep(long millis)方法: 在指定的实际内让“正在执行的线程”休眠,这个线程是指

currentThread()返回的线程

  • getId()方法:获取当前正在执行线程的ID

  • this.currentThread():返回的线程。

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

1.6 停止线程

停止线程是多线程开发的一个很重要的技术点。停止一个线程意味着线程处理完任务之前停止正在做的操作,也就是放弃当前操作。停止一个线程可以采用Thread.stop()方法,但不推荐使用该方法,此方法是不安全的,该方法正在被弃用。

在大多数的情况下,停止一个线程,采用Thread.interrupt()方法,但是这个方法不会终止一个正在运行的线程,需要添加一些判断方法。

在Java中有3种方法,可以使当前正在运行的线程结束:
1) 使用退出标志使线程正常退出;

​ 2)使用stop()方法强行终止线程,但是不推荐使用,发生线程不安全问题

​ 3)使用interrupt()方法终止线程【推荐】

1.6.1终止不了的线程

本案例采用interrupt()方法来终止线程运行,但是结果发现,其无法终止正在运行的线程,需要添加一些判断。

创建项目名字为t11的,MyThread.java代码如下

/**
 * 停止不了的线程
 */
public class MyThread extends Thread {

    @Override
    public void run () {
        for (int i = 0; i <50000 ; i++) {
            System.out.println("i="+(i+1));
        }
    }

}

运行类Run.java代码如下

public class Run {
    public static void main (String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();
        Thread.sleep(200);
        myThread.interrupt();
        myThread.isInterrupted();
        System.out.println("ZZZZZZZZZ");
    }
}

运行结果如下

image-20210102200518217

通过将输出结果导出控制台,再其他记录查看软件上面可以发现,interrupt方法并没有将线程停止。如何将线程停止呢?

1.6.2 判断线程是否为停止状态

在学习如何将线程停止之前,先学两个方法。

判断线程的状态是否为停止状态,Java提供了两种方法:

1)public static boolean interrupted():测试currentThread()是否已经中断

2)public boolean this.isInterrupted():测试this关键字所在的类的对象是否已经中断

这两个方法都可以判断线程是否停止,那么有什么区别呢?

  • this.interrupted()方法的解释:测试当前线程是否中断,当前线程是运行this.interrupted()方法的线程。

下面进行项目分析,创建t12项目

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

创建运行Run.java类

public class Run {
    public static void main (String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        myThread.start();
        Thread.sleep(1000);
        myThread.interrupt();
        Thread.currentThread().interrupt();
        System.out.println("线程是否停止1="+myThread.isInterrupted());//判断 myThread线程是否结束
        System.out.println("线程是否停止2="+ Thread.interrupted());//判断当前线程是否结束
        System.out.println("线程是否isAlive2="+ Thread.currentThread().isAlive());
        System.out.println("线程getName="+ Thread.currentThread().getName());
        System.out.println("end");
    }
}

运行结果如下:

image-20210102201551456

采用 myThread.interrupt();方法,并没有使得myThread线程结束,但是采用Thread.currentThread().interrupt();却可以使得线程结束,从控制台输出的结果来看,线程并没有停止,Thread.interrupt()只能使得当前线程停止,因为当前线程是main而不是myThread 所以输出false

Thread.interrupted()方法能够判断当前线程是否为停止状态。但是它会将线程中断的状态清除,如果第二次在使用该方法,得到的结果将会与之前相反

this.isInterrupted():测试线程Thread对象是否是中断状态,不清楚状态标志。

isInterruped()方法不是静态方法,所以需要对象来调用,但是其调用了结果,并不能再当前Run类中判断,而是要到原对象MyThread中去判断.

1.6.3 停止的线程-异常法

通过for循环判断线程是否处于停止状态即可判断后面的代码是否可以运行。如果处于停止状态,后面的代码不在执行

创建t13项目,MyThread.java

/**
 * 线程停止了,但是还会执行后面代码,所以是虚假停止
 */
public class MyThread  extends  Thread{
    @Override
    public void run () {
        for (int i = 1; i <=500000; i++) {
            if(this.isInterrupted()){
                System.out.println("已结是停止状态了,我要退出了");
                break;
            }
            System.out.println("i="+i);
        }
        System.out.println("我被输出了,我是for后面执行的代码,线程并未停止");
    
}

启动运行Run.java

public class Run {
    public static void main (String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();
        Thread.sleep(800);
        myThread.interrupt();
        System.out.println("end");
    }
}

运行结果

image-20210102205259611

虽然停止了线程,但是如果for后面还有语句,他还是会执行的,这个是一个小缺陷,下面我们来解决这个问题。重新创建一个MyThread.java代码

/**
 * 通过抛出异常 正常停止线程
 */
public class MyThread extends Thread {
    @Override
    public void run () {
        try {
            for (int i = 1; i <= 500000; i++) {
                if (this.isInterrupted()) {
                    System.out.println("已结是停止状态了,我要退出了");
                    throw new InterruptedException();
                }
                System.out.println("i=" + i);
                System.out.println("我被输出了,我是for 后面执行的代码");
            }
        } catch (InterruptedException e) {
            System.out.println("进入MyThread中的Catch类了");
            e.printStackTrace();
        }
    }
}

启动类Run.java

public class Run {
    public static void main (String[] args)   {
        MyThread myThread =new MyThread();
        myThread.start();
        try {
            Thread.sleep(800);
            myThread.interrupt();
        } catch (InterruptedException e) {
            System.out.println("main catch");
            e.printStackTrace();
        }
        System.out.println("end");
    }
}

运行结果

image-20210102205621991

由运行结果看出,线程最终被正常停止了,这种方法就是采用interrupt()方法中断线程。

1.6.4 在sleep状态下停止线程

线程在sleep状态下,停止线程会自动进入catch语句,并且停止状态值为false

1.6.5采用stop停止线程

  • 此方法已经被抛弃了
  • 会抛出java.lang.ThreadDeath异常,但是在通常的情况下,不会被显性捕捉
  • 使用stop停止线程会让清理工作无法完成,会导致数据不完整
  • 对锁进行解锁会导致数据无法同步
  • 停止线程还可以采用return 方法,但是相对于interrupt()方法处理日志比较冗余

总结,在java中有以下三种方法可以终止正在运行的线程。
(1)使用退出标记,使线程正常退出,也就是当run方法完成之后线程终止。
(2)使用stop方法强行终止线程,但是不推荐使用这种方法,因为stop和suspend及resume一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
(3)使用interrupt方法中断线程。需要注意的是interrupt方法仅仅是在当前线程中打了一个停止的标记,并不是真正的停止线程,需要与标记一起使用来停止线程。

1.6.6 暂停线程

暂停线程意味此线程可以恢复运行,在多线程中采用suspend()方法来悬挂暂停线程和resume()方法唤醒来执行。

1.6.6.1 suspend()和resume()

创建t16项目,MyThread.java

/**
 */
public class MyThread 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++;
        }
    }
}

运行结果类:

public class Run {
    public static void main (String[] args) {
        try {
            MyThread myThread = new MyThread();
            myThread.start();
            Thread.sleep(5000);
            //A 段
            myThread.suspend();//悬挂
            System.out.println("A="+System.currentTimeMillis()+" i= "+myThread.getI());
            Thread.sleep(5000);
            System.out.println("A="+System.currentTimeMillis()+" i= "+myThread.getI());

            //B段

            myThread.resume();//唤醒
            Thread.sleep(5000);

            //C段
            myThread.suspend();
            System.out.println("B="+System.currentTimeMillis()+" i= "+myThread.getI());
            Thread.sleep(5000);
            System.out.println("B="+System.currentTimeMillis()+" i= "+myThread.getI());
        } catch (Exception e) {
            System.out.println("线程进入main catch块");
            e.printStackTrace();
        }
        System.out.println("end");
    }
}

运行结果

image-20210102210952813

从控制台来看,线程虽然被暂停了,但是可以恢复成原来的样子,

  • 暂停线程: 暂停线程意味着该线程还可以继续使用,可以使用
  •  suspend()方法来暂停线程
    
  •  resume()方法来恢复线程
    
  •  suspend\resume的缺点 
    
  •  1.  在于独占 同步块, 导致其他线程无法访问公共同步块,所以他们两个就被废弃了
    
  •                     2. 数据不完整性, 暂停线程容易导致数据不完整性的出现
    
  •  目前使用线程暂停与恢复,使用==wait()、notify()或者notifyAll()三种法==
    

1.7守护线程

在java中有两种线程,一种是用户线程,另一种是守护线程。
守护线程是一种特殊的线程,当进程中不存在非守护线程则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当线程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。

该方法必须在启动线程前调用。守护线程和其他的线程城在开始和运行都是一样的,轮流抢占cpu的执行权,结束时不同。正常线程都需要手动结束,对于后台线程,如果所有的前台线程都结束了,后台线程无论处于什么状态都自动结束

标签:Java,Thread,编程,System,myThread,线程,println,多线程,public
来源: https://www.cnblogs.com/blogger-Li/p/14224217.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有