一.线程的创建和启动
java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每条线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序流的代码)。Java使用run方法来封装这样一段程序。
1.继承Thread类创建线程类
/**继承Thread来创建线程类*/ public class FirstThread extends Thread { private int i; //重写run方法,run方法的方法体就是线程执行体 public void run() { for(;i<10;i++){ System.out.println(this.getName()+":"+i); } } public static void main(String []args){ for(int i=0;i<20;i++){ System.out.println(Thread.currentThread().getName()+" .."+i); if(i==10){ System.out.println("--------------------------------------------"); new FirstThread().start(); new FirstThread().start(); System.out.println("---------------------------------------------"); } } } } 结果:红色部分每次运行都不一致,因为多线程也是并发的 main ..0 main ..1 main ..2 main ..3 main ..4 main ..5 main ..6 main ..7 main ..8 main ..9 main ..10 -------------------------------------------- Thread-0:0 --------------------------------------------- Thread-1:0 Thread-1:1 Thread-1:2 Thread-1:3 Thread-0:1 Thread-1:4 Thread-1:5 main ..11 Thread-1:6 Thread-1:7 Thread-1:8 Thread-1:9 Thread-0:2 Thread-0:3 main ..12 main ..13 ...... |
总结 :从上面结果可以看出Thread-0和Thread-1两条线程输出的i变量都不连续(注意:i变量是FirestThread的实例属性,而不是局部变量,但因为程序每次创建线程都会创建一个FirstThread对象,所以Thread-0和Thread-1不能共享该实例属性)。
使用继承Thread类的方法来创建线程类,多条线程之间无法共享线程类的实例变量。
2.实现Runnable接口创建线程类
public class SecondThread implements Runnable { private int i; public void run() { for(;i<20;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } } public static void main(String [] args){ for(int i=0;i<20;i++){ System.out.println(Thread.currentThread().getName()+" .."+i); if(i==10){ SecondThread st=new SecondThread(); //通过new Thread( Runable target,String name)来创建新线程 new Thread(st,"线程1").start(); new Thread(st,"线程2").start(); } } } 结果:红色部分每次运行都不一致,因为多线程也是并发的 main ..0 main ..1 main ..2 main ..3 main ..4 main ..5 main ..6 main ..7 main ..8 main ..9 main ..10 -------------------------------------------- 线程1:0 -------------------------------------------- 线程1:1 线程2:1 线程2:3 main ..11 线程2:4 线程2:5 线程2:6 线程1:2 线程2:7 线程2:9 线程2:10 线程2:11 线程2:12 线程2:13 main ..12 线程2:14 线程2:15 线程2:16 线程2:17 线程1:8 线程2:18 main ..13 main ..14 线程1:19 main ..15 main ..16 main ..17 。。。。 |
总结:根据源代码中Thread类构造方法 Ruanalbe接口对象target只能作为参数传递到Thread构造方法中,所以多个线程可以共用一个Runnable对象,因为都用同一个Runnable对象所以在Runnable实现类的实例变量也可以共享了。
所以Runable非常适合多个相同线程来处理同一份资源的情况。
二.线程的生命周期
1.New新建 :当线程被创建时,该线程处于新建状态,此时它和其他java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量的值。(此时的线程没有表现出任何表现出任何线程的动态特征,程序也不会执行线程的线程执行体)new
Thread()||new Thread(Runnable target,String name)。
2.Runnable就绪:就绪也就是说启动线程,但是启动线程使用start方法,而不是run方法!永远不要调用线程对象的run()方法!调用start方法来启动线程,系统会将该run方法当成线程执行体来处理。如果直接调用线程对象的run方法。则run方法会立即执行,且在这个run方法的执行体未执行结束前其他线程无法并发执行(即系统会将run方法当做一个普通对象的普通方法,而不是线程执行体对待)
附1:如果有一个主线程,一个子线程。当根据逻辑代码该调用子线程时不一定会立即调用,为了想在子线程start()后立即调用子线程,可以考虑使用Thread.sleep(1),这样会让当前线程(主线程)睡眠1毫秒,因为cpu在这1毫秒中是不会休息的,这样就会去执行一条处于就绪状态的线程。
附2:不能对已经处于就绪状态的线程,再次使用start()
3.Running 运行:当处于就绪状态时,该线程获得cpu,执行体开始运行,就处于运行状态了。
4.Blocked 阻塞:线程不可能一直处于运行状态(线程执行体足够短,瞬间就可以完成的线程排除),线程会在运行过程中需要被中断,因为是并发,目的是会让其他线程获得执行的机会,线程的调度细节取决于OS采用的策略。(抢占式调度xp
win7 linux unix..)。如果是一些特殊的小型设备可能采用 协作式调度(只有线程自己调用它的sleep()或yield()才会放弃所占用的资源)。
5.Dead死亡:根据上图所示。测试测试某条线程是否已经死亡,可以调用线程对象的isAlive()方法,当线程处于就绪,运行,阻塞时,返回true。线程处于新建,死亡时返回false。
不能对已经死亡的线程调用start()方法使它重新启动,死亡就是死亡,是不能再次作为线程执行的。
当主线程结束时候,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,它不会受到主线程的影响。
三.控制线程
1.join线程:
让一个线程等待另一个线程完成的方法:join()。当在某个程序执行流中调用其他线程的join()方法,那该执行流对应的线程就会阻塞,知道被join()加入的join线程完成为止。join方法通常有使用线程的程序调用,将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用
主线程来进一步操作(Thread t=new Thread();t.start();t.join简单来说就是加入到t线程。等t线程执行完成后才会返回出来执行线程。)
Join方法有三种重用形式:
Join():等待被join的线程执行完成
Join(long millis):等待join线程的时间最长为millis毫秒,如果在这个时间内,被join的线程还没有执行结束则不再等待)
Join(long millis,int nanos)千分之一毫秒(不用)
public class JoinThread implements Runnable{ @Override public void run() { for(int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } } public static void main(String [] args) throws InterruptedException{ //实例化一个Runnable JoinThread jt=new JoinThread(); //创建一个线程 new Thread(jt).start(); for(int i=0;i<10;i++){ if(i==3){ Thread th=new Thread(jt); //启动第二个线程 th.start(); //main的线程中调用了th线程的join方法 //让第二个线程执行完成后再执行main th.join(); } System.out.println(Thread.currentThread().getName()+":"+i); } } } 结果: Thread-0:0 Thread-0:1 Thread-0:2 main:0 main:1 Thread-0:3 main:2 Thread-0:4 Thread-1:0 Thread-1:1 Thread-1:2 Thread-1:3 Thread-1:4 main:3 main:4 main:5 main:6 main:7 main:8 main:9 |
2.后台线程:
public class DaemonThread 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){ //要将前台线程转换成后台线程,需要在该线程刚新建还未start()之前转换。main线程也是前台线程 //所有前台线程死亡时,后台线程也就随之死亡。 DaemonThread dt=new DaemonThread(); Thread td=new Thread(dt,"线程1"); System.out.println("main方法是否是后台线程"+Thread.currentThread().isDaemon()); System.out.println("td线程最初是否是后台线程"+td.isDaemon()); //指定td为后台线程 td.setDaemon(true); System.out.println("td线程执行setDaemon方法后是否是后台线程"+td.isDaemon()); //就绪启动后台线程 td.start(); for(int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName()+" "+i); } } } 结果:只要前台线程结束,后台线程也会随之结束,并不是马上结束 main方法是否是后台线程false td线程最初是否是后台线程false td线程执行setDaemon方法后是否是后台线程true main 0 main 1 线程1:0 线程1:1 main 2 线程1:2 线程1:3 main 3 线程1:4 线程1:5 main 4 线程1:6 线程1:7 线程1:8 线程1:9 线程1:10 线程1:11 线程1:12 线程1:13 |
3.线程睡眠:sleep
/** * 线程睡眠:sleep有两种重载形式: * static void sleep(long millis) * static void sleep(long millis,int nanos) * */ public class SleepThread { public static void main(String [] args) throws InterruptedException{ for(int i=0;i<5;i++){ System.out.println("线程:"+Thread.currentThread().getName()+"当前时间:"+new Date()); //让当前线程暂停2秒 Thread.sleep(2000); } } } 结果: 线程:main当前时间:Fri Nov 04 18:51:33 CST 2011 线程:main当前时间:Fri Nov 04 18:51:35 CST 2011 线程:main当前时间:Fri Nov 04 18:51:37 CST 2011 线程:main当前时间:Fri Nov 04 18:51:39 CST 2011 线程:main当前时间:Fri Nov 04 18:51:41 CST 2011 |
4.线程让步(yield)
/** * yield()方法是一个和sleep方法有点类似的静态方法。yield也可以让当前正在执行的线程暂停 * 但它不会阻塞该线程,它只是将该线程转入就绪状态。yield只是让当前线程暂停一会儿,让系统的 * 调度器重新调度一次(完全可能的情况是:当一个线程调用了yield方法暂停之后,线程调度器又马上 * 将其调度出来重新执行。) * 实际上,当前线程调用了yield方法后,只有优先级和当前线程相同,甚至优先级高于当前线程的处于 * 就绪状态的线程才会获得执行机会。 * */ public class YieldThread implements Runnable{ @Override public void run() { for(int i=0;i<50;i++){ System.out.println(Thread.currentThread().getName()+":"+i); if(i==20){ Thread.yield(); } } } public static void main(String [] args){ //启动第一条子线程 Thread td1=new Thread(new YieldThread(),"线程1"); //最高级 //td1.setPriority(Thread.MAX_PRIORITY); //启动第二条子线程 Thread td2=new Thread(new YieldThread(),"线程2"); //最低级 td2.setPriority(Thread.MIN_PRIORITY); td1.start(); td2.start(); System.out.println(Thread.currentThread().getName()); } } |
总结:sleep和yield区别
A.sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。而yield只会给优先级>=当前优先级的线程执行机会
B.Sleep方法会将线程转入阻塞状态,知道经过阻塞时间才会转入就绪状态。而yield是不会将线程转入阻塞状态的,它只是强制当前线程进入就绪状态。
C.Sleep会抛出InterruptedException异常。而yield没有声明任何异常
D.Sleep方法比yield方法有更好的移植性。
E.通常不依靠yield来控制并发线程控制
四.多线程的同步
以一个取钱列子来分析:(用户登录那些省略)
Accout类:
/**银行取钱,账户类*/ public class Accout { //账户编号 private String accoutNo; //账户余额 private double balance; //账户名称 private String accoutName; public Accout(){ super(); } public Accout(String accoutNo,String accoutName, double balance) { super(); this.accoutNo = accoutNo; this.balance = balance; this.accoutName=accoutName; } public String getAccoutNo() { return accoutNo; } public void setAccoutNo(String accoutNo) { this.accoutNo = accoutNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public String getAccoutName() { return accoutName; } public void setAccoutName(String accoutName) { this.accoutName = accoutName; } //根据accoutNohe来计算Accout的hashcode和判断equals @Override public int hashCode() { return accoutNo.hashCode(); } @Override public boolean equals(Object obj) { if(obj!=null&&obj.getClass()==Accout.class){ Accout target=(Accout)obj; return target.getAccoutNo().equals(accoutNo); } return false; } } |
DrawThread类:
/**取钱的线程类*/ public class DrawThread implements Runnable{ //模拟用户账户 private Accout accout; //当前取钱线程所希望取得值 private double drawAmount; public DrawThread(Accout accout, double drawAmount) { super(); this.accout = accout; this.drawAmount = drawAmount; } //如果多个线程修改同一个共享数据时,会发生数据安全问题 public void run() { //账户余额大于取款金额时 if(accout.getBalance()>=drawAmount){ //取款成功 System.out.println(Thread.currentThread().getName()+
accout.getAccoutName()+"取款成功:吐出钞票:"+drawAmount); //修改余额 accout.setBalance(accout.getBalance()-drawAmount); System.out.println("当前余额为:"+accout.getBalance()); } //账户金额不够时 else{ System.out.println("账户金额不够,您的余额只有"+accout.getBalance()); } } } |
TestDraw测试类:
public class TestDraw { public static void main(String[]args) throws InterruptedException{ //创建一个用户 Accout acct=new Accout("123456", "小明", 1000); //模拟四个线程同时操作 DrawThread dt=new DrawThread(acct,600); //DrawThread dt1=new DrawThread(acct,800); Thread th1=new Thread(dt,"线程1"); Thread th2=new Thread(dt,"线程2"); Thread th3=new Thread(dt,"线程3"); Thread th4=new Thread(dt,"线程4"); th4.join(); th1.start(); th2.start(); th3.start(); th4.start(); } } |
1.同步代码块
Java多线程支持引入了同步监视器来解决多线程安全,同步监视器的常用方法就是同步代码块:
Synchronized(obj){ //...同步代码块 } |
括号中的obj就是同步监视器:上面的语句表示:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。这就意味着任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。
虽然java中对同步监视器使用的对象没有任何要求,但根据同步监视器的目的:阻止两条线程对同一个共享资源进行并发访问。所以一般将可能被并发访问的共享资源充当同步监视器。
修改后如下:
public void run() { synchronized (accout) { //账户余额大于取款金额时 if(accout.getBalance()>=drawAmount){ //取款成功 System.out.println(Thread.currentThread().getName()+accout.getAccoutName()+"取款成功:吐出钞票:"+drawAmount); //修改余额 accout.setBalance(accout.getBalance()-drawAmount); System.out.println("当前余额为:"+accout.getBalance()); } //账户金额不够时 else{ System.out.println("账户金额不够,您的余额只有"+accout.getBalance()); } } } |
2.同步方法
(synchronized可以修饰方法,代码块。不能修饰属性和构造方法)
除了同步代码块外还可以使用synchronized关键字来修饰方法,那么这个修饰过的方法称为同步方法。对于同步方法来说,无需显式指定同步监视器,同步方法的同步监视器是this,也就是该对象本身,也就是上面TestDraw中定义的Accout类型的acct。
public void run() { draw(); } public synchronized void draw(){ if(accout.getBalance()>=drawAmount){ //取款成功 System.out.println(Thread.currentThread().getName()+accout.getAccoutName()+"取款成功:吐出钞票:"+drawAmount); //修改余额 accout.setBalance(accout.getBalance()-drawAmount); System.out.println("当前余额为:"+accout.getBalance()); } //账户金额不够时 else{ System.out.println("账户金额不够,您的余额只有"+accout.getBalance()); } } |
这里最好是将draw()方法写到Accout中,而不是像之前将取钱内容保存在run方法中,这种做法才更符合面向对象规则中的DDD(Domain
Driven Design领域驱动设计)
对于可变类的同步会降低程序运行效率。不要对线程安全类德所有方法进行同步,只对那些会改变共享资源的方法同步。
单线程环境(可以使用线程不安全版本保证性能) 多线程环境(线程安全版本)
2.1同步监视器的锁定什么时候释放
A.当前线程的同步方法,同步块执行结束。当前线程释放同步监视器
B.在同步方法,块中遇到break,return终止了该代码块,方法.释放
C.在代码块,方法中出现Error,Exception
D.执行同步时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,释放
2.2同步监视器的锁定在以下情况不会被释放
A.执行同步时,程序调用Thread.sleep(),Thread.yield()方法来暂停当前线程的执行,不会释放
B.执行同步时,其他线程调用了该线程的suspend方法将该线程挂起,不会释放(但是尽量避免使用suspend和resume来控制线程,容易导致死锁)
3.同步锁(Lock)
在jdk1.5后,java除了上面两种同步代码块和同步方法之外,还提供了一种线程同步机制:它通过显示定义同步锁对象来实现同步,在这种机制下,同步锁应该使用Lock对象充当。
Lock是控制多个线程对共享资源进行访问的工具,每次只能有一个线程对Lock对象枷锁,线程开始访问共享资源之前应先获得Lock对象。(特例:ReadWriteLock锁可能允许对共享资源并发访问)。在实现线程安全控制中,通常喜欢使用可重用锁(ReentrantLock),使用该Lock对象可以显示的加锁,释放锁。
CODE:
//声明锁对象 private final ReentrantLock relock=new ReentrantLock(); public void run(){ draw(); } public void draw() { //加锁 relock.lock(); try{ //账户余额大于取款金额时 if(accout.getBalance()>=drawAmount){ //取款成功 System.out.println(Thread.currentThread().getName()+accout.getAccoutName()+"取款成功:吐出钞票:"+drawAmount); //修改余额 accout.setBalance(accout.getBalance()-drawAmount); System.out.println("当前余额为:"+accout.getBalance()); } //账户金额不够时 else{ System.out.println("账户金额不够,您的余额只有"+accout.getBalance()); } } //释放锁 finally{ relock.unlock(); }} |
总结:
同步方法和同步代码块使用与共享资源相关的,隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。
虽然同步方法,代码块的范围机制使多线程安全编程非常方便,还可以避免很多涉及锁的常见编程错误,但有时也需要以更灵活的方式使用锁。Lock提供了同步方法,代码块中没有的其他功能(用于非块结构的tryLock方法,获取可中断锁lockInterruptibly方法,获取超时失效锁的tryLock(long,TimeUnit)方法)。
ReentrantLock锁具有重入性,即线程可以对它已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,线程每次调用lock()加锁后,必须显示的调用unlock()释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
4.死锁
当两个线程相互等待对方释放同步监视器的时候就会发生死锁,一旦出现死锁,整个程序既不会发生任何异常,也不会有任何提示,只是所有线程处于阻塞状态,无法继续。
五.线程通信
线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法准确控制线程的轮换执行,可以通过以下方法来保证线程协调运行.
1.线程协调运行
如果对于一些方法是用同步方法或者同步代码块,那么可以调用Object类提供的wait(),notify(),notifyAll()。这三个不属于Thread,属于Object类,但必须由同步监视器来调用(同步方法的监视器是this:则需this.wait()....,同步代码块的监视器是括号中的obj.wait());
2.使用条件变量来控制协调
如果程序没有使用sychronized来保证同步,可以使用Lock来保证同步,则系统中就不存在隐式的同步监视器对象,也就不能使用wait,notify,notifyAll来协调了。
private final Lock lock=new ReentrantLock(); private final Condition cond=lock.newCondition(); |
通过上面两行代码,条件Condition实际是绑定在一个Lock对象上的。
相对应的Condition类也有三个方法:await(),signal(),signalAll()
Account账号类:
代码:
/** 账户类,用面向对象的DDD设计模式来设计 */ /* * DDD领域驱动模式,即将每个类都认为是一个完备的领域对象,
例如Account代表账户类,那么就应该提供用户账户的相关方法(存,取,转),而不是将 * setXXX方法暴露出来任人操作。
只要设计到DDD就需要重写equals和hashcode来判断对象的一致性 */ public class Account { // 账户编码 private String accountNo; // 账户余额 private double balance; // 标示账户是否已有存款(此项目为了测试存入款就需要马上取出) private boolean flag = false; // private final Lock lock=new ReentrantLock(); // private final Condition cond=lock.newCondition(); public Account() { super(); } public Account(String accountNo, double balance) { super(); this.accountNo = accountNo; this.balance = balance; } // 取款(利用同步方法) public synchronized void draw(double drawAmount) { // 如果flag为假,没人存款进去,取钱方法(利用wait)阻塞(wait阻塞时,当前线程会释放同步监视器) try { if (!flag) { this.wait();//条件 cond.await(); } //否则执行取钱 else { // System.out.println("账户余额:"+balance); System.out.println(Thread.currentThread().getName()+"---->取钱:"+drawAmount); balance-=drawAmount; System.out.println("账户余额: "+balance); //设置flag(限定一个操作只能取一次钱) flag=false; //唤醒其他wait()线程 this.notifyAll();//cond.signalAll(); } } catch (InterruptedException e) { e.printStackTrace(); } } //存款 public synchronized void deposit(double depositAmount){ //如果flag为真,证明有人存钱了,存钱阻塞 try { if (flag) { this.wait(); //cond.await(); } //否则执行存款 else { // System.out.println("账户余额:"+balance); System.out.println(Thread.currentThread().getName()+"---->存钱:"+depositAmount); balance+=depositAmount; System.out.println("账户余额: "+balance); //设置flag(限定一个操作只能取一次钱) flag=true; //唤醒其他wait()线程 this.notifyAll(); //cond.signalAll(); } } catch (InterruptedException e) { e.printStackTrace(); } } // DDD设计模式重写equals和hashcode(判断用户是否一致,
只需要判断他们的账号编码就可以了,不需要再判断整个对象,提高性能) @Override public int hashCode() { return accountNo.hashCode(); } @Override public boolean equals(Object obj) { if (obj != null && obj.getClass() == Account.class) { Account account = (Account) obj; return account.getAccountNo().equals(accountNo); } return false; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } |
取钱线程:
public class DrawThread implements Runnable { /* * 模拟用户 */ private Account account; //用户取钱数 private double drawAmount; public DrawThread(Account account, double drawAmount) { super(); this.account = account; this.drawAmount = drawAmount; } @Override public void run() { //重复10次取钱操作 for(int i=0;i<10;i++){ account.draw(drawAmount); } } } |
存钱线程:
public class DepositThread implements Runnable{ /* * 模拟用户 */ private Account account; //用户存钱数 private double depositAmount; public DepositThread(Account account, double depositAmount) { super(); this.account = account; this.depositAmount = depositAmount; } @Override public void run() { //重复10次存钱操作 for(int i=0;i<10;i++){ account.deposit(depositAmount); } } } |
测试类:
public class Test { public static void main(String []args){ //创建一个用户没余额,等待先存款后取钱 Account acct=new Account("123张",0); //取款800 new Thread(new DrawThread(acct,800),"取款者").start(); //存款2个人 new Thread(new DepositThread(acct,800),"存款者甲").start(); new Thread(new DepositThread(acct,800),"存款者乙").start(); new Thread(new DepositThread(acct,800),"存款者丙").start(); } } 结果: 存款者甲---->存钱:800.0 账户余额: 800.0 取款者---->取钱:800.0 账户余额: 0.0 存款者丙---->存钱:800.0 账户余额: 800.0 取款者---->取钱:800.0 账户余额: 0.0 存款者甲---->存钱:800.0 账户余额: 800.0 取款者---->取钱:800.0 账户余额: 0.0 存款者丙---->存钱:800.0 账户余额: 800.0 取款者---->取钱:800.0 账户余额: 0.0 存款者甲---->存钱:800.0 账户余额: 800.0 取款者---->取钱:800.0 账户余额: 0.0 存款者丙---->存钱:800.0 账户余额: 800.0 |
但根据上面情况来看,显示用户被阻塞无法继续向下执行,这是因为存钱有三个线程
共有3*10=30次操作,而取钱只有10次,所以阻塞。
阻塞和死锁是不一致的,这里阻塞只是在等待取钱。。。
3.使用管道流(通信)
上面的1,2两种线程操作,与其称为线程间的通信,不如称为线程之间协调运行的控制策略还要恰当些。如果需要在两条线程之间惊醒更多的信息交互,则可以考虑使用管道流进行通信。
管道流有3中形式:
1.字节流:PipedInputStream,PipedOutputStream
2.字符流:PipedReader,PipedWriter
3.新IO的管理Channel:Pipe.SinkChannel,Pipe.SourceChannel
使用管道的步骤:
1.new创建管道输入输出流
2.使用管道输入或输出流的connect方法连接这两个输入输出流
3.将两个管道流分别传入两个线程
4.两个线程分别依赖各自的流来进行通信
但是因为两个线程属于同一个进程,它们可以非常方便的共享数据,利用共享这个方式才应该是线程之间进行信息交流的最好方式,而不是使用管道流。如果是操作诸如聊天室那样的话,用管道通信效果会好些,共享资源的话,性能太低,出错概率高。
CODE:
/**读取管道中的信息线程*/ public class ReaderThread implements Runnable { private PipedReader pr; private BufferedReader br; public ReaderThread() { super(); } public ReaderThread(PipedReader pr) { super(); this.pr = pr; //包装管道流 this.br=new BufferedReader(pr); } public void run(){ String buffer=null; System.out.println(Thread.currentThread().getName()); try{ //开始逐行读取管道流数据(假定管道流数据时字符流) System.out.println("------打印管道中的数据-------"); while((buffer=br.readLine())!=null){ System.out.println(buffer); } } catch(IOException e){ e.printStackTrace(); } finally{ try { if(br!=null) br.close(); } catch (IOException e) { e.printStackTrace(); } } } } |
/**像管道流中写入数据*/ public class WriterThread implements Runnable { //定义一个数组来充当向管道中输入数据 String []str=new String[]{"1.www.csdn.net论坛","2.www.google.com谷歌",
"3.www.hibernate.orgHibernate","4.www.xiami.com虾米"}; private PipedWriter pw; public WriterThread() { } public WriterThread(PipedWriter pw) { this.pw = pw; } public void run(){ System.out.println(Thread.currentThread().getName()); try{ //向管道流中写入数据,以供读取 for(int i=0;i<100;i++){ pw.write(str[i%4]+"\n"); } } catch(IOException e){ e.printStackTrace(); } finally{ try { if(pw!=null){ pw.close(); } }catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } |
public class TestPiped { public static void main(String[] args) { PipedReader pr = null; PipedWriter pw = null; try { pw = new PipedWriter(); pr = new PipedReader(); // 链接管道 pr.connect(pw); new Thread(new ReaderThread(pr),"读取管道线程").start(); new Thread(new WriterThread(pw),"写入管道线程").start(); } catch (IOException e) { e.printStackTrace(); } } } 结果: 读取管道线程 ------打印管道中的数据------- 写入管道线程 1.www.csdn.net论坛 2.www.google.com谷歌 3.www.hibernate.orgHibernate 4.www.xiami.com虾米 1.www.csdn.net论坛 2.www.google.com谷歌 3.www.hibernate.orgHibernate 4.www.xiami.com虾米 1.www.csdn.net论坛 .....(一共100条) |
六.线程组(ThreadGroup)
1.线程组介绍:
线程组可以对一批线程进行分类管理,Java也允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这批线程。用户创建的所有线程都属于指定线程组,如果没有显示指定线程属于哪个线程组,那么这个线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内:例如A创建B线程,B没有指定线程组,那么A和B属于同一个线程组。
一旦某个线程加入到指定线程组,那么该线程就一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组(中途不能改变线程组,所以Thread类只有getThreadGroup()方法来获得线程组,而没有set方法。)。
public final ThreadGroup getThreadGroup() { return group; } |
Thread类中构造方法:
其中参数的个数根据情况而定,init中会根据参数个数而变,没这个参数的就直接null
long类型就0.
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { init(group, target, name, stackSize); } |
ThreadGroup类中的构造方法:
//不属于任何一个线程组,是一个必须的用来创建系统线程的组 /** * Creates an empty Thread group that is not in any Thread group. * This method is used to create the system Thread group. */ private ThreadGroup() { // called from C code this.name = "system"; this.maxPriority = Thread.MAX_PRIORITY; this.parent = null; } ------------------------------------------------------ private ThreadGroup(Void unused, ThreadGroup parent, String name) { this.name = name; this.maxPriority = parent.maxPriority; this.daemon = parent.daemon; this.vmAllowSuspension = parent.vmAllowSuspension; this.parent = parent; parent.add(this); } //指定线程组名创建一个线程组 public ThreadGroup(String name) { this(Thread.currentThread().getThreadGroup(), name); } //指定的父线程和线程组名来创建一个线程组 public ThreadGroup(ThreadGroup parent, String name) { this(checkParentAccess(parent), parent, name); } |
看出上面两个public构造器都需要指定一个名字给线程组,所以线程组总是具有一个字符串名字,该名称可调用ThreadGroup的getName()获得,但不允许改变线程组的名字。
源码setMaxPriority(int pri)
public final void setMaxPriority(int pri) { int ngroupsSnapshot; ThreadGroup[] groupsSnapshot; synchronized (this) { checkAccess(); if (pri < Thread.MIN_PRIORITY || pri > Thread.MAX_PRIORITY) { return; } maxPriority = (parent != null) ? Math.min(pri, parent.maxPriority) : pri; ngroupsSnapshot = ngroups; if (groups != null) { groupsSnapshot = Arrays.copyOf(groups, ngroupsSnapshot); } else { groupsSnapshot = null; } } for (int i = 0 ; i < ngroupsSnapshot ; i++) { groupsSnapshot[i].setMaxPriority(pri); } } |
例子:
/**测试线程组*/ public class TestThread implements Runnable { //指定线程组 @Override public void run() { for(int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+"线程"+i+
"属于"+Thread.currentThread().getThreadGroup().getName()+"线程组"); } } } public class ThreadGroupTest { public static void main(String [] args){ //获取主线程的线程组 ThreadGroup mainGroup=Thread.currentThread().getThreadGroup(); System.out.println("主线程的组的名字:"+mainGroup.getName()); System.out.println("主线程组是否属于后台线程组:"+mainGroup.isDaemon()); //新建一个线程组名字为“新线程组”,不设置父线程组 ThreadGroup tg=new ThreadGroup("私人"); tg.setDaemon(true); System.out.println(tg.getName()+"是否是后台线程组:"+tg.isDaemon()); Thread th=new Thread(tg,new TestThread(),"线程1"); System.out.println("1活动的线程有"+tg.activeCount()); th.start(); Thread th1=new Thread(tg,new TestThread(),"线程2"); th1.start(); System.out.println("2活动的线程有"+tg.activeCount()); //Thread t1=new Thread(); } } 结果: 主线程的组的名字:main 主线程组是否属于后台线程组:false 私人是否是后台线程组:true 1活动的线程有0 2活动的线程有2 线程1线程0属于私人线程组 线程1线程1属于私人线程组 线程2线程0属于私人线程组 线程1线程2属于私人线程组 线程2线程1属于私人线程组 线程1线程3属于私人线程组 线程2线程2属于私人线程组 线程1线程4属于私人线程组 线程2线程3属于私人线程组 线程1线程5属于私人线程组 线程2线程4属于私人线程组 线程1线程6属于私人线程组 线程2线程5属于私人线程组 线程1线程7属于私人线程组 线程2线程6属于私人线程组 线程2线程7属于私人线程组 线程2线程8属于私人线程组 线程2线程9属于私人线程组 线程1线程8属于私人线程组 线程1线程9属于私人线程组 |
2.线程异常处理
不想在多线程中遇到无谓的Exception,从jdk1.5后,java加强了线程的异常处理,如果线程执行过程中抛出了一个未处理的异常,JVM在结束该线程之前就会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理对象,将会调用该对象的uncaughtException(Thread
t,Throwable e)方法来处理该异常。
自定义线程异常需要继承Thread.UncaughtExceptionHandler
Thread源码:
public interface UncaughtExceptionHandler { /** * Method invoked when the given thread terminates due to the * given uncaught exception. * <p class="artcon">Any exception thrown by this method will be ignored by the * Java Virtual Machine. * @param t the thread * @param e the exception */ void uncaughtException(Thread t, Throwable e); } |
Thread类中提供两个方法来设置异常处理器:
1.static setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler
eh)
为该线程类的所有线程实例设置默认的异常处理器
源码:
Public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission( new RuntimePermission("setDefaultUncaughtExceptionHandler") ); } defaultUncaughtExceptionHandler = eh; } |
2.setUncaughtExceptionHandler(UncaughtExceptionHandler
eh)
为指定线程实例设置异常处理器
源码:
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) { checkAccess(); uncaughtExceptionHandler = eh; } |
其实ThreadGroup类继承了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器。
所以可以认为当一个线程出现异常时,JVM首先会调用该线程的异常处理器(setUncaughtExceptionHandler),如果找到就执行该异常。没找到就会调用该线程所属线程组的异常处理器。
ThreadGroup类中异常处理器
源码:
public void uncaughtException(Thread t, Throwable e) { if (parent != null) { parent.uncaughtException(t, e); } else { Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler(); if (ueh != null) { ueh.uncaughtException(t, e); } else if (!(e instanceof ThreadDeath)) { System.err.print("Exception in thread \"" + t.getName() + "\" "); e.printStackTrace(System.err); } } } |
例子:
/** * 定义自己的异常类 */ class MyEx implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println(t+"线程出现了异常:"+e); } } /**为主线程设置异常处理器,当程序开始运行时抛出未处理的异常,自定义的异常处理器会起作用*/ class MyThread extends Thread{ public void run(){ int a=5/0; } } public class ExHandler { public static void main(String []args){ Thread td=new MyThread(); //为指定的td线程实例设置异常处理器 td.setUncaughtExceptionHandler(new MyEx()); td.start(); //设置主线程的异常类 Thread.currentThread().setUncaughtExceptionHandler(new MyEx()); byte [] b=new byte[2]; System.out.println(b[2]); } } 结果: Thread[main,5,main]线程出现了异常:java.lang.ArrayIndexOutOfBoundsException: 2 Thread[Thread-0,5,main]线程出现了异常:java.lang.ArithmeticException: / by zero |
七.Callable和Future接口
C#可以把任意方法包装成线程执行体,包括那些有返回值的方法。Java也从jdk1.5开始,加入了Callable接口用来扩展Runnable接口的功能,Callable接口提供一个call()来增强Runnable的run()。因为call()可以有返回值,可以声明抛出异常。
但是Callable是新增的接口 并没有继承Runnable接口,那么肯定不能作为Runnable
target来直接作为Thread构造方法的参数。必须由一个中间的类来包装Callable对象。这个类就是实现了Future接口(继承至Runnable接口)的FutureTask类。
CODE:
import java.util.concurrent.Callable; public class CallableThread implements Callable<Integer> { @Override public Integer call() throws Exception { int i=0; for(;i<6;i++){ System.out.println(Thread.currentThread().getName()+"循环:"+i); } return i; } } ---------------------------------------------------------------------------- public class TestCallable { public static void main(String []args){ try { CallableThread ct=new CallableThread(); FutureTask<Integer> target=new FutureTask<Integer>(ct); for(int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName()+"循环变量:"+i); if(i==2){ new Thread(target,"子线程").start(); //boolean isDone():如果Callable任务已完成,则返回true,否则返回false System.out.println(target.isDone()); Thread.sleep(1); } } //V get():返回Callable任务里call()的返回值,调用该方法会导致阻塞,必须等到子线程结束时才会得到返回值 System.out.println("子返回值是:"+target.get()); System.out.println(target.isDone()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } ----------- 结果: main循环变量:0 main循环变量:1 main循环变量:2 false 子线程循环:0 子线程循环:1 子线程循环:2 子线程循环:3 子线程循环:4 main循环变量:3 子线程循环:5 main循环变量:4 子返回值是:6 true |
八.线程池
Jdk1.5后java也内置支持线程池,为了提高性能。(当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池)
Jdk1.5提供一个Executors工厂类来产生线程池。工厂类中包含5个静态工厂方法来创建线程池:
一.返回类型是ExecutorService 的共3个:
1. newFixedThreadPool(int nThreads)
创建一个可重用的,具有固定线程数的线程池
CODE:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } |
2. ExecutorService newCachedThreadPool()
创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
CODE:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } |
3.newSingleThreadExecutor()
创建一个只有单线程的线程池,等于newFixedThreadPool(1)。
CODE:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } |
二.返回类型是ScheduledExecutorService(是ExecutorService的子类)
的共2个:
1.newSingleThreadScheduledExecutor()
创建只有一条线程的线程池,它可以在指定延迟后执行线程任务
CODE:
public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1)); } |
2.newScheduledThreadPool(int corePoolSize)
创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。其中参数指池中所保存的线程数,即使线程时空闲的也被保存在线程池内。
CODE:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } |
ExecutorService代表尽快执行线程的线程池(只要线程池中有空闲线程立即执行线程任务),程序只需要传入Runnable或Callable对象即可。
ExecutorService提供三个方法来执行线程:
1.Future<?>submit(Runnable target):Runnable中run是没有返回值的所以执行完成后返回null,可以调用Future的isDone(),isCanclled()来判断当前target的执行状态。
2.<T>Future<T>submit(Runnable
target,T result):因为Runnable中run是没有返回值,这里显示指定该Runnable对象线程执行完成返回一个值,这个值就是result。
3.<T>Future<T>submit(Callable<T>
target):Callable中的call是有返回值的
ScheduledExecutorService提供以下四个方法:
1.ScheduledFuture<V> schedule(Callable(V)
c,long delay,TimeUnit unit):指定c任务将在delay延迟后执行。
2.ScheduledFuture<?> shedule(Runnable
r,long delay,TimeUnit unit):指定r任务将在delay延迟后执行。
3.ScheduledFuture<?> scheduleAtFixedRate(Runnable
r,long initialDelay,long period,TimeUnit unit):指定r任务将在delay延迟后执行,而且以设定的频率重复执行(在initialDelay后开始执行,然后开始依次在initialDelay+period,initialDelay+period*2...处重复执行)。
4.ScheduledFuture<?>scheduledWithFixedDelay(Runnable
r,long nitialDelay,long delay,TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行中止和下一次执行开始之间都存在给定的延迟。如果任务的任意依次执行时异常,就会取消后序执行。否则,只能通过程序来显示取消或中止该任务。
当用完一个线程池后,应该调用该线程池的shutdown()方法,启用了shutdown()方法后线程池不再接受新任务,但会将以前所有已提交的任务执行完成,然后启动线程池的关闭序列。
当线程池所有任务都执行完成后,池中所有线程都会死亡,另外也可以调用线程池的shutdownNow()方法来关闭线程池,该方法试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
例子:
class TestThread implements Runnable{ public void run() { for(int i=0;i<10;i++){ System.out.println(Thread.currentThread().getName()+":"+i); } } } public class ThreadPoolTest { public static void main(String []args){ //创建一个固定线程数为3的线程池 ExecutorService pool=Executors.newFixedThreadPool(3); //像线程池提交10个线程 for(int i=0;i<10;i++){ pool.submit(new TestThread()); if(i==4){ } } //执行完成后关闭线程 pool.shutdown(); } } 结果: pool-1-thread-2:1 pool-1-thread-2:2 pool-1-thread-2:3 pool-1-thread-1:9 pool-1-thread-3:8 pool-1-thread-3:9 pool-1-thread-2:4 ...... |
九.附录:线程相关的类
1.ThreadLocal(线程局部变量)
为了避免并发线程的安全问题,可以添加支持泛型的ThreadLocal类(ThreadLocal<T>)。通过使用ThreadLocal可以简化多线程编程时的并发访问,使用这个工具类可以很简洁的写出有没的多线程程序(例如Hibernate的官方提供HibernateUtil类里设置当前线程的局部变量是当前线程副本中session的值):
Hibernate代码:
public class HibernateUtil { /** 日志 */ private static final Log LOG=LogFactory.getLog(HibernateUtil.class); /** 线程本地变量*/ private static final ThreadLocal MAP = new ThreadLocal(); /** 会话工厂 */ private static final SessionFactory SESSION_FACTORY; private HibernateUtil() { } static { try { LOG.debug("HibernateUtil.static - loading config"); SESSION_FACTORY = new Configuration().configure() .buildSessionFactory(); LOG.debug("HibernateUtil.static - end"); } catch (HibernateException e) { throw new RuntimeException("建立会话工厂错误" + e.getMessage(), e); } } /** * 获得一个会话从当前的线程变量,当这个任务完成后,用户必须返回会话关闭方法 */ public static Session currentSession()throws HibernateException{ Session session=(Session)MAP.get(); //如果会话还没有,就打开会话 if(session==null){ session=SESSION_FACTORY.openSession(); //设置此线程局部变量的值是当前线程副本中session的值 MAP.set(session); } return session; } /** * 关闭会话 */ public static void closeSession(){ Session session=(Session)MAP.get(); MAP.set(null); if(session!=null){ session.close(); } } } |
根据上面代码来看:
线程局部变量功能很简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。仿佛就好像每一个线程都可以完全拥有该变量
ThreadLocal类提供的常用方法:
1.T get():返回此线程局部变量中当前线程副本中的值。
2.void remove():删除此线程局部变量中当前线程的值。
3.void set(T value):设置此线程局部变量中当前线程副本中的值。
CODE:
/** * 线程局部变量测试 * @author Cloudy * */ class Accout{ //定义一个ThreadLocal变量,只要调用该类的线程都会保留该变量的一个副本 private ThreadLocal<String> threadLocal=new ThreadLocal<String>(); //初始化threadLocal public Accout(String str) { this.threadLocal.set(str); System.out.println("-------初始化(为ThreadLocal在main中副本)值是:"+this.threadLocal.get()); } public String getThreadLocal() { return threadLocal.get(); } public void setThreadLocal(String threadLocal) { this.threadLocal.set(threadLocal); } } /**定义一个线程*/ class MyThread implements Runnable{ //模拟一个Accout private Accout accout; public MyThread(Accout accout) { super(); this.accout = accout; } //线程执行体 public void run() { for(int i=0;i<3;i++){ if(i==2){ //设置此线程局部变量的值为当前线程名字 accout.setThreadLocal(Thread.currentThread().getName()); } System.out.println(i+"------"+Thread.currentThread().getName()+
"线程局部变量副本值:"+accout.getThreadLocal()); } } } public class ThreadLocalVarTest { public static void main(String []args){ ExecutorService pool=Executors.newFixedThreadPool(3); //启动三条线程,公用同一个Accout Accout ac=new Accout("ThreadLocal本尊"); /* * 虽然Accout类的只有一个变量所以ThreadLocal类型的变量就导致了同一个Accout对象, * 当i=2后,将会看到3条线程访问同一个ac 而看到不同的ThreadLocal值。 */ pool.submit(new MyThread(ac)); pool.submit(new MyThread(ac)); pool.submit(new MyThread(ac)); pool.shutdown(); } } |
结果:
-------初始化(为ThreadLocal在main中副本)值是:ThreadLocal本尊
0------pool-1-thread-1线程局部变量副本值:null
1------pool-1-thread-1线程局部变量副本值:null
2------pool-1-thread-1线程局部变量副本值:pool-1-thread-1
0------pool-1-thread-2线程局部变量副本值:null
1------pool-1-thread-2线程局部变量副本值:null
2------pool-1-thread-2线程局部变量副本值:pool-1-thread-2
0------pool-1-thread-3线程局部变量副本值:null
1------pool-1-thread-3线程局部变量副本值:null
2------pool-1-thread-3线程局部变量副本值:pool-1-thread-3
总结:
ThreadLocal并不能代替同步机制,两者面向的问题领域不同,同步机制是为了多个线程同步对相同资源的并发访问,是多个线程之间进行通信的有效方式。而ThreadLocal是隔离多个线程的数据共享,根本就没有在多个线程之间共享资源,也就更不用对多个线程同步了。
所以:如果进行多个线程之间共享资源,达到线程之间通信功能,就同步。
如果仅仅需要隔离多个线程之间的共享冲突,就是用ThreadLocal。
2.包装线程不安全的集合成为线程安全集合
Java集合中的ArrayList,LinkedList,HashSet,TreeSet,HashMap都是线程不安全的(线程不安全就是当多个线程想这些集合中放入一个元素时,可能会破坏这些集合数据的完整性)
如何将上面的集合类包装成线程安全的呢?
例子:使用Collections的synchronizedMap方法将一个普通HashMap包装成线程安全的类
HashMap hm=Collections.synchronizedMap(new
Map());
如果需要包装某个集合成线程安全集合,则应该在创建之后立即包装如上。
3.线程安全的集合类
位于java.util.concurrent包下的ConcurrentHashMap集合和ConcurrentLinkedQueue集合都支持并发访问,分别对应支持并发访问的HashMap和Queue,它们都可以支持多线程并发写访,这些写入线程的所有操作都是线程安全的,但读取的操作不必锁定。(为什么?因为算法
我也看不懂)
当多个线程共享访问一个公共集合时,使用ConcurrentLinkedQueue是一个恰当的选择,因为ConcurrentLinkedQueue不允许使用null元素。ConcurrentLinkedQueue实现了多线程的高效访问,多线程访问ConcurrentLinkedQueue集合时不需要等待。
ConcurrentHashMap支持16条多线程并发写入。
用迭代器访问这两种可支持多线程的集合而言,该迭代器可能不反应出创建迭代器之后所做的修改,但不会抛出异常,而如果Collection作为对象,迭代器创建之后修改,则会抛出ConcurrentModificationException。
|