Java并发编程基础

导读:本篇文章讲解 Java并发编程基础,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

概述

java从诞生开始就明智地选择了内置对多线程的支持,这使得Java语言相比同一时期的其他语言具有明显的优势。线程作为操作系统调度的最小单元,多个线程能够同时执行,这将显著提升程序的性能,在多核环境中表现得更加明显。但是,过多的创建线程对线程的不当管理也容易造成问题。本章将着重介绍Java并发编程的基础知识,从启动一个线程到线程间不同的通信方式。

线程简介

现在操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

为什么要使用多线程

更多的处理器核心

更快的响应时间

更好的编程模型

线程优先级

线程的状态

在这里插入图片描述
线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java线程状态变迁如下图:
在这里插入图片描述
Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中的Lock接口的线程状态却是等待状态,因为java.concurrent包中的Lock接口对应阻塞的实现使用了LockSupport类中的相关方法。

Daemon线程

Daemon线程是一种支持型线程,因为它主要被用做程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

启动和终止线程

通过调用线程的start()方法进行启动,随着run()方法的执行完毕,线程也随之终止。

理解中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断操作好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。
线程通过检测自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。
从java的API中可以看出,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis))这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

package com.study.practice;

import com.study.practice.controller.TestStrReplace;
import lombok.SneakyThrows;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.TimeUnit;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PracticeApplicationTests {
	public static void main(String[] args) throws InterruptedException {
		Thread sleepThread = new Thread(new SleepRunner(), "sleep");
		Thread busyThread = new Thread(new BusyRunner(), "busy");
		busyThread.setDaemon(true);
		sleepThread.start();
		busyThread.start();
		TimeUnit.SECONDS.sleep(5);
		sleepThread.interrupt();
		busyThread.interrupt();
		System.out.println("sleep runner is " + sleepThread.isInterrupted());
		System.out.println("busy runner is " + busyThread.isInterrupted());
		TimeUnit.SECONDS.sleep(2);
	}

	static class SleepRunner implements Runnable {
		@SneakyThrows
		@Override
		public void run() {
			while (true) {
				TimeUnit.SECONDS.sleep(10);
			}
		}
	}

	static class BusyRunner implements Runnable {
		@Override
		public void run() {
			while (true) {
			}
		}
	}
}

输出结果:

Exception in thread "sleep" java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.study.practice.PracticeApplicationTests$SleepRunner.run(PracticeApplicationTests.java:44)
	at java.lang.Thread.run(Thread.java:748)
sleep runner is false
busy runner is true

从结果我们可以看出,抛出InterruptedException的线程sleepThread,其中断标识位被清除了,而一直忙碌运作的busyThread,中断标识没有被清除,实际让如果busyThread不自己检测中断标识并处理,那么它会一直正常运行下去。

过期的suspend()、resume()和stop()

suspend()使线程暂停,resume()使线程恢复,stop()使线程停止。
不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占用的资源(比如锁)而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给与线程完成资源释放工作的机会,因此会导致程序可能工作在不确定的状态下。

正因为suspend()、resume()和stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法,而暂停和恢复操作可以用等待/通知机制来完成。

安全的终止线程

中断状态是线程的一个标识位,因此中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该进程。
下面是代码示例

package com.study.practice;

import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.TimeUnit;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PracticeApplicationTests {

    public static void main(String[] args) throws InterruptedException {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "countThread");
        countThread.start();
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "countThread");
        countThread.start();
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {

        private long i = 0;

        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()) {
                i++;
            }
            System.out.println("Count i = " + i);
        }

        public void cancel() {
            on = false;
        }
    }
}

示例在执行过程中,main线程通过中断操作和cancel()方法操作均可使countThread线程终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。

线程间通信

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,这将会带来巨大的价值。

volatile和synchronized关键字

Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
但是过多的使用volatile是不必要的,因为它会降低程序执行的效率。
关键字synchronized可以修饰方法或者以同步块的形式进行使用,它主要确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口,进入BLOCKED状态。
在这里插入图片描述
从上图可以看到,任意线程对Object(Object有synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得锁的线程)释放了锁,则释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

等待/通知机制

调用wait()、notify()以及notifyAll()时需要注意的细节,如下:

  1. 使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
  2. 调用wait()方法后,线程状态有Running变为Waiting,并将 当前线程放置到对象的等待队列中。
  3. notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回。
  4. notify()方法将等待队列中的一个等待线程从等待队列中移动到同步队列中,而notifyAll(()方法则是将等待队列中所有的线程全部移动到同步队列中,被移动的线程状态有WATING变为BLOCKED。
  5. 从wait()方法返回的前提是获得了调用对象的锁。
    如下图示所示:
    在这里插入图片描述
    上图中,WaitThread首先获取了对象的锁,然后调用wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。
    具体示例参考:
    java多线程协作
    Java并发编程-共享模型之管程(Monitor/Synchronized)(四)

管道输入/输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,他们主要用于线程之间的数据传输,而传输的媒介是内存。
管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
在下面的示例中,创建了printThread线程,它用来接受main线程的输入,任何main线程的输入均通过PipedWriter写入,而printThread在另一端通过PipeReader将内容读出并打印。
示例:

package com.study.practice;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

/**
 * @Description : test
 * @Version : V1.0.0
 * @Date : 2022/4/3 11:35
 */
public class Test {

    public static void main(String[] args) throws IOException {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);
        Thread printThread = new Thread(new Print(in), "PrintThread");
        printThread.start();
        int receive = 0;
        try {
            while ((receive = System.in.read()) != -1) {
                out.write(receive);
            }
        } finally {
            out.close();
            in.close();
        }
    }

    static class Print implements Runnable {

        private final PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
            int receive = 0;
            try {
                while ((receive = in.read()) != -1) {
                    System.out.print((char)receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

输出结果:

Repeat my words.
Repeat my words.
^D

对于Piped类型的流,必须先要进行绑定,也就是调用connect()方法,如果没有将输入/输出绑定起来,对于该流的访问将会抛出异常。

Thread.join()的使用

如果线程A中执行了threadB.join()语句,其含义是:当前线程A等待threadB线程终止之后才从threadB.join()返回。
说白了Thread.join()就像插队一样,被插队的人需要等待插队的人完成后才可继续。
示例:

public static void main(String[] args) throws IOException, InterruptedException {
        System.out.println("threadA begin");
        Thread threadB = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + " end");
            } catch (InterruptedException e) {
            }
        }, "threadB");
        threadB.start();
        threadB.join();
        System.out.println("thread A end");
    }

执行结果:

hreadA begin
threadB end
thread A end

我们看下Thread.join

// 获取插入线程实例的锁
public final synchronized void join() throws InterruptedException {
	// 条件不满足, 继续等待
	while(isAlive()) {
		wait(0);
	}
	// 当线程被唤醒后,从这里继续往下执行
}

当插入线程终止时,会调用插入线程自身的notifyAll()方法,会通知所有等待在插入线程对象上的线程。可以看到join()方法的逻辑就是等待/通知经典使用范式。

ThreadLocal的使用

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
参考:ThreadLocal详解

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/100137.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!