zoukankan      html  css  js  c++  java
  • java线程高并发编程

    java线程具体解释及高并发编程庖丁解牛


    线程概述:

    祖宗:

    说起java高并发编程,就不得不提起一位老先生Doug Lea,这位老先生可不得了。看看百度百科对他的评价,一点也不为过:

    假设IT的历史,是以人为主体串接起来的话,那么肯定少不了Doug Lea。

    这个鼻梁挂着眼镜。留着德王威廉二世的胡子,脸上永远挂着谦逊腼腆笑容。服务于纽约州立大学Oswego分校计算机科学系的老大爷。

    说他是这个世界上对Java影响力最大的个人。一点也不为过。

    由于两次Java历史上的大变革,他都间接或直接的扮演了举足轻重的角色。一次是由JDK 1.1到JDK 1.2。JDK1.2非常重要的一项新创举就是Collections。其Collections的概念能够说承袭自Doug Lea于1995年公布的第一个被广泛应用的collections。一次是2004年所推出的Tiger。

    Tiger广纳了15项JSRs(Java Specification Requests)的语法及标准,当中一项便是JSR-166。JSR-166是来自于Doug编写的util.concurrent包。

    值得一提的是: Doug Lea也是JCP (Java社区项目)中的一员。

    Doug是一个无私的人,他深知分享知识和分享苹果是不一样的,苹果会越分越少,而自己的知识并不会由于给了别人就降低了。知识的分享更能激荡出不一样的火花。《Effective JAVA》这本Java经典之作的作者Joshua Bloch便在书中特别感谢Doug Lea是此书中很多构想的共鸣板,感谢Doug Lea慷慨分享丰富而又宝贵的知识。

    我记住了两句话:他是这个世界上对Java影响力最大的个人和差点儿所有的java高并发编程核心包都是他写的。


    线程和进程:

            进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包括1--n个线程。

      线程:同一类线程共享代码和数据空间。每个线程有独立的运行栈和程序计数器(PC)。线程切换开销小。

      线程和进程一样分为五个阶段:创建、就绪、运行、堵塞、终止。

      多进程是指操作系统能同一时候运行多个任务(程序)。

      多线程是指在同一程序中有多个顺序流在运行。

    多线程的优势:

           进程之间不能共享内存,但线程能够。

           系统创建进程须要为该进程又一次分配系统资源。开销大,但线程则小得多,所以使用多线程实现并发比用多进程实现并发的性能要高得多。

    线程的创建和启动:

    继承Thread类创建线程:

    public class FirstThread extends Thread{
    	private int i;
    	@Override
    	public void run() {
    		for(;i<100;i++){
    			System.out.println(getName()+" "+i);
    		}
    	}
    	public static void main(String[] args) {
    		for(int i=0;i<100;i++){
    			System.out.println(Thread.currentThread().getName()+" "+i);
    			if(i==20){
    				new FirstThread().start();
    				new FirstThread().start();
    			}
    		}
    	}
    }

    假设希望调用子线程start()后子线程立刻运行,则能够在当前运行的线程休眠1ms;

    实现Runnable接口创建线程:

    public class SecondRunnable implements Runnable{
    	private int i;
    	@Override
    	public void run() {
    		for(;i<100;i++){
    			System.out.println(Thread.currentThread().getName()+" "+i);
    		}
    	}
    	public static void main(String[] args) {
    		for(int i=0;i<100;i++){
    			System.out.println(Thread.currentThread().getName()+" "+i);
    			
    			if(i==20){
    				SecondRunnable sr=new SecondRunnable();
    				
    				new Thread(sr,"thread-1").start();
    				new Thread(sr,"thread-2").start();
    			}
    		}
    	}
    }
    


    使用Callable和Future穿创建线程:

    Java5開始,Java提供了Callable接口。该接口提供了一个call方法作为线程运行体,可是call方法比run方法更强大。call方法能够有返回值。能够抛异常。

    Java5还提供了Future接口来代表Callable接口里的call方法的返回值,并为Future接口提供了一个FutureTask实现类,这个类还实现了Runnable接口,能够作为Thread类的target。在Future接口里定义了例如以下几个公共方法来控制它关联的Callable任务。

    boolean cancle(boolean mayInterruptIfRunning):试图取消Future里关联的Callable任务。

    V get()返回Callable任务里call方法的返回值,调用该方法会导致程序堵塞,必须等到子线程结束才会得到返回值。如图所看到的能够看出。

    V get(long timeout,TimeUnit unit)返回Callable任务里call方法的返回值。该方法让程序最多堵塞timeout和unit指定的时间,假设超时依旧没有返回值,则会抛出TimeOutExecption。

    boolean isCancelled()假设在Callable任务正常完成之前被取消。返回true。

    boolean isDone()假设Callable任务已完成。则返回true。

    public class ThirdFutureTask {
    	public static void main(String[] args) {
    		FutureTask<Integer>task=new FutureTask<Integer>(new Callable<Integer>() {
    			@Override
    			public Integer call() throws Exception {
    				int i=0;
    				for(;i<100;i++){
    					System.out.println(Thread.currentThread().getName()+" "+i);
    				}
    				return i;
    			}
    			
    		});
    			for(int i=0;i<100;i++){
    				System.out.println(Thread.currentThread().getName()+" "+i);
    				if(i==20){
    					new Thread(task,"return").start();
    				}
    			}
    	
    			try {
    				System.out.println("子线程的返回值"+task.get());
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			} catch (ExecutionException e) {
    				e.printStackTrace();
    			}
    
    	}
    	
    }
    

    创建线程的三种方式对照:

    採用继承Thread的方式创建线程优缺点:

    优势:编写简单。訪问当前线程无需Thread.currentThread()

    劣势:出于java单继承的原因。仅仅能继承一个类

    採用实现Runnable,Callable接口创建线程优缺点:

    优势:能够继承其他类。多个线程能够共享同一个target,非常适合多个线程来处理同一份资源,体现了面向对象思想。

    劣势:编写更复杂。要訪问当前线程须要Thread.currentThread()

    线程的生命周期:


    新建和就绪状态:

    当程序使用newkeyword创建了一个线程后,该线程就处于新建状态,此时和其他java对象一样,仅仅由虚拟机为其分配内存,初始化成员变量。

    当程序对象调用了start后,该线程就处于就绪状态,虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有開始运行。仅仅是说能够运行了,至于何时运行。取决于jvm线程调度器里的调度。

    运行和堵塞状态:

    假设处于就绪状态的线程获取了cpu,開始运行run()的线程运行体,则该线程处于运行状态

    假设发生以下情况,则进入堵塞状态:

    线程调用sleep(),堵塞;

    线程调用了一个堵塞式的io方法。在方法返回之前,堵塞。

    线程试图获得一个同步监视器,当该同步监视器正在被其他线程所持有,堵塞;

    线程在等待通知notify(),堵塞;

    程序调用suspend()将线程挂起。堵塞;可是该方法easy导致死锁,尽量不用!

    针对上面的几种情况,如发生以下特定的情况能够解除堵塞,又一次进入就绪状态:

    调用sleep方法的线程过了指定的时间

    io方法返回

    获得同步监视器

    收到通知

    被挂起后resume()

    线程死亡:

    run()或call()运行完成,死亡

    线程抛出一个未捕获的异常或error,死亡

    调用stop()easy死锁,不建议使用。死亡

    控制线程死亡能够用valotile的状态标记量。

    想要知道线程的生死。调用isAlive();就绪,运行,堵塞返回true,其他返回false

    控制线程:

    join线程:

    当在某个程序运行流中调用其他线程的join方法。调用线程将堵塞,直到被join的线程运行完成为止

    join有三种重载方式:

    join()等待被join的线程运行完成

    join(long millis)等待被join的线程最多millis毫秒,否则不再等待。

    join(long millis,int nanos)等待被join的线程最多millis毫秒+nanos毫微秒,否则不再等待。

    public class TestJoin implements Runnable{
    	@Override
    	public void run() {
    		for(int i=0;i<100;i++){
    			System.out.println(Thread.currentThread().getName()+" "+i);
    		}
    	}
    	
    	public static void main(String[] args) throws InterruptedException {
    		for(int i=0;i<100;i++){
    			System.out.println(Thread.currentThread().getName()+" "+i);
    			if(i==20){
    				TestJoin tj=new TestJoin();
    				Thread thread=new Thread(tj);
    				
    				thread.start();
    				
    				thread.join(1);//放在start之后。不然有问题
    				
    			}
    		}
    	}
    
    }


    后台/守护线程:

    调用Thread对象的setMaemon(true)设置为后台线程。

    假设前台线程都死亡,则后台线程不管执没运行完,都会死亡。

    public class TestDaemonThread implements Runnable{
    
    	@Override
    	public void run() {
    		for(int i=0;i<1000;i++){
    			System.out.println(Thread.currentThread().getName()+" "+i);
    		}
    	}
    	
    	public static void main(String[] args) {
    		for(int i=0;i<100;i++){
    			System.out.println(Thread.currentThread().getName()+" "+i);
    			if(i==20){
    				TestDaemonThread tdt=new TestDaemonThread();
    				Thread thread=new Thread(tdt);
    				thread.setDaemon(true);//设置为后台线程,则前台线程都死亡,这个线程会自己主动死亡,必须放在start之前
    				thread.start();
    				
    				
    			}
    		}
    	}
    
    }
    


    线程睡眠sleep:

    让当前线程暂停一段时间,进入堵塞状态。当调用该方法暂停后,就绪状态的线程获得运行的机会。不理会优先级

    sleep(long millis)

    sleep(long millis,int nanos)

    线程让步yield:

    让当前线程暂停一段时间,进入就绪状态,让系统的线程调度器又一次调度一次,当调用该方法暂停后。仅仅有优先级大于等于当前线程的就绪状态的线程才会获得运行的机会。

    而且该方法无需抛异常,不太建议用yield()。


    改变线程优先级:

    Thread类提供了setPriority(int priority)来设置优先级。參数范围是1-10,也能够用Thread的三个静态常量

    ->MAX_PRIORITY 10

    ->NORM_PRIORITY 5

    ->MIN_PRIORITY 1

    线程同步:

    线程安全问题:

    经典样例就是取款问题。假设取款方法不是线程安全的。那么当两个线程同一时候进来取款时非常有可能会发生明明剩余金额不足,却把钱给取出来了的情况。

    同步代码块:

    synchronized(account){
        if(account.getBalance>=drawAmmount){
             取钱成功
        }else{
           取钱失败
        }
    }

    同步方法:

    public synchronized void draw(double drawAmmout){
        if(balance>=drawAmmount){
             取钱成功
        }else{
           取钱失败
        }
    }

    释放同步监视器的锁定:

    释放同步监视器的情况:

    当前线程的同步方法,同步代码块运行完成

    当前线程的同步方法,同步代码块出现了未处理的error,exception。导致了改代码块,方法异常结束

    当前线程的同步方法,同步代码块遇到了break,return终止了代码块,方法的运行

    运行了wait()

    不会释放同步监视器的情况:

    sleep(),yield(),suspend()

    同步锁Lock:

    Java5開始,java提供了功能更加强大的线程同步机制,通过显示定义同步锁对象来实现同步,由Lock对象充当同步锁。

    在实现线程安全的控制中,使用较多的是ReentrantLock(可重入锁)。

    package com.lry.java扎实基础;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Account {
    	static Account account=new Account("123456", 1000);
    	private ReentrantLock lock=new ReentrantLock();
    	private String accountNo;
    	private double money;
    	public Account(String accountNo, double money) {
    		super();
    		this.accountNo = accountNo;
    		this.money = money;
    	}
    	/**
    	 * 取钱
    	 * @param drawAmount 要取多少钱
    	 */
    	public  void drawMoney(double drawAmount){
    		lock.lock();
    		try{
    		if(money>=drawAmount){
    			System.out.println(Thread.currentThread().getName()+"取钱成功,吐出钞票:"+drawAmount);
    			
    			try {
    				Thread.sleep(1);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			
    			money-=drawAmount;
    			System.out.println("账户剩余金额为"+money);
    		}else{
    			System.out.println(Thread.currentThread().getName()+"取钱失败,钞票不足。");
    		}
    		}finally{
    			lock.unlock();
    		}
    	}
    	
    	@Override
    	public int hashCode() {
    		final int prime = 31;
    		int result = 1;
    		result = prime * result + ((accountNo == null) ? 0 : accountNo.hashCode());
    		return result;
    	}
    	@Override
    	public boolean equals(Object obj) {
    		if (this == obj)
    			return true;
    		if (obj == null)
    			return false;
    		if (getClass() != obj.getClass())
    			return false;
    		Account other = (Account) obj;
    		if (accountNo == null) {
    			if (other.accountNo != null)
    				return false;
    		} else if (!accountNo.equals(other.accountNo))
    			return false;
    		return true;
    	}
    	
    
    	static class DrawThread implements Runnable{
    		@Override
    		public void run() {
    			account.drawMoney(600);
    		}
    	}
    	public static void main(String[] args) {
    		DrawThread dt=new DrawThread();
    		
    		new Thread(dt).start();
    		new Thread(dt).start();
    		new Thread(dt).start();
    		new Thread(dt).start();
    	}
    }
    


    死锁:

    死锁原因:

    当两个线程相互等待对方释放同步监视器是发生死锁。虚拟机没有监測。没有採取措施处理死锁,一旦出现死锁,不会报异常,不会有提示,仅仅会堵塞,因此自己要防止这样的情况的发生。

    死锁必要条件:

    从以上分析可见,假设在计算机系统中同一时候具备以下四个必要条件时,那麽会发生死锁。

    换句话说,仅仅要以下四个条件有一个不具备,系统就不会出现死锁。

        〈1〉相互排斥条件。

    即某个资源在一段时间内仅仅能由一个进程占有,不能同一时候被两个或两个以上的进程占有。这样的独占资源如CD-ROM驱动器,打印机等等。必须在占有该资源的进程主动释放它之后,其他进程才干占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源,双方的人不能同一时候过桥。

        〈2〉不可抢占条件。进程所获得的资源在未使用完成之前,资源申请者不能强行地从资源占有者手中夺取资源。而仅仅能由该资源的占有者进程自行释放。

    如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才干过桥。

        〈3〉占有且申请条件。

    进程至少已经占有一个资源。但又申请新的资源;由于该资源已被另外进程占有,此时该进程堵塞;可是。它在等待新资源之时。仍继续占用已占有的资源。

    还以过独木桥为例,甲乙两人在桥上相遇。

    甲走过一段桥面(即占有了一些资源),还须要走其余的桥面(申请新的资源),但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于相同的状况。

        〈4〉循环等待条件。存在一个进程等待序列{P1。P2。...,Pn}。当中P1等待P2所占有的某一资源,P2等待P3所占有的某一源。......,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。就像前面的过独木桥问题。甲等待乙占有的桥面,而乙又等待甲占有的桥面,从而彼此循环等待。


    死锁预防:

      前面介绍了死锁发生时的四个必要条件,仅仅要破坏这四个必要条件中的随意一个条件,死锁就不会发生。这就为我们解决死锁问题提供了可能。

    一般地。解决死锁的方法分为死锁的预防。避免。检測与恢复三种(注意:死锁的检測与恢复是一个方法)。我们将在以下分别加以介绍。

      死锁的预防是保证系统不进入死锁状态的一种策略。

    它的基本思想是要求进程申请资源时遵循某种协议,从而打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。

       1打破相互排斥条件。即同意进程同一时候訪问某些资源。

    可是。有的资源是不同意被同一时候訪问的,像打印机等等。这是由资源本身的属性所决定的。

    所以,这样的办法并无实用价值。

       2打破不可抢占条件。

    即同意进程强行从占有者那里夺取某些资源。

    就是说。当一个进程已占有了某些资源,它又申请新的资源。但不能马上被满足时,它必须释放所占有的所有资源,以后再又一次申请。它所释放的资源能够分配给其他进程。

    这就相当于该进程占有的资源被隐蔽地强占了。这样的预防死锁的方法实现起来困难,会降低系统性能。    

        3打破占有且申请条件。能够实行资源预先分配策略。即进程在运行前一次性地向系统申请它所须要的所有资源。

    假设某个进程所需的所有资源得不到满足。则不分配不论什么资源,此进程暂不运行。

    仅仅有当系统能够满足当前进程的所有资源需求时。才一次性地将所申请的资源所有分配给该进程。由于运行的进程已占有了它所需的所有资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。可是,这样的策略也有例如以下缺点:

    (1)在很多情况下,一个进程在运行之前不可能知道它所须要的所有资源。这是由于进程在运行时是动态的,不可预測的。

    (2)资源利用率低。不管所分资源何时用到,一个进程仅仅有在占有所需的所有资源后才干运行。

    即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。

    这显然是一种极大的资源浪费。

    (3)降低了进程的并发性。由于资源有限,又加上存在浪费,能分配到所需所有资源的进程个数就必定少了。    

     

    4打破循环等待条件,实行资源有序分配策略。採用这样的策略,即把资源事先分类编号。按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源。才干申请大号资源,就不会产生环路,从而预防了死锁。

    这样的策略与前面的策略相比,资源的利用率和系统吞吐量都有非常大提高。可是也存在以下缺点:

    (1)限制了进程对资源的请求,同一时候给系统中所有资源合理编号也是件困难事。并添加了系统开销;

    (2)为了遵循按编号申请的次序,暂不使用的资源也须要提前申请。从而添加了进程对资源的占用时间。

    死锁的避免: 

      上面我们讲到的死锁预防是排除死锁的静态策略,它使产生死锁的四个必要条件不能同一时候具备,从而对进程申请资源的活动加以限制,以保证死锁不会发生。以下我们介绍排除死锁的动态策略--死锁的避免,它不限制进程有关申请资源的命令,而是对进程所发出的每个申请资源命令加以动态地检查。并依据检查结果决定是否进行资源分配。就是说,在资源分配过程中若预測有发生死锁的可能性。则加以避免。

    这样的方法的关键是确定资源分配的安全性。

            死锁的样例:

    public class DeadLock implements Runnable {
    	public int flag = 1;
    	// 静态对象是类的所有对象共享的
    	private static Object o1 = new Object(), o2 = new Object();
    
    	@Override
    	public void run() {
    		System.out.println("flag=" + flag);
    		if (flag == 1) {
    			synchronized (o1) {
    				try {
    					Thread.sleep(500);
    				} catch (Exception e) {
    					e.printStackTrace();
    				}
    				synchronized (o2) {
    					System.out.println("1");
    				}
    			}
    		}
    		if (flag == 0) {
    			synchronized (o2) {
    				try {
    					Thread.sleep(500);
    				} catch (Exception e) {
    					e.printStackTrace();
    				}
    				synchronized (o1) {
    					System.out.println("0");
    				}
    			}
    		}
    	}
    
    	public static void main(String[] args) {
    
    		DeadLock td1 = new DeadLock();
    		DeadLock td2 = new DeadLock();
    		td1.flag = 1;
    		td2.flag = 0;
    		// td1,td2都处于可运行状态,但JVM线程调度先运行哪个线程是不确定的。
    		// td2的run()可能在td1的run()之前运行
    		new Thread(td1).start();
    		new Thread(td2).start();
    
    	}
    }
    
     一个简单的死锁类 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
     * 而td1在睡眠的时候还有一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
     * td1睡眠结束后须要锁定o2才干继续运行。而此时o2已被td2锁定; td2睡眠结束后须要锁定o1才干继续运行,而此时o1已被td1锁定;
     * td1、td2相互等待。都须要得到对方锁定的资源才干继续运行。从而死锁。



    线程通信:

    传统的线程通信:

    首先介绍三个方法:

    Object类以下的wait(),notify(),notifyAll()

    这三个方法必须由同步监视器对象来调用。能够分为两种情况:

    synchronized修饰的同步方法,该类的默认实例(this)就是同步监视器,所以能够在同步方法直接调用这三个方法

    synchronized修饰的同步代码块,同步监视器synchronized后括号中的对象,所以必须使用该对象调用这三个方法

    关于这三个方法的解释:

    wait():导致当前线程等待,直到其他线程调用该同步监视器的notify或notifyAll()来唤醒该线程

    notify():唤醒在此同步监视器等待的单个线程,假设多个线程在此同步监视器等待。则会随意选择一个唤醒

    notify()唤醒在此同步监视器等待的所有线程。

    银行取钱案例:


    账户类:取钱方法,存钱方法

    public class Account {
    	private String accountNo;
    	private double balance;
    	private boolean flag=false;//已有存款标志
    	
    	public Account(String accountNo, double balance) {
    		super();
    		this.accountNo = accountNo;
    		this.balance = balance;
    	}
    	
    	public synchronized void draw(double drawAmount){
    		try{
    			if(!flag){//为假,所以没有人存钱进去。取钱堵塞
    				wait();
    			}else{//能够取钱
    				if(balance>=drawAmount){
    					System.out.println(Thread.currentThread().getName()+"取钱:"+drawAmount);
    					balance-=drawAmount;
    					System.out.println("取钱成功,账户剩余金额:"+balance);
    					flag=false;
    					notifyAll();//唤醒存钱线程
    				}else{//剩余金额不足
    					System.out.println("想取"+drawAmount+"账户剩余金额不足:"+balance);
    					flag=false;
    					notifyAll();//唤醒存钱线程
    				}
    			}
    		}catch(InterruptedException ex){
    			ex.printStackTrace();
    		}
    	}
    	
    	public synchronized void deposit(double depositAmount){
    		try{
    			if(flag){
    				wait();//没人取钱。则存钱堵塞
    			}else{
    				System.out.println(Thread.currentThread().getName()+"存钱:"+depositAmount);
    				balance+=depositAmount;
    				System.out.println("存钱成功,账户剩余金额:"+balance);
    				flag=true;
    				notifyAll();//唤醒取钱线程
    			}
    		}catch(InterruptedException ex){
    			ex.printStackTrace();
    		}
    	}
    	@Override
    	public int hashCode() {
    		final int prime = 31;
    		int result = 1;
    		result = prime * result + ((accountNo == null) ?

    0 : accountNo.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Account other = (Account) obj; if (accountNo == null) { if (other.accountNo != null) return false; } else if (!accountNo.equals(other.accountNo)) return false; return true; } }

    取钱线程(消费者)
    public class DrawThread extends Thread{
    	private Account account;
    	private double drawAmount;//取钱数
    	public DrawThread(String name,Account account,double drawAmount){
    		super(name);
    		this.account=account;
    		this.drawAmount=drawAmount;
    	}
    	public void run() {
    		for(int i=0;i<10;i++){
    			account.draw(drawAmount);
    		}
    	}
    }
    
    
    存钱线程(生产者)

    public class DepositThread extends Thread{
    	private Account account;
    	private double depositAmount;//取钱数
    	public DepositThread(String name,Account account,double depositAmount){
    		super(name);
    		this.account=account;
    		this.depositAmount=depositAmount;
    	}
    	public void run() {
    		for(int i=0;i<10;i++){
    			account.deposit(depositAmount);
    		}
    	}
    }

    測试类:

    public class AccountTest {
    	public static void main(String[] args) {
    		Account account=new Account("123456", 0);
    		new DrawThread("取款者lry", account, 1200).start();
    		
    		new DepositThread("存款者甲", account, 1000).start();
    		new DepositThread("存款者乙", account, 1000).start();
    		new DepositThread("存款者丙", account, 1000).start();
    	}
    }

    測试结果:


    測试结果分析:

    不难发现取钱线程和存钱线程交替运行。仅仅有当取钱者取钱后,存款者才干够存款,同理。仅仅有当存款者存款后,取钱者才干够取钱,程序终于显示被堵塞无法继续运行。这是由于有三个存款线程。可是取钱仅仅有一个线程。这并非死锁。这样的情况仅仅是取钱线程已经运行完成,而存款线程并没有,她在等待其他线程来取钱而已,并非等待其他线程释放同步监视器。

    使用Lock+Condition控制线程通信:

    假设程序中不使用synchronized来保证同步,而是直接使用Lock来保证同步,则系统中不存在隐式的同步监视器,也就不能用wait。notify。notifyAll来进行线程通信了。

    当使用lock时,java提供了一个Conditin类保持协调。使用lock对象能够让那些已经得到lock对象却无法继续运行的线程释放lock对象,conditin对象也能够唤醒其他处于等待的线程。condition提供了三个方法使用方法分别相应Object类的wait,notify,notifyAll。各自是await。signal,signalAll。使用方法类似,不再赘述。

    还是引用上个取款案例:

    仅仅是改动了Account账户类:

    public class Account1 {
    	private String accountNo;
    	private double balance;
    	private boolean flag=false;//已有存款标志
    	private final Lock lock=new ReentrantLock();
    	private final Condition cond=lock.newCondition();
    	
    	public Account1(String accountNo, double balance) {
    		super();
    		this.accountNo = accountNo;
    		this.balance = balance;
    	}
    	
    	public  void draw(double drawAmount){
    		lock.lock();
    		try{
    			if(!flag){//为假,所以没有人存钱进去。取钱堵塞
    				cond.await();
    			}else{//能够取钱
    				if(balance>=drawAmount){
    					System.out.println(Thread.currentThread().getName()+"取钱:"+drawAmount);
    					balance-=drawAmount;
    					System.out.println("取钱成功,账户剩余金额:"+balance);
    					flag=false;
    					cond.signalAll();//唤醒存钱线程
    				}else{//剩余金额不足
    					System.out.println("想取"+drawAmount+"账户剩余金额不足:"+balance);
    					flag=false;
    					cond.signalAll();//唤醒存钱线程
    				}
    			}
    		}catch(InterruptedException ex){
    			ex.printStackTrace();
    		}finally {
    			lock.unlock();
    		}
    	}
    	
    	public  void deposit(double depositAmount){
    		lock.lock();
    		try{
    			if(flag){
    				cond.await();//没人取钱。则存钱堵塞
    			}else{
    				System.out.println(Thread.currentThread().getName()+"存钱:"+depositAmount);
    				balance+=depositAmount;
    				System.out.println("存钱成功,账户剩余金额:"+balance);
    				flag=true;
    				cond.signalAll();//唤醒取钱线程
    			}
    		}catch(InterruptedException ex){
    			ex.printStackTrace();
    		}finally {
    			lock.unlock();
    		}
    	}
    
    	@Override
    	public int hashCode() {
    		final int prime = 31;
    		int result = 1;
    		result = prime * result + ((accountNo == null) ? 0 : accountNo.hashCode());
    		return result;
    	}
    
    	@Override
    	public boolean equals(Object obj) {
    		if (this == obj)
    			return true;
    		if (obj == null)
    			return false;
    		if (getClass() != obj.getClass())
    			return false;
    		Account1 other = (Account1) obj;
    		if (accountNo == null) {
    			if (other.accountNo != null)
    				return false;
    		} else if (!accountNo.equals(other.accountNo))
    			return false;
    		return true;
    	}
    	
    }

    測试结果是一样的。


    使用堵塞队列(BlockingQueue)控制线程通信:

    java5提供了一个BlockingQueue接口。尽管这个接口是queue的子接口,但它的主要用途不是作为容器。而是作为线程同步的工具。

    BolckingQueue具有一个特征,当生产者试图向BolckingQueue里put元素。假设队列已满,则该线程会被堵塞,直到消费者消费了一个。

    当消费者试图从blockingQueue里take元素时,假设队列为空。则会堵塞。直到生产者生产了一个。

    在队列尾部插入元素,包括add,offer,put,当队列已满时。这三个方法分别抛出异常,返回false。堵塞

    在队列头部删除并返回删除的元素,包括remove,poll,take,当队列已空时,这三个方法分别会抛出异常,返回false,堵塞

    在队列头部取出元素,不删除。

    包括element,peek,当队列已空时,分别抛出异常,返回false

    经典生产者-消费者案例:


    生产者:

    public class Producer extends Thread{
    	private BlockingQueue<String> bq;
    	
    	public Producer(BlockingQueue<String> bq){
    		this.bq=bq;
    	}
    	
    	@Override
    	public void run() {
    		String[] strArr=new String[]{"java","structs","spring"}; 
    		for(int i=0;i<1000;i++){
    			System.out.println(getName()+"生产者准备生产集合元素");
    			try {
    				Thread.sleep(200);
    				//尝试put元素,假设队列已满,则堵塞
    				bq.put(strArr[i%3]);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			System.out.println(getName()+"生产者生产完成"+bq);
    		}
    	}
    }
    
    消费者:

    public class Consumer extends Thread{
    private BlockingQueue<String> bq;
        
        public Consumer(BlockingQueue<String> bq){
            this.bq=bq;
        }
        
        @Override
        public void run() {
            for(int i=0;i<1000;i++){
                System.out.println(getName()+"消费者准备消费集合元素");
                try {
                    Thread.sleep(200);
                    //尝试put元素。假设队列已满。则堵塞
                    bq.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName()+"消费者消费完成"+bq);
            }
        }
    }
    

    測试类:

    public class BlockingQueueTest {
    	public static void main(String[] args) {
    		BlockingQueue<String>bq=new ArrayBlockingQueue<String>();
    		
    		new Producer(bq).start();
    		new Producer(bq).start();
    		new Producer(bq).start();
    		
    		new Consumer(bq).start();
    	}
    }
    
    測试结果:


    測试结果分析:

    能够看出,3个生产者线程都想向队列中put元素,但仅仅要当中随意线程put元素后,其他生产者必须等待(由于堵塞)。等待消费者消费完。


    线程组和未处理的异常:

    线程组:

    java使用ThreadGroup来表示线程组。他能够对一批线程进行分类管理,java同意程序直接对线程组进行控制,对线程组的控制相当于同一时候控制这批线程。

    假设程序没有显示指定线程属于哪个线程组。则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内。

    一旦但线程添加了指定线程组后,该线程一直属于该线程组。直到死亡。线程运行中途不能改变他所属的线程组。

    Thread类提供了例如以下几个构造器来设置新创建的线程属于哪个线程组。

    Thread(ThreadGroup group,Runnable tartget):

    Thread(ThreadGroup group,Rannable target,String name);

    Thread(ThreadGrop group,String name)

    Thread没有提供setThreadGroup(),可是提供getThreadGroup返回ThreadGroup对象。

    ThreadGroup提供了例如以下两个简单的构造器来创建实例。

    ThreadGroup(String name)以指定的线程组名字来创建新的线程组。

    ThreadGroup(ThreadGroup parent,String name)以指定的名字,指定的父线程组创建一个线程组

    ThreadGrop还提供了几个经常使用的方法来操作整个线程组的所有线程。

    int activeCount()返回此线程组中活动线程的数目

    interrupt()中断此线程组的所有线程

    isDaemon()推断该线程组是否是后台线程组

    setDaemon(boolean daemon)把该线程组设为后台线程组

    setMaxPriority(int pri)设置线程组的最高优先级

    案例:

    public class TestThreadGroup extends Thread{
    	public TestThreadGroup(String name){
    		super(name);
    	}
    	
    	public TestThreadGroup(ThreadGroup group,String name){
    		super(group,name);
    	}
    	
    	@Override
    	public void run() {
    		for(int i=0;i<20;i++){
    			System.out.println(getName()+"-线程的i变量"+i);
    		}
    	}
    	
    	public static void main(String[] args) {
    		ThreadGroup mainGroup=Thread.currentThread().getThreadGroup();
    		System.out.println("主线程组的名字:"+mainGroup.getName());
    		new TestThreadGroup("主线程组的线程").start();
    		
    		ThreadGroup tg=new ThreadGroup("新线程组");
    		tg.setDaemon(true);
    		System.out.println("新线程组是否是后台线程组"+tg.isDaemon());
    		new TestThreadGroup(tg,"新线程组").start();
    		
    	}
    }

    结果:

    未处理的异常:

    Thread里还定义一个非常实用的方法:void uncaughtExecptin(Thread t,Throwable e),该方法能够处理该线程组内随意线程所抛出未处理异常。

    从java5開始。java加强了线程的异常处理,假设线程运行过程中抛出了一个未处理异常。jvm在结束该线程之前会自己主动查找是否有相应的Thread.UncaughtExecptionHandler对象,假设找到该处理器的对象。则会调用该对象的uncaughtExecption(Thread t,Throwable e)方法来处理该异常。

    假设该线程组有父线程组。则调用父线程组的uncaughtException来处理异常

    假设该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExecptionHandler()设置),那么就调用该异常处理器来处理该异常

    假设该异常对象是ThreadDeath的对象,则不做不论什么处理。否则将异常跟踪栈的打印信息到system.err错误输出流。并结束该线程。

    案例:

    public class TestThreadExHandler implements UncaughtExceptionHandler{
    
    	@Override
    	public void uncaughtException(Thread t, Throwable e) {
    		System.out.println(t+"线程出现异常:"+e);
    	}
    	
    	public static void main(String[] args) {
    		Thread.currentThread().setUncaughtExceptionHandler(new TestThreadExHandler());
    		
    		int i=1/0;
    		
    		System.out.println("程序正常结束");
    	}
    	
    }
    

    结果:

    分析:

    不难看出异常处理器捕获到异常后。程序仍然不会正常退出。

    这说明异常处理器与通过catch捕获异常是不同的。当使用catch捕获异常时。异常不会向上传递给上一级调用者,可是异常处理器会。


    线程池:

    系统启动一个新线程的成本是比較高的。由于他涉及与操作系统交互,在这样的情况下,就诞生了线程池。


    Java8改进的线程池:

    java5之前开发人员须要自己实现自己的线程池,java5后,新增了一个Executors工厂类来产生线程池,该工厂类包括例如以下几个静态方法来创建线程池。

    newCacheThreadPool()创建一个具有缓存功能的线程池,系统依据须要创建线程。这些线程将会被缓存在线程池中。

    newFixedThreadPool(int nThreads)创建一个可重用的,具有固定线程数的线程池

    newSingleThreadExecutor()创建一个仅仅有单线程的线程池,它相当于调用newFixedThreadPool(1)

    newScheduledThreadPool(int corePoolSize)创建具有指定线程数的线程池

    newSingleThreadScheduledExecutor()创建一个仅仅有一个线程的线程池

    ExecutorService newWorkStealingPool(int parallelism)创建持有足够的线程的线程池来支持给定的并行级别,该方法会使用多个队列来降低竞争。

    (后台线程池)

    ExecutorService newWorkStealingPool()该方法是前一个方法的简化版,假设当前机器有4个cpu,则目标并行级别被设置为4。

    (后台线程池)

    案例1实现自己的线程池:

    public class ThreadPool {
    	LinkedBlockingQueue<Runnable>workQueue=new LinkedBlockingQueue<>(100);
    	
    	public static void main(String[] args) {
    		new ThreadPool().threadPool();
    		
    	}
    	public void threadPool(){
    		
    		ThreadFactory threadFactory=new ThreadFactory() {
    			AtomicInteger atomic=new AtomicInteger(0);
    			@Override
    			public Thread newThread(Runnable r) {
    				Thread thread=new Thread(r);
    				thread.setName("MyThread"+atomic.getAndIncrement());
    				return thread;
    			}
    		};
    		
    		/**corePoolSize核心池最大数量
    		 * maximumPoolSize最大线程池上限个数
    		 * keepAliveTime任务运行完。销毁线程的延时
    		 * unit时间单位   TimeUnit.SECONDS;
    		 * workQueue 用于储存任务的工作队列
    		 * threadFactory
    		 */
    		ThreadPoolExecutor pool=new ThreadPoolExecutor(5, 10, 1, TimeUnit.SECONDS, workQueue, threadFactory);
    		for(int i=0;i<100;i++){
    			pool.execute(new Runnable() {
    				public void run() {
    					method();
    				}
    			});
    		}
    	}
    	
    	
    	private void method(){
    		
    		System.out.println("ThreadName:"+Thread.currentThread().getName()+"进来了");
    		try {
    			Thread.sleep(2000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		
    		System.out.println("ThreadName:"+Thread.currentThread().getName()+"出去了");
    	}
    }
    

    结果:

    案例二用java自带的线程池:

    public class TestJavaThreadPool {
    	public static void main(String[] args) {
    		ExecutorService pool=Executors.newFixedThreadPool(6);
    		Runnable target=new Runnable() {
    			@Override
    			public void run() {
    				for(int i=0;i<100;i++){
    					System.out.println(Thread.currentThread().getName()+"的i的值:"+i);
    				}
    			}
    		};
    		pool.submit(target);
    		pool.submit(target);
    		
    		pool.shutdown();
    	}
    }
    


    Java8增强的ForkJoinPool:

    java7提供了ForkJoinPool来支持将一个任务拆分成多个小任务并行计算。ForkJoinPool是ExecutorService的实现类,因此是一种特殊的线程池。它提供了两种经常使用的构造器:

    ForkJoinPool(int parallelism)创建一个包括parallelism个并行线程的ForkJoinPool.

    ForkJoinPool()以Runtime.availableProcessors()方法的返回值作为parallelism參数来创建ForkJoinPool.(取决于机器的cpu核数)

    java8进一步扩展了ForkJoinPool的功能,添加了通用池功能,ForkJoinPool类通过例如以下两个静态方法提供通用池功能:

    ForkJoinPool commonPool()该方法返回一个通用池,通用池的运行状态不会受shutdown()或showdownNow()的影响。

    int getCommonPoolParalelism()该方法返回通用池的并行级别

    创建了ForkJoinPool实例之后。就能够调用ForkJoinPool的submit或invoke方法来运行指定任务了。ForkJoinPool是一个抽象类,他还有两个抽象子类:

    RecursiveAction和RecursiveTask。当中RecursiveAction代表无返回值的任务。RecursiveTask代表有返回值的任务。


    无返回值的案例(打印0-300的数字。将一个大任务分解成多个小任务并行运行):

    public class 无返回值的ForkJoinPool extends RecursiveAction{
    	private static final int smallTask=50;
    	private int start;
    	private int end;
    	public 无返回值的ForkJoinPool(int start,int end){
    		this.start=start;
    		this.end=end;
    	}
    	@Override
    	protected void compute() {
    		if(end-start<smallTask){//任务足够小。能够运行
    			for(int i=start;i<end;i++){
    				System.out.println(Thread.currentThread().getName()+"的i的值"+i);
    			}
    		}else{
    			//当任务不够小的时候,分解任务
    			int middle=(start+end)/2;
    			无返回值的ForkJoinPool left=new 无返回值的ForkJoinPool(start, middle);
    			无返回值的ForkJoinPool right=new 无返回值的ForkJoinPool(middle, end);
    			left.fork();
    			right.fork();
    		}
    	}
    	
    	public static void main(String[] args) throws InterruptedException {
    		ForkJoinPool pool=new ForkJoinPool();//cpu核数
    		pool.submit(new 无返回值的ForkJoinPool(0, 300));//提交要分解的任务
    		
    		pool.awaitTermination(2, TimeUnit.SECONDS);
    		
    		pool.shutdown();
    	}
    	
    }
    

    结果:

    结果分析:能够看出启动了四个线程。这是由于我的cpu是四核的


    有返回值的案例对一个长度为100的数组进行求和:

    public class 有返回值的ForkJoinPool extends RecursiveTask<Integer>{
    	private static final int smallTask=20;
    	private int arr[];
    	private int start;
    	private int end;
    	
    	public 有返回值的ForkJoinPool(int[] arr, int start, int end) {
    		super();
    		this.arr = arr;
    		this.start = start;
    		this.end = end;
    	}
    
    	@Override
    	protected Integer compute() {
    		int sum=0;
    		if(end-start<smallTask){
    			for(int i=start;i<end;i++){
    				sum+=arr[i];
    			}
    			return sum;
    		}
    		else{
    			int middle =(start+end)/2;
    			有返回值的ForkJoinPool left=new 有返回值的ForkJoinPool(arr, start, middle);
    			有返回值的ForkJoinPool right=new 有返回值的ForkJoinPool(arr, middle, end);
    			left.fork();
    			right.fork();
    			return left.join()+right.join();
    		}
    	}
    	
    	public static void main(String[] args) throws InterruptedException, ExecutionException {
    		int []arr=new int[100];
    		Random random=new Random();
    		int total=0;
    		for(int i=0,len=arr.length;i<len;i++){
    			int temp=random.nextInt(20);
    			total+=(arr[i]=temp);
    		}
    		System.out.println("普通计算总量:"+total);
            ForkJoinPool pool=ForkJoinPool.commonPool();
    		Future<Integer>future=pool.submit(new 有返回值的ForkJoinPool(arr, 0, arr.length));
    		System.out.println("任务分解返回值"+future.get());
    		pool.shutdown();
    	}
    	
    }
    

    结果:

    普通计算总量:887
    任务分解返回值887
    
    结果分析:

    能够看出两种计算结果一样。

    线程相关类:

    ThreadLocal类:

    ThreadLocal,是Thread Local Variable(线程局部变量)的意思,他的功用是为每个使用该变量的线程都提供一个变量值的副本。是每个线程能够独立的改变自己的个副本,而不会和其他线程的副本冲突

    ThreadLocal使用方法非常easy:

    T get()返回线程副本的值

    void remove()删除此线程局部变量中当前线程的值

    void set(T value)改动此线程局部变量中当前线程副本的值

    包装线程不安全的集合:

    ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap等

    假设程序中有多个线程可能訪问这些集合,则能够使用Collections提供的类方法把这些集合包装成线程安全的集合

    <T>Collection<T>synchronizedCollection(Collection<T>c)返回指定collection相应的线程安全的collection

    线程安全的集合类:

    以Concurrent开头的集合类(Doug Lea)

    ConcurrenthashMap, ConcurrentSkipListMap, ConcurrentSkipListSet,ConcurrentLinkedQueue, ConcurrentLinkedDeque

    以CopyOnWrite开头的集合类

    CopyOnWriteArrayLsit  CoprOnWriteArraySet

    当中以Concurrent开头的代表支持并发訪问的集合,他们支持多个线程并发写入訪问,这些操作是线程安全的。有较好的性能。

    不同意存null

    CopyOnWrite开头的集合类会在底层复制一份新的数组。接下来对新的数组运行写入操作,是线程安全的。

    须要频繁的复制数组,性能较差。

    读操作都非常安全

    总结:

    写了大概有6到7个小时,真的累了,可是假设你能受益。我就是值得的,喜欢就赞我吧!

  • 相关阅读:
    [学习笔记]多维偏序
    SCOI2009 游戏
    置换群和Burnside引理,Polya定理
    AC自动机——多个kmp匹配
    51nod 1667 概率好题
    分块——优化的暴力
    [Cqoi2014]数三角形——组合数
    C++ 中的导致编译错误汇总
    哈夫曼树Huffman
    导出查询结果到csv文件
  • 原文地址:https://www.cnblogs.com/llguanli/p/8779654.html
Copyright © 2011-2022 走看看