多线程-day-08多线程和线程并发工具总结

10 篇文章 0 订阅
6 篇文章 0 订阅

目录

多线程和线程并发工具总结

线程基础、线程之间的共享协作

基础概念

Java线程

线程常用方法和线程状态

共享线程

线程间协作

线程并发工具类

Fork-Join分而治之、工作密取

Fork-Join标准范式

Fork-Join运用案例

常用并发工具类

          4.1 CountDownLatch

          4.2 CyclicBarrier

          4.3 Semaphore

          4.4 Exchange

          4.5 Callable、Future、FutureTask


正文:

线程基础、线程之间的共享和协作

1,CPU核心数和线程数之间的关系

        我们在选购电脑的时候,CPU是一个需要考虑到核心因素,因为它决定了电脑的性能等级。CPU从早期的单核,发展到现在的双核,多核。CPU除了核心数之外,还有线程数之说,下面就来解释一下CPU的核心数与线程数的关系和区别。 
        简单地说,CPU的核心数是指物理上,也就是硬件上存在着几个核心。比如,双核就是包括2个相对独立的CPU核心单元组,四核就包含4个相对独立的CPU核心单元组,等等,依次类推。 
        线程数是一种逻辑的概念,简单地说,就是模拟出的CPU核心数。比如,可以通过一个CPU核心数模拟出2线程的CPU,也就是说,这个单核心的CPU被模拟成了一个类似双核心CPU的功能。我们从任务管理器的性能标签页中看到的是两个CPU。 
        比如Intel 赛扬G460是单核心,双线程的CPU,Intel 酷i3 3220是双核心 四线程,Intel 酷睿i7 4770K是四核心 八线程 ,Intel 酷睿i5 457睿0是四核心 四线程等等。 
        对于一个CPU,线程数总是大于或等于核心数的。一个核心最少对应一个线程,但通过超线程技术,一个核心可以对应两个线程,也就是说它可以同时运行两个线程。 
        CPU的线程数概念仅仅只针对Intel的CPU才有用,因为它是通过Intel超线程技术来实现的,最早应用在Pentium4上。如果没有超线程技术,一个CPU核心对应一个线程。所以,对于AMD的CPU来说,只有核心数的概念,没有线程数的概念。 
        CPU之所以要增加线程数,是源于多任务处理的需要。线程数越多,越有利于同时运行多个程序,因为线程数等同于在某个瞬间CPU能同时并行处理的任务数。 
        在Windows中,在cmd命令中输入“wmic”,然后在出现的新窗口中输入“cpu get *”即可查看物理CPU数、CPU核心数、线程数。其中, 
        Name:表示物理CPU数 
        NumberOfCores:表示CPU核心数 
   NumberOfLogicalProcessors:表示CPU线程数

因此简单总结为:

①、一块CPU只有一块处理器

②、Inter提出了多核处理器

③、CPU核心数 和 线程数 是 1:1 的关系

④、Inter提出了超线程,CPU核心数 和 线程数 是 1:2 的关系

⑤、CPU同一时间只能运行16个线程

2、CPU时间片轮转机制

①、RR调度:首先将所有就绪的队列按FCFS策略排成一个就绪队列,然后系统设置一定的时间片,每次给队首作业分配时间片。如果此作业运行结束,即使时间片没用完,立刻从队列中去除此作业,并给下一个作业分配新的时间片;如果作业时间片用完没有运行结束,则将此作业重新加入就绪队列尾部等待调度。

运行如下图:

 

②、CPU时间片轮转机制可能会导致上下文切换。

CPU上下文切换详解

        上下文切换(有时也称做进程切换或任务切换)是指CPU从一个进程线程切换到另一个进程或线程。
        进程(有时候也称做任务)是指一个程序运行的实例。在Linux系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。
        上下文是指某一时间点 CPU 寄存器和程序计数器的内容。寄存器是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
        稍微详细描述一下,上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行以下的活动:(1)挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处,(2)在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复,(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。
上下文切换有时被描述为内核挂起 CPU 当前执行的进程,然后继续执行之前挂起的众多进程中的某一个。尽管这么说对于澄清概念有所帮助,但是这句话本身可能有一点令人困惑。因为通过定义可以知道,进程是指一个程序运行的实例。所以说成挂起一个进程的运行可能更适合一些。

上下文切换与模式切换

        上下文切换只能发生在内核态中。内核态是 CPU 的一种有特权的模式,在这种模式下只有内核运行并且可以访问所有内存和其他系统资源。其他的程序,如应用程序,在最开始都是运行在用户态,但是他们能通过系统调用来运行部分内核的代码。系统调用在类 Unix 系统中是指活跃的进程(正在运行在 CPU 上的进程)对于内核所提供的服务的请求,例如输入/输出(I/O)和进程创建(创建一个新的进程)。I/O 可以被定义为任何信息流入或流出 CPU 与主内存(RAM)。也就是说,一台电脑的 CPU和内存与该电脑的用户(通过键盘或鼠标)、存储设备(硬盘或磁盘驱动)还有其他电脑的任何交流都是 I/O。
        这两种模式(用户态和内核态)在类 Unix 系统中共存意味着当系统调用发生时 CPU 切换到内核态是必要的。这应该叫做模式切换而不是上下文切换,因为没有改变当前的进程。
        上下文切换在多任务操作系统中是一个必须的特性。多任务操作系统是指多个进程运行在一个 CPU 中互不打扰,看起来像同时运行一样。这个并行的错觉是由于上下文在高速的切换(每秒几十上百次)。当某一进程自愿放弃它的 CPU 时间或者系统分配的时间片用完时,就会发生上下文切换。
        上下文切换有时也因硬件中断而触发。硬件中断是指硬件设备(如键盘、鼠标、调试解调器、系统时钟)给内核发送的一个信号,该信号表示一个事件(如按键、鼠标移动、从网络连接接收到数据)发生了。
英特尔的 80386 和更高级的 CPU 都支持硬件上下文切换。然而,大多数现代的操作系统通过软件实现上下文切换,而非使用硬件上下文切换,这样能够运行在任何 CPU 上。同时,使用软件上下文切换可以尝试获得更好的性能。软件的上下文切换最先在 Linux 2.4 中实现。
        软件上下文切换号称的一个主要优点是,硬件的机制保存了几乎所有 CPU 的状态,软件能够有选择性的保存需要被保存的部分并重新加载。然而这个行为对于提升上下文切换的性能到底有多重要,还有一点疑问。其拥护者还宣称,软件上下文切换有提高切换代码的可能性,它有助于提高正在加载的数据的有效性,从而进一步提高性能。

上下文切换的消耗

        上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
        Linux相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

 

3、什么是进程和线程?

①、进程是程序运行资源分配的最小单位。进程内部有多个线程,会共享这个进程中的资源。

②、线程是CPU调度的最小单位。必须依赖进程而存在。

 

4、并行和并发的区别?

①、并行:同一时刻,可同时处理问题的能力。比如售票窗口多开,多开的窗口表示同时处理的能力。

②、并发:与单位时间相关,在单位时间内处理事情的能力。

 

5、高并发编程的意义、好处和注意事项

①好处:充分利用CPU资源,加快用户响应时间,程序模块化,异步化。

②问题:A、线程共享资源,存在冲突;B、容易导致死锁;C、启用太多线程,可能会搞垮机器。

 

6、学习多线程

         Java实现多线程有三种方式:

                ①、继承Thread类

                ②、实现Runnable接口(无返回值)

                ③、实现Callable接口(有返回值)

①、Java里的程序天生就是多线程的。

        A、ThreadMXBean是Java虚拟机为我们提供线程管理的接口,通过该类可以拿到应用中有多少个线程。

        B、至少会运行5个线程:

                1、main主函数线程
                2、Reference Handler负责清除引用的线程
                3、Finalizer调用对象的Final方法的线程
                4、Signal Dispatcher分发,处理发送给虚拟机信号的线程
                5、Attach Listener获取当前程序运行相关信息

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
 
public class OnlyMain {
 
	public static void main(String[] args) {
 
		// Java虚拟机 为我们提供的线程里面,线程管理的接口,通过该类可以拿到应用程序里面有多少个线程
		ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
		// 是否看锁,这里不看锁,一般用不到,返回值是ThreadInfo的数组
		ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
		// 遍历数组
		for (ThreadInfo threadInfo : threadInfos) {
			/**
			 *  main主函数线程
			 *  Reference Handler负责清除引用的线程
			 *  Finalizer调用对象的Final方法的线程
			 *  Signal Dispatcher分发,处理发送给虚拟机信号的线程
			 *  Attach Listener获取当前程序运行相关信息
			 */
			System.out.println("【" + threadInfo.getThreadId() + "】 " + threadInfo.getThreadName());
		}
	}
}
 
控制台输出:
【5】 Attach Listener
【4】 Signal Dispatcher
【3】 Finalizer
【2】 Reference Handler
【1】 main

         C、实现多线程的三种方式及区别:

                1、继承Thread类

                2、实现Runnable接口

                3、实现Callable接口,允许有返回值,并且不能直接用new Thread来启动该多线程接口对象,而是需要先用FutureTask对象来转换一次,再调用new Thread()来启动Callable多线程对象

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class NewThread {
 
	/* 扩展自Thread类 */
 
	/* 实现Runnable接口 */
	private static class UseRunnable implements Runnable {
 
		@Override
		public void run() {
			System.out.println("It is a Runnable!");
		}
 
	}
 
	/* 实现Callable接口,允许有返回值 */
	private static class UseCallable implements Callable<String> {
 
		@Override
		public String call() throws Exception {
			System.out.println("It is a Callable!");
			return "CallableResult";
		}
	}
 
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		// 实例化Runnable接口对象
		UseRunnable useRunnable = new UseRunnable();
		// 通过new Thread来执行Runnable多线程对象
		new Thread(useRunnable).start();
 
		// 实例化Callable接口对象
		UseCallable useCallble = new UseCallable();
		// Thread不能够运行Callable接口对象,只能通过FutureTask接口转换后在运行
		FutureTask<String> futureTask = new FutureTask<>(useCallble);
		new Thread(futureTask).start();
		System.out.println(futureTask.get());
	}
}
 
控制台输出:
It is a Runnable!
It is a Callable!
CallableResult

 

理解中断

如何安全的终止线程

1,理解中断

        线程自然终止:自然执行完 或 抛出未处理异常

        stop()、resume()、suspend() 三个方法已经在后续的jdk版本已过时,不建议使用

        stop()方法:会导致线程不正确释放资源;

        suspend()方法:挂起,容易导致死锁

        Java线程是协作式工作,而非抢占式工作;

        介绍三种中断方式:

        ①、interrupt()方法

                interrupt()方法中断一个线程,并不是强制关闭该线程,只是跟该线程打个招呼,将线程的中断标志位置为true,线程是否中断,由线程本身决定;

        ②、inInterrupted()方法

                inInterrupted()方法判断当前线程是否处于中断状态;

        ③、static 方法interrupted()方法

                static方法interrupted()方法判断当前线程是否处于中断状态,并将中断标志位改为false;

        注:方法里如果抛出InterruptedException,线程的中断标志位会被置为false,如果确实需要中断线程,则需要在catch里面再次调用interrupt()方法

public class HasInterruptException {
 
	// 定义一个私有的Thread集成类
	private static class UseThread extends Thread {
 
		@Override
		public void run() {
			// 获取当前线程名字
			String threadName = Thread.currentThread().getName();
			// 判断线程是否处于中断标志位
			while (!isInterrupted()) {
				// 测试用interrupt中断后,报InterruptedException时状态变化
				try {
					System.out.println(threadName + "is run !!!!");
					// 设置休眠毫秒数
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					// 判断中断后抛出InterruptedException后中断标志位的状态
					System.out.println(threadName + " interrupt flag is " + isInterrupted());
					// 如果抛出InterruptedException后中断标志位置为了false,则需要手动再调用interrupt()方法,如果不调用,则中断标志位为false,则会一直在死循环中而不会退出
					interrupt();
					e.printStackTrace();
				}
				// 打印出线程名称
				System.out.println(threadName);
			}
			// 查看当前线程中断标志位的状态
			System.out.println(threadName + " interrupt flag is " + isInterrupted());
		}
	}
 
	public static void main(String[] args) throws InterruptedException {
		UseThread useThread = new UseThread();
		useThread.setName("HasInterruptException--");
		useThread.start();
		Thread.sleep(800);
		useThread.interrupt();
	}
}
 
控制台输出结果:
HasInterruptException--is run !!!!
HasInterruptException-- interrupt flag is false
java.lang.InterruptedException: sleep interrupted
HasInterruptException--
HasInterruptException-- interrupt flag is true
	at java.lang.Thread.sleep(Native Method)
	at com.xiangxue.ch1.safeend.HasInterruptException$UseThread.run(HasInterruptException.java:18)

 

线程基础、线程之间的共享和协作

线程常用方法和线程的状态

        start():调用start()方法后,使线程从新建状态处于就绪状态。

        sleep():调用sleep()方法后,设置休眠时间,使线程从运行状态处于阻塞(休眠)状态,休眠时间到,线程从阻塞状态转变为就绪状态。

        wait():调用wait()方法后,使线程从运行状态处于阻塞(休眠)状态,只有通过notify()或者notifyAll()方法重新使线程处于就绪状态。

        notify():当线程调用wait方法后,进入阻塞状态,如果有多个线程阻塞,调用notify()方法后,CPU会调用处于线程栈上的第一个阻塞线程,并将线程阻塞状态置为就绪状态。只激活一个阻塞线程。具体看下面介绍和实现。

        notifyAll():当线程调用wait方法后,进入阻塞状态,如果有多个线程阻塞,调用notifyAll()方法后,CPU会将所有处于线程栈上的所有调用了wait()方法阻塞的线程进行依次执行,直到所有线程执行完或抛出异常。具体看下面介绍和实现。

        interrupt():调用interrupt()方法后,不是强制关闭线程,只是跟线程打个招呼,将线程的中断标志位置为true,线程是否中断,由线程本身决定。

        isInterrypt():线程中断标志位,true/false两个Boolean值,用来判断是否调用interrupt()方法,告诉线程是否中断。

        interrupted():判断线程是否处于中断状态,并将中断标志位改为false。

        run():运行线程的方法。

        join():thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

        yield():Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

 

synchronized内置锁

1、用处

        synchronized作为线程同步的关键字,设计到锁的概念,下面就对锁的概念进行详细介绍。

        Java内置锁是一个互斥锁,这就说明最多只有一个线程能够获得该锁,例如两个线程:线程A和线程B,如果线程A尝试去获得线程B的内置锁,则线程A必须等待或者阻塞,直到线程B释放这个锁为止;如果线程B永不释放这个锁,则线程A则永远处于等待或阻塞状态。使用内置锁是线程安全的。

        Java的对象锁和类锁在锁的概念上,与内置锁几乎是一致的,但是对象锁和类锁的区别是非常大的。

2、对象锁

        用synchronized修饰非静态方法、用synchronized(this)作为同步代码块、用synchronized(非this对象)的用法锁的是对象,线程想要执行对应的同步代码,需要先获得对象锁。

3、类锁

        用synchronized修饰静态方法、用synchronized(类.class)的用法锁的是类,线程想要执行对应的同步代码,需要先获得类锁。

 

对synchronized关键字的用法,对象锁,类锁的使用参考之前总结的文章实现案例:

synchronized内置锁用法

总结:静态方法一定会同步,非静态方法需在单例模式才生效,但是也不能都用静态同步方法,总之用得不好可能会给性能带来极大的影响。另外,有必要说一下的是Spring的bean默认是单例的。

对象锁:锁的是类的对象实例。

类锁 :锁的是每个类的的Class对象,每个类的的Class对象在一个虚拟机中只有一个,所以类锁也只有一个。

 

等待和通知

一、应用场景:

一个线程修改了一个值,另一个线程感受到了值的变化,进行相应的操作。前一个线程类比于一个生产者,后一个线程是消费者。如何让消费者感受到生产者的一个值的变化呢?

解决方案一:

轮询:每隔一分钟就去轮询一次,总有一个时间点能够获取到生产者的变换。比如煲汤,每个一分钟就去看一下是否煲好了。结果:这样会很累,很占用资源。

轮询的缺点:很难确保一个及时性,每隔一段时间就要去操作一次,资源开销很大,做很多无用功。

解决方案二:

等待和通知机制方式:当一个线程调用了wait()方法,会进入一个等待状态,而另外一个线程对值进行操作后,调用notify()或者notifyAll()方法后,通知第一个线程去操作某件事情。注意:wait()、notify()/notifyAll()是对象上的方法。

wait()等待方会怎么做?

1、获取对象的锁;一定是在循环里面去操作;

2、循环里判断是否满足条件,不满足条件调用wait()方法,一直等待;

3、满足条件,执行业务逻辑;

notify()、notifyAll()会怎么做?

1、依然要获取对象的锁;

2、改变相关条件;

3、通知所有等待在对象的线程

以上介绍了wait、notify/notifyAll的标准范式。

三、notify()和notifyAll()区别:

应该尽量应用notifyAll(),使用notify()的话,jvm会执行已经加入等待线程栈里面的第一个线程,给我们一种感观就是随机的选择了一种线程,如果该线程达到条件就正好执行那一条,其实这是一个误区,而是jvm会选择在线程栈里面的第一个线程。因此如果用notify()的话,可能会造成信号丢失的情况。

关于wait()、notify()、notifyAll()的用法看之前的文章实现实例:

等待通知wait()、notify()、notifyAll()原理和介绍

 

join()方法
线程A,执行了线程B的join方法,线程A必须要等待B执行完成了以后,线程A才能继续自己的工作

调用yield() 、sleep()、wait()、notify()等方法对锁有何影响? 
线程在执行yield()以后,持有的锁是不释放的

sleep()方法被调用以后,持有的锁是不释放的

调动方法之前,必须要持有锁。调用了wait()方法以后,锁就会被释放,当wait方法返回的时候,线程会重新持有锁

调动方法之前,必须要持有锁,调用notify()方法本身不会释放锁的

 

ForkJoinPool

用途,概念:

        ForkJoinPool的优势在于,利用多核CPU,将一个任务,拆分成多个小任务 ,将这些小任务分配到多个处理器上并行执行;当小任务都执行完成之后,再将结果进行合并汇总。每个小任务间都没有关联,与原任务的形式相同。体现了“分而治之”的概念。任务递归分配成若干个小任务 -- 并行求值 -- 结果合并。

1、ForkJoinPool

        Java7提供了ForkJoinPool来支持将一个任务拆分成多个小任务进行并行计算,再将多个“小任务”的结果进行join汇总。

2、invoke、invokeAll

        执行指定的任务,等待任务,完成任务返回结果。

3、递归算法

        在继承RecurisiveTask(有返回结果),RecurisiveAction(无返回结果)类时,通过递归的方式,来将任务拆分成一个一个的小任务,通过invokeAll()方法来调度子任务,等待任务完成返回结果。注意,只有在ForkJoinPool执行计算过程中调用它们。

关于ForkJoin的实现案例看之前文章:

ForkJoin原理及实现

ForkJoinPool异步和同步的区别:

        1,调用execute()方法时,为异步执行

        2,调用invoke()方法时,为同步执行

invokeAll()方法

1,invokeAll()方法, 我们查看源代码的实现可以看出,返回一个集合形式的结果,因此可以在for循环的泛型里面直接用invokeAll(集合对象)方法

2、调用了invokeAll()方法后,将所有循环内的子方法都join()起来,等待子任务的完成

 

CountDownLatch、CyclicBarrier

一、CountDownLatch

官方介绍:

        CountDownLatch是在java1.5被引入的,它存在于java.util.concurrent包下。CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行。

        CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

        什么意思呢?就是运行过程中,有几个线程,那么只有当几个线程同时就绪之后,类似于100米赛跑一样,必须所有运动员到达起跑线之后,通过一个发令枪才能够一起冲向终点,运行起来。

里面就有几个方法,分别介绍一下:

        countDown()方法:该方法初始值设置为允许运行的线程数,这里比如赛道上只能容纳10个人,则初始值为10,然后每一次线程执行完,则将初始值10减1,一直减到0为止,然后表示所有的运动员都就位了,然后就等待发令枪声响就开始同时运行了。这里要注意的点是,必须在每一个线程执行完之后,调用countDown()方法,否则数据将出错!

        await()方法:该方法就相当于发令枪,当判断countDown()将初始值一直减到0 之后,表示所有的线程已经就绪了,就执行await()方法,所有线程就开始同时执行后续操作。

 

未整理完,今晚先到这里。明天继续整理

 

以上就是多线程和线程并发工具总结

欢迎关注博客,后续会持续的进行总结和阶段性的整合总结。希望能够和更多人探讨和指正。

 

更多精彩敬请关注公众号

Java极客思维

微信扫一扫,关注公众号

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值