进程是操作系统中的一个任务,他是包含了某些资源的内存区域。一个进程可以包含了一个或多个执行单元称作线程,这些线程可以被看做是同时执行的(实际是轮流占用CPU资源,快速切换,达到看似同时执行)。每个进程还有一个私有虚拟的地址空间,该空间只能被包含的线程所访问。当操作系统创建一个进程之后,该进程会自动申请一个名为主线程的线程。
一个线程是进程的一个顺序执行流,同类的线程可以共享一块内存空间和一组系统资源。线程在切换的时候负荷很小,因此,线程也被成为轻进程。一个进程可以包含多组线程。
一个进程最少有一个线程,线程的划分尺度要比进程小,为什么这么谁呢。因为每个进程在执行过程中都有一块独立的内存单元,进程在切换时候需要消耗大量资源。而多个线程是共享这块内存的,在线程切换过程中并不需要切换内存单元,所以极大的提高了运行效率。多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将这些线程看做独立运行的程序来进行进程调度和资源分配。
多个线程同时执行,这只是在宏观上看的。事实上是OS将CPU运行时间划分为许多时间片,获得时间片的线程执行,其他的线程等待。由于切换速度非常快,所以在宏观上是同时执行的,但微观上并不是,这种叫做并发。而并行是无论微观还是宏观都是同时执行的,这里通过多个CPU可以实现并行执行程序。
通过继承Thread类,并且重写Thread类中run方法来定义一个具体的线程。重写run方法的目的在于定义线程要执行的逻辑。启动线程不是调用run方法,而是调用线程的start()方法。start()方法将线程纳入线程调度序列,是当前线程获取时间片段后可以执行。Thread类是一个线程类,每个实例表示一个可以并发运行的线程。
class SampleThread extends Thread { public void run() { System.out.println("通过继承Thread类重写run方法实现"); } public static void main(String[] args) { SampleThread thread = new SampleThread(); thread.start(); } }
实现Runnable接口并且重写run方法定义线程体,然后创建线程的时候将Runnable实例传入并启动线程。这样做的好处是,将定义线程与线程要执行的任务分开来,来减少耦合。并且,java是单继承的,如果一个类继承了Thread类,就无法继承其他类了。(Thread类也是实现Runnable接口的)
在Runnable接口中,只定义了抽象run方法:
public abstract void run();
class SampleThread implements Runnable { public void run() { System.out.println("通过继承Thread类重写run方法实现"); } public static void main(String[] args) { SampleThread sample = new SampleThread(); Thread thread = new Thread(sample); thread.start(); } }
Thread thread = new Thread(){ public void run() { }; };
当执行完run方法,或者没有捕获异常而使线程终止。但是如果想强制终止线程,可以使用interrupt方法来请求终止线程。
线程中断是改变线程的中断状态,中断之后可能是死亡、可能是等待新的任务、可能是继续执行到下一步,这个要看程序本身。线程会不断检测这个中断标识位,以判断是否应该被中断。但是如果线程被阻塞,在使用interrupt终止线程,会抛出InterruptedException异常。
使用isInterrupted来检测线程是否被中断
boolean isInterrupted()
boolean interrupted():测试当前线程是否被中断,但是这一调用会产生副作用,他将当前线程中断状态设置为false
void interrupt():发送中断请求,中断状态被设置为true
线程可以有6中状态:
New:新创建
Runnable:可运行
Blocked:被阻塞
Waiting:等待
Timed waiting:计时等待
Terminated:被终止
通过getState()方法检测当前线程状态
使用new操作符创建一个线程,如new Thread(t),该线程还没有开始运行。当一个线程处于新创建状态时,说明程序还没有运行线程中的代码。
一旦调用start()方法,线程处于runnable状态,运行状态中的线程可能正在运行,也可能没有运行,这个要看操作系统提供的运行时间。其实通过中断就是为了让其他线程获得运行的时间。
被阻塞线程和等待线程,他们都是暂时不活动,不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。
当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且调度器允许线程持有他的时候,该线程变为非阻塞状态。
当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。如调用Object.wait方法或Thread.join方法,或者等待Lock或Condition是,出现这种情况。
有几个方法有一个超时参数,调用他们导致线程进入计时状态,这一状态将一直到超时期满或者接到适当的通知。带计时的方法Thread.sleep()、Object.wait()、Thread.join()、Lock.tryLock()、Condition.await
有两个原因导致线程终止:
run方法正常退出而自然死亡
因为没有捕获一个异常而终止了run方法而意外死亡
Thread Thread.currentThread()方法:获取当前运行代码段的线程
Thread thread = Thread.currentThread();
boolean isAlive():测试线程是否处于活动状态
boolean isDaemon():测试线程是否是守护线程
boolean isInterrupted():测试线程是否已经被中断
long getId():获取线程标识符
String getName():返回线程名称
int getPriority():返回线程优先级
Thread.state getState():获取线程状态
线程的切换是由线程调度控制的,我们无法通过代码无法干涉,但是我们可以通过提高线程的优先级来最大程度的改善线程获取时间片的几率
线程优先级被分为10个等级,分别为1-10,其中1最小,10最大。线程提供了三个常量值表示最低、最高以及默认优先级
Thread.MIN_PRIORIITY
Thread.MAX_PRIORITY
Thread.NORMAL_PRIORITY
通过setPriority()方法进行设置
java中线程分为两类:User Thread(用户线程)和Daemon Thread(守护线程)。Daemon线程是为其他线程运行提供便利的,如垃圾回收器就是一个守护线程。Daemon与User Thread没有什么不同,主要的不同就是虚拟机的离开:当所有User Thread全部运行退出后,只剩下Daemon Thread,这时候虚拟机也就退出了。守护进程并非只有虚拟机可以提供,用户也可以设定通过:setDaemon(boolean on)
不要轻易设置守护线程,因为你不知道User Thread什么时候完成,如果当User Thread运行完毕后,你定义的这个Daemon Thread还没运行完毕,也会被强制退出
sleep(long mills)方法是使当前进程阻塞状态,进入阻塞的时间为指定的毫秒数,阻塞时间过去后进入Runable状态,等待分配时间片。这个方法在声明的时候抛出了InterruptedException,所以在使用该方法的时候也要捕获这个异常。
static void yield():主动让出当次时间片回到Runnable状态,等待分配时间片。
用于等待当前线程结束,该方法会抛出InterruptedException。
当多个线程同时读写同一临界资源的时候会发生线程并发安全问题。比如:当两个线程读取相同的对象的时候,并且每个一个线程都修改了该对象的状态,这时候错误就出现了,这样的情况称为竞争条件。
如果想解决线程安全问题,需要将异步操作变为同步操作:
异步操作:多线程并发操作,各自执行各自的
同步操作:有先后顺序的操作,你执行我不执行
java提供了两种机制防止代码块受并发访问的干扰,一种是都熟悉的使用synchronized关键字,另一种是使用ReentrantLock类。
ReentrantLock:
Lock lock = new ReentrantLock(); lock.lock(); try{ //临界区代码块,这样能够保证任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句 //当调用lock时,他们被堵塞,直到第一个线程释放锁对象 }finally{ lock.unlock(); }
每个该类对象都有自己的ReentrantLock()对象,如果两个线程访问同一个该类对象,则提供串行服务。但当访问不同的对象的时候,每个线程都有不同的锁对象,两个线程不会发生堵塞。操作不同的实例,线程间是不会受影响的。
考虑,如果当一个线程进入临界区后发现需要某个条件才能够运行,这时候他已经获得了锁对象,但是无法向下执行,java中通过条件对象(条件变量)来解决。
一个锁对象可以有多个条件对象,使用newCondition()方法获得条件对象,当到了可能需要某种条件才能向下执行的时候(可以通过判断,来确定是否条件满足)使用await()方法,这时候当前线程被阻塞,并放弃了锁。当另一线程执行完毕后,条件满足时调用signalAll()方法,这样才能激活因为一个条件而等待的所有线程。
Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); lock.lock(); try{ if(//判断某一个条件是否满足,如果不满足) conditon.await(); //临界区代码块,这样能够保证任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句 //当调用lock时,他们被堵塞,直到第一个线程释放锁对象 ... //其他线程执行完之后,可能满足当前条件,重新激活 condition.signalAll(); }finally{ lock.unlock(); }
将方法用synchronized关键字声明。从jdk1.0,java就为每个对象提供了一个内部锁,如果一个方法使用synchronized关键字修饰,那么对象锁将保护整个方法,即要调用该方法,线程必须获得内部锁对象。内部锁对象也有一个相关条件,wait()方法添加一个线程到等待集中,notify/notofyAll方法接触等待线程的阻塞状态。
即wait()方法相当于上面的await()方法,notify/notifyAll相当于上面的signal/signalAll方法。
public synchronized void method(){ if(//条件状态) wait(); .... notifyAll(); }
这种直接给方法进行加锁是当前方法都需要加锁,还有一种是通过同步阻塞。
synchronized (同步监视器—锁对象){ //代码块 }
synchronized需要对一个对象上锁以保证线程同步,那么这个对象应该保证,当多个需要同步的线程在访问该同步块时,看到的应该是同一个锁对象,否则达不到同步效果,通常使用this作为锁对象。
那么什么情况下使用显示的线程锁lock/unlock,什么时候使用synchronized关键字?
最好的情况下是都不使用,java.util.concurrnt包中的一种机制,为你所处理代码加锁。例如可以使用阻塞队列来完成
如果使用synchronized关键字适合你的程序,那么尽量使用它,可以减少代码量,防止出错。
线程池作用:控制线程数量、重用线程
当一个程序中若创建大量线程,并在任务结束后销毁,会给系统带来过度消耗资源,以及切换线程带来的危险,从而导致系统崩溃,可以使用线程池解决这个问题。
原理:首先创建一些线程,他们的集合成为线程池,当服务器从客户端接受一个请求后,从线程池取出一个空闲线程,服务完成后不关闭线程而是还回到线程池中。在线程池编程模型下,任务是提交给线程池的,不是提交给某个线程,线程池拿到任务后,在线程池中找有无空闲线程,将任务交给空闲线程。一个线程只能执行一个任务,但可以同时向线程池提交多个任务。
实现方式:
1、
ExecutorService pool = Executors.newCachedThreadPool();
2、
ExecutorService pool = Executors.newFixedThreadPool(nThreads);
3、
ExecutorService pool = Executors.newScheduledThreadPool(corePoolSize)
4、
ExecutorService pool = Executors.newSingleThreadExecutor();
class SampleThread { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(2); Handler handler = new Handler(); for(int i =0 ;i<5;i++){ pool.execute(handler); } } } class Handler implements Runnable{ @Override public void run() { String name = Thread.currentThread().getName(); for(int i=0;i<10;i++){ System.out.println("当前执行的线程为:"+name); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("任务执行完毕"); } }
BlockingQueue是双缓冲序列,在多线程并发时,若需要使用队列,我们可以使用Query 。但是同步操作会降低并发对Query的操作,可以使用BlockingQuery。
BlockingQueue内部使用两条队列,可允许两个两个线程同时向队列一个做存储,一个做读操作。在保证并发安全的同时提高了队列的存取效率
当BlockingQueue为空的时候向其队列取东西,则线程将被中断进度等待状态,直到BlockingQueue有东西,该线程才会被唤醒继续操作。如果BlockingQueue队列是满的,再向里面放入东西的时候,该线程也会被中断进入等待状态,直到BlockingQueue中有空间,才会被唤醒继续操作。
抛出异常 | 特殊值 | 阻塞 | 超时 | |
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
offer(e):向队列中加入元素,如果BlockingQueue可以容纳,则返回true,否则返回false
put(e):向队列中加入元素,如果BlockingQueue可以容纳,则插入,如果不能插入则该线程进入阻塞状态,直到能够被插入
下面的方法也是这样的原理
ArrayBlockingQueue:规定大小的BlockingQueue,需要指定一个int型参数指定其大小,所含的对象是FIFO顺序排序的。
LinkedBlockingQueue:大小不定的BlockingQueue,如果构造函数指定一个大小,则其为BlockingQueue队列大小,如果不指定则为Integer.MAX_VALUE大小,所含对象是FIFO顺序排序的。
PriorityBlockingQueue:与LinkedBlockQueue类似,但是不是FIFO顺序排序的。而是依据对象的自然顺序或者构造函数指定的Comparator决定的顺序。
SynchronousBlockingQueue:是一种特殊的BlockingQueue,对其操作需要方和取交替执行。
其中LinkedBlockingQueue和ArrayBlockingQueue比较起来,它们背后所用的数据结构不一样,导致LinkedBlockingQueue的数据吞吐量要大于ArrayBlockingQueue,但在线程数量很大时其性能的可预见性低于ArrayBlockingQueue。
查看其源码会发现,对队列每个操作都加上了ReentrantLock锁对象。
测试offer方法,如果插入超过两秒没有进入序列返回false
BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(10); for(int i =0;i<20;i++){ try { boolean result = queue.offer(i, 2, TimeUnit.SECONDS); //插入2秒后如果没有成功返回false System.out.println("存入是否成功:"+ result); } catch (InterruptedException e) { e.printStackTrace(); } }
BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(10); for(int i =0;i<10;i++){ queue.offer(i); } for(int i=0;i<20;i++){ try { Integer result = queue.poll(2, TimeUnit.SECONDS); //如果两秒后取不出元素,则返回null System.out.println("结果:"+result); } catch (InterruptedException e) { e.printStackTrace(); } }
版权声明:本文为博主原创文章,未经博主允许不得转载。
原文地址:http://blog.csdn.net/colin_yjz/article/details/46897653