多线程:线程状态、synchronized关键字、读写锁、条件对象、Volatile、阻塞队列等小结
关于多线程,在这里做下总结,也方便以后自己查阅。线程和进程的关系应该都知道了,这里不细说。在线程出现以前,进程是资源分配和处理机调度的基本单位,有了线程以后,进程只是资源分配的单位,而线程是处理机调度的单位。进程只分配资源,线程不分配或者说只包含极少的必要的资源。进程之下有线程,线程共享进程的资源。
这里说下线程的几种状态,在操作系统里是这样划分的:
线程状态有五种,分别是:新建、就绪、运行、阻塞、终止。其线程生命周期过程如下:
这里简单说明下各种状态以及之间的转换:
新建:指的是线程正在被创建,尚未尚未转到就绪状态。
就绪:已经处于准备运行的状态,即获得了处理机之外的所有资源,一旦拥有处理机,即可运行。
运行:正在处理机上运行,由就绪状态转变而来,也可以变为就绪状态和阻塞状态,此时时间片用尽或线程剥夺处理机,或资源被别的线程剥夺。
阻塞:正在等待某一事件或资源,一旦拥有后变为阻塞状态。
下面说下Java中线程的六种状态:在Java中,线程分为新创建、可运行、被阻塞、等待、计时等待六种。
新创建:NEW 、可运行:RUNNABLE(并不一定在运行) 、被阻塞:BLOCKED、
等待:WAITING、计时等待:TIMED_WAITING
针对上图,可知,被阻塞是因为没有得到锁而引起的,等待是线程线程在等待某个事件通知,而计时等待是等待超时。其实在操作系统里,阻塞和等待是同一个概念,并没有这样细化分。
这里简单说下守护线程,守护线程唯一的用途就是为其他线程提供服务,如计时器线程,守护线程应该永远不去访问固定资源,因为它在任何一个操作之间都可能会发生终端,将一个线程转变为守护线程: t.setDaemon(true)
还要解释下一直困扰我的一个问题,就是线程和代码的关系,如果弄不清楚这个问题,就很难理清线程并发的问题。我的理解是这样的,线程在操作系统中拥有一定的数据结构,如进程的PCB一样,在这个结构中,拥有一个代码入口,所以我们的线程才会执行一定的代码。这里就产生了多线程的问题,比如说,我们写了一个类或者说是方法,其中包含全局变量,有多个线程拥有此代码块的地址,所以会有多线程在一段时间同时执行,如果都是局部变量则不会有问题,但是对于全局变量,在内存会有一个内存区存储此变量,而多个线程之间运行此代码块的同时,操作的是同一个地址,所以产生了竞争的问题。
锁对象和条件对象:Java锁对象是JDK5之后新增内容,在java.util.concurrent包下,大概有ReentrantLock、ReentrantReadWriteLock等。条件对象指的是当满足一定条件时,调用await()方法使当前线程阻塞,并放弃锁,此时线程进入等待集,当锁可用时,此线程不能马上解除,相反,它处于阻塞状态,直到有线程调用同一条件上的signalAll()方法。
简单用法如下:
Class myClass{
private Condition condition;
private ReentrantLock lock = new ReentrantLock();
public myClass(){
Condition = lock.newCondition();
}
public void test(){
Try{
lock.lock();
while(...)
condition.await();
condition.signalAll();
}finally{
lock.unlock()
}
}
}
此处,signalAll()不会立即激活一个等待线程,它仅仅解除等待线程的阻塞,以便这些线程在当前线程退出同步方法后,通过竞争实现对对象的访问。而signal()方法则是随机选取一个线程激活,所以要使用signalAll()方法。对于notifyAll()也是同样的道理。
对于读写锁ReentrantReadWriteLock,用法如下:
1)构造一个读写锁ReentrantReadWriteLock对象
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
2)获取读写锁
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
3)对所有的获取方法加读写锁
Public void transfer(...){
writeLock.lock();
try{....}
finally{
writeLock.unlock();
}
}
Lock readLock(): 得到一个可以被多个读操作共用的读锁,但会排斥所有写操作
Lock writeLock():得到一个写锁,排斥所有其他的读操作和写操作
synchronized关键字:这个应该不用多说,就是同步代码块和同步方法,以及加锁方式,可以用对象加锁,也可以用.class加锁,取决于线程共享范围。另外说明一下,对应于条件对象的signalAll()和singnal()方法,同步关键及加锁用的是notifyAll()和notify()方法,分别是唤醒全部等待此锁的线程和随机唤醒一个线程。
注:synchroinzed关键字锁不可中断,ReentrentLock可中断(JDK5新特性)
Volatile关键字:对于这个关键字,一直有些争议,其实说的简单些,就是实现线程之间共享数据的可见性,所以就有人说,既然是可见的,那不就是线程安全的吗,这里并不是这样的,volatile关键字的原理,我理解是这样的,多个线程读取同一个内存地址,获得了数据,并对持有了这个数据(每个线程都有一份,或者说,在多处理器的机器上,存储在本地处理器缓存中),当线程修改了这个数据之后,使用此关键字会强制对内存缓冲区进行刷新,将任何一次修改都同步到临界内存地址,所以说,能否满足线程安全同步是要看不同的业务需求的。(V简而言之,Volatile域会被立即写入内存,而读操作就发生在内存中)锁和同步关键字的区别在于,锁不仅保持了线程之间的可见性,还提供了互斥独占的效果。比如说,如果要对一个资源进行同步,我们不仅要对写方法加锁,还要对其读方法加锁,这样才能实现同步,因为单纯的加写锁,只是实现了互斥的写操作,而并没有保证读之间的可见性,加锁之后,可以让线程能够看到最后上一个线程修改内容,这样才能有效实现安全同步。
对于多线程安全,建议优先使用并发包下的数据结构,i比如阻塞队列等,其次考虑同步关键字,最后考虑锁以及条件对象。就简单的总结这么多吧,本文是总结性的东西,并不是对每一点详细解释,不当之处,敬请指出。
注:条件对象的await方法、 Object的wait方法会使阻塞线程释放锁,而sleep方法、yield方法不释放锁!