当习惯Java多线程设计之后,我们会留意方法后面是否加了throws InterruptedException. 如果方法加了,则表明该方法(
或该方法进一步调用的方法中)可能会抛出InterruptedException异常.
这里包含两层意思:
管理多个实例的接口或类统称为集合(collection)。
Java中的大部分集合都是非线程安全的。因此,在多线程操作集合的时候,需要去查看API文档, 确认要用的类或接口是否线程安全的。
该例子中ArrayList(及迭代器)在被多个线程同时读写而失去安全性时,便会抛出ConcurrentModificationException异常。
该运行时(runtime)的异常用于表示“执行并发修改了”。
异常不过是调查Bug根本原因的提示而已,所以编写编程不能依赖于抛出的异常。
对例子1的改造,使得其具有安全性。
1 | synchronized(list){ |
例子2使用Collections.synchronized进行同步。
这里使用CopyOnWriteArrayList类通过copy-on-write避免读写冲突。
1 | public class Main { |
程序如果频繁“写”操作,使用copy-on-write会比较耗时,如果写操作比较少,读操作比较多,是比较适合使用的。
表示该类无法扩展。也就是说,无法创建final类的子类。 由于无法创建final类的子类,所以final类中声明的方法也不会被重写。
表示该方法不会被子类的方法重写。 如果在静态方法的声明上加上final,则表示该方法不会被子类的方法隐藏(hide)。如果试图重写或隐藏final
方法,编译时会提示错误。
在设计模式的Template Method中,有时候模板方法会声明为final方法。
表示该字段只能赋值一次。
1 | final字段赋值有两种方式: |
注意: final字段不可用setValue这样的setter方法再次赋值.
从创建线程安全的角度来说,将字段声明为final是非常重要的。
局部变量和方法的参数也可用声明为final。final变量只可以赋值一次。 而final参数不可以赋值,因为在调用方法时,已经对其赋值了。
Single Threaded Execution模式用于确保某个区域“只能有一个线程”执行。下面我们将这种模式扩展。确保某个区域“最多只能由N个线程”
执行。这时就要用计数信号量来控制线程数量。
假设能够使用的资源个数为N个,而需要这些资源的线程个数又多于N个。此时就会导致竞态。
java.util.concurrent包中提供了表示信号量的Semaphore类。
资源的许可个数(permits)将通过Semaphore的构造函数来指定。
Semaphore的acquire方法用于确保存在可用资源。当存在可用资源的时候,线程会立即从acquire方法返回,同时信号量内部的资源
个数会减1。如果没有可用资源,线程则阻塞在acquire方法内,知道出现可用资源。
Semaphore的release方法用于释放资源。释放资源后,信号量内部的资源个数会增加1。另外,如果acquire中存在等待的线程,那么其中一个线程
会被唤醒,并从acquire方法返回。
1 | synchronized void method(){} |
其实可以看做是“{”处获得锁,“}”处释放锁。
假设,存在一个获得锁的lock方法,或释放锁的unlock方法。
1 | 显示处理锁的方法: |
上述问题并不仅仅在于return语句,异常处理也存在这样的问题,调用的方法(或该方法调用的方法) 抛出异常时候,锁也就无法被释放。
避免异常导致的问题,必须使用finally语句
1 | void method(){ |
synchronized就像门上的锁。当你看到门上的锁时,我们还应该确认其他的门和窗户是不是都锁好了。
只要是访问多个线程共享的字段的方法,就需要使用synchronized进行保护。
1 | public class Gate { |
比如上面的代码,已经在pass方法上加了synchronized方法。 如果set和get方法都同时加上synchronized并不能保证安全
synchronized方法只允许一个线程同时执行。如果某个线程正在执行synchronized方法,
其他线程就无法进入该方法。也就是说,从多线程的角度来看,这个synchronized方法执行
的操作是“不可分割的操作”。这种不可分割的操作通常称为“原子atomic”操作。
atom是物理学中的“原子”,本意为不可分割的物体。
Java中定义了一些原子的操作。 例如char,int等基本类型的赋值和引用都是原子的;引用类型的赋值和引用操作也是原子的。
因此,就算不使用synchronized也不会被分割。
例外:long和double的赋值和引用操作并不是原子的。 最简单的方法就是在synchronized方法中执行;或字段上加上volatile关键字。
总结:
对象损坏是一种比喻,实际上,对象是内存上的一种虚拟事物,并不会实际损坏。对象损坏是指对象的状态和设计者的意愿不一致, 通常是指对象的字段的值并非预期值。
如果一个类即使被多个线程同时使用,也可确保安全性,那么这个类就称为线程安全(thread-safe)类。由于类库中还存在非线程安全的类,所有在多线程的程序中
需要特别注意。比如:java.util.Vector类是线程安全的类,而java.util.ArrayList则是非线程安全的类。
生存性(liveness)是无论是什么时候,必要的处理都一定能够被执行。
即使对象没有损坏,也不代表程序就一定好。极端一点说,假如程序在运行过程中突然停止了,这时,由于处理已经停止,对象的状态就不会发生变化了,所以对象状态
也就不会异常。这虽然符合前面将的“安全性”条件,当无法运行的程序根本没有任何意义。无论是什么时候,必要处理都一定能够被执行。
有时候安全性和生存性是相互制约的。例如,有时只重视安全性,生存性就会下降。最典型的是死锁(deadlock),即多个线程相互等待对方释放锁的情形。
类如果能够作为组件从正常运行的软件中分割出来,就说明这个类有很高定复用性。
编写多线程的时候如果能够巧妙地将线程的互斥机制和方针隐藏到类中,那这就是一个可复用性高的程序。
其他
线程:我们追踪程序运行的流程,其实就是在追踪线程的流程。
单线程: “在某一时间点执行的处理只有一个”。(在研究单线程的时,我们忽略Java的后台线程,如:GC等)
多线程:常见的多线程常见有以下几个。
ps:nio即便不使用多线程,也可用执行兼具性能和可扩展性的IO处理。
两种方式
其他方式
-
java.util.concurrent.ThreadFactory利用ThreadFactory启动线程
无论那种方式,启动新线程的方法最终都是Thread类的start方法。
ps:Thread类本身还实现Runnable接口,并持有run方法,但run()主体是空的。仍需要子类重写。
需要注意的是线程类的实例并不等于线程。线程就算停止,线程类的实例并不会消失。
在实际程序中,使用sleep的频率并不高。
Sleep的例子
线程A和线程B相互竞争引起与预期相反的情况称为竞态条件(race condition)或数据竞争(data race)。
处理上述的竞态的操作称为互斥。
synchronized代码块用于精准控制互斥处理的执行范围。
以下两种写法是等效的。也就是说synchronizedui实例方法是使用this的锁来执行线程的互斥处理的。
1 | synchronized void method(){ |
1 | void method(){ |
synchronized静态方法每次只能由一个线程运行,这一点和synchronized实例方法相同。
但是,synchronized静态方法使用的锁和synchronized实例方法使用的锁是不一样的。
1 | class Something{ |
1 | class Something{ |
wait方法,notify方法,notifyAll方法(都是java.lang.Object类的方法,所以既不是Thread类中的方法,但又是)
注意:
它们只能由持有要调用的实例的锁的线程调用。如果未持有锁的线程调用上面方法,异常java.lang.IllegalMonitorStateException会抛出。
所有实例都拥有一个等待队列,它是在实例的wait方法执行后停止操作的线程的队列。
等待队列是一个虚拟的概念, 既不是实例中的字段,也不是用于获取正在实例上等待的线程的列表的方法。
要执行wait方法,线程必须持有锁(这是规则)。如果线程进入等待队列之后,便会释放其实例的锁。
1 | wait(); |
要执行notify方法,线程也必须持有调用的实例的锁。
1 | obj.notify(); |
notify唤醒的线程并不会在执行notify的一瞬间重新运行。因为在执行notify的那一瞬间,执行notify的线程还持有着锁,所以其他
线程还无法获取这个实例的锁。
执行notify后如何选择线程:假如在执行notify方法之后,正在等待队列中等待的线程不止一个,对于“这时该如何来选择线程”这个
问题规范中并没有做出规定。究竟是选择最先wait的线程,还是随机选择,或采取其他方法取决于Java平台运行环境。因此编写程序时需要
注意,最好不要编写依赖于所选线程的程序。
notifyAll方法会将等待队列中所有的线程都取出来。
1 | notifyAll(); |
-
使用notify方法还是notifyAll方法?由于notify唤醒的线程较少,所以处理速度要比使用notifyAll快。但使用notify时,如果处理不好,程序便
可能会停止。一般来说,使用notifyAll时的代码会比notify时更为健壮。
参考下图的线程状态转移: