如果你还不了解Java多线程相关的知识,建议去看我的上一篇Java多线程入门2,如果你还没有开始了解Java,那么可以从我的知乎开始: 知乎传送门
线程同步 在线程同步方面有一个比较经典的例子是银行取钱之类的。他们的逻辑也只有一个,那就是保证不会出现负数的物品或者是负数金额。而在多线程的程序中,线程的执行是随机的,而且会同时访问某个对象,如果不加处理,可能会出现数据错误的问题。
同步代码块 1 2 3 4 synchronized (obj){ ... }
上面代码的含义是,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。而任何时刻只能由一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。换个说法就是,同一时间只有一个线程执行其中的代码。
从银行取钱问题出发
账户类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class Account { private String accountNo; private double balance; public Account () { } public Account (String accountNo,double balance) { this .accountNo = accountNo; this .balance = balance; } public String getAccountNo () { return accountNo; } public void setAccountNo (String accountNo) { this .accountNo = accountNo; } public double getBalance () { return balance; } public void setBalance (double balance) { this .balance = balance; } @Override public int hashCode () { final int prime = 31 ; int result = 1 ; result = prime * result + ((accountNo == null ) ? 0 : accountNo.hashCode()); long temp; temp = Double.doubleToLongBits(balance); result = prime * result + (int ) (temp ^ (temp >>> 32 )); return result; } @Override public boolean equals (Object obj) { if (this == obj) return true ; if (obj!=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false ; } }
下面是取钱线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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 () { synchronized (account) { if (account.getBalance() >= drawAmount) { System.out.println(getName()+"取钱成功!吐出钞票:" +drawAmount); try { Thread.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } account.setBalance(account.getBalance()-drawAmount); System.out.println("\t余额为:" +account.getBalance()); }else { System.out.println(getName()+"取钱失败!余额不足" ); } } } }
下面是主启动类
1 2 3 4 5 6 7 8 9 10 11 public class DrawTest { public static void main (String[] args) { Account acct = new Account("1234567" ,1000 ); new DrawThread("甲" , acct, 800 ).start(); new DrawThread("乙" , acct, 800 ).start(); } }
这三个类就简单地组成了银行取钱问题,当在多线程运行时,如何保证金额不为负数。当然你也可以尝试把同步代码块去掉。多运行几次就会发现问题。
同步方法 同步方法与同步代码块都是用同一个关键字,只不过这次修饰得是方法。对比同步代码块,同步方法的同步监视器是this,也就是调用该方法的对象。
通过同步方法可以很简单地实现一个线程安全地类,线程安全的类具有如下特征:
该类的对象可以被多个线程安全地访问。
每个线程调用该对象的任意方法之后都将得到正确的结果
每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态
下面的三个类均是重新用同步方法修改过后的样子,我会把之前的代码注释掉,来看对比
账户类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 public class Account { private String accountNo; private double balance; public Account () { } public Account (String accountNo,double balance) { this .accountNo = accountNo; this .balance = balance; } public String getAccountNo () { return accountNo; } public void setAccountNo (String accountNo) { this .accountNo = accountNo; } public double getBalance () { return balance; } @Override public int hashCode () { final int prime = 31 ; int result = 1 ; result = prime * result + ((accountNo == null ) ? 0 : accountNo.hashCode()); long temp; temp = Double.doubleToLongBits(balance); result = prime * result + (int ) (temp ^ (temp >>> 32 )); return result; } @Override public boolean equals (Object obj) { if (this == obj) return true ; if (obj!=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false ; } public synchronized void draw (double drawAmount) { if (balance >= drawAmount) { System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:" +drawAmount); try { Thread.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } balance -= drawAmount; System.out.println("\t余额为:" +balance); }else { System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足" ); } } }
取钱线程类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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 () { account.draw(drawAmount); } }
从面向对象的设计来说后者可能更好一些。Account类中自己实现了取钱方法,其他类只用去调就好了。领域驱动设计(DDD),这种方式认为每个类都应该是完备的领域对象,例如账户类就应该提供用户账户的相关方法;而不是直接提供setBalance()
方法来任人操作。
同步的代价和释放同步监视器的锁定 可变类(就是类中的数据会改变的类)的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,程序可以采用如下策略。
不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步。
如果可变类由两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本,在对线程环境中使用线程安全版本。
线程会在如下几种情况下释放对同步监视器的锁定
当前线程的同步方法、同步代码块执行结束
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块
出现了未处理的Error或Exception,导致异常结束时
当前线程执行了同步监视器对象的wait()
方法,则当前线程暂停,并释放同步监视器
线程在如下所示情况下,线程不会释放同步监视器
程序调用Thread.sleep(),Thread.yield()
方法来暂停当前线程的执行,当前线程不会释放同步监视器
其他线程调用了该线程的suspend()
方法将该线程挂起,该线程不会释放同步监视器。程序应该尽量避免使用suspend()
和resume()
方法来控制线程。
同步锁 Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁由Lock对象充当。
某些锁可能允许对共享资源并发访问,如 ReadWriteLock 读写锁,Lock、ReadWriteLock是Java5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了 ReentrantReadWriteLock 实现类。
Java 8 新增了新型的 StampedLock 类,在大多数场景中它可以替代传统的 ReentrantReadWriteLock。 ReentrantReadWriteLock 为读写操作提供 了三种锁模式: Writing、 ReadingOptimistic、 Reading。在实现线程安全的控制中,比较常用的是 ReentrantLock(可重入锁)。使用该 Lock 对象可以显式地加锁、释放锁,通常使用 ReentrantLock 的代码格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class X { private final ReentrantLock lock = new ReentrantLock(); public void m () { lock.lock(); try { }finally { lock.unlock(); } } }
下面用Lock的方法重写之前的Account
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 package many_thread;import java.util.concurrent.locks.ReentrantLock;public class Account { private final ReentrantLock lock = new ReentrantLock(); private String accountNo; private double balance; public Account () { } public Account (String accountNo,double balance) { this .accountNo = accountNo; this .balance = balance; } public String getAccountNo () { return accountNo; } public void setAccountNo (String accountNo) { this .accountNo = accountNo; } public double getBalance () { return balance; } @Override public int hashCode () { final int prime = 31 ; int result = 1 ; result = prime * result + ((accountNo == null ) ? 0 : accountNo.hashCode()); long temp; temp = Double.doubleToLongBits(balance); result = prime * result + (int ) (temp ^ (temp >>> 32 )); return result; } @Override public boolean equals (Object obj) { if (this == obj) return true ; if (obj!=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false ; } public void draw (double drawAmount) { lock.lock(); try { if (balance >= drawAmount) { System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:" +drawAmount); try { Thread.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } balance -= drawAmount; System.out.println("\t余额为:" +balance); }else { System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足" ); } }finally { lock.unlock(); } } }
当获取了多个锁时,他们必须以相反的顺序释放,且必须在与所有锁被获取时相同范围内释放所有锁。Lock提供了同步代码块和同步方法所没有的其他功能,包括用于非块结构的 tryLock() 方法, 以及 试图 获取 可 中断 锁 的 lockInterruptibly() 方法, 还有获取超时失效锁的 tryLock( long, TimeUnit)方法。但是使用时一定要注意其编程规范,使用锁就一定要有开锁的方法,且一定要确保开锁的方法能够正常执行。
ReentrantLock 锁具有可重入性,也就是说,一个线程可以对已被加锁的 ReentrantLock 锁再次加锁, ReentrantLock 对象会维持一个计数器来追踪 lock() 方法的嵌套调用,线程在每次调用 lock() 加锁后,必须显式调用 unlock() 来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
死锁 当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有措施来处理死锁情况,一旦产生死锁,整个程序既不会发生任何异常,也不会给出任何提示,知识所有线程处于阻塞状态,无法继续。
看看下面这个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 class A { public synchronized void foo (B b) { System.out.println("当前线程名:" +Thread.currentThread().getName()+"进入A实例的foo()方法" ); try { Thread.sleep(200 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("当前线程名:" +Thread.currentThread().getName()+"企图调用B实例得last方法" ); b.last(); } public synchronized void last () { System.out.println("进入了A类的last()方法内部" ); } }class B { public synchronized void bar (A a) { System.out.println("当前线程名:" +Thread.currentThread().getName()+"进入B实例的bar()方法" ); try { Thread.sleep(200 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("当前线程名:" +Thread.currentThread().getName()+"企图调用A实例的last方法" ); a.last(); } public synchronized void last () { System.out.println("进入了B类的last()方法内部" ); } }public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init () { Thread.currentThread().setName("主线程" ); a.foo(b); System.out.println("进入了主线程之后" ); } public void run () { Thread.currentThread().setName("副线程" ); b.bar(a); System.out.println("进入副线程之后" ); } public static void main (String[] args) { DeadLock d1 = new DeadLock(); new Thread(d1).start(); d1.init(); } }
之前讲到,当线程处于休眠时是不会释放锁的,所以借此空挡。主线程先执行init方法,锁住了A对象,但是主线程休眠了200ms,所以暂时无法到达下面的方法。CPU切换到执行另一个线程,让B对象执行bar方法,该线程又对B对象加锁,然后副线程也暂停200ms。主线程下先醒过来,但是B对象已经被锁,所以主线程阻塞;副线程也醒了过来,但是A对象也被锁了。所以两边都在等待彼此解锁。
一般死锁都是程序设计有问题导致的,解决死锁问题可通过以下几种方式:
避免多次锁定:尽量避免同一个线程对多个同步监视器进行锁定。比如主线程对AB锁定了,副线程就不要对AB锁定。
具有相同的加锁顺序,主线程先对A加锁,副线程先对B加锁。则有可能死锁,假如同样的顺序加锁,就可以避免死锁问题。
使用定时锁:程序调用 Lock 对象的 tryLock() 方法加锁时可指定 time 和 unit 参数,当超过指定时间后会自动释放对 Lock 的锁定,这样就可以解开死锁了。
死锁检测:这是一种依靠算法来实现的死锁预防机制,主要针对那些不可能实现按序加锁,也不能使用定时锁的场景。
线程通信 线程同步我们讲到了一个取款的例子有一个比较经典的例子是生产者消费者程序,或者是银行取钱存钱之类的。他们的逻辑也只有一个,用户存款以后才可以取款。而在多线程的程序中,线程的执行是随机的,那么我们就需要使用一些特殊的手段让其按照一定规则运行。
传统的线程通信 为了实现这个功能,我们可以用Object类提供的wait,notify,notifyAll三个方法,这三个方法都属于Object类,而不属于Thread类,这三个类都需要由同步监视器对象来调用,也就是需要使用关键字 synchronized。
方法
说明
wait()
导致当前线程等待,直到其他线程调用该同步见识器的notify()方法或notifyAll()方法来唤醒该线程,可以接收带毫秒的参数和接收带毫秒,毫微秒的参数。
notify()
唤醒在此同步监视器上等待的单个线程,如果所有线程都在此同步监视器上等待,则会随机选择唤醒其中一个。
notifyAll()
唤醒此同步监视器上等待的所有线程
下面程序使用一个Flag标记来判断是否调用让该线程等待。
账号类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 public class Account { private String accountNo; private double balance; private boolean flag = false ; public Account () { } public Account (String accountNo,double balance) { this .accountNo = accountNo; this .balance = balance; } public String getAccountNo () { return accountNo; } public void setAccountNo (String accountNo) { this .accountNo = accountNo; } public double getBalance () { return balance; } @Override public int hashCode () { final int prime = 31 ; int result = 1 ; result = prime * result + ((accountNo == null ) ? 0 : accountNo.hashCode()); long temp; temp = Double.doubleToLongBits(balance); result = prime * result + (int ) (temp ^ (temp >>> 32 )); return result; } @Override public boolean equals (Object obj) { if (this == obj) return true ; if (obj!=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false ; } public synchronized void draw (double drawAmount) { try { if (!flag) { wait(); }else { System.out.println(Thread.currentThread().getName()+"取钱:" +drawAmount); balance -= drawAmount; System.out.println("账户余额为:" +balance); flag = false ; notifyAll(); } }catch (InterruptedException e) { e.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 e) { e.printStackTrace(); } } }
取钱线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package thread_talk;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<100 ;i++) { account.draw(drawAmount); } } }
存钱线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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<100 ;i++) { account.deposit(depositAmount); } } }
主启动类
1 2 3 4 5 6 7 8 9 10 11 12 public class DrawTest { public static void main (String[] args) { Account acct = new Account("1234567" ,0 ); new DrawThread("取钱者" , acct, 800 ).start(); new DepositThread("存钱者甲" , acct, 800 ).start(); new DepositThread("存钱者乙" , acct, 800 ).start(); new DepositThread("存钱者丙" , acct, 800 ).start(); } }
程序最后被阻塞无法继续向下执行, 这是因为 3 个存款者线程共有 300 次尝试存款操作,但 1 个取钱者线程只有 100 次尝试取钱操作,所以程序最后被阻塞!阻塞不是死锁,取钱线程已经执行结束,而存款线程只是登其他线程来取钱而已,死锁是互相等待其他线程释放同步监视器。
使用Condition控制线程通信 如果不是用synchronized关键字的话就不能使用wait,notify,notifyAll方法进行线程通信了。当使用Lock、
对象来保证同步时,Java提供了Condition类来保持协调。同样的提供了三个方法来与线程通信
方法
说明
await()
类似于wait,让当前线程等待
signal()
类似于notify,唤醒其中一个线程
signalAll()
唤醒在此Lock对象上等待的所有线程
下面是用Condition重写的存款与取款程序
账号类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 package thread_talk_lock;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class Account { private final Lock lock = new ReentrantLock(); private final Condition cond = lock.newCondition(); private String accountNo; private double balance; private boolean flag = false ; public Account () { } public Account (String accountNo,double balance) { this .accountNo = accountNo; this .balance = balance; } public String getAccountNo () { return accountNo; } public void setAccountNo (String accountNo) { this .accountNo = accountNo; } public double getBalance () { return balance; } @Override public int hashCode () { final int prime = 31 ; int result = 1 ; result = prime * result + ((accountNo == null ) ? 0 : accountNo.hashCode()); long temp; temp = Double.doubleToLongBits(balance); result = prime * result + (int ) (temp ^ (temp >>> 32 )); return result; } @Override public boolean equals (Object obj) { if (this == obj) return true ; if (obj!=null && obj.getClass() == Account.class) { Account target = (Account)obj; return target.getAccountNo().equals(accountNo); } return false ; } public void draw (double drawAmount) { lock.lock(); try { if (!flag) { cond.await(); }else { System.out.println(Thread.currentThread().getName()+"取钱:" +drawAmount); balance -= drawAmount; System.out.println("账户余额为:" +balance); flag = false ; cond.signalAll(); } }catch (InterruptedException e) { e.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 e) { e.printStackTrace(); }finally { lock.unlock(); } } }
取钱类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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<100 ;i++) { account.draw(drawAmount); } } }
存钱类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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<100 ;i++) { account.deposit(depositAmount); } } }
使用阻塞队列 Java5提供了一个BlockingQueue接口,虽然它也是 Queue 的子接口,但它的主要用途还是作为线程同步的工具。当生产者线程视图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图取出元素时,如果该队列已空,则该线程被阻塞。
该队列主要的方法为:
put(E e):尝试把E元素放入队列中,如果已满,则阻塞该线程
take():尝试从头部取出元素,如果队列元素已空,则阻塞该线程
当然这个队列也实现了Queue接口,所以也可以调用其他队列的方法,不过当使用Queue接口时,其中的方法如果检测到该队列已满,或者已空,则会报异常且返回false。
ArrayBlockingQueue: 基于数组实现的 BlockingQueue 队列。
LinkedBlockingQueue: 基于链表实现的 BlockingQueue 队列。
PriorityBlockingQueue: 它并不是标准的阻塞队列。与前面介绍的 PriorityQueue 类似,该队列调用 remove()、 poll()、 take() 等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。PriorityBlockingQueue 判断元素的大小即可根据元素( 实现 Comparable 接口)的本身大小来自然排序,也可使用 Comparator 进行定制排序。
SynchronousQueue:同步队列。对 该 队列 的 存、取操作必须交替进行。
DelayQueue: 它是 一个 特殊 的 BlockingQueue, 底层 基于 PriorityBlockingQueue 实现。 不过, DelayQueue 要求 集合 元素 都 实现 Delay 接口( 该 接口 里 只有 一个 long getDelay() 方法), DelayQueue 根据 集合 元素 的 getDalay() 方法 的 返回 值 进行 排序。
下面是一个小的demo程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;public class BlockingQueueTest { public static void main (String[] args) throws InterruptedException { BlockingQueue<String> bq = new ArrayBlockingQueue<>(2 ); bq.put("java" ); bq.put("java" ); bq.put("java" ); } }
使用BlockingQueue来实现线程通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;class Producer extends Thread { private BlockingQueue<String> bq; public Producer (BlockingQueue<String> bq) { this .bq = bq; } public void run () { String[] strArr = new String[] { "Java" , "Struts" , "Spring" }; for (int i =0 ;i<99999999 ;i++) { System.out.println(getName()+"生产者准备生产集合元素!" ); try { Thread.sleep(200 ); bq.put(strArr[i%3 ]); } catch (Exception e) { e.printStackTrace(); } System.out.println(getName()+"生产完成:" +bq); } } }class Consumer extends Thread { private BlockingQueue<String> bq; public Consumer (BlockingQueue<String> bq) { this .bq = bq; } public void run () { while (true ) { System.out.println(getName()+"消费者准备消费集合元素!" ); try { Thread.sleep(200 ); bq.take(); } catch (Exception e) { e.printStackTrace(); } System.out.println(getName()+"消费完成:" +bq); } } }public class BlockingQueueTest2 { public static void main (String[] args) { BlockingQueue<String> bq = new ArrayBlockingQueue<>(1 ); new Producer(bq).start(); new Producer(bq).start(); new Producer(bq).start(); new Consumer(bq).start(); } }
线程组 Java使用 ThreadGroup 来表示线程组,可以对一批线程进行分类管理。对线程组的控制相当于同时控制这批线程。如果程序没有显式指定线程属于哪个线程组,则该线程属于默认线程组。在默认情况下,子线程和创建它的父线程处于同一个线程组内。
一旦某个线程假如指定线程组之后,该线程讲一直属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。
Thread类提供了几个构造器来设置新创建的线程属于哪个线程组
Thread(ThreadGroup group,Runnable target)
:以target的run 方法作为线程执行体创建新线程,属于group线程组。
Thread(ThreadGroup group,Runnable target,String name)
:以target的run 方法作为线程执行体创建新线程,属于group线程组。且线程名为name
Thread(ThreadGroup group,String name)
:创建新线程,线程名为name,属于group线程组。
因为中途不可改变线程所属的线程组,Thread类没有set方法来改变线程组,但提供了get方法来返回该线程所属的线程组,getThreadGroup()
方法的返回值是 ThreadGroup 对象,表示一个线程组。
ThreadGroup 类的两个简单构造器
ThreadGroup(String name)
: 以指定的线程组名字来创建新的线程组
ThreadGroup(ThreadGroup parent,String name)
:以指定的名字、指定的父线程组创建一个新线程组
ThreadGroup类的常规方法来操作整个线程组里的所有线程
方法
描述
int activeCount()
返回此线程组中活动线程的数目
interrupt()
中断此线程组中的所有线程
isDaemon()
判断该线程组是否是后台线程组
setDaemon(boolean daemon)
把该线程组设置成后台线程组。当后台线程组的最后一个线程执行结束后,后台线程组将自动销毁
setMaxPriority(int pri)
设置线程组的最高优先级
下面是关于线程组的实例程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class MyThread extends Thread { public MyThread (String name) { super (name); } public MyThread (ThreadGroup group,String name) { super (group,name); } public void run () { for (int i=0 ;i<20 ;i++) { System.out.println(getName()+"线程的i变量" +i); } } }public class ThreadGroupTest { public static void main (String[] args) { ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); System.out.println("主线程组的名字:" +mainGroup.getName()); System.out.println("主线程是否是后台线程组:" +mainGroup.getName()); new MyThread("主线程组的线程" ).start(); ThreadGroup tg = new ThreadGroup("新线程组" ); tg.setDaemon(true ); System.out.println("tg线程组是否是后台线程组:" +tg.isDaemon()); MyThread tt = new MyThread(tg,"tg组的线程甲" ); tt.start(); new MyThread(tg, "tg组的线程乙" ).start();; } }
未处理的线程异常 Java5开始,如果线程执行过程中抛出了一个未处理异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler
对象,如果找到该处理器对象,则会调用该对象的uncaughtException(Thread t,Throwable e)
方法来处理该异常。
上述的Thread.UncaughtExceptionHandler
是Thread
类的一个静态内部接口,该接口只有一个方法:void uncaughtException(Thread t,Throable e)
,该方法中的t代表出现异常的线程,而e代表该线程抛出的异常。
Thread类提供了如下两个方法来设置异常处理器
static setDefaultUncaughtExceptionHandler( Thread. UncaughtExceptionHandler eh)
:为该线程类的所有线程实例设置默认的异常处理器
setUncaughtExceptionHandler( Thread. UncaughtExceptionHandler eh)
: 为指定的线程实例设置异常处理器。
线程组处理异常的默认流程如下
如果该线程组有父线程组,则调用父线程组的uncaughtException()方法来处理该异常
如果该线程实例所属的线程类有默认的异常处理器(由setDefaultUncaughtExceptionHandler()方法设置的异常处理器),那么调用该异常处理器来处理该异常。
如果该异常对象是ThreadDeath对象,则不做任何处理;否则,将异常跟踪栈的信息打印到System.err错误输出流,并结束该线程。
下面是一个程序的主方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class MyExHandler implements Thread .UncaughtExceptionHandler { public void uncaughtException (Thread t, Throwable e) { System.out.println(t+"线程出现了异常:" +e); } }public class ExHandler { public static void main (String[] args) { Thread.currentThread().setUncaughtExceptionHandler(new MyExHandler()); int a = 5 /0 ; System.out.println("程序正常结束" ); } }
上面的程序的执行结果来看,虽然我们指定了异常处理器对未捕获的异常进行处理,而且该异常处理器也起作用了,但是程序依然不会正常结束。说明了异常处理器与通过catch捕获异常是不同的——当使用catch捕获异常时,异常不会向上传播给上一级调用者,但是异常处理器对异常处理后,依然会传播给上一级调用者。