多线程
为什么要使用多线程?使用多线程的理由有千千万,但是归结起来就是为了充分利用硬件的处理能力,使计算机硬件在同一时间段能够处理多个任务,也就是使用操作系统提供的多线程,多进程机制实现多个任务并行执行。
Java中使用Thread,Runnable,Callable等可以很方便的创建和运行线程。
多线程的问题
在使用多线程时,如果多个线程同时读同一个资源(如文件),则不会产生任何问题,但是,如果此时有一个或者多个线程写一个资源时就会产生多个线程的同一个资源内容不一致的问题了,比如有下面的程序:
public class Counter {
protected long count = 0;
public void add(long value){
this.count = this.count + value;
}
}
如果多个线程同时执行这个程序,则会出现问题。假设线程1读取到了counte的值,保存在寄存器内,还没有来得及调用add方法,此时CPU转而执行线程2,那么线程2读到了counte值,此后,线程1和线程2都对counte值加1,再将这个结果写入内存,那么count值最终只会增加1,如果正常执行,那么每个线程都调用一次add方法,最后count的值应该增加2,所以多线程执行时,如果不加以控制,就可能会产生对同一资源竞争写的问题。
线程安全与资源共享
为了解决多线程并发执行时,同时写一个资源时产生的不一致的问题,首先要清楚哪些资源在多线程并发执行时是安全的,哪些是不安全的。
- 基本类型与局部变量是线程安全的。
- 如果一个对象仅仅被同一个线程使用,那么它是线程安全的。
- 如果多个线程都可以改变同一资源,那么这就不是线程安全的。
可以通过创建一个不变对象来消除线程不安全的隐患,如:
public class ImmutableValue{
private int value = 0;
public ImmutableValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
public ImmutableValue add(int valueToAdd){
return new ImmutableValue(this.value + valueToAdd);
}
}
这个类有一个成员变量value,创建一个ImmutableValue对象需要传入一个value参数,但是这个类没有提供方法来直接改变某个ImmutableValue对象中的value值,类中的add方法返回的是一个ImmutableValue对象,即每次调用ImmutableValue.add()方法都会创建另外一个ImmutableValue对象。
但是即使是使用一个不变对象,在使用这个不变对象的引用时,也可能不是线程安全的,如下面的程序:
public void Calculator{
private ImmutableValue currentValue = null;
public ImmutableValue getValue(){
return currentValue;
}
public void setValue(ImmutableValue newValue){
this.currentValue = newValue;
}
public void add(int newValue){
this.currentValue = this.currentValue.add(newValue);
}
}
在Calculator.add()方法中,由于ImmutableValue.add()方法每次返回一个新的对象,所以currentValue这个引用值改变了,所以Calculator对象本身是可以改变的。
synchronized
为了解决Java中的多线程并发访问的问题,Java提供了synchronized
来保证同一时间只有一个线程能够访问使用synchronized
关键字修饰的方法或者代码块。
synchronized是同步,使协调的意思,即在Java程序运行过程中,多个线程遇到synchronized修饰的方法或者代码块时会检查是否可以执行这段代码,如果可以就进入执行,如果不能则此时有其他线程在执行这段代码,那么这个线程就阻塞,直到正在执行这段代码的线程从这块代码中退出。
synchronized方法
synchronized
方法就是将synchronized
关键字置于方法的返回类型前方法,如
public synchronized void t() {}
就是一个synchronized
方法。
synchronized代码块
synchronized代码块就是使用synchronized关键字修饰的一段代码块,这段代码常常是方法中的一段代码。
如
synchronized(obj) {
//do something
}
就是一个synchronized代码块。
synchronized方法与代码块的区别
一个进程在调用synchronized代码块时,该线程就将synchronized后面的小括号中的对象obj锁定,此时其他线程若执行到这段代码,就检测到obj已经被某个线程锁定了,那么该线程就阻塞。与synchronized代码块不同的是,synchronized方法默认对这个方法所在对象加锁。
那么当一个线程执行某个对象的某个synchronized方法时,它是否可以执行这个对象的其他synchronized方法呢?答案是肯定的,即synchronized关键字是可重入的,当线程1进入对象2的synchronized方法3时,对象2就被加锁,并且加锁计数为1,当线程1由方法3进入synchronized方法4时,此时线程1已经拥有对象2的锁,那么加锁计数增加1,依此类推,当线程1退出某个synchronized方法时,加锁计数会减1,直到加锁计数为0,说明线程1对对象2已经解锁了,此时其他线程可以进入到对象2的synchronized方法。
此外,synchronized关键字还可以修饰static方法,此时线程加锁的对象是当前的Class对象,那么只有一个线程可以执行这个方法。
线程通信
线程通信可以使用共享对象来实现,即定义一个对象,设置一个标识,多个线程同时持有这个对象的引用,当某个线程执行完毕时,就改变这个标识,在这个标识改变之前,其他线程都不停的检查这个标识的状态,检查到标识改变后,某个线程再改变这个标识…
如果采用这样的方式,那么在同一时刻只有一个线程在正常执行,其他线程都在不断的检查这个标识的状态,这显然没有有效的利用CPU。Java提供了wait()、notify()和notifyAll()等方式来实现这个等待机制。某个运行的线程调用了wait()方法后就会进入非运行状态,直到另一个线程调用notify()方法或者notifyAll()方法,notify方法和notifyAll方法的区别是,notify方法只会唤醒所有等待的线程中的一个线程,而norigyAll方法则会唤醒所有的线程。
为了调用某个对象的wait
或者notify
方法,线程必须先获得那么对象的锁,也即线程必须在同步块里调用某个对象的wait
或者notify
方法。
假设一个线程1调用了对象A上的wait方法,那么线程1就释放了A对象的锁,此时在另一个线程2上调用了A对象的notify方法,那么将有一个因为调用A.wait()
方法而阻塞的线程将被唤醒,假设是线程1,当线程2调用A.notify()
之后,线程1并不是马上就退出wait
方法,而是要等到线程2退出调用A.notify()
的同步块之后,线程1才能重新获得A对象的锁,进而退出wait方法,即每个线程在退出wait()前必须获得监视器对象的锁,因为wait方法也是在同步块中调用的,退出wait之前必须重新获得这个同步块上的锁
Java中的锁
除了sychronized关键字,Java多线程编程中还会使用到volatile
,Atomic类(AtomicBoolean这些)以及锁机制。
volatile
主要是保证对成员变量操作的原子性,如将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改,即使使用了本地缓存,volatile域也会被立即写入到主存中,读取操作也是发生在主存中。对volatile
修饰的变量进行操作,那么这个操作必须是原子的,否则就会出现多线程中数据不一致的问题。
更多volatile的内容,可以参考http://hedengcheng.com/?p=725和http://www.ibm.com/developerworks/cn/java/j-jtp06197.html这两篇文章,很清晰的介绍了C/C++以及Java中的volatile关键字。
Atomic类在java.util.concurrent.atomic包中,用于对这些对象进行原子操作,JVM可以保证对这些对象的写操作是原子的,主要使用了CAS(compare and set)的方式进行写,即在写之前先与旧值比较,如果与旧值相同,就将新值写入,如果不同就放弃写操作。
Java中的锁与synchronized关键字类似,都是在执行写操作前,先对将要执行写操作的对象加锁,然后执行写操作,再释放锁。与锁相关的类定义在包java.util.concurrent.locks中,通过这些类可以对这些锁进行扩展,从而得到特定需求下的锁。
与synchronized关键字相比,锁机制更加灵活。使用synchronized关键字在线程试图获取某对象上的锁失败后,线程就阻塞了,假设此时有多个线程阻塞,但是有些线程的优先级高,有些优先级低,那么就应该让优先级高的先执行,使用synchronized关键字无法实现,此外如果要求线程阻塞一段时间后,如果还没有获取到等待的对象的锁,就要求线程退出,同样synchronized关键字无法实现,因synchronized而阻塞的线程无法中断。
此外,如果要手动对一个线程执行暂停操作,使用Lock生成的Condition对象来调用Condition.await()方法,则可以很方便的实现线程的暂停。
使用锁就可以得到比synchronized更加细的控制粒度,并且可以根据Java concurrent类库提供的接口来实现特定需求下的锁。
synchronized
不可被中断,而使用锁时,可以选择使用可以被中断的方式,更加灵活,如下面的程序,就无法结束,尽管使用了Future.cancle()
来中断线程。
public class SynchronizedBlock implements Runnable{
public synchronized void f() {
while(true) {//不会释放锁
Thread.yield();
}
}
public SynchronizedBlock() {
new Thread() {
public void run() {
f();
}
}.start();
}
@Override
public void run() {
f();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
Future<?> f = exec.submit(new SynchronizedBlock());
f.cancel(true);
}
java.util.concurrent.locks.Lock接口定义了三个加锁的方法,分别是lock方法,tryLock方法和lockInterruptibly方法,其中
- lock方法与synchronized类似,它是不可中断的,如下面的代码中,线程Thread A不会被中断:
@Test
public void test3() throws Exception{
final Lock lock=new ReentrantLock();
lock.lock();
Thread.sleep(1000);
Thread t1=new Thread(new Runnable(){
@Override
public void run() {
lock.lock();
System.out.println(Thread.currentThread().getName()+" interrupted.");
}
}, "Thread A");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
-
tryLock方法有两个重载方法,一个带参数一个不带参数,不带参数方法表示尝试获取锁,如果获取不到则立即返回,带参数的方法表示在调用的时刻获取锁,如果锁可用则,立即返回true,否则等待指定的时间,直到锁可用或这个线程被中断,或者等待了指定的时间,如果锁获取成功则返回true,否则返回false。
-
lockInterruptibly方法则是可以被中断的,即在调用方法的时刻,如果锁不可用,则阻塞等待,在等待过程中这个线程是可以被其他线程中断的,如下面的代码中,线程Thread A会被中断:
public void test4() throws Exception{
final Lock lock=new ReentrantLock();
lock.lock();
Thread.sleep(1000);
Thread t1=new Thread(new Runnable(){
@Override
public void run() {
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+" interrupted.");
}
}
}, "Thread A");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
线程池
线程池就是存放线程的一个容器,这个容器可以是一个数组,可以是一个链表,可以是一个队列等,常常用于限制应用程序中同一时刻运行的线程数,线程池本身也是一个线程,在这个线程对象中有一个容器,添加到线程池的容器中的线程会在某一个时刻被线程池的线程调用执行,从而创建一个新的线程。
与线程池类似的还有数据库连接池,对象池,内存池等。使用XX池的原因是,单个创建和销毁(如线程)会造成很大的消耗,首先创建一部分,等到使用时直接可用,同时提高响应速度。
Reference
Java 中的进程与线程:http://www.ibm.com/developerworks/cn/java/j-lo-processthread/
Java并发性和多线程介绍:http://ifeve.com/java-concurrency-thread-directory/
Java 理论与实践: 正确使用 Volatile 变量:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
C/C++ Volatile关键词深度剖析:http://hedengcheng.com/?p=725
《Java编程思想》