文章目录
1.进程线程的由来
最初的计算机只能接收一些特定的指令,用户输入一个指令以后,计算机才操作。然后出现了批处理操作系统,把一系列的操作指令写在一个清单上,一次性交给计算机,但是由于批处理操作系统的指令运⾏⽅式仍然是串⾏的,内存中始终只有⼀个程序在运⾏,后⾯的程序需要等待 前⾯的程序执⾏完成后才能开始执⾏,⽽前⾯的程序有时会由于I/O操作、⽹络等 原因阻塞,所以批处理操作效率也不⾼。
1.1 进程的由来
进程就是应⽤程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不⼲扰。同时进程保存着程序每⼀个时刻运⾏的状态。
运行机制:
CPU采⽤时间⽚轮转的⽅式运⾏进程:CPU为每个进程分配⼀个时间段,称作它的时间⽚。如果在时间⽚结束时进程还在运⾏,则暂停这个进程的运⾏,并且 CPU分配给另⼀个进程(这个过程叫做上下文切换)。如果进程在时间⽚结束前阻塞或结束,则CPU⽴即进⾏切换,不⽤等待时间⽚⽤完。使用方式,进程让操作系统的并发成为了可能。
上下文切换:(有时也称做进程切换或任务切换)是指 CPU 从⼀个进程(或线程) 切换到另⼀个进程(或线程)。上下⽂是指某⼀时间点 CPU 寄存器和程序计数器的内容。但是在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,所以任务从保存到加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的,意味着此操作系统会消耗大量的CPU时间,故线程也不是越多越好。
1.2线程的由来
让⼀个线程执⾏⼀个⼦任务,这样⼀个进程就包含了多个线程,每个线程负责⼀个单独的⼦任务。线程让进程的内部并发成为了可能。
总之,上面的目的都是提高操作系统的效率。
1.3进程与线程的区别(可以参考富士康厂房和流水线)
流水线:富士康按照厂房(进程)分配所需要的原料,每个厂房有几条生产线(线程),每个生产线上有很多工人(协程)。
进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即,CPU分配时间的单位。
进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):
。进程有单独的内存地址空间,所以程序间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
。进程单独占有⼀定的内存地址空间,⼀个进程出现问题不会影响其他进程,不 影响主程序的稳定性,可靠性⾼;⼀个线程崩溃可能影响整个程序的稳定性, 可靠性较低。
。进程单独占有⼀定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及⻚调度,开销较⼤;线程只需要保存寄存 器和栈信息,开销较⼩。
2.Java多线程入门类和接口
2.1Thread类和Runnable接口
如何使用多线程?
首先,我们需要有一个“线程”类。JDK提供了Thread类和Runnalble接口来让我们实现自己的“线程类”。
。继承Thread类,并重写run方法;
。实现Runable接口的run方法;
2.1.1继承Thread类
首先是继承Thread类:
public class Demo1 {
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread");
}
}
/*
1.我们在程序里面调用start()方法后,虚拟机会为我们创建一个线程,然后等到
这个线程第一次得到时间片时再调用run()方法。
2.注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()
方法会抛出异常。
*/
public static void main(String[] args) {
Thread myThread=new MyThread();
myThread.start();
}
}
注意:要调用start()方法后,该线程才算启动!
Thread几个常用的方法:
currentThread():静态方法,返回对当前正在执行的线程对象的引用。
start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
yield():yield放弃的意思,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续执行这个线程的;
sleep():静态方法,使当前线程睡眠一段时间;
join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的使Object类的wait方法实现的;
2.1.2Runnable接口
Runnable函数式接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
示例代码:
public class Demo2 {
public static class MyThread implements Runnable{
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
//创建自定义类对象,线程任务对象
MyThread mr= new MyThread();
//创建线程对象
Thread t=new Thread(mr);
t.start();
//Java 8函数式编程,可以省略MyThread类
new Thread(()->{
System.out.println("Java 8 匿名内部类");
}).start();
}
}
2.2Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runnable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
(1)适合多个相同的程序代码的线程去共享一个资源。
(2)可以避免java中单继承的局限性。
(3)增加线程的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
(4)线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
注意:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。
2.3匿名内部类方式实现线程的创建
使用线程的匿名内部类方法,可以方便的实现每个线程执行不同的线程任务操作。
使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法;
public class Demo3
{
public static void main(String[] args) {
Runnable r=new Runnable() {
@Override
public void run() {
System.out.println("你好");
}
};
new Thread(r).start();
}
}
3.线程组和线程优先级
3.1线程组(ThreadGroup)
Java中⽤ThreadGroup来表示线程组,我们可以使⽤线程组对线程进⾏批量控制。
ThreadGroup和Thread的关系就如同他们的字⾯意思⼀样简单粗暴,每个Thread必然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。
执⾏main() ⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程 (当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组.
示例代码:
public class Demo4 {
public static void main(String[] args) {
Thread testThread = new Thread(()->{
System.out.println("testThread当前线程组名字:"+Thread.currentThread().getThreadGroup().getName());
System.out.println("testThread线程名字:"+Thread.currentThread().getName());
});
testThread.start();
System.out.println("执行main方法线程名字:"+Thread.currentThread().getName());
}
}
输出结果:
执行main方法线程名字:main
testThread当前线程组名字:main
testThread线程名字:Thread-0
3.2 线程优先级
Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10 级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
Java默认的线程优先级为5,线程的执⾏顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。通常情况下,高优先级的线程将会比低优先级的线程有更高的机率得到执行。线程的优先级在创建线程时可以设置,也可以使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级,getPriority()方法获取线程的优先级。
Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统⼀个建议,操作系统不⼀定会采纳。而真正的调⽤顺序,是由操作系统的线程调度算法决定的。
如果某个线程优先级大于线程所在线程组的最⼤优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。
4.Java线程状态及主要转化方式
4.1操作系统中的线程状态转换
在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态保持一致的。
主要的三个状态:
(1)就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
(2)执行状态(running):线程正在使用CPU。
(3)等待状态(waiting):线程经过等待事件的调用或者正在等待其他资源(I/O).
4.2 线程生命周期经理的5种状态
(1)新建状态(New):线程创建后处于该状态;
(2)可运行状态(Runnable):新建的线程调用start( )方法,将使线程的状态从New转换为Runnable;
(3)运行状态(Running):运行状态使线程占有CPU并实际运行的状态;
(4)阻塞状态(Blocked):导致该状态的原因很多,注意区别;
(5)终止状态(Dead):线程执行结束的状态,没有任何方法可改变它的状态。
5. Java线程的通信
5.1 锁与同步
在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。线程和锁的关系,我们可以用婚姻关系来理解。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果和一个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得到这个线程和这个锁“离婚”(释放)。
5.1.1 线程同步
什么是同步呢,假如我们现在有2位正在 抄暑假作业答案的同学:线程A和线程B。当他们正在抄的时候,⽼师突然来修改了⼀些答案,可能A和B最后写出的暑假作业就不⼀样。我们为了让A,B能写出2本相 同的暑假作业,我们就需要让⽼师先修改答案,然后A,B同学再抄。或者A,B同 学先抄完,⽼师再修改答案。这就是线程A,线程B的线程同步。
可以以解释为:线程同步是线程之间按照⼀定的顺序执行。
5.1.2 进程同步
进程同步是指进程之间一种直接的协同工作关系,这些进程相互合作,共同完成一项任务。进程间的直接相互作用构成进程的同步。
为了达到线程同步,我们可以使用锁来实现它。
无锁案例:
public class NoneLock {
static class ThreadA implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("Thread A"+i);
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("Thread B"+i);
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
}
每次运行结果会不同。
加锁案例:
public class ObjectLock {
private static Object lock = new Object();
static class ThreadA implements Runnable{
@Override
public void run() {
synchronized (lock){
for(int i=0; i<100; i++){
System.out.println("ThreadA"+i);
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i=0; i<100; i++){
System.out.println("Thread B"+i);
}
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
Thread.sleep(10);
new Thread(new ThreadB()).start();
}
}
每次输出的结果都是顺序的。
这里声明了一个名字为lock的对象锁。我们ThreadA和ThreadB内需要同步的代码块里,都是synchronized关键字加上了同一个对象锁lock。
根据线程和锁的关系,同⼀时间只有⼀个线程持有⼀个锁,那么 线程B就会等线程A执⾏完成后释放 lock ,线程B才能获得锁 lock 。
5.2等待/通知机制
上面一种基于“锁”的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务资源。
而等待/通知机制是另一种方式。
Java多线程的等待/通知机制是基于Object类的wait()方法和notify(),notifyAll()方法来实现的.
notify()⽅法会随机叫醒⼀个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。
前⾯我们讲到,⼀个锁同⼀时刻只能被⼀个线程持有。⽽假如线程A现在持有了⼀ 个锁 lock 并开始执⾏,它可以使⽤ lock.wait() 让⾃⼰进⼊等待状态。这个时 候,lock 这个锁是被释放了的。
这时,线程B获得了 lock 这个锁并开始执⾏,它可以在某⼀时刻,使⽤ lock.notify() ,通知之前持有 lock 锁并进⼊等待状态的线程A,说“线程A你不⽤等了,可以往下执⾏了”。
示例代码:
public class WaitAndNotify {
private static Object lock=new Object();
static class ThreadA implements Runnable{
@Override
public void run() {
synchronized(lock){
for(int i=0;i<100;i++){
try {
System.out.println("ThreadA"+i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i=0;i<100;i++){
try {
System.out.println("ThreadB"+i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
运行结果:
ThreadA0
ThreadB0
ThreadA1
ThreadB1
ThreadA2
ThreadB2
ThreadA3
在这个Demo⾥,线程A和线程B⾸先打印出⾃⼰需要的东⻄,然后使⽤ notify() ⽅法叫醒另⼀个正在等待的线程,然后⾃⼰使⽤ wait() ⽅法陷⼊等待 并释放 lock 锁。
注意:
需要注意的是等待/通知机制使⽤的是同⼀个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能⽤等待/通知机制通信的。
5.3信号量
JDK提供了⼀个类似于“信号量”功能的类 Semaphore 。但这里不是要介绍这个类,而是介绍⼀种基于 volatile 关键字的⾃⼰实现的信号量通信。
volitile关键字能够保证内存的可⻅性,如果⽤volitile关键字声明了⼀个变 量,在⼀个线程⾥⾯改变了这个变量的值,那其它线程是⽴⻢可⻅更改后的值的。
如何让A与B交替输出数据呢?
示例代码:
public class Signal {
private static volatile int signal=0;
static class ThreadA implements Runnable{
@Override
public void run() {
while (signal<5){
if(signal%2==0){
System.out.println("ThreadA:"+signal);
synchronized (this){
signal++;
}
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
while (signal<5){
if(signal%2==1){
System.out.println("ThreadB:"+signal);
synchronized (this){
signal++;
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
输入结果:
ThreadA:0
ThreadB:1
ThreadA:2
ThreadB:3
ThreadA:4
我们可以看到,使⽤了⼀个 volatile 变量 signal 来实现了“信号量”的模型。这⾥需要要注意的是,volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操作,所以我们需要使⽤ synchronized 给它“上锁”。
原子操作:就是无法被别的线程打断的操作,要么不执行,要么执行成功。
5.4 管道
管道是基于“管道流”的通信⽅式。JDK提供了 PipedWriter 、PipedReader 、PipedOutputStream、PipedInputStream 。其中,前⾯两个是基于字符的,后⾯两个是基于字节流的。
Java管道:Java/IO系统是建立再数据流概念之上的,它具有将一个程序的输出当作另一个程序的输入。
5.5 join方法
join()⽅法是Thread类的⼀个实例⽅法。它的作⽤是让当前线程陷⼊“等待”状态,等 join的这个线程执⾏完成后,再继续执⾏当前线程。
有时候,主线程创建并启动了⼦线程,如果⼦线程中需要进⾏⼤量的耗时运算,主线程往往将早于⼦线程结束之前结束。
如果主线程想等待⼦线程执⾏完毕后,获得⼦线程中的处理完的某个数据,就要⽤到join⽅法了。
示例代码:
public class Join {
static class ThreadA implements Runnable{
@Override
public void run() {
try {
System.out.println("我是子线程,我先睡一秒");
Thread.sleep(1000);
System.out.println("我是子线程,我睡完了一秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(new ThreadA());
thread.start();
thread.join();
System.out.println("如果不加join⽅法,我会先被打出来,加了就不⼀样了");
}
}
5.6 sleep方法
sleep⽅法是Thread类的⼀个静态⽅法。它的作⽤是让当前线程睡眠⼀段时间。它有这样两个⽅法:
这⾥需要强调⼀下:sleep⽅法是不会释放当前的锁的而wait方法会。这也是最常见的⼀个多线程⾯试题。
sleep跟wait方法的区别:
(1)wait可以指定时间,也可以不指定;而sleep必须指定时间。
(2)wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。
(3)wait必须放在同步块或同步方法中,而sleep可以任意位置。
在Java程序中怎么保证多线程的运行安全?
- 方法一:使用安全类,比如 Java. util. concurrent 下的类。
- 方法二:使用自动锁 synchronized。
- 方法三:使用手动锁 Lock。
手动锁Java示例代码如下:
Lock lock = new ReentrantLock();
lock. lock();
try {
System. out. println("获得锁");
} catch (Exception e) {
// TODO: handle exception
} finally {
System. out. println("释放锁");
lock. unlock();
}
多线程中synchronized锁升级的原理是什么?
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
说一下synchronized底层实现原理?
synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
创建线程池的几种方式?
线程池创建有七种方式,最核心的是最后一种:
- newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
- newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
- newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
- newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
- newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
- newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
- ThreadPoolExecutor():是最原始的线程池创建,上面1-3创建方式都是对ThreadPoolExecutor的封装。
线程池都有那些状态
- RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
- SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
- STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
- TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
- TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
深入理解select,poll 和 epoll及区别
某教室有10名学生和1名老师,这些学生上课会不停的提问,所以一个老师处理不了这么多的问题。那么学校为每个学生都配一名老师,也就是这个教室目前有10名老师。此后,只要有新的转校生,那么就会为这个学生专门分配一个老师,因为转校生也喜欢提问题。如果把以上例子中的学生比作客户端,那么老师就是负责进行数据交换的服务端。则该例子可以比作是多进程的方式。
后来有一天,来了一位具有超能力的老师,这位老师回答问题非常迅速,并且可以应对所有的问题。而这位老师采用的方式是学生提问前必须先举手,确认举手学生后在回答问题。则现在的情况就是IO复用。
目前的常用的IO复用模型有三种:select,poll,epoll。
- select==>时间复杂度O(n)
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。 - poll==>时间复杂度O(n)
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的. - epoll==>时间复杂度O(1)
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,一般操作系统均有实现