Java Thread
使用Java多线程编程很容易. Java线程总是实现接口java.lang.Runnable, 一般有两种方法: 创建一个类实现接口Runnable, 创造该类的实例作为参数传给Thread构造函数, 创造Thread实例.
继承java.lang.Thread, 创造该类对象. 这种方法一般要重写(Override)run方法, 不然该线程就什么也没做就结束了.
两者可以实现同样的功能. 但个人比较喜欢前者. 由于Java是单继承, 所以后者不能再继承其它类了. 而前者还有个好处就是可以把线程间共享的数据作为类的字段, 然后把该类实现Singleton, 只实例化一个对象, 作为参数传给Thread. 当然如果不想共享成员, 而对于每个Thread提供不同的Runnable对象. 而后者要实现共享就要在继承的类中声明一堆static属性.
Java Thread通过方法start启动, 而实际运行的内容在方法run中. 不过简单地调用run, 如thread.run(); 只是把run作为普通的方法调用了一遍, 并没有启动新的线程. 当run中的内容运行完之后, 线程便自动结束了. 类Thread提供一个静态方法sleep. 任何时候调用该方法都可以把当前执行的线程暂停指定的时间.
Java Thread同步与synchronized
先说明几点:
1. 无论synchronized关键字加在方法上还是对象上, 它取得的锁都是对象的锁 (JDK DOC中描述为an object's monitor), 而不是指对方法或一段代码加锁. 加在方法上取得的锁是该方法所属对象的锁, 即加在非static方法上时取得的是this的锁, 加在static方法上时取得的是class的锁. 这样并行调用了一个类的多个对象加了synchronized的方法, 它们之间是没有丝毫关系的.
2. 每个对象只有一个锁与之相关联.而每个线程可以取得N个对象的锁.
3. 这个锁实际上是加在对象在堆中的地址上. 所以像基本类型等位于栈上的是不能加锁的. 而对象引用本身也是在栈上, 所以也不会被加锁. 这就是说像如下的代码, 实际上两个线程取得不是同一个锁:
4. 实现同步是要很大的系统开销作为代价的, 所以不需要同步就不要做同步。
Wait, notify, notifyAll
这三个函数都是Object的成员方法, 也就是说所有的对象都能调用这三个函数, 这与所有的对象上都有锁是一致的, 而且这三个方法的也是对自己所属对象锁进行操作, 来影响需要该对象锁的线程的行为.
注意下面所说的new, run, schedule, wait, sleep, dead状态并不是官方的说法, 但这样解释可以理解很多问题. 并且目前还没碰到解释不通的现象.
这样说吧, 任何一个对象的锁就好比通行证, synchronized就用来标志某一块程序只有用它指定的对象的通行证才能进入. 这种通行证每个对象都有且只有一个, 而一个synchronized也能且只能指定一个对象(当然synchronized可以嵌套, 但每一个synchronized也只能指定一个对象), 多个synchronized可以指定同一个对象, 因为对象的通行证只有一个, 所以这几个synchronized块中的东西就如同一块一样, 不能同时进入. 但是没有用synchronized修饰的程序自然不需要通行证, 无论哪个线程都可进入.
一个线程创建好了, 但没启动 (没调用start), 此时它处于new状态, 一个线程结束了就处于dead状态. 这两种状态很简单, 不再多说.
对于线程来说, 要么进入不需通行证的代码(没用synchronized修饰的代码), 要么想方设法取得通行证. 但线程不像人, 它是很守规矩的. 当一个线程想进入synchronized块时, 如果该synchronized指定的对象的通行证没被其他线程使用, 它就拿走通行证进入该synchronized块, 注意, 一量它出来, 它一定会把通行放回原处. 此时它处于run状态. 但此时又有线程来了, 它也想进入该synchronized块, 它只能等待在run状态的线程归还通行证, 它会一直这样耐心等待. 如果此时又有其他线程来了, 这样就有了一个等待的队列, 我们称这些线程处于schedule状态, 这种状态的线程一切都准备好了, 只等着有通行证就进入run状态. 当然当通行证可用时, 只有一个线程能够得到, 其他仍然处于schedule状态. 不过到底谁有幸得到通行证, 那就由具体的JVM决定了, 程序员千万别想着JVM会跟自己想法一样. 我说过, 线程很守规矩. 实际上线程们还很友好. 一个处于run状态的线程run了一会之后, 它可能想让其他线程先run一把, 于是它就调用它拥有的通行证的对象的wait方法, 告知自己要wait了, 此时通行证归还, 而它自己则进入wait状态. 这个线程可以设定它将处于wait状态的时间或者无限长(参数0时无限长, 负数会抛java.lang.IllegalArgumentException异常. 一旦时间到了, 就自动进入schedule状态, 等待通行证. 在这个时间之内, 除非其他线程唤醒它, 否则它将一直不闻不问了, 一直wait, 实际上说sleep更确切. 怎么唤醒在下面会讲.既然线程如此友好, 自然JVM也待它不薄, 一个线程wait后, 当它重新有机会进入schedule, 再进入run, 它装从wait的下一个语句开始执行, 就好像它没有wait过一样. 当很多线程都wait之后, 就有了wait队列, 而处于run的线程, 可能会想唤醒一个或一些wait的进程, 它可以调用拥有的通行证的对象的notify方法(唤醒一个线程, 具体哪一个, 也是JVM说了算)或者notifyAll方法(全部唤醒). 这里需要说明的是, 被唤醒的是那些处于相应对象通行证wait队列的线程, 其它无关的线程并不会被唤醒, 唤醒之后处于schedule状态, 此时通行证仍然为那个run状态的线程所有, 最后到底谁能进入run状态, 也是JVM决定的. 有时线程太好心了, 没有某个对象的通行证, 却硬要调用这个对象的wait, notify或notifyAll方法, 结果会抛出java.lang.IllegalMonitorStateException异常.
注意上面所讲的, 各个通行证之间是独立的, 比如, 一个线程调用一个对象的wait, 它归还了这个对象的通行证, 但要是它还拥有其它对象的通行证, 它会仍然拥有, 当然这可能造成死锁. 不过这应该是程序员的责任.
类Thread
难道所有线程都这么公平, 当然不是. 类Thread提供了很多方法都针对某个线程进行操作, 它要么不影响要么影响指定线程所拥有的锁. 也就是说这些方法与上面的不同, 是针对线程而不是针对锁的.
一个线程处于wait时, 除了上面讲的等到指定时间, notify, notifyAll之外, 它还能被其他线程interrupt唤醒. interrupt()是Thread类的方法. 所以它作的是指名道姓的唤醒, 一个线程想唤醒另一个线程, 它就必须直接叫那个线程的名字, 即调用那个线程的interrupt()方法. 这个处于wait的线程就会抛出java.lang.InterruptedException异常, consume异常之后, 该线程就会进入到schedule状态, 并且如果之前其interrupt status(这个status与前面所提的run, schedule, wait状态没关系, 其实更该称之为属性, 估计这就是Thread类的属性, 只是JDK DOC 上这样称呼. 所以这里用status以示区别), 这个interrupt status会被清除. 这里要说明一下, 线程还有sleep状态, 这种状态行为与wait基本一样, 只是不能被nofity和notifyAll唤醒. 线程可以调用Thread的静态方法sleep, 使当前线程(自己)进入sleep状态, 参数指定时间, 或Thread的成员方法join, 参数几乎和调用wait一样, 但它是使自己处于进入sleep状态, 一直到被调用线程运行完了, 或者时间到了, 它才继续运行.
只是Thread的这几个方法显然与拥有哪个对象的通行证没有关系. 实际上他们拥有的对象的通行证没有任何改变, 否则是不安全的. 实际上像stop, suspend, resume等等deplicated的方法要么调用时会释放所有它拥有的锁(通行证), 导致不安全, 要么调用之后一定要某个方法来重新启动, 容易造成死锁. sleep状态和wait一样的是都能被interrupt唤醒, 唤醒的行为一模一样. 如果一个线程没有处于wait和sleep状态, 并且也没有” blocked in an I/O operation upon an interruptible channel”或”blocked in a Selector”, 它的interrupt status会被设置. Thread同时提供了两个方法来查询一个进程的interrupt status: 一种是静态方法interrupted()查询当前进程的interrupt status, 返回boolean值, 同时清除interrupt status; 另一种是成员方法isInterrupted(), 但它不会清除interrupt status. 所以要想查询当前线程interrupt status又不想清除之, 可以这样: Thread.currentThread.isInterrupted().
前面所说的一开始线程的interrupt status都为false, 如果一开始interrupt status就被设为true, 那么该线程一调用wait, sleep或join就会抛出java.lang.InterruptedException异常, 并且interrupt status被清除. 实际上我们如果把interrupt()的调用看到先设置interrupt status, 接下来的行为就一致了, 而不管是先设interrupt status, 还是先进为wait或sleep状态.
线程们之间友好还体现在Thread静态方法yield(), 它所做的是让自己暂停一下. 实际上就相当于把自己的状态从run变到schedule, 重新竞争通行证(锁), 最后到底谁会进入run, 或者它自己又进入了run, 由JVM决定.
线程可以设置或查询优先级, 高优先级进程优先. 线程还能设置或查询是否成为Daemon线程. Daemon线程和非Daemon线程的区别是JVM对它们的态度不同. 只要没有非Daemon线程存在, JVM就退出, 就像调用了Sysem.exit()一样, 而不管到底有没有或有多少Daemon线程仍在运行.
类ThreadGroup
至于ThreadGroup, 它所做的就是把线程分组来管理, 比如, 调用ThreadGroup的interrupt方法, 相当于调用组内所有线程的interrupt方法. 组是树装结构的, 树的根结点组的名称是”system”. 如果不指定组名, 创建的Thread或ThreadGroup就是当前组的子结点. Java程序main方法所在的组名为”main”, 是”system”的子结点, 调用ThreadGroup的list方法就可以把从该组开始的树状结构比文本形式打印出来.
ThreadGroup和Thread的name都可以一样.
ThreadGroup提供了一个处理非Catch异常的方法, 只要重载ThreadGroup中的uncaughtException方法, 属于该组的Thread只要抛了非Catch异常, 就会调用此方法.
对于JDK1.5以止版本, Thread本身就提供了setUncaughtExceptionHandler, 在线程粒度处理非Catch异常. JDK DOC中描述如下:
“Uncaught exception handling is controlled first by the thread, then by the thread's ThreadGroup object and finally by the default uncaught exception handler. If the thread does not have an explicit uncaught exception handler set, and the thread's thread group (including parent thread groups) does not specialize its uncaughtException method, then the default handler's uncaughtException method will be invoked. “
Thread还提供方法getContextClassLoader, 得到该线程的ClassLoader对象.