java运行时存储空间

Java运行时空间可以非位堆空间,栈空间和非堆空间**。其中堆空间和非堆空间时被多个线程共享的**,而栈空间则是线程的私有空间。每个线程都有自己私有的栈空间。
栈空间时为线程的执行而准备的一段固定大小的内存空间,栈空间时线程创建的时候非配的。线程的方法调用是利用栈帧。局部变量的变量之存储在栈空间中,基础类型变量和引用的类型的变量的变量值都是直接存储在栈帧中的。
由于一个线程无法访问另一个线程的栈空间,所以线程堆局部变量以及对只能通过当前线程的局部变量才能访问的对象进行操作具有固有的线程安全性
堆空间用于存储对象,它也时垃圾回收器工作的场所,即堆空间中没有可达引用的对象会被回收。
非堆空间用于存储常量以及类的元数据等,它是Java虚拟机系统的时候分配的一段可以动态扩容的内存空间。类的元数据包括类的静态变量、类有哪些方法以及这些方法的元数据(包括名称,参数和返回值等)

无状态对象

对象是对操作和数据的封装,对象所包含的数据就被称为该对象的状态。如果一个类的同一个实例被多个线程共享并不会使这些线程存在共享状态,那么这个类及其任意一个实例就被称为无状态对象。

线程安全的本质来源于多个线程之间存在共享数据,因此无状态对象使线程安全的。

无状态对象具有线程安全性,这有两层含义:

  1. 无状态对象的客户端代码在调用任何该对象的任何方法时都无须进行加锁。
  2. 其次无状态对象自身的方法实现也无需使用锁。

无状态对象不包含任何实例变量或者可更新静态变量,但是一个不包含任何实例变量或者静态变量却不一定是无状态对象。特殊情况下,不包含任何实例变量或者静态变量的类,其方法实现时仍然需要借助锁来保障线程安全。

不可变对象

不可变对象是指一经创建其就会保持不变的对象。不可变对象也具有固有的线程安全性。

严格意义的不可变对象需要满足的条件:

  1. 类本身使用final修饰:防止通过创建子类来改变其定义的行为
  2. 所有字段都需要使用final修饰:确保字段值保持不变,更重要的是在多线程环境下保证了被修饰字段的初始化安全。
  3. 对象在此初始化过程中没有逸出
  4. 任何字段如果引用了其它状态可变的对象(如集合,数组)则这些字段必须是private修饰的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//一个典型的不可变对象
public final class Canditiion implements Iterable<Endpoint>{

//引用了其它状态可变的对象,需要使用private修饰
private final Set<Endpoint> endpoints;
private final int totalWeight;

public Canditiion(Set<Endpoint> endpoints) {
int sum=0;
for(Endpoint endpoint:endpoints){
sum+=endpoint.weight;
}
this.totalWeight=sum;
this.endpoints=endpoints;
}

@Override
public final Iterator<Endpoint> iterator() {
return ReadOnlyIterator.with(endpoints.interator);
}
}

虽然不可变对象自身的实例变量或者静态变量的值是不可变的,但是这些变量所引用的对象本身的状态可能是可变的。如果这些对象所包含的状态对外暴露的话,那么我们需要注意这些对象状态也不能被改变。

不可变对象的应用场景

  1. 被建模对象的状态变化不频繁
  2. 同时对一组相关的数据进行写操作,因此需要保证原子性。
  3. 使用不可变对象作为安全可靠恶的Map键。

线程特有对象

各个线程都创建一个自有的实例,一个实例只能被一个线程访问的对象就被称为线程特有对象。ThreadLocal类相当于线程访问器线程特有对象的代理。线程通过这个对象可以创建并访问各自的线程特有对象。
nrHVBT.png

ThreadLocal类的常用方法
nrOcyn.png

ThreadLocal实例通常会被作为某个类的静态字段使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//一个随机数生成器
//使用ThreadLocal避免锁的争用
public enum ThreadSpecilficSecureRandom{
INSTANCE;
final static ThreadLocal<SecureRandom> SECURE_RANDOM
=new ThreadLocal<SecureRandom>(){
@Override
protected SecureRandom initialValue(){
SecureRandom srnd;
try{
srnd=SecureRandom.getInstance("SHA1PRNG");
}catch (NoSuchAlgorithmException e){
srnd=new SecureRandom();
e.printStackTrace();
}
srnd.nextBytes(new byte[20]);
return srnd;

}
};

public int nexeInt(int upperBound){
SecureRandom random = SECURE_RANDOM.get();
return random.nextInt(upperBound);
}

public void setSeed(long seed){
SecureRandom random = SECURE_RANDOM.get();
random.setSeed(seed);
}

}

线程特有对象的典型应用场景

  1. 需要使用非线程安全的对象,但不希望因此而引入锁。
  2. 使用线程安全对象,但希望避免其它使用锁的开销和相关问题。
  3. 隐式参数传递。线程特有对象在一个具体的线程中,他是线程全局可见的。
  4. 特定于线程的单例模式。

装饰器模式

装饰器模式可以用来实现线程安全,其基本思想式为非线程安全对象创建一个相应的线程安全的外包装对象。客户端代码不直接访问非线程安全对象而实访问器外包装对象。

java.util.Conllections.synchroniezdX(X可以是set,list,map等)方法就是使用装饰器模式将指定的非线程安全的集合对象对外暴露为线程安全的对象。
使用装饰器模式装饰的线程安全的对象也要注意。他们返回的同步集合存在弊端,这些集合返回的Interator实例并不是线程安全的。所以我们需要给遍历操作加锁

1
2
3
4
5
6
7
8
9
10
11
//正确的遍历同步集合
final List<String> synchronizedList = Collections.synchronizedList(new ArrayList<String>());
public void dump(){
Iterator<String> iterator = synchronizedList.iterator();
//必须正确的加锁
synchronized(synchronizedList){
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}

对同步集合进行遍历操作时,我们需要以被遍历同步集合对象本身作为内部锁。这样做的实质是利用了内部锁的排他性,从而阻止了遍历过程中其它线程改变了同步集合的内部结构。这种不利用提高并发。

并发集合

java 1.5以后java.util.concurrent包中引入了一些线程安全的集合对象,他们被称为并发集合。
nsZjWd.png
并发集合对象自身就支持对其进行线程安全的遍历操作。
如果由多个线程需要对同一并发集合进行遍历操作,那么这些线程不适合共享同一个Iterator实例
并发集合内部再保障其线程安全的时候通常不借助锁,而是使用CAS操作,或者对锁的使用进行了优化。

死锁

如果两个或多个线程因相互等待对方而被永远暂停,那么我们就称这些线程产生了死锁。由于产生死锁的线程的生命周期状态永远是非运行状态,因此这些线程所要执行的任务也永远无法进展

n6FwkQ.png

死锁产生的条件与规避

可能产生死锁的必要非充分条件:

  1. 资源互斥。涉及到的资源必须是独占的,即每个资源一次只能够杯一个线程使用。
  2. 资源不可抢夺。涉及的资源只能够杯其持有者主动释放,而无法杯资源的持有者和申请者之外的第三方线程所抢夺(被动释放)
  3. 占用并等待资源。涉及到的线程当前至少持有一个资源,并申请其它资源,如果需要等待其它线程释放资源,在等待过程中,线程不会释放其已经持有的资源。
  4. 循环等待资源。涉及到的形成必须在等待别的线程持有的资源,而这些线程又反过来在等待第一个线程所持有的资源。

死锁的代码特征:
n6VAAO.png

**一个线程在已经持有一个锁的情况下再次申请这个锁,并不会导致死锁。**这是因为Java中的锁(包括内部锁和显式锁)都是可重入的。

规避死锁的方法

规避死锁只能从两个方面下手:消除“占用并等待资源”和消除“循环等待资源”。

几种方法:

  1. 粗锁法。使用粗粒度的锁代替多个锁。从而消除“占用并等待资源”
  2. 锁排序法。相关线程使用全局同一的顺序申请锁。假设又多个线程需要申请资源(锁),呢吗我们只徐娅让这些线程一张一个全局统一的顺序区申请这些资源,就可以消除“循环等待资源”这个条件,从而规避死锁。
  3. 使用ReentrantLock.tryLock(long,TimeUnit)申请锁。它允许我们位锁的申请这个操作设定一个超时时间。可以避免线程无限制的等待,从而消除“占用并等待资源”
  4. 使用开放调用,在调用外部方法时不加锁
  5. 使用锁的替代品

锁死

等待线程由于唤醒其所需的条件永远无法成立,或者其它线程无法唤醒这个线程而一直处于非运行状态(线程并未终止)导致其任务一直无法进展。

信号丢失锁死

信号丢失锁死是由于没有相应的通知线程来唤醒等待线程而使得等待线程一直处于等待状态的一种活性故障。

信号丢失的典型例子:
等待线程在执行Object.wait()/Condition.await()前没有对保护条件进行判断,而此时保护条件时间上已经成立,然而此后可能并无其它线程更新相应保护条件涉及的共享遍历使其成立并通知等待线程。

嵌套监视器锁死

嵌套监视器锁死是嵌套锁导致等待线程永远无法被唤醒的一种活性故障,

n6IIiQ.png

等待线程在执行到monitorY.wait()的时候会被暂停并且其所持有的所monitorY会被释放,但是等待线程持有的外层锁monitorX并不会被释放掉。通知线程在唤醒等待线程后,monitorY所引导的临界区位于monitorX引导的临界区之内,因此通知线程必须向持有外层锁monitorX,而这个时候monitorX被其它线程持有,这个时候就通知线程就无法唤醒受保护的方法,这样就形成了锁实。

线程饥饿

线程饥饿是指线程一直无法获得其所需要的资源而导致其任务一直无法进展的一种活性故障。

把锁看作一种资源,那么我们不难发现死锁也是一种线程饥饿。失锁的结果是故障线程都无法获得其所需要的全部锁中的一个锁,导致使其任务一直无法进展。

线程饥饿的一个典型例子是在高争用的环境下使用非公平模式的读写锁。

活锁

活锁是指线程一直处于运行状态,但是其任务却一直无法进展的一种活性故障。也就是说,产生活锁的线程一直在做无用功。

线程一直在申请其所需的资源而一直未申请成功,那么此时线程饥饿实际上就演变成为了活锁。

线程组

线程组(ThreadGroup类)可以用来表示一组相似的线程。
Thread类有几个构造器允许我们在创建线程的时候指定线程所属的线程组,如果没有指定线程组,那么这个线程就属于其父线程所属的线程组。由于java虚拟机在创建main线程的时候会为其指定一个线程组,因此java平台中的任何一个线程都有一个线程组与之关联。
线程组的设计有许多缺陷,现在已经用得很少

线程的未捕获异常与监控

如果线程的run方法抛出未被捕获的异常,那么随着run方法的退出,相应的线程也提前终止。对于线程的这种异常终止,我们可以得知并做出补救。
Thread类内部定义了一个UncaughtExceptionHandler接口。该接口是在Thread类内部定义的。它只有一个方法:
void uncaughtException(Thread t,Throwable e)
该方法包含两个参数,一个是异常终止的线程本身,以及导致线程提前终止的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ThreadMonitorDemo {
volatile boolean inited=false;
static int threadIndex=0;
final BlockingQueue<String> channel=new ArrayBlockingQueue<String>(100);


public static void main(String[] args) throws InterruptedException {
ThreadMonitorDemo threadMonitor = new ThreadMonitorDemo();
threadMonitor.init();
for(int i=0;i<100;i++){
threadMonitor.service("message"+i);
}
Thread.sleep(5000);
System.exit(0);

}


public synchronized void init(){
if(inited){
return ;
}
System.out.println("开始启动");
WokerThread thread=new WokerThread();
thread.setName("woker-"+threadIndex++);
thread.setUncaughtExceptionHandler(new ThreadMonitor());
thread.start();
inited=true;

}

public void service(String message) throws InterruptedException{
channel.put(message);
}


class ThreadMonitor implements Thread.UncaughtExceptionHandler{

@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName()+"异常终止");
System.out.println("启动替代线程");
inited=false; //重置启动标志
init();
}
}

class WokerThread extends Thread{
@Override
public void run(){
System.out.println("工作线程开始工作");
String msg;
try{
for(;;){
msg=channel.take();
process(msg);

}
}catch (InterruptedException e){

}
}

private void process(String message){
System.out.println("处理message:"+message);
if((int)(Math.random()*100)<2){
throw new RuntimeException("test");
}
Utils.randomPauser(100);
}
}

}

线程工厂

从jdk1.5开始Java标准库提供了创建线程的工厂方法。ThreadFactory接口时工厂方法的模式的一个实例,它定义了如下工厂方法:
public Thread newThread(Runnable r)
我们可以在ThreadFactory.newThread方法中封装线程创建的逻辑,这使得我们能够以统一的方式未线程的创建、配置做一些非常有用的动作。

线程池

线程是一种昂贵的资源,其开销包括以下几个方面:

  1. 线程的创建和启动的开销
  2. 线程的销毁开销
  3. 线程的调度开销
  4. 一个系统能够创建的线程总是受限于该系统所拥有的处理器数目

线程池的实现和一些对象池的实现不同,线程池内部可以预先创建一定数量的工作者线程,客户端代码并不需要向线程池借用线程而是将其需要执行的任务作为一个对象提交给线程池,线程池可能将这些任务缓冲在队列之中,而线程池内部的各个工作者向则不断地从队列中取出任务并执行。

nglvdJ.png

java.util.concurrent.ThreadPpplExecutor类就是一个线程池,客户端代码可以调用
public Future<?> submint(Runnable task)

ThreadPoolexecutor中地参数数量最多地一个构造器

1
2
3
4
5
6
7
ThreadPoolExecutor(int corePoolSize, 
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

其中workQueue是被称为工作队列地阻塞队列,corePoolSize用于指定线程池核心大小,maximumPoolSize用于指定最大线程池大小。keepAliveTime和unit何在一起用于指定线程池中空闲线程地最大存活时间。threadFactory指定用于创建工作者线程地的线程工厂。

在初始状态下,客户端每提交一个任务线程池就创建一个工作线程来处理任务,随着客户端不断地提交任务,当前线程池地大小也相应地增加。在当前线程池大小达到核心线程池大小地时候,新来地任务会被存入工作队列之中。
当工作队列满地时候,线程池会继续传教新地工作者线程,直到当前线程池带线啊哦达到最大线程池大小。
当线程池饱和时,即工作者队列满并且当前线程池大小达到最大线程池地大小地情况下,客户端试图提交地任务会被拒绝。Java标准库中引入了一个RejectedExecutionHandler接口用于封装被拒绝地任务地处理策略。接口地定义如下:
void rejectedExecution(Runnable r,ThreadpoolExecutor executor)
ng8xoQ.png

在当前线程池大小超过线程池核心大小地时候,超过线程池核心大小部分地工作者线程空闲时间达到keepAliveTime所指定地时间后就会被清理掉。

线程池的启动和关闭

ThreadPoolExecutor.prestartAllCoreThreads()则使得我们可以使线程池在未接受到任何任务的情况下预先创建并启动所有核心线程,这样可以减少任务被线程池处理时所需要的等待时间。

调用ThreadPoolExecutor.shutdown()/shutdownNow()方法可能用来关闭线程池。使用shutdown()关闭线程池的时候,已提交的任务会被继续执行,而新提交的任务会被拒绝。调用返回时线程池可能尚未关闭,因为其中还有任务没结束。

可以通过调用ThreadPoolExecutor.awaitTermination(long timeout,TimeUnit unit)来等待线程池关闭结束。
可以调用ThreadPoolExecutor.shutdownNow()来立即关闭线程池,正在执行的任务会被终止。

任务的处理结果、异常处理与取消

如果客户端关心任务的处理结果,那么它可以使用ThreadPoolExecutor中另一个submit方法来提交任务。
public <T> Future<T> submit(Callable<T> task)
task表示客户端需要提交的任务,其类型为java.util.concurrent.Callable。其接口定义为:
V call() throws Exception

上述submit方法的返回值类型为java.util.concurrent.Future.该接口的实例可以看作提交给线程池执行的任务的处理结果句柄,Future.get()方法可以用来获取task参数所指定的任务的处理结果。
V get() throws InterruptedException,ExecutionException
Future.get()被调用的时,如果相应的任务尚未执行完毕,法吗Future.get()会使当前线程暂停,直到相应的任务执行结束。

客户端代码应该尽早地向线程池提交任务,并仅在需要相应任务地处理结果数据地那一刻才调用Future.get()方法

线程池死锁

如果线程池中执行的任务在执行过程中又会向同一个线程池提交另外一个任务,而前一个任务的执行结果又依赖于后一个任务的执行结果,那么线程池中的所有工作者线程都处于等待其它任务的处理结果而这些任务仍在工作队列中等待执行,这时由于线程池中已经没有可以对工作队列中的任务进程任务的工作者线程,这种等待就会一直持续下去从而形成死锁。

同一个线程池只能用于执行相互独立的任务。彼此有依赖关系的任务需要提交给不同的线程池执行以避免死锁

同步计算与异步计算

从多个任务的角度来看,任务可以是串行执行的,也可以是并发执行的。从单个任务的角度来看,任务的执行方式可以是同步的,也可以是异步的。

n2GVED.png

Java Executor框架

Runnable接口和Callable接口都是对任务处理逻辑的抽象。
java.util.concurrent.Executor接口则是对任务的执行进行的抽象。
该接口的定义为:
void execute(Runnable command)
其中command参数代表需要执行的任务,Executor接口使得任务的提交方,只需要知道它调用Executor.execute方法便可以使指定的任务被执行,而无需关心任务的具体执行细节。
解耦任务的提交与任务的具体执行细节在一定程度上能够屏蔽任务同步执行和异步执行的差异。
Executor接口比较简单,功能也十分有限,它只能为客户端代码执行任务,而无法将任务的处理结果返回给客户端代码。

使用工具类Executors

实用工具类java.util.concurrent.Executors
n2adxJ.png