1.线程和进程的区别?
答: 进程:进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
2.多线程编程的好处?
答: (简单回答)
多线程的概念就和CPU多核一样,好处就是一心多用的意思,比如一个程序可以同时文件读写、网络收发、用户输入等等;多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
(详细点回答)
多线程的好处在于:可以使CPU多个核同时使用,令计算机效率更高效化,以前单核的机器,同时有两个线程在运行时,是先把其中某线程先执行的形式,这样无疑延长了计算所有的时间,多核的情况下,一线程可以交给一个核去处理,另一个线程可以交给另一个核去处理,这个,计算机的资源利用就大大升高.减少用户等待时间.
3.用户线程和守护线程有什么区别?
答:所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了
4.如何创建一个多线程?
答:JDK1.5之前 有两种实现方法,有两种实现方法,
分别是继承Thread类与实现Runnable接口。
分别使用new Thread()和new Thread(runnable)形式,
第一种直接调用thread的run方法,所以,我们往往使用Thread子类,即new SubThread()。第二种调用runnable的run方法。
从JDK1.5开始,出现了以线程池创建多线程的方式:
5.操作共享数据的安全性?
答:如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
其实,线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
6.如何暂停一条线程?
答:两种方式暂停一条线程,一个是采取Thread类的sleep()方法,一个是在同步代码中使用wait()方法.
7.线程的调度和时间分片?
答:java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
线程的调度不是跨平台的,它不仅仅取决于java虚拟机,还依赖于操作系统。在某些操作系统中,只要运行中的线程没有遇到阻塞,就不会放弃CPU;在某些操作系统中,即使线程没有遇到阻塞,也会运行一段时间后放弃CPU,给其它线程运行的机会。
java的线程调度是不分时的,同时启动多个线程后,不能保证各个线程轮流获得均等的CPU时间片。
8.线程之间如何通信?
答:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。
通常情况下,一个次级线程要为主线程完成某种特定类型的任务,这就隐含着表示在主线程和次级线程之间需要建立一个通信的通道。一般情况下,有下面的几种方法实现这种通信任务:使用全局变量、使用事件对象、使用消息。
9.Java中多线程同步是什么?
答:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态
10.为什么需要start与run方法,可以只用run方法来完成任务吗?
答: 启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法是该线程所关联的执行代码。
如果只用run方法仅仅是作为一个对象调用方法执行,并不会开启新的线程,所以不能叫任务.
11.什么是ThreadLocal类,怎么使用它?
答:JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量.
使用:通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
12.Sleep suspend wait 之间有什么区别?
答:sleep就是正在执行的线程主动让出cpu,cpu去执行其他线程,在sleep指定的时间过后,cpu才会回到这个线程上继续往下执行,如果当前线程进入了同步锁,sleep方法并不会释放锁,即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法,调用wait方法的线程就会解除wait状态和程序可以再次得到锁后继续向下运行。
suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend()。
13.在一个对象上两个线程可以调用两个不同的同步实例方法么?
答:不能,因为一个对象已经同步了实例方法,线程获取了对象的对象锁。所以只有执行完该方法释放对象锁后才能执行其它同步方法。
14.什么是死锁?
答:死锁就是两个或两个以上的线程被无限的阻塞,线程之间相互等待所需资源。这种情况可能发生在当两个线程尝试获取其它资源的锁,而每个线程又陷入无限等待其它资源锁的释放,除非一个用户进程被终止。
15.什么是线程饿死,什么是活锁。
答:活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
活锁可以认为是一种特殊的饥饿。 下面这个例子在有的文章里面认为是活锁。实际上这只是一种饥饿。因为没有体现出“活”的特点。 假设事务T2再不断的重复尝试获取锁R,那么这个就是活锁。
饿死 ,与死锁和活锁非常相似。是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。
可以通过下图来解释一下
解释:这是个独木桥(单进程),桥上只能走一个人,B来到时A在桥上,B等待;
而此时比B年龄小的C来了,B让C现行(A走完后系统把进程分给了C),
C上桥后,D又来了,B又让D现行(C走完后系统把进程分个了D)
以此类推B一直是等待状态.
解释:线程A和B都需要过桥(都需要使用进程),而都礼让不走(那到的系统优先级相同,都认为不是自己优先级高),就这么僵持下去.
16.多线程的效率问题,同步机制。
答:如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
17.都用到了哪些线程池。
答:Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
18.synchronized的CPU原语级别是如何实现的?
代码片段
synchronized代码块主要是靠monitorenter和monitorexit这两个原语来实现同步的。当线程进入monitorenter获得执行代码的权利时,其他线程就不能执行里面的代码,直到锁Owner线程执行monitorexit释放锁后,其他线程才可以竞争获取锁。
普通方法
常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。这种方式与语句块没什么本质区别,都是通过竞争monitor的方式实现的。只不过这种方式是隐式的实现方法。
静态方法
常量池中用ACC_STATIC标志了这是一个静态方法,然后用ACC_SYNCHRONIZED标志位提醒线程去竞争monitor。由于静态方法是属于类级别的方法(即不用创建对象就可以被调用),所以这是一个类级别(XXX.class)的锁,即竞争某个类的monitor。
19.monitor介绍
1.每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.这里涉及重入锁,如果一个线程获得了monitor,他可以再获取无数次,进入的时候monito+1,退出-1,直到为0,才可以被其他线程获取。
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
20. volatile关键字CPU原语
总结一下用volatile做同步的场景:
1、对volatile变量的操作,必须是原子性的。或者可以保证volatile变量只能由一个线程来修改,其他线程只是使用此volatile变量。
2、单独由这一个volatile变量控制同步,不能与其他变量一起参与。
1、Lock:只在主内存中操作,将一个变量标识为被一条线程独占的状态。
2、unLock:与Lock对应,释放变量的锁定状态。
3、read:从主内存中读取一个变量传输到线程的工作内存中。
4、load:紧接着read操作之后,在工作内存开辟一个变量副本,装载read回来的变量。
5、use:把工作内存中的变量传递给执行引擎。
6、assign:与use对应,执行引擎向工作内存传输变量值(一般是变量的赋值操作)。
7、store:对应read操作,从工作内存读取一个变量传输到主内存。
8、write:对应load操作,紧接着store之后,在主内存中开辟一段空间,保存store回来的变量。
JVM规定这8个操作必须满足的条件:
1、read/load或是store/write这两组操作,必须成对出现,保证值一定可以正常读取、写入。
2、assign操作后面必须会有store/write这组操作,不允许出现工作内存有变量更新,不刷新回主内存的现象存在。
3、没有assign就不会平白无故地出现store/write这组操作。
4、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量,即有use/store操作出现,前面肯定会有assign/load操作。
5、一个变量在同一个时刻只能被一个线程Lock,Lock操作可以执行多次,想到解锁,就必须unLock相同的次数。
6、线程对一个变量进行Lock操作,将会清掉线程此变量在工作内存中的值,想用这个变量,就要重新load->assign操作。
7、没Lock的变量是不能执行unLock的。
8、unLock执行前,必须先执行store->write将此变量刷新到主内存中。
21.禁止指令重排序
变量加了volatile修饰后,汇编代码里会多出一个"lock addl $0x0,(%esp)"操作,这种操作相当于一个内存屏障(Memory Barrier或Memory Fence,指重排序时不能据后面的指令重排序到内存屏障之前的位置),这样就可以保证volatile修饰的变量,是先赋值,再使用。
如果没有volatile修饰,指令重排序操作(属于硬件架构CPU优化的范畴,简单来说就是CPU在保证正确处理指令依赖、执行结果正确的前提下,会对一些指令进行顺序的优化,这样指令和实际的代码顺序就会有一些差别)会影响线程之间共享变量的使用。
22. 什么是上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
如有披露或问题欢迎留言或者入群探讨