简述线程,程序、进程的基本概念
线程:与进程相似,但线程是比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
程序:是含有指令和数据文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态代码。
进程:是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,
因为同一进程中的线程极有可能会相互影响。
线程有哪些基本状态
Java 线程在运行的生命周期中可能处于下面 6 种不同状态
新建(NEW)可运行(RUNNABLE)阻塞(BLOCKED)等待(WAITING)超时等待(TIMED_WAITING)终止(TERMINATED)
可运行状态是个复合状态,又可以分为READY:就绪状态。RUNNING:表示该线程正在执行。
如何理解内存泄漏问题?有哪些情况会导致内存泄露?如何解决
内存泄漏:对于应用程序来讲,当对象不再被使用的时候,但是java的垃圾回收不能回收他们,就产生了内存泄漏。
上图中包含了未引用对象和引用对象。未引用对象将会被垃圾回收器回收,而引用对象却不会。未引用 对象很显然是无用的对象。然而,无用的对象并不都是未引用对象,有一些无用对象也有可能是引用对象,这部分对象正是内存泄露的来源。
比如说:对象A引用了对象B,A的生命周期比B的生命周期长,当对象B执行完,垃圾回收器是不会回收对象B的,这就可能造成内存不足的问题。因为A还引用着B,还可能引用其他比A生命周期短的对象。
怎样阻止内存泄露
1.使用List、Map等集合时,在使用完成后赋值为null
2.使用大对象时,在用完后赋值为null
3.目前已知的jdk1.6的substring()方法会导致内存泄露
4.避免一些死循环等重复创建或对集合添加元素,撑爆内存
5.简洁数据结构、少用静态集合等
6.及时的关闭打开的文件,socket句柄等
7.多关注事件监听(listeners)和回调(callbacks),比如注册了一个listener,当它不再被使用的时候,忘了注销该listener,可能就会产生内存泄露。
线程池的原理,为什么要创建线程池?创建线程池的方式;
线程池的优点:
1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
线程池的实现原理:
线程池的创建:
/**
* corePoolSize:线程池核心线程数量
* maximumPoolSize:线程池最大线程数量
* keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
* unit:存活时间的单位
* workQueue:存放任务的队列
* handler:超出线程范围和队列容量的任务的处理程序
* @return
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
线程池的源码解读
1、ThreadPoolExecutor的execute()方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//如果线程数大于等于基本线程数或者线程创建失败,将任务加入队列
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)){
//线程池处于运行状态并且加入队列成功
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//线程池不处于运行状态或者加入队列失败,则创建线程(创建的是非核心线程)
else if (!addIfUnderMaximumPoolSize(command)){
//创建线程失败,则采取阻塞处理的方式
reject(command); // is shutdown or saturated
}
}
}
2、创建线程的方法:addIfUnderCorePoolSize(command)
private boolean addIfUnderCorePoolSize(Runnable firstTask) {
Thread t = null;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (poolSize < corePoolSize && runState == RUNNING)
t = addThread(firstTask);
}finally {
mainLock.unlock();
}
if (t == null)
return false;
t.start();
return true;
}
我们重点来看第7行,这里将线程封装成工作线程worker,并放入工作线程组里:
private Thread addThread(Runnable firstTask) {
Worker w = new Worker(firstTask);
Thread t = threadFactory.newThread(w);
if (t != null) {
w.thread = t;
workers.add(w);
int nt = ++poolSize;
if (nt > largestPoolSize)
largestPoolSize = nt;
}
return t;
}
worker类的方法run方法,worker在执行完任务后,还会通过getTask方法循环获取工作队里里的任务来执行:
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTask()) != null) {
runTask(task);
task = null;
}
} finally {
workerDone(this);
}
}
由于线程池的饱和策略内容过多,我将重新记录一篇来讲Java提供的4中策略:
1、AbortPolicy:直接抛出异常
2、CallerRunsPolicy:只用调用所在的线程运行任务
3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy:不处理,丢弃掉。
线程池创建的几种方式
线程池的创建方式都对应了不同的使用场景,总体来说线程池的创建可以分为以下两类:
- 通过 ThreadPoolExecutor 手动创建线程池。
- 通过 Executors 执行器自动创建线程池。
而以上两类创建线程池的方式,又有 7 种具体实现方法,这 7 种实现方法分别是:
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序。
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池。
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池。
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
ThreadPoolExecutor:手动创建线程池的方式,它创建时最多可以设置 7 个参数。
什么时候会出现僵死进程;
僵死进程是指子进程退出时,父进程并未对其发出的SIGCHLD信号进行适当处理,导致子 进程停留在僵 死状态等待其父进程为其收尸,这个状态下的子进程就是僵死进程。
说说线程安全问题,什么是线程安全,如何实现线程安全;
线程安全 - 如果线程执行过程中不会产生共享资源的冲突,则线程安全。
线程不安全 - 如果有多个线程同时在操作主内存中的变量,则线程不安全
实现线程安全的三种方式:
1)互斥同步
临界区:syncronized、ReentrantLock
信号量 semaphore
互斥量 mutex
2)非阻塞同步
CAS(Compare And Swap)
3)无同步方案
可重入代码
使用Threadlocal 类来包装共享变量,做到每个线程有自己的copy
线程本地存储
java的内存模型来了解java的多线程安全机制
我们知道java的内存模型中有主内存和线程的工作内存之分,主内存上存放的是线程共享的变量(实例字段,静态字段和构成数组的元素),线程的工作内存是线程私有的空间,存放的是线程私有的变量(方法参数与局部变量)。线程在工作的时候如果要操作主内存上的共享变量,为了获得更好的执行性能,并不是直接去修改主内存,而是将数据拷贝到线程的工作内存中,在对变量进行修改之后再把修改的值刷回主内存的变量中。
如果只有一个线程当然不会有什么问题,但是如果有多个线程同时在操作主内存中的变量,因为8种操作 的非连续性和线程抢占cpu执行的机制就会带来冲突的问题,也就是多线程的安全问题。线程安全的定 义就是:如果线程执行过程中不会产生共享资源的冲突就是线程安全的。
Java里面一般用以下几种机制保证线程安全:
1.互斥同步锁(悲观锁)
1)Synchorized
2)ReentrantLock(我会写一章ReentrantLock详解)
互斥同步锁也叫做阻塞同步锁,特征是会对没有获取锁的线程进行阻塞。Java里面的互斥同步锁就是Synchorized和ReentrantLock,前者是由语言级别实现的互斥同步锁,理解 和写法简单但是机制笨拙,在JDK6之后性能优化大幅提升,即使在竞争激烈的情况下也能保持一个和 ReentrantLock相差不多的性能,所以JDK6之后的程序选择不应该再因为性能问题而放弃synchorized。 ReentrantLock是API层面的互斥同步锁,需要程序自己打开并在finally中关闭锁,和synchorized相比 更加的灵活,体现在三个方面:等待可中断,公平锁以及绑定多个条件。但是如果程序猿对 ReentrantLock理解不够深刻,或者忘记释放lock,那么不仅不会提升性能反而会带来额外的问题。另 外synchorized是JVM实现的,可以通过监控工具来监控锁的状态,遇到异常JVM会自动释放掉锁。而 ReentrantLock必须由程序主动的释放锁。
2.非阻塞同步锁
- 原子类(CAS)
3.无同步方案
1)可重入代码
在执行的任何时刻都可以中断-重入执行而不会产生冲突。特点就是不会依赖堆上的共享资源
2)ThreadLocal/Volaitile
线程本地的变量,每个线程获取一份共享变量的拷贝,单独进行处理。 - 线程本地存储
如果一个共享资源一定要被多线程共享,可以尽量让一个线程完成所有的处理操作,比如生产者消费者 模式中,一般会让一个消费者完成对队列上资源的消费。典型的应用是基于请求-应答模式的web服务器的设计