线程基础以及线程之间的共享与协作<一>

基础概念

什么是进程和线程

进程是操作系统进行资源分配的最小单位。
进程是操作系统进行资源分配的最小单位。其中资源包括:CPU、内存空间、磁盘I/O等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合的一次运行活动,进程是操作系统进行资源分配的最小单位。

进程是程序在计算机上的一次执行活动。当你运行了一个程序,你就启动了一个进程。显然,程序是死的,是静态的;而进程是活的,是动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由用户启动的进程。

线程是CPU调度的最小调度,必须依赖于进程存在。
线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的、 能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

线程无处不在
任何一个程序都必须要创建线程,特别是 Java 不管任何程序都必须启动一个 main 函数的主线程;JavaWeb 开发里面的定时任务、定时器、JSP 和 Servlet、异步消息处理机制,远程访问接口RM等,任何一个监听事件,onclick的触发事件等都离不开线程和并发的知识。

CPU核心数和线程数的关系

多核心:也指单芯片多处理器(ChipMultiprocessors,简称 CMP),CMP 是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的 SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个 CPU 同时并行地 运行程序是实现超高速计算的一个重要方向,称为并行处理。

多线程:SimultaneousMultithreading.简称 SMT.让同一个处理器上的多个线程同步执行并共享处理器的执行资源。

核心数、线程数:目前主流 CPU 都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是 1:1 对应关系,也 就是说四核 CPU 一般拥有四个线程。但 Intel 引入超线程技术后,使核心数与线程数形成 1:2 的关系。
在这里插入图片描述

CPU时间片轮转机制

我们平时在开发的时候,感觉并没有受 cpu 核心数的限制,想启动线程就启动线程,哪怕是在单核 CPU 上,为什么?这是因为操作系统提供了一种 CPU 时间片轮转机制。

时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称 RR 调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。

百度百科对 CPU 时间片轮转机制原理解释如下:
如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。 如果进程在时间片结束前阻塞或结来,则 CPU 当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾。

时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一 个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切(processwitch),有时称为上下文切换(contextswitch),需要 5ms, 再假设时间片设为20ms,则在做完20ms有用的工作之后,CPU将花费5ms来进行进程切换。CPU 时间的 20%被浪费在了管理开销上了。

为了提高 CPU 效率,我们可以将时间片设为 5000ms。这时浪费的时间只有 0.1%。但考虑到在一个分时系统中,如果有 10 个交互用户几乎同时按下回车键, 将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待 5s 才获得运行机会。多数用户无法忍受一条简短命令要 5 才能 做出响应,同样的问题在一台支持多道程序的个人计算机上也会发。

结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU效率: 而设得太长又可能引起对短的交互请求的响应变差。将时间片设为 100ms 通常是一个比较合理的折衷。
在 CPU 死机的情况下,其实大家不难发现当运行一个程序的时候把 CPU 给弄到了100%再不重启电脑的情况下,其实我们还是有机会把它KⅢ掉的,我想也正是因为这种机制的缘故。

理解并行和并发

我们举个例子,如果有条高速公路 A 上面并排有 8 条车道,那么最大的并行车辆就是 8 辆此条高速公路 A 同时并排行走的车辆小于等于 8 辆的时候,车辆就可以并行运行。CPU 也是这个原理,一个 CPU 相当于一个高速公路 A,核心数或者线程数就相当于并排可以通行的车道;而多个CPU就相当于并排有多条高速公路,而 每个高速公路并排有多个车道。

当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少? 离开了单位时间其实是没有意义的。

俗话说,一心不能二用,这对计算机也一样,原则上一个 CPU 只能分配给一个进程,以便运行这个进程。我们通常使用的计算机中只有一个 CPU,也就是说只有 一颗心,要让它一心多用同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”。

综合来说:
并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已。

并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行。

两者区别:一个是交替执行,一个是同时执行。
在这里插入图片描述

高并发编程的意义和好处

由于多核多线程的 CPU 的诞生,多线程、高并发的编程越来越受重视和关注。 多线程可以给程序带来如下好处。

(1)充分利用 CPU 的资源
从上面的CPU的介绍,可以看的出来,现在市面上没有CPU的内核不使用多线程并发机制的,特别是服务器还不止一个CPU,如果还是使用单线程的技术做思路, 明显就out 了。因为程序的基本调度单元是线程,并且一个线程也只能在一个 CPU 的一个核的一个线程跑,如果你是个i3的CPU的话,最差也是双核心4线程的运算能力:如果是一个线程的程序的话,那是要浪费3/4的CPU性能:如果设计一个多线程的程序的话,那它就可以同时在多个CPU的多个核的多个线程上跑,可以充分地利用 CPU,减少 CPU 的空闲时间,发挥它的运算能力,提高并发量。

就像我们平时坐地铁一样,很多人坐长线地铁的时候都在认真看书,而不是为了坐地铁而坐地铁,到家了再去看书,这样你的时间就相当于有了两倍。这就是为什么有些人时间很充裕,而有些人老是说没时间的一个原因,工作也是这样,有的时候可以并发地去做几件事情,充分利用我们的时间,CPU也是一样,也要充分利用。

(2)加快响应用户的时间
比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。 我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应时间若提升 1s,如果流量大的话,就能增加不少转换量。做过高性能 web 前端调优 的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域 名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网 站的响应速度。多线程,高并发真的是无处不在。

(3)可以使你的代码模块化,异步化,简单化
例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分,将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。 这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。

多线程应用开发的好处还有很多,大家在日后的代码编写过程中可以慢慢体会它的魅力。

多线程程序注意事项

(1)线程之间的安全性
在同一个进程里面的多线程是资源共享的,也就 是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

(2)线程之间的死锁
为了解决线程之间的安全性引入了 Java 的锁机制,而一不小心就会产生 Java 线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。

假如线程 A 获得了刀,而线程 B 获得了叉。线程 A 就会进入阻塞状态来等待获得叉,而线程 B 则阻塞来等待线程 A 所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生。

(3)线程太多了会将服务器资源耗尽形成死机宕机线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及 CPU 的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?

某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。

认识Java里的线程

Java程序天生就是多线程的

比如下面的代码,只有一个 main 方法,输出当前启用的线程名称,

public static void main(String[] args) {
        //Java 虚拟机线程系统的管理接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos =
                threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程ID和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] "
                    + threadInfo.getThreadName());
        }
    }

执行 main 方法,打印如下:
在这里插入图片描述
该方法只有一个 main 方法,看似只有一个主线程,但实际上 Java 程序天生就是多线程程序,所以可以看到输出了多个线程的名称。

线程的启动和终止

启动

Java 有几种启动线程的方式?
我们先看一下 Thread 的源码是怎么说的。
在这里插入图片描述
在这里插入图片描述
Thread 的源码注释上写得很清楚,有两种启动线程的方式,一种是声明一个类去继承 Thread 类;另一种是声明一个类去实现 Runnable 接口

(1)继承 Thread

	/**
     * extends Thread类 继承Thread类
     */
    private static class UseThread extends Thread {

        @Override
        public void run() {
            super.run();
            System.out.println("I am extended Thread!");
        }
    }

(2)实现 Runnable 接口

	/**
     * implements Runnable接口 派生自Runnable接口
     */
    private static class UseRunnable implements Runnable {

        @Override
        public void run() {
            System.out.println("I am implements Runnable!");
        }
    }

分别用两种分时启动线程,

public static void main(String[] args) {
        // UseThread
        UseThread useThread = new UseThread();
        useThread.start();
        // UseRunnable
        UseRunnable useRunnable = new UseRunnable();
        Thread thread = new Thread(useRunnable);
        thread.start();
    }

继承 Thread 类和实现 Runnable 接口的区别?
(1)从本质上讲,在 Java 里面,Thread 是对操作系统线程的抽象,Runnable 是对任务/业务逻辑的抽象。可以理解为 Thread 是对线程的唯一抽象。
(2)从灵活性上讲,实现 Runnable 接口要比继承 Thread 类更灵活,也能减少程序之间的耦合度,实现面向接口编程。

终止/中断

我们先看下 Thread 类源码中为我们提供了哪些终止或者中断线程的方法
在这里插入图片描述
我把这些方法分成了两类,第一类用红色块包裹的方法,有终止线程 stop()、破坏线程destroy()、挂起线程 suspend() 方法。
面试中,经常会问到这个问题,Thread 的 stop() 方法为什么不建议使用?
要回答这个问题,我们去看看 stop() 方法的源码,代码如下:

@Deprecated
    public final void stop() {
    	// 获取当前系统的安全管理器
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
        	// 确认当前正在运行的线程是否有修改此线程的权限
            checkAccess();
            if (this != Thread.currentThread()) {
                security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
            }
        }
        // threadStatus 值默认为 0 对应 NEW,不可能变成 1 not-NEW,因为锁被当前线程锁持有
        if (threadStatus != 0) {
            resume(); // 如果线程被挂起则唤醒该线程; 否则不唤醒
        }

        // 终止线程
        stop0(new ThreadDeath());
    }

通过源码,我们可以看到 stop() 方法的终止过程比较暴力,不管当前线程的执行情况,也不管在执行什么任务,获取到权限就直接终止,这种方式可能会导致线程作占用的资源得不到正常的释放。

第二类是用绿色块包裹的方法,与中断线程相关的方法。主要有 interrupt() 方法、interrupted() 方法、isInterrupted() 方法。
(1)interrupt() 方法
首先读一下源码,源码如下:

public void interrupt() {
        if (this != Thread.currentThread())
        	// 获取安全权限
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
            	// 设置中断标志位为 true
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

interrupt() 方法是中断线程,设置线程中断的标志位为 true,但是要注意的是,interrupt() 方法并不会立即中断线程,可以理解为给线程打了个招呼,告诉它一声 “你要中断了”,线程完全可以不理会,继续做自己的工作;也可能是过一段时间线程才会中断。

(2)isInterrupted() 方法
isInterrupted() 方法是用来查看线程的中断标志位的

(3)interrupted() 方法
interrupted() 方法是一个静态方法,读一下源码:

 /**
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words, if this method were to be called twice in succession, the
     * second call would return false (unless the current thread were
     * interrupted again, after the first call had cleared its interrupted
     * status and before the second call had examined it).
     * 测试当前线程是否已被中断。通过此方法可以清除线程的中断状态。
     * 换句话说,如果此方法被连续调用两次,则第二次调用将返回false(除非
     * 当前线程在第一次调用清除其中断的状态之后再次被中断,并且在第二次调用检查之前它)。
     * 
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     * 线程中断将被忽略,因为一个线程在中断时还没有存活,该方法将反映返回false。
     * 
     * @return  <code>true</code> if the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * 如果线程已经被中断,返回 true, 否则 false
     * @see #isInterrupted()
     * @revised 6.0
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

该方法是用来测试当前线程是否已被中断,如果线程已经被中断,返回 true, 否则 false。但是该方法会清除线程的中断状态。

比如说,某个线程调用了 interrupt() 方法中断线程,然后用 interrupted() 方法去测试线程是否被中断,如果线程已被中断,正常情况下,线程的中断状态为 true,但是执行interrupted() 之后,中断状态会被重置为 false。

代码演示一下,上面三个方法的作用与区别:

package cn.enjoyedu.ch1.base.safeend;

/**
 * @author liz
 * @Description 线程中断的方法以及区别
 * @date 2020/9/7-21:45
 */
public class EndThread {

	private static class UseThread extends Thread {

		public UseThread(String name) {
			super(name);
		}

		@Override
		public void run() {
			int i = 1;
			// 当前线程的名称
			String threadName = Thread.currentThread().getName();
			// 输出线程状态上是否被中断
			System.out.println(threadName + "线程状态上是否被中断:" + isInterrupted());
			// 如果中断标识位为 false,进入循环,一开始标识位为 false,
			// 中断线程之后,最后输出的标识位变为 true
//            while (!isInterrupted()) {
			// 如果 Thread.interrupted()为 false,进入循环,一开始标识位为 false,
			// 中断线程之后,最后输出的标识位变为 false
			// 说明 Thread.interrupted() 方法,会把线程的中断标识位重置为 false
//			while (!Thread.interrupted()) {
				// while (true) 情况下,一开始线程状态标识位是 false 说明是未被中断,后面标识位变为 true
				// 1000 这个数字可以调整,可能不一定能看到中断标志变化,可以适当调大一些,比如5000
             while (i < 1000) {
				System.out.println(threadName + i + "正在运行……");
				System.out.println(threadName + i + "内部是否被中断状态:" + isInterrupted());
				i++;
			}
//			System.out.println(threadName + "线程状态上是否被中断:" + isInterrupted());
		}
	}

	public static void main(String[] args) throws InterruptedException {
		Thread endThread = new UseThread("endThread");
		endThread.start();
		Thread.sleep(20);
		// 中断线程,设置线程中断的标志位
		endThread.interrupt();
	}
}

第一种情况:用一个循环去观察线程的中断标志位的变化,执行结果如下:
在这里插入图片描述
从结果可以看出中断标志位由false变成了true

第二种情况:通过 isInterrupted() 判断当前线程的中断状态作为循环条件
代码如下:

public class EndThread {

	private static class UseThread extends Thread {

		public UseThread(String name) {
			super(name);
		}

		@Override
		public void run() {
			int i = 1;
			// 当前线程的名称
			String threadName = Thread.currentThread().getName();
			// 输出线程状态上是否被中断
			System.out.println(threadName + "线程状态上是否被中断:" + isInterrupted());
			// 如果中断标识位为 false,进入循环,一开始标识位为 false,
			// 中断线程之后,最后输出的标识位变为 true
            while (!isInterrupted()) {
			// 如果 Thread.interrupted()为 false,进入循环,一开始标识位为 false,
			// 中断线程之后,最后输出的标识位变为 false
			// 说明 Thread.interrupted() 方法,会把线程的中断标识位重置为 false
//			while (!Thread.interrupted()) {
				// while (true) 情况下,一开始线程状态标识位是 false 说明是未被中断,后面标识位变为 true
//             while (i < 5000) {
				System.out.println(threadName + i + "正在运行……");
				System.out.println(threadName + i + "内部是否被中断状态:" + isInterrupted());
				i++;
			}
			System.out.println(threadName + "线程状态上是否被中断:" + isInterrupted());
		}
	}

	public static void main(String[] args) throws InterruptedException {
		Thread endThread = new UseThread("endThread");
		endThread.start();
		Thread.sleep(20);
		// 中断线程,设置线程中断的标志位
		endThread.interrupt();
	}
}

执行结果:
在这里插入图片描述
一开始,线程的中断标志位为 false,线程中断之后,线程的中断标志位会变成 true,表示线程已经被中断

第三种情况:通过 Thread.interrupted() 判断当前线程的中断状态作为循环条件
代码如下:

public class EndThread {

	private static class UseThread extends Thread {

		public UseThread(String name) {
			super(name);
		}

		@Override
		public void run() {
			int i = 1;
			// 当前线程的名称
			String threadName = Thread.currentThread().getName();
			// 输出线程状态上是否被中断
			System.out.println(threadName + "线程状态上是否被中断:" + isInterrupted());
			// 如果中断标识位为 false,进入循环,一开始标识位为 false,
			// 中断线程之后,最后输出的标识位变为 true
//            while (!isInterrupted()) {
			// 如果 Thread.interrupted()为 false,进入循环,一开始标识位为 false,
			// 中断线程之后,最后输出的标识位变为 false
			// 说明 Thread.interrupted() 方法,会把线程的中断标识位重置为 false
			while (!Thread.interrupted()) {
				// while (true) 情况下,一开始线程状态标识位是 false 说明是未被中断,后面标识位变为 true
//             while (i < 5000) {
				System.out.println(threadName + i + "正在运行……");
				System.out.println(threadName + i + "内部是否被中断状态:" + isInterrupted());
				i++;
			}
			System.out.println(threadName + "线程状态上是否被中断:" + isInterrupted());
		}
	}

	public static void main(String[] args) throws InterruptedException {
		Thread endThread = new UseThread("endThread");
		endThread.start();
		Thread.sleep(20);
		// 中断线程,设置线程中断的标志位
		endThread.interrupt();
	}

执行结果:
在这里插入图片描述
一开始,线程的中断标志位为 false,线程中断之后,最后输出的线程中断标志位还是 false,说明 Thread.interrupted() 方法在线程中断之后会重置标志位为 false。

总结:
interrupt() 方法可以中断线程,但是线程并不会立即中断,可能会过一段时间中断线程,会更新线程的中断标志位为 true。

isInterrupted() 方法的作用是判断线程是否已被中断,true 表示已中断,反之为 false。线程中断之后,线程标志位有 false 变成 true

Thread.interrupted() 方法也是用来判断线程是否已被中断,true 表示已中断,反之为 false。但是线程中断之后,该方法会把线程标志位重置为 false。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值