共享内存及问题
共享内存
每个线程表示一条单独的执行流,有自己的程序计数器,有自己的栈,但线程之间可以共享内存,它们可以访问和操作相同的对象。
不同执行流可以访问和操作相同的变量,
不同执行流可以执行相同的程序代码。
当多条执行流执行相同的程序代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份。
当多条执行流可以操作相同的变量时,可能会出现一些意料之外的结果:
竞态条件
所谓竞态条件(race condition)是指,当多个线程访问和操作同一个对象时,最终执行结果与执行时序有关,可能正确也可能不正确。
解决方法:
使用synchronized关键字
使用显式锁同步
使用原子变量
内存可见性
多个线程可以共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就能看到,甚至永远也看不到,这可能有悖直觉。
在计算机系统中,除了内存,数据还会被缓存在CPU的寄存器以及各级缓存中,当访问一个变量时,可能直接从寄存器或CPU缓存中获取,而不一定到内存中去取,当修改一个变量时,也可能是先写到缓存中,而稍后才会同步更新到内存中。
在单线程的程序中,这一般不是个问题,但在多线程的程序中,尤其是在有多CPU的情况下,这就是个严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存,二是另一个线程根本就没从内存读。
线程的优点及成本
优点
1.充分利用多CPU的计算能力,单线程只能利用一个CPU,使用多线程可以利用多CPU的计算能力。
2.硬件资源,CPU和硬盘、网络是可以同时工作的,一个线程在等待网络IO的同时,另一个线程完全可以利
用CPU,对于多个独立的网络请求,完全可以使用多个线程同时请求。
3.在用户界面(GUI)应用程序中,保持程序的响应性,界面和后台任务通常是不同的线程,否则,如果所有
事情都是一个线程来执行,当执行一个很慢的任务时,整个界面将停止响应,也无法取消该任务。
4.简化建模及IO处理,比如,在服务器应用程序中,对每个用户请求使用一个单独的线程进行处理,相比使
用一个线程,处理来自各种用户的各种请求,以及各种网络和文件IO事件,建模和编写程序要容易的多。
成本
1.创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建也需要一定的时间。
2.线程调度和切换也是有成本的,当有可运行线程的时候,操作系统会忙于调度,为一个线程分配一段时间,执行完后,再让另一个线程执行,
一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前CPU寄存器的值、程序计数器的值等,
而一个线程被切换回来后,操作系统需要恢复它原来的上下文状态,整个过程被称为上下文切换,这个切换不仅耗时,而且使CPU中的很多缓存失效,是有成本的。
当然,这些成本是相对而言的,如果线程中实际执行的事情比较多,这些成本是可以接受的。
另外,如果执行的任务都是CPU密集型的,即主要消耗的都是CPU,那创建超过CPU数量的线程就是没有必要的,并不会加快程序的执行。
线程池
为什么用线程池
1.单线程速度慢,影响处理效率
2.创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,资源消耗大、影响处理效率
线程池
int corePoolSize 核心线程数
int maximumPoolSize 线程总数最大值 线程总数 = 核心线程数 + 非核心线程数
BlockingQueue workQueue 该线程池中的任务队列,接收任务
execute():通过这个方法可以向线程池提交一个任务,交由线程池去执行。
原理:
线程池状态:volatile int runState表示当前线程池的状态,当创建线程池后,初始时,线程池处于RUNNING状态;
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
当新任务在方法 execute(java.lang.Runnable) 中提交时,如果:
线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务;
线程数量达到了corePoolSize,则将任务移入队列(workQueue)等待;
队列已满,新建线程(非核心线程)执行任务;
队列已满,总线程数又达到了maximumPoolSize,就会抛出异常。
任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue,通常可以取下面三种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
Java虚拟机,运行时数据区域RuntimeDataArea:
它主要分为五个部分
1、Heap(堆)(线程共享):一个Java虚拟实例中只存在一个堆空间,是Java垃圾收集器管理的主要区域;
2、MethodArea(方法区域)(线程共享):被加载的类信息、常量、静态变量存储在方法区中,方法区也可以被垃圾收集;
所有类可能变成没有被引用(unreferenced)的状态。当类变成这种状态时,就可能被垃圾收集掉。没有加载的类包括两种状态:1.真正的没有加载,2.“unreferenced”的状态;
3、JavaStack(java的栈)(线程私有):虚拟机只会直接对Javastack执行两种操作:以帧为单位的压栈或出栈,当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。
4、ProgramCounter(程序计数器)(线程私有):每一个线程都有它自己的PC寄存器,该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的地址;
5、Nativemethodstack(本地方法栈)(线程私有):保存native方法进入区域的地址。