什么情况下使用多线程
解决进程多任务的实时性问题;就是解决"阻塞问题,阻塞就是程序运行到某个函数或者过程后等待某些事件的发生而暂时停止CPU占用的情况,使得CPU闲置.还有对于一个函数运算逻辑的性能问题.我们可以通过多线程的技术,使得一个函数的多个逻辑运算通过多线程技术达到一个并行执行,从而提升性能.
多线程使用场景
- 通过并行计算提高程序执行性能
- 需要等待网络,I/O响应导致耗费大量的执行时间,可以采用异步线程方式来减少阻塞.
- 客户端阻塞 如果客户端只有一个线程,这个线程发起读取文件的操作必须等待IO流返回,线程(客户端)才能做其他事情
- 线程级别阻塞BIO 客户端一个线程情况下,一个线程导致整个客户端阻塞.可以使用多线进行,一部分等待IO做操返回,其他线程可以做其他的事情,从客户端的角度,客户端没有闲着.
如何使用多线程
java 实现多线程 继承类Thread,实现Runnable接口,使用ExecutorService、Callable,Future实现带返回结果的多线程。
继承Thread创建线程
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例.
启动线程是用的Thread类的start()实例方法,start()方法是个native方法.它会启动一个新线程.
并执行run()方法,这种实现多线程很简单.通过自己的类extends Thread 并复写run()方法,就可以启动新的线程并自己执行自己定义run()方法.
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread.run()");
}
public static void main(String[] args) {
MyThread myThread1=new MyThread();
MyThread myThread2=new MyThread();
myThread1.start();
myThread2.start();
}
}
实现Runnable创建线程
不直接继承Thread ,直接实现Runnable
public class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread.run()");
}
}
实现Callable接口通过FutureTask包装器来创建Thread线程
有的时候,让线程执行完成后进行返回到当前的主线程.主线程需要做后续的逻辑处理,java提供了这样的实现方式.
public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
int a=1;
int b=2;
return "执行结果"+(a+b);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService=Executors.newFixedThreadPool(1);
Future<String> future =executorService.submit(new CallableDemo());
System.out.println(future.get());
executorService.shutdown();
}
}
怎样优化多线程
合理的利用异步操作,可以大大提高程序的处理性能.
通过阻塞的 队列及多线程的方式,实现异步话处理,提高程序性能
Requst
public class Request {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Request{" +
"name='" + name + '\'' +
'}';
}
}
RequestProcessor
public interface RequestProcessor {
void processRequest(Request request);
}
PrintProcessor
public class PrintProcessor extends Thread implements RequestProcessor {
LinkedTransferQueue<Request> requests=new LinkedTransferQueue<Request>();
private final RequestProcessor nextProcessor;
public PrintProcessor(RequestProcessor nextProcessor) {
this.nextProcessor = nextProcessor;
}
@Override
public void processRequest(Request request) {
requests.add(request);
}
@Override
public void run() {
while (true){
try {
Request request=requests.take();
System.out.println("print data:"+request.getName());
nextProcessor.processRequest(request);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
SaveProcessor
public class SaveProcessor extends Thread implements RequestProcessor {
LinkedTransferQueue<Request> requests=new LinkedTransferQueue<Request>();
@Override
public void processRequest(Request request) {
requests.add(request);
}
@Override
public void run() {
while (true){
try {
Request request=requests.take();
System.out.println("begin save request:"+request);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Demo
public class Demo {
PrintProcessor printProcessor;
public Demo(){
SaveProcessor saveProcessor=new SaveProcessor();
saveProcessor.start();
printProcessor=new PrintProcessor(saveProcessor);
printProcessor.start();
}
public void doTest( Request request){
printProcessor.processRequest(request);
}
public static void main(String[] args) {
Request request=new Request();
request.setName("zjm");
new Demo().doTest(request);
}
多线程基础
线程是操作系统调度的最小单位,并且能够多线程同时执行,极大的提高 了程序的性能,在多核环境下的优势更加明显,但是我们使用的多线程过程中,如果对他的特性和原理不够的理解的话,很容易造成各种问题.
线程 状态
线程是有生命周期的,一共有6中状态(NEW ,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED)
NEW : 初始状态,线程被创建,但是没有调用start方法
RUNNABLE :运行状态,JAVA线程把操作系统的就绪和运行两种状态统一称为"运行中".
BLOCKED: 阻塞状态,表示线程进入等待状态.也是某种原因放弃了CPU的使用权 阻塞分为:
- 等待阻塞 :运行中的线程执行wait方法,jvm会把当前线程放入等待队列中
- 同步阻塞 :运行的线程在获取对象锁的时候,被其他线程所占用,JVM会把当前线程放入到锁池中.
- 其他阻塞 :运行的线程执行了Thread.sleep,或者是t.join(),或者发出了I/O请求时,JVM会把当前线程设置为阻塞状态.当sleep,join线程终止.IO处理完毕则线程恢复.
TIME_WAITING:超时等待状态,超时以后会自动返回.
TERMIMATED:终止状态:表示当前线程已经执行完毕.
WAITING:等待,等待其他线程执行完成
代码:
public static void main(String[] args) {
//超时等待 TIMED_WAITING
Thread thread=new Thread(()->{
while (true){
try{
TimeUnit.SECONDS.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
}
},"timedwaiting");
//等待:waiting
new Thread(()->{
while (true){
synchronized (Demo1.class){
try {
Demo1.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"waiting");
//线程加锁不会释放锁
new Thread(new BlockDemo(),"BlockDemo-0");
new Thread(new BlockDemo(),"BlockDemo-1");
}
static class BlockDemo extends Thread{
@Override
public void run() {
synchronized (BlockDemo.class){
while (true){
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
通过命令符查看线程状态
- 打开命令符或者打开终端,输入jps查看当前的运行的jvm
- 根据查看出的pid,用jstack打印出java进程的id,及线程的名称和远程服务堆栈的信息
线程的终止
线程的终止不是stop, 但是和其他的线程控制方法如 suspend、 resume 一样都是过期了的不建议使 用,就拿 stop 来说, stop 方法在结束一个线程时并不会保证线程的资源正常 释放,因此会导致程序可能出现一些不确定的状态
interrupt方法
中断线程执行,至于什么时候中断,取决于当前线程.
线程通过检查,是否被中断进行对应.可以通过isInterrupted()来判断是否被中断.
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
i++;
}
System.out.println("Num:"+i);
},"interrputDemo2");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
System.out.println(thread.isInterrupted());
}
这种通过标志位或者是操作中断的方式使线程终止时有机会去清理资源.而不是果断的终止线程,这样更加的安全.
Thread.interrupted标志位复位
上面调用的interrput是中断线程 ,Thread.interrupted() 进行状态复位
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread= new Thread(()->{
while (true){
boolean flag=Thread.currentThread().isInterrupted();
if(flag){
System.out.println("中断的状态"+flag);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.interrupted();//进行复位
System.out.println("复位的状态"+flag);
}
}
},"interrupted-");
thread.start();
thread.interrupt();
System.out.println(thread.isInterrupted());
}
}
其他线程中断复位
除了使用Thread.interrupt,还有一种方式,还有一种场景,就是让当前的线程抛出异常 InterruptException,在InterruptException之前,jvm会清除标志位,然后抛出InterruptException异常,这时候如果调用isInterrupted 就会是false.
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
while (true){
try{
TimeUnit.SECONDS.sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
},"InterruptedDemo1");
System.out.println(thread.isInterrupted());
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();//设置复位标志TimeUnit.SECONDS.sleep(1);
System.out.println("Interrupted"+thread.isInterrupted());
}
hotsport源代码
void Thread::interrupt(Thread* thread) {
trace("interrupt", thread);
debug_only(check_for_dangling_thread_pointer(thread);)
os::interrupt(thread);
}
void os::interrupt(Thread* thread) {
assert(Thread::current() == thread || Threads_lock->owned_by_self(),
"possibility of dangling Thread pointer");
OSThread* osthread = thread->osthread();
if (!osthread->interrupted()) {
osthread->set_interrupted(true);
// More than one thread can get here with the same value of osthread,
// resulting in multiple notifications. We do, however, want the store
// to interrupted() to be visible to other threads before we execute unpark().
OrderAccess::fence();
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL) slp->unpark() ;
}
// For JSR166. Unpark even if interrupt status already was set
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL) ev->unpark() ;
}
其实unpark是唤醒线程当前线程,并且设置一个标识位位true,并没有所谓的中断线程操作,线程复位可以来实现多线程的之间的通讯.
线程另一种停止方式
除了用interrupt标识中断线程方式,可以用volatile 来控制变量的形式来终止线程,利用volatile多线程变量共享方式特点来实现
private static volatile Boolean stop=false;
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
int i=0;
while (!stop){
i++;
System.out.println(i);
}
});
thread.start();
System.out.println("Begin Start Thread");
TimeUnit.SECONDS.sleep(1000);
stop=true;
}
多线程安全问题
考虑到原子性,可见性,有序性.
CPU高速缓存
线程是cpu 最小的执行单位,线程主要的目的就是充分利用好cpu的资源,但是绝大数处理运算不仅仅是靠处理器去处理,是靠处理器和内存交互完成的,例如读取运算数据和计算,现在计算机的处理增加一层高速缓存作为处理器和内存的缓冲区,讲运算所需要的数据,能快速的运算,运算结束后将计算结果从缓存中同步到内存.
高速缓存从下到上越接近CPU速度越快,同时容量也越来越小,现在大部分的处理器都有二级或者是三级,从上向下L3 cache,L2 cache,L1 cache,缓存可以分为指令缓存和数据缓存,指令缓存用来缓存程序代码,数据缓存用来存储程序的数据.
L1 Cache ,一级缓存,本地Core的缓存,分成32k的数据缓存L1d和32k指令缓存L1i,访问L1需要2cyles,耗时大约1ns
L2 Cache ,二级缓存,本地Core的缓存,被设计为L1和L3缓存之间的缓冲
L3Cache,三级缓存,在同一个插槽的所有的core共享L3缓存,分为2M的段,访问L3需要38cycles,耗时要12ns
缓存一致性问题
CPU-0当前的数据写到高速缓存中,CPU-1也做了同样的事情,而CPU-1把数据修改了2 并且同步到CPU-0的高速缓存中,但是这个修改并没有写入主内存.CPU-0访问该字节,由于缓存没有更新,所以仍然是之前的值,就会导致缓存一致性问题
引发这个原因就是CPU指令并行执行的.而各个CPU核心之间数据不共享导致缓存一致性的问题.
总栈锁
当一个cpu对缓存进行操作的时候,向总线发了lock信号.其他处理器的请求会被阻塞,那么该处理器可以独占共享内存.总线锁是把cpu和内存之间的通信锁住了,所以会导致CPU的性能下降.现在P6处理器,就是缓存锁
缓存锁
如果缓存在处理器行中的内存区域在LOCK操作期间被锁定,当他执行锁的操作回写内存时候,处理不在总线上声明Lock信号,而是修改缓存地址.然后通过缓存一致性来保证原子性.因为缓存一直会阻止修改两个以上的处理器缓存的内存区域的数据.当其他处理器进行回写数据的会导致当前行的处理器缓存无效.
声明CPU的锁,会产生一个Lock指令.会产生两个作用
1.lOCK前缀指令会引起处理器缓存回写到内存,在P6以后的处理器中,Lock信号一般不会锁总线.而是缓存锁.
2.一个处理器的缓存回写到内存会导致其他处理器的缓存无效
缓存一致性问题
处理器有一套完整的协议,来保证Cache的一致性,比较典型的MESI协议.它的方法是在CPU缓存中保存一个标记位,这个标记位有四种状态
-
M(Modified) 修改缓存,当前的CPU缓存已经被修改了,表示已经和内存中的数据不一致了.
-
I(Invaild)失效缓存,说明CPU的缓存已经不能使用的了
-
E(Exclusive)独占缓存,当前Cpu的缓存和内存中保持一致,而其他的处理器没有缓存该数据
-
S(Shared)共享内存 数据的和内存是一致,并且该数据粗在多个CPU缓存中.
每个Core的Cache控制器不仅知道自己的读写操作,也监听他Cache的读写操作嗅探 协议
CPU 读取会遵循几个规则
- 如果缓存状态是I,那么从内存中读取,否则直接从缓存中读取
-
如果缓存处于M或者E的CPU嗅探到CPU有读的操作,就把自己的缓存写入到内存,并把自己的状态设置为S
-
只有缓存状态是M或E的时候,CPU 才可以修改缓存中的数据,修改后,缓存状态变为MS
CPU的优化执行
除了增加高速缓存以外,可以利用处理器的内部的运算单元,处理器可能回对输入的代码回进行乱序优化,处理器会在计算之后将会乱序执行结果,保证了结果与顺序执行的结果一致,但是不保证程序各个语句的计算的先后顺序和输入代码 一致,这个是处理器的优化.还有编程语言的编译器也有优化,做指令重排
并发编程问题
并发编程问题就是有序性问题,原子性问题,可见性.
缓存一致性会导致原子性问题,处理器的指令重排序导致有序问题,缓存一致性导致可见性问题.
内存模型
内存模型定义了系统之间的多线程读写操作的行为规范,解决硬件和操作系统的内存访问的差异.实现java程序的各个平台下都能达到一致性的.java内存模型目标是定义了程序各个变量的访问规则.也就是在虚拟机中将变量存储到内存及从内存中取出变量.通过规范来报纸读写的指令的正确性,它和处理器有关,处理器优化,与并发有关,与编译器有关,解决了多级缓存,处理器优化,指令重排的内存访问问题.保证并发场景下的原子性,有序性,可见性.内存屏障解决的并发方式:限制处理器优化和是用内存屏障.
java内存模型定义了线程和内存交互的方式.在JMM抽象模型的中,分为主内存,工作内存,主内存是所有的线程的共享.工作内存是每个线程特有的.线程对变量的操作(读写,赋值)都必须在工作内存中执行.不能写主内存中变量.并且不同的线程不能访问对方的工作内存的变量,线程之间的传递要通过主内存进行传递.他们的关系
JMM是一种规范,解决多线程共享内存进行通信时,存在的本地内存数据不一致,编译器会对代码指令重排或者处理器对代码乱序执行等带来的问题.目的就是保证可见性,原子性和有序性