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最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
JavaFX是Java的下一代图形用户界面工具包。JavaFX是一组图形和媒体API,我们可以用它们来创建和部署富客户端应用程序。 JavaFX允许开发人员快速构建丰富的跨平台应用程序,允许开发人员在单个编程接口中组合图形,动画和UI控件。本文详细介绍了JavaFx的常见用法,相信读完本教程你一定有所收获!
Java最新图形化界面开发技术——JavaFx教程(含UI控件用法介绍、属性绑定、事件监听、FXML)
|
2天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
2天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
2天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
13 3
|
2天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
12 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
60 1
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
32 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
25 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
41 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
47 1