java基础【多线程】
内容都为原创,转载请注明出处http://www.cnblogs.com/xingdongpai/
文章目录:
1. 进程,线程(多线程)简介
2. 创建线程
3. 线程的启动
4. 创建进程
5. 同步代码
6. 详解线程状态
7. 线程通信
8. 线程组
9. 线程池
10. 线程局部变量
一 进程,线程(多线程) 简介
1.1 进程的由来
在以前,计算机的发明是为了计算数学问题,当时计算机只能接受一些特定的指令,用户输入一个指令,计算机就做一个操作,当用户思考或输入数据的时候,计算机处于等待状态,显然这样子效率很低下。为了解决这一问题,首先出现了批处理,把需要操作的指令提前写下来,然后交给计算器去执行,计算器只需要读取指令就可以进行相应的操作,但这样子还有一个问题:
假如有A,B两个任务,任务A在执行到一半的时候,需要读取大量的数据输入,此时cpu处于等待A输入的待机状态,这样还是浪费了cpu资源,于是人们思考是否可以在A任务输入数据的时候,cpu去执行B任务。当A任务输入完毕以后,B任务暂停,cpu继续处理A任务。
思路已经出现,但还面临一个问题,就是原本计算机内存中只有一个运行程序。而如果既处理任务A,又要处理任务B,必然内存中要装多个数据,那么如何让内存中有多个任务程序呢,任务程序该怎么切换呢?等一系列问题就出现了。
解决方案就是进程,每个进程对应一个程序,每个进程对应一定的内存地址空间,并且进程之间只能使用自己的内存中,各个进程之间互不干扰。进程需要保存了程序每个时刻的运行状态,有了状态这个东西,进程之间的切换才有了可能,比如当进程暂停,相应进程会保存当前进程的状态(进程标识,进程使用的资源)。暂停结束后能回到暂停前的状态
1.2 进程的概念
当一个程序被运行起来之后,这个程序会被加载到内存中,而把内存中这个程序运行期间所占有的内存空间,被称为进程。
1.3 线程的由来
进程的出现,解决了操作系统的并发问题,但显然不能满足人们的需求,一个进程在一个时段只能做一件事,如果一个进程有多个任务,只能依次取处理这些任务。比如监控系统,为了呈现监控图像,不仅要与服务器通信去获取图像数据,还有处理与监控者的交互操作。现在监控系统正在获取从服务器传过来的图像数据,还有获取用户在监控系统上的操作。那么监控系统只能等待先接受完数据,后处理操作的策略。一旦服务器传过来很大的图像数据,那么延迟性就很大。显然,这就是弊端
面对这个问题,我们的解决是把获取服务器图像数据,与实施交互操作 分为两个子任务,在获取图像数据时,执行相应子任务。一旦监控者实施了交互,立刻暂停获取图像数据,去处理交互操作。操作完成之后,继续获取图像数据。这就是线程,每个线程对应一个子任务。一个进程(监控系统),有多个线程(获取数据,交互操作)。由进程来分配让哪些线程来得到cpu资源。
1.4 线程的概念
在进程中负责执行某一任务(任务代表一个独立代码单元,一个独立的功能)
1.5 多线程
在一个进程中可以有多个线程,多个线程同时运行,同时执行任务。并且这些线程共享进程空间与资源。具体执行要交给cpu来处理。
多线程中所谓同时执行,应该从单核cpu,双核cpu的情况讨论
单核cpu:
一个程序在运行的过程中,某一时刻,某个时间点上,cpu只能处理一个线程任务
cpu在这些独立的运行单元(线程)之间,做着高速切换,快到我们感觉不到(纳秒为单位),所以我们认为是在同时进行。
这种情况称为并发
双核cpu:
一个程序在运行的过程中,每个cpu同时处理进程中的两个线程,
这种情况成为并行
多线程的缺点:
在合理的cpu使用范围内,可以提高效率,如果线程开的过多,就会降低效率
【小结】
1。进程让操作系统的并发性成为可能,而线程让进程内部的并发成为可能。
2。java采用单线程编程模型,意味着如果我们不主动创建线程,那么始终只有一个线程,该线程也叫做主线程
【注意】
这里所谓的只有一个线程是说jvm本身是一个进程,为main方法开启一个线程,所以main方法始终只有一个线程,除非你开启了线程,并不是说jvm只创建了main一个线程,因为还有垃圾回收线程,等等其他线程。
【扩展】
这里提及一个概念,线程的也需要独立的内存,创建一个线程意味着创建一个运行栈区域,在学习java时应该都知道有栈这个概念,更为具体的说这个栈就是主线程的运行栈。cpu会执行栈顶的栈帧(方法),所以线程之间是相互独立的。
二 创建线程
线程是要和操作系统交互的,而我们的java程序运行在虚拟机中。当我们需要使用多线程技术时,要运行的多线程代码交给jvm,那么如何jvm知道哪些代码是多线程呢?有以下两种方式告知jvm。
方式一:继承Thread类
Thread类定义:
public class Thread implements Runnable { private Runnable target; //多个构造函数,只截取了一个构造函数 public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } @Override public void run() { if (target != null) { target.run(); } } }
方式二:实现Runnable接口
Runnable接口定义:
public interface Runnable { public abstract void run(); }
两种创建线程方式的讨论
我听到过一种概念,描述的很好,Runnbale可以理解为"任务",Thread可以理解"线程对象"。我们所创建的是线程对象,线程对象不仅包括了线程要执行的任务,也包括了线程的状态(名称,标识符,等一些东西,很好的体现了面向对象的思想),只有“任务”,没有任何用,你需要把"任务"交给线程对象,由线程对象专业的native方法去与操作系统打交道。告诉操作系统,我开启了一个线程,有机会去宠幸下新线程。
三 线程的启动
3.1 开启线程
无论用上面的哪种方式去创建一个线程,要想启动线程,并不是直接调用Runnable接口/Thread类的run(),而是调用Thread类的start()。
start()方法源码如下:
if (threadStatus != 0) throw new IllegalThreadStateException(); /* 通知group,线程将要被启动,目的是把它可以添加到group的线程列表中,并且让group的未启动线程数量减少 */ group.add(this); boolean started = false; try { start0(); //调用了native方法,显然开启线程是与操作系统进行交互, started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* 什么都不做,如果start0()没有抛出异常,那么将放弃调用栈 */ } }
3.2 start() / run() 详解
当我们启动jvm之前,jvm会首先给我们程序中的线程在栈中分配各自独立的内存空间,当我们调用main方法的时候,jvm为main方法开启运行栈区域,把main方法中调用的方法进行压栈,当这个主线程执行到了start()方法时,就开启线程,把具体run方法中的代码也交给cpu执行(注意:这里没有进行压栈,而是在栈中又开辟了一块运行栈区域,然后cpu选择其中一个运行栈的栈顶执行),具体执行哪一个,由cpu自己选择。当然主线程执行完毕后,并不会影响其他线程的运行。
而直接调用run(),实际是把run压入主线程栈。主线程会执行run方法,执行完毕后把run方法弹出,并不会开启线程,而是由主线程自己执行run方法。
内容都为原创,转载请注明出处http://www.cnblogs.com/xingdongpai/
四 创建进程
方式一:Runtime.exec()
Runtime类的设计是单例,需要通过getRuntime()来获得Runtime对象实例。那么Runtime类是干什么的呢?
4.1 Runtime类
每个Java应用程序都有一个Runtime类实例,每个进程对应一个Runtime类实例。该类的作用是与运行环境相连接,比如exec()方法,里面的参数就相当于我们在windows的dos环境下输入命令。比如exec("shutdown -s -t 1000"),该关机命令,就会开启一个关机的进程。该命令的返回值是一个Process类实例。这个Process类实例可以用来控制进程并获得相关信息
4.2 Process类
Process类提供了从进程输入,执行输出到进行,等待进程完成,检查进程的退出状态以及销毁进程的方法。说白了该实例就是进程的驾驶操,负责控制进程。
方式二:ProcessBuilder的start方法
与runtime.exec(String command);相似,作用都是创建一个本机进程,并返回一个Process实例。
五 同步代码
5.1 什么情况下需要同步代码块
当多线程并发时,有多段代码同时执行,我们希望某一时间段代码执行的过程中,cpu不要切换到去执行其他代码块,那我们就需要把这两段代码设置为同步代码并且最关键的是这两段同步代码需要有相同的锁,这样子在执行其中一个同步代码块的同时,cpu就不会切换去执行另外一个同步代码块。
两种方式使其变为同步代码块:
方式一:jdk1.5版本之前使用synchronized关键字
使用:synchronized(锁对象)
{ 同步代码块 }
方式二:jdk1.5版本之后使用ReentrantLock互斥锁
使用:ReentrantLock lock=new ReentrantLock();
lock.lock();
{ 同步代码块 }
lock.unlock();
5.2 锁的概念
只有同步代码块持有相同的锁对象,才会保证在执行其中一个代码块的同时,不会去执行另一个同步代码块,所以问题的关键在判断锁对象是什么?
1. 如果是静态同步方法:锁对象是所在类的class对象
2. 如果是非静态同步方法:锁对象是this
5.3 死锁的产生
死锁很好理解,就是嵌套代码块的同时,互相请求对方的锁,你在请求我的锁,我在请求你的锁,这时没有人让步就出现了死锁。解决方案就是尽可能的避免同步代码块。
5.4 该使用哪种方式同步代码?
这其实很好判断,对于大多数情况,后推出的技术大部分是优化了之前的技术,最为重要的是提供了更加丰富的功能,对于synchrnozied来说,在之后要讲解的线程之间的通信(wait(),notify(),notifyAll())的时候,最大的弊端就是notify总是随机唤醒一个等待线程,我们无法掌控到具体唤醒哪个线程,ReentrantLock解决了这个问题,可以使用Condition类精确控制唤醒哪个线程,这就是最大的好处。
六 详解线程状态
要想理解线程之间的通信,我们需要先了解线程的状态
Thread类中的方法改变线程状态
(1)sleep方法
调用sleep让运行状态线程变成冻结状态,注意,sleep方法不会释放锁,如果当前线程持有对象锁,其他对象无法访问这个对象。这与Object类的wait()方法有鲜明的对比,sleep与wait方法都是让运行状态线程变为冻结状态,sleep不释放锁(占着茅坑不拉屎),wait释放锁(不拉屎不占茅坑)。
(2)yield方法
调用yield方法会让运行状态线程交出cpu权限变成阻塞状态,让cpu去执行其他的线程,他跟sleep方法类似,同样不会释放锁,
(3)join方法
调用join方法会让当前调用方法的线程执行,该方法有三种重载形式,参数可以输入时间,意思是调用方法的线程执行多少毫秒。
(4)interrupt方法
调用interrupt方法可是使处于阻塞状态的线程抛出一个异常,中断一个处于阻塞状态的线程,与isInterrupted()方法组合,可以中断运行中的线程
内容都为原创,转载请注明出处http://www.cnblogs.com/xingdongpai/
七 线程通信
仅仅使用同步,还是无法精确控制线程的执行顺序,只是控制了线程执行代码块的完整性。要想精确控制多线程的执行顺序,就需要使用线程通信技术。
7.1 目前来讲,通信方式有两种:
(1)等待:让线程等待。
(2)唤醒:让线程唤醒。
7.2 使用通信方式的前提:
(1)通信需要在同步代码块中才能使用
(2)如果使用Object类的wait和notify方法时,要使用与锁相同的对象。
具体的实现分为两种:
实现一:jdk 1.5版本之前
Object类的方法
(1)等待=wait()
(2)随机唤醒=notify()
实现二:jdk 1.5版本之后
Condition类的方法
(1)等待=await()
(2)唤醒=signal()
实现二的一个例子:
public class ReentrantDemo { private static ReentrantLock lock=new ReentrantLock(); private static Condition condition=lock.newCondition(); private static Condition condition2=lock.newCondition(); private static boolean flag=false; public static void print1(){ lock.lock(); try{ while(flag){ condition2.await(); } System.out.print("段"); System.out.print("哥"); System.out.print("厉"); System.out.print("害"); flag=true; condition.signal(); }catch(Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public static void print2() { lock.lock(); try{ while(!flag){ condition.await(); } System.out.print("A"); System.out.print("N"); System.out.print("D"); System.out.print("R"); System.out.print("O"); System.out.print("I"); System.out.print("D"); flag=false; condition2.signal(); }catch(Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } public static void main(String[] args)throws Exception { Thread t1=new Thread(new Runnable() { public void run() { print1(); } }); Thread t2=new Thread(new Runnable() { public void run() { print2(); } }); t1.start(); t2.start(); } }
八 线程组
默认线程组都是住线程组(main)。可以在创建线程的时候指定一个线程组,然后调用线程组的方法,比如setDaemon(true),来进行整体设置。线程组对应的类时ThreadGroup。
九 线程池
9.1 为什么使用线程池
程序启动一个线程的成本过高,因为它涉及到要与操作系统进行交互。频繁的开启关闭线程必定消耗资源。那么我们不在关闭线程,而是把线程放入线程池,并且线程处于空闲状态。有基础的coder应该知道连接池这个概念,他与线程池的类似。在jdk1.5之前我们需要自己实现线程池,在jdk1.5之后提供了实现好的线程池。
9.2 内置线程池-ExecutorService
通过工厂获取线程池对象,该工厂名为Executors,该工厂提供以下方法
/** * nThread:为线程池中含有的线程数量。 */ public static ExecutorService newFixedThreadPool(int nThreads); // 获取单线程的线程池 public static ExecutorService newSingleThreadExecutor();
获得了线程池对象(executorService),把”任务“(Runnable)交给线程池
Future submit(Runnable task); //Callable是一个新的”任务形式“。 Future submit(Callable task);
[番外篇:Callable]
与Runnable类似,都可以理解为”任务“,只不过Callable在执行完任务后有一个返回值,Runnable的任务方法是void run(){..}。Callable的任务方法是V call(){..return v}。我们可通过上面线程池对象的submit方法的返回值Future获取到返回值。通过Future的get()方法。
内容都为原创,转载请注明出处http://www.cnblogs.com/xingdongpai/
十 ThreadLocal
ThreadLocal为变量在每个线程中都创建了一个变量副本,每个线程可以访问自己内部的副本变量
10.1 为什么要使用ThreadLocal?
这个问题很关键,Struts2中的数据中心,Hibernate中的Session,他们底层原理都是使用了线程局部变量,这个问题放到最后来思考,先看看线程局部变量是什么?
10.2 什么是ThreadLocal?
ThreadLocal译为线程局部变量,我们都知道多个线程共享进程的空间,线程之间是独立的单元,所谓的多线程安全问题就是多线程操作共享数据时出现的问题,这个问题先不谈,既然有共享,那么肯定也有属于自己的数据。这就是ThreadLocal(线程局部变量)。
10.3 ThreadLocal与Thread之间的关系
ThreadLocal是线程局部变量,Thread类是描述线程的信息。那么肯定Thread类中应该有ThreadLocal的引用,于是我在Thread类中找到了如下的代码。
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. 翻译:线程局部变量的值依附于一个线程,这个线程局部变量的值(Map)被保持在ThreadLocal.class中。
由此注释可知:
1.线程局部变量是一个Map
2.线程局部变量在ThreadLocal中
*/ ThreadLocal.ThreadLocalMap threadLocals = null;
10.4 ThreadLocal类定义
public class ThreadLocal<T> {
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } } }
可以看到,在ThreadLocal中,有一个叫做ThreadLocalMap的内部类,它就是存储线程局部变量的地方。Map的key是ThreadLocal,Map的value就是线程局部变量值的副本。为什么key是ThreadLocal变量?因为一个线程可以有多个线程局部变量.
WeakReference表示弱引用,表示当垃圾回收器线程扫描它所管辖的内存区域时,一旦发现了弱引用对象,不管内存空间够不够,都会回收。但垃圾回收器的优先级很低。因此不一定会很快发现那些弱引用对象。
WeakReference只有两个构造方法,其中都是把原本的引用变为弱引用。那么显然,ThreadLocalMap的Key就是一个弱引用。
由此引发了一个传言,ThreadLocal会引发内存泄露,问题描述是这样:如果一个ThreadLocal没有外部强引用引用它,那么系统gc(垃圾回收)的时候,这个ThreadLocal会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value了,如果当前线程迟迟不结束,这些key为null的Entry的value就会一直存在一个强引用链
实际上,在JDK的ThreadLocalMap的设计中已经考虑这个问题了,在ThreadLocalMap的getEntry函数中,首先从ThreadLocal的直接索引位置获取Entry entry对象,如果entry不为null,并且key相同,那么就返回entry。如果entry为null,或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回相应的entry。在这个过程中遇到的key为null的Entry都会被擦除,所以不必担心内存泄露的问题。
10.5 对ThreadLocal类的操作
public class ThreadLocal<T>{ static class ThreadLocalMap{} public T get() { Thread t = Thread.currentThread(); //获取当前线程 ThreadLocalMap map = getMap(t); if (map != null) { //如果map有值,把当前调用该方法的ThreadLocal实例传进去 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } //如果map没有值,就进行初始化 return setInitialValue(); } /* 获取线程中的线程局部变量中的Map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } /* 对线程局部变量进行初始化 */ private T setInitialValue() { T value = initialValue(); //返回值为null Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } /* 对线程局部变量进行赋值 */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } protected T initialValue() { return null; }
上述ThreadLocal中的方法,就是对内部ThreadLocalMap(真正的线程局部变量)的操作。
其中最后一个initialValue()方法,返回一个null。该方法调用ThreadLocal对象的get()方法时,如果没有线程局部对象的value值,才会执行。值得注意的是该函数用了protected类型。显然建议子类重写该函数。所以通常该函数都会以匿名内部类的形式被重载,如下所用:
ThreadLocal<String> value=new ThreadLocal<String>(){ @Override protected String initialValue(){ return "默认值" } };
10.6 线程局部变量操作流程
1.在当前线程new ThreadLocal().set(value)的时候,会先获取当前线程,从当前线程(Thread)中拿到属性threadLocals,该属性的类型是ThreadLocal的内部类ThreadLocalMap类型,也就是线程局部变量真正的地址
2.判断线程局部变量存不存在,如果不存在,就创建一个Map,key为当前调用set()方法的ThreadLocal,value就是线程局部变量的副本。如果存在则直接赋值
3.创建Map的原理就是new ThreadLocalMap(ThreadLocal tl,Object value).