java线程
应用多线程一来可以为主线程分担耗时较多的任务,提高主线程的响应速度,二来随着计算机多处理能力的增加,可以提高计算机的使用性能。
首先我们来看java是如何创建线程的。创建一个线程传统上有两种方式,一种是继承线程Thread类,创建Thread类实例,调用start()方法;还有一种就是实现runnable接口,创建new Thread(runnable()).start().两种方式本质上还有创建一个线程类。还可以采用executer.execute()的方式提交一个线程,或者更高级的executorService.submit()方法来提交线程。
当多个线程同时运行的时候,由于它们之间对于进程的资源是共享的,对于共享的资源占有会有先后,因而会产生竞争关系,同时还会产生资源的一致性问题。这些关系综合在一起会产生如果处理不当会产生不良的后果。当线程多的时候,多个线程之间的关系会有多种,一种是主子线程关系,即子线程为主线程服务,协助主线程完成一个任务,还有是并列关系,即为了提高程序响应速度,采用多个线程来并发处理同样的任务,还有一种是顺序/互补关系
1.主子关系
主子关系更多的是主线程将一个耗时较多的任务分给子线程,子线程处理完成后通知给主线程。这种关系通常不会产生对资源的同步访问,但是会存在子线程处理完任务后通知主线程的问题。那么如何解决这个问题呢?其实,java中一个线程在执行完成任务后就会消亡,所以不会存在子线程主动通知主线程,那么子线程如何通知主线程呢?这里有两种方式,一种是设置共享变量,子线程完成后,更改共享变量的值来达到通知主线程其任务完成的目的,另外一种就是主线程在空闲的时候主动探查子线程是否执行完成任务,一种方式是查看子线程是否还活动(alive)t.isAlive(),如果还活动,那么可以选择等待(t.join()),也可以继续执行它自己的任务。或者如果采用executorService.submit()方法来提交线程的话,会得到一个furture对象,用于检测子线程是否完成(furture.isDone)。
2. 并列关系
并列关系是比较复杂的多线程之间的关系,因为会设计到多个线程对同一个共享资源的访问,为了保证多个线程对同一个共享资源访问不出现冲突,java设计了一整套的方法来保证。
2.1 原子操作
首先,如果多个线程对于一个资源的访问过程都是一次性操作,而不存在操作过程中资源的中间状态,那么这样的操作称为原子操作,如果线程对于资源的操作都是原子操作,那么多个线程之间就不需要同步,因为其本身并不存在冲突,那么对哪些资源的操作是原子操作呢?java中对于基本类型以及对象的引用类型,以及被声明为volatile的变量
2.2 操作同步
那么如果对于一个资源的访问不是原子操作,而是带有中间状态的操作会怎样呢?单以简单的c++为例,不考虑虚拟机的操作,c++可以分解为一下3步:
1)获取c的值
2)将获取的值加1;
3)将新的值写回到c
这样的3步操作如果有多个线程同时进行,那么后果就不是我们能够预料的了。那么如何保证其操作的安全有序呢?java给出的解决方案是采用加锁的方式来产生排他性操作,即我要访问某个资源,如果已经被我占有了,那么我就会给它加上一把锁,这样,在我占有使用的过程中,让其他线程无法使用,只有我用完了,把锁释放了后其他线程才能使用。那么如何加锁呢?最普遍常见的方法是在方法上使用synchronized关键字。加上synchronized关键字的方法是在一个线程执行过程中会对其他线程产生排他性操作。那么synchronized关键字是否给方法上了锁呢?答案是是的,这里涉及到内在锁的概念,在java中,每一个对象都有一个内在锁与它关联,一个线程要想排他性的访问一个对象的字段必须首先获得对象的内部锁,一旦获得了该对象的内部锁,在其释放之前,其他线程是无法获取到该锁的。那么这个内部锁到底是什么呢,我们可以理解为其就是对象本身,所以我们只要锁定对象本身,我们就获得了对象的访问权,所以我们还可以显式的去锁定对象synchronized(this),这样我们就可以更灵活的不去锁定这个方法本身,而是锁定方法中需要同步的某个代码块。进而我们还可以显式的定义与每个字段关联的对象锁,方便对每个字段的排他性访问而互不影响。
public class MsLunch {
PRivate long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
2.3 锁
锁单独讲,其所应有的含义应该是能加锁和释放锁。java.util.concurrent.locks中的lock接口就给出了这样的解释。它本身包含lock()和unlock()方法。这样,我们就可以在需要加锁的地方显式的进行加锁,lock.lock(),用完之后显式的释放掉锁lock.unlock(),java.util.concurrent.locks中给我们提供了两种常用的锁的实现。
2.3.1 重入锁ReentrantLock
重入锁是在线程可以再次获取到它已经拥有的锁,即对对象进行二次加锁。对象的内部锁也是支持重入的。
2.3.2 重入读写锁ReentrantReadWriteLock重入读写锁是对一个资源既存在读操作又存在写操作的情况下定义的锁,该锁实际上包含两把锁,读锁和写锁。读锁对其他的读操作没有排他性,但是写锁对于其他操作有排他性,也就是说当获取读锁的时候只要该资源没有写锁就可以,但是当获取写锁的时候必须要当前资源没有锁,否则该线程将会处于等待过程中。很显然,读写锁对于资源处于大多数读操作少量写操作的时候有很大的优势,反之,会降低程序的性能。
3.互补关系
当两个线程之间的执行是后一个线程需要前一个线程为其提供条件,而后一个线程的执行又为前一个线程的执行提供保障,我称之为互补关系,典型的例子是生产-消费者模型。消费者需要生产者为其提供产品,消费者同样需要消费产品为生产者提供空间。这样的两个线程之间,虽然也存在对共同资源的访问-产品存放空间,这个通过前述各种同步就能够很好地解决,但是还有一个新的问题,就是当生产者有了产品的时候如何通知消费者,同样消费者消耗掉产品如何通知生产者继续生产,如果通过前述的方式,二者设置共享变量,那么就会存在生产者和消费者不断地对变量进行轮询(Guarded Blocks),从而消耗大量cpu资源,又二者不属于主从关系,因此无法使用join,那么解决这个问题就引入了新的机制,等待-通知机制(wait-notify/notifyAll)。当生产者发现生产空间已经占满,就处于等待状态wait,程序将生产者线程挂起,当消费者取走产品释放出空间的时候,就通知notify生产者去生产产品,同样当消费者发现没有产品的时候,也处于等待状态(wait),生产者将产品生产好以后,就通知(notify)消费者。采用显式加锁的方案是对锁对象产生条件性(condition)等待,当对生产空间进行加锁lock后,生产者对于生产空间添加产品,发出非空信号(notEmpty.signal()),同时产生非满(notFull=lock.newCondition())等待(notFull.await()),消费者对于生产空间产生非空(notEmpty=lock.newCondition())等待(notEmpty.await()),当被唤醒后取走产品发出(notFull.signal())唤醒生产者。
新闻热点
疑难解答