Java多线程编程核心技术(一)Java多线程技能

简介: 本文为《Java并发编程系列》第一章,主要介绍并发基础概念与API

本文为《Java并发编程系列》第一章,主要介绍并发基础概念与API

1、进程和线程

一个程序就是一个进程,而一个程序中的多个任务则被称为线程。

进程是表示资源分配的基本单位,线程是进程中执行运算的最小单位,亦是调度运行的基本单位。

举个例子:

打开你的计算机上的任务管理器,会显示出当前机器的所有进程,QQ,360等,当QQ运行时,就有很多子任务在同时运行。比如,当你边打字发送表情,边好友视频时这些不同的功能都可以同时运行,其中每一项任务都可以理解成“线程”在工作。

2、使用多线程

在Java的JDK开发包中,已经自带了对多线程技术的支持,可以很方便地进行多线程编程。实现多线程编程的方式有两种,一种是继承 Thread 类,另一种是实现 Runnable 接口。使用继承 Thread 类创建线程,最大的局限就是不能多继承,所以为了支持多继承,完全可以实现 Runnable 接口的方式。需要说明的是,这两种方式在工作时的性质都是一样的,没有本质的区别。如下所示:

1.继承 Thread 类

public class MyThread extends Thread {

    @Override
    public void run() {
        //...
    }

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

2.实现 Runnable 接口

 public static void main(String[] args) throws InterruptedException {
     new Thread(new Runnable() {
         @Override
         public void run() {
             //...
         }
     }).start();
 }

Thread.java 类中的start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用 Thread 中的 run() 方法,也就是使线程得到运行,多线程是异步的,线程在代码中启动的顺序不是线程被调用的顺序。

Thread构造方法

Thread()             
分配新的 Thread 对象。
Thread(Runnable target)             
分配新的 Thread 对象。
Thread(Runnable target, String name)            
分配新的  Thread 对象。
Thread(String name)             
分配新的 Thread 对象。
Thread(ThreadGroup group, Runnable target)             
分配新的 Thread 对象。
Thread(ThreadGroup group, Runnable target, String name)            
分配新的  Thread 对象,以便将 target 作为其运行对象,将指定的 name  作为其名称,并作为 group 所引用的线程组的一员。
Thread(ThreadGroup group, Runnable target, String name, long stackSize)            
分配新的 Thread 对象,以便将  target 作为其运行对象,将指定的 name 作为其名称,作为 group  所引用的线程组的一员,并具有指定的堆栈大小。
Thread(ThreadGroup group, String name)           
 分配新的 Thread 对象。

3、实例变量与线程安全

自定义线程类中的实例变量针对其他线程可以有共享与不共享之分。当每个线程都有各自的实例变量时,就是变量不共享。共享数据的情况就是多个线程可以访问同一个变量。来看下面的示例:`

public class MyThread implements Runnable {
    private int count = 5;

    @Override
    public void run() {
        count--;
        System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count);
    }
}

以上代码定义了一个线程类,实现count变量减一的效果。运行类Runjava代码如下:

public class Ruu {

    public static void main(String[] args) throws InterruptedException {
        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();
    }
}

打印结果如下:

线程C 计算 count = 3
线程B 计算 count = 3
线程A 计算 count = 2
线程D 计算 count = 1
线程E 计算 count = 0

线程C,B的打印结果都是3,说明C和B同时对count进行了处理,产生了“非线程安全问题”。而我们想要的得到的打印结果却不是重复的,而是依次递减的。

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

取得原有变量的值。

计算i-1。

对i进行赋值。

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

解决方法就是使用 synchronized 同步关键字 使各个线程排队执行run()方法。修改后的run()方法:

public class MyThread implements Runnable {
    private int count = 5;

    @Override
    synchronized public void run() {
        count--;
        System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count);
    }
}

打印结果:

线程B 计算 count = 4
线程C 计算 count = 3
线程A 计算 count = 2
线程E 计算 count = 1
线程D 计算 count = 0

关于System.out.println()方法

先来看System.out.println()方法源码:

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

虽然println()方法内部使用 synchronized 关键字,但如下所示的代码在执行时还是有可能出现非线程安全问题的。

System.out.println("线程"+Thread.currentThread().getName()+" 计算 count = "+count--);
原因在于println()方法内部同步,但 i-- 操作却是在进入 println()之前发生的,所以有发生非线程安全问题的概率。

4、多线程方法

1. currentThread()方法

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

Thread.currentThread().getName()

2. isAlive()方法

方法isAlive()的功能是判断当前的线程是否处于活动状态。

thread.isAlive();

3. sleep()方法

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

Thread.sleep()

4. getId()方法

getId()方法的作用是取得线程的唯一标识。

thread.getId()

5、停止线程

停止线程是在多线程开发时很重要的技术点。停止线程并不像break语句那样干脆,需要一些技巧性的处理。

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

1)使用退出标志,使线程正常退出,也就是当run()方法完成后线程停止。

2)使用stop()方法强行终止线程,但是不推荐使用这个方法,因为该方法已经作废过期,使用后可能产生不可预料的结果。

3)使用interrupt()方法中断线程。

1.暴力法停止线程

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

        try {
            myThread.stop();
        } catch (ThreadDeath e) {
            e.printStackTrace();
        }

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

public class UserPass {
    private String username = "aa";
    private String password = "AA";

    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 println(String username, String password){
        this.username = username;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.password = password;
    }

    public static void main(String[] args) throws InterruptedException {
        UserPass userPass = new UserPass();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                userPass.println("bb","BB");
            }
        });
        thread.start();
        Thread.sleep(500);
        thread.stop();
        System.out.println(userPass.getUsername()+" "+userPass.getPassword());
    }

}

运行结果:

bb AA

2.异常法停止线程

使用interrupt()方法并不会真正的停止线程,调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。

那我们如何判断该线程是否被打上了停止标记,Thread类提供了两种方法。

interrupted() 测试当前线程是否已经中断。
isInterrupted() 测试线程是否已经中断。

interrupted() 方法 不止可以判断当前线程是否已经中断,而且可以会清除该线程的中断状态。而对于isInterrupted() 方法,只会判断当前线程是否已经中断,不会清除线程的中断状态。

仅靠上面的两个方法可以通过while(!this.isInterrupted()){}对代码进行控制,但如果循环外还有其它语句,程序还是会继续运行的。这时可以抛出异常从而使线程彻底停止。示例如下:

public class MyThread extends Thread {
    @Override
    public void run() {
        try {
            for (int i=0; i<50000; i++){
                if (this.isInterrupted()) {
                    System.out.println("已经是停止状态了!");
                    throw new InterruptedException();
                }
                System.out.println(i);
            }
            System.out.println("不抛出异常,我会被执行的哦!");
        } catch (Exception e) {
//            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();
        Thread.sleep(100);
        myThread.interrupt();
    }

}

打印结果:

...
2490
2491
2492
2493
已经是停止状态了!

注意

如果线程在sleep()状态下被停止,也就是线程对象的run()方法含有sleep()方法,在此期间又执行了thread.interrupt() 方法,则会抛出java.lang.InterruptedException: sleep interrupted异常,提示休眠被中断。

3.return法停止线程

return法很简单,只需要把异常法中的抛出异常更改为return即可。代码如下:

public class MyThread extends Thread {
    @Override
    public void run() {

        for (int i=0; i<50000; i++){
            if (this.isInterrupted()) {
                System.out.println("已经是停止状态了!");
                return;//替换此处
            }
            System.out.println(i);
        }
        System.out.println("不进行return,我会被执行的哦!");

    }
}

不过还是建议使用“抛异常”来实现线程的停止,因为在catch块中可以对异常的信息进行相关的处理,而且使用异常能更好、更方便的控制程序的运行流程,不至于代码中出现多个return,造成污染。

6、暂停线程

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

这俩方法已经和stop()一样都被弃用了,因为如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。示例如下:

public class MyThread extends Thread {
    private Integer i = 0;

    @Override
    public void run() {
        while (true) {
            i++;
            System.out.println(i);
        }
    }

    public Integer getI() {
        return i;
    }

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

}

打印结果:

...
3398
3399
3400
3401

执行上段程序永远不会打印main end。出现这样的原因是,当程序运行到 println() 方法内部停止时,PrintStream对象同步锁未被释放。方法 println() 源代码如下:

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

这导致当前PrintStream对象的println() 方法一直呈“暂停”状态,并且锁未被myThread线程释放,而主线程中的代码System.out.println("main end") 还在傻傻的排队等待,导致迟迟不能运行打印。

使用 suspend() 和 resume() 方法也容易因为线程的暂停而导致数据不同步的情况,示例如下:

public class UserPass2 {
    private String username = "aa";
    private String password = "AA";

    public String getUsername() {
        return username;
    }


    public String getPassword() {
        return password;
    }


    public void setValue(String username, String password){
        this.username = username;
        if (Thread.currentThread().getName().equals("a")) {
            Thread.currentThread().suspend();
        }
        this.password = password;
    }

    public static void main(String[] args) throws InterruptedException {
        UserPass2 userPass = new UserPass2();
        new Thread(new Runnable() {
            @Override
            public void run() {
                userPass.setValue("bb","BB");
            }
        },"a").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(userPass.getUsername()+" "+userPass.getPassword());
            }
        },"b").start();

    }

}

打印结果:

bb AA

7、yield()方法

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

public static void yield()    暂停当前正在执行的线程对象,并执行其他线程。

8、线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。

设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。

设置线程优先级使用setPriority()方法,此方法的JDK源码如下:

    public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

在Java中,线程优先级划分为1 ~ 10 这10个等级,如果小于1或大于10,则JDK抛出异常。

从JDK定义的3个优先级常量可知,线程优先级默认为5。

    public final static int MIN_PRIORITY = 1;

    public final static int NORM_PRIORITY = 5;

    public final static int MAX_PRIORITY = 10;

线程优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。

线程优先级具有规则性,线程的优先级与在代码中执行start()方法的顺序无关,与优先级大小有关。

线程优先级具有随机性,CPU尽量使线程优先级较高的先执行完,但无法百分百肯定。也就是说,线程优先级较高的不一定比线程优先级较低的先执行。

9、守护线程

在Java中有两种线程,一种是用户线程,一种守护线程。

什么是守护线程?守护线程是一种特殊的线程,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有了存在的必要了,自动销毁。可以简单地说:任何一个守护线程都是非守护线程的保姆。

如何设置守护线程?通过Thread.setDaemon(false)设置为用户线程,通过Thread.setDaemon(true)设置为守护线程。如果不设置属性,默认为用户线程。

thread.setDaemon(true);

示例如下:

public class MyThread 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) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        thread.setDaemon(true);
        thread.start();
        Thread.sleep(5000);
        System.out.println("我离开后thread对象也就不再打印了");
    }
}

打印结果:

i=1
i=2
i=3
i=4
i=5

我离开后thread对象也就不再打印了

参考与总结

《Java多线程编程核心技术》高洪岩 著

本文主要介绍了Thread类的API,算是为学习多线程更深层次知识打下一些基础,文章若有错误请在评论区指正。

文章来源:微信公众号 薛勤的博客

目录
相关文章
|
1天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
2天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
2天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
2天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
3天前
|
存储 安全 Java
Java中的容器,线程安全和线程不安全
Java中的容器,线程安全和线程不安全
10 1
|
3天前
|
Java 开发者
Java中多线程并发控制的实现与优化
【4月更文挑战第17天】 在现代软件开发中,多线程编程已成为提升应用性能和响应能力的关键手段。特别是在Java语言中,由于其平台无关性和强大的运行时环境,多线程技术的应用尤为广泛。本文将深入探讨Java多线程的并发控制机制,包括基本的同步方法、死锁问题以及高级并发工具如java.util.concurrent包的使用。通过分析多线程环境下的竞态条件、资源争夺和线程协调问题,我们提出了一系列实现和优化策略,旨在帮助开发者构建更加健壮、高效的多线程应用。
3 0
|
Java 数据库 前端开发
|
10天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
10天前
|
算法 Java 开发者
Java中的多线程编程:概念、实现与性能优化
【4月更文挑战第9天】在Java编程中,多线程是一种强大的工具,它允许开发者创建并发执行的程序,提高系统的响应性和吞吐量。本文将深入探讨Java多线程的核心概念,包括线程的生命周期、线程同步机制以及线程池的使用。接着,我们将展示如何通过继承Thread类和实现Runnable接口来创建线程,并讨论各自的优缺点。此外,文章还将介绍高级主题,如死锁的预防、避免和检测,以及如何使用并发集合和原子变量来提高多线程程序的性能和安全性。最后,我们将提供一些实用的性能优化技巧,帮助开发者编写出更高效、更稳定的多线程应用程序。
|
8天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第11天】 在Java中,高效的并发编程是提升应用性能和响应能力的关键。本文将探讨Java并发的核心概念,包括线程安全、锁机制、线程池以及并发集合等,同时提供实用的编程技巧和最佳实践,帮助开发者在保证线程安全的前提下,优化程序性能。我们将通过分析常见的并发问题,如竞态条件、死锁,以及如何利用现代Java并发工具来避免这些问题,从而构建更加健壮和高效的多线程应用程序。