Java多线程的同步方式和锁机制
Object.wait(miliSec)/notify()/notifyAll()
线程调用wait()之后可以由notify()唤醒,如果指定了miliSec的话也可超时后自动唤醒。wait方法的调用会让当前线程放弃已经获取的object锁标志位,比如在同步代码块synchronized中调用wait(),则表示当前线程被唤醒之后需要重新获取同步代码块的锁。另外wait/notify由于要操作对象的锁标志位,因此必须在synchronized代码块中调用,否则会抛出运行时异常IllegalMonitorStateException。
wait/notify机制出现之前,生产/消费实现模型的同步一般通过while(true)轮询实现,弊端是极大耗用CPU资源做无用的轮询。在调用wait方法之前,线程需要获取当前实例对象的锁,执行wait方法返回之后,线程释放掉对象锁并进入block状态;其他线程在调用notify方法之前,也需要获取当前实例对象的锁,执行notify方法时,如果有多个线程处理block状态则从中按某规则选择一个唤醒,notify方法调用之后不会立即释放锁,要等线程的同步方法执行完毕之后才释放对象锁,因此一次notify调用只会唤醒一个线程,其他block的线程依旧处理block状态。
1 public class App1 extends Thread { 2 private Object lock; 3 public App1(Object lock) { 4 super(); 5 this.lock = lock; 6 } 7 @Override 8 public void run() { 9 try { 10 synchronized (lock) { 11 System.out.println(Thread.currentThread().getName() 12 + " : start to wait."); 13 lock.wait(); 14 System.out.println(Thread.currentThread().getName() 15 + " : wait ends, execute again."); 16 } 17 } catch (Exception e) {} 18 } 19 } 20 public class App2 extends Thread { 21 private Object lock; 22 public App2(Object lock) { 23 super(); 24 this.lock = lock; 25 } 26 @Override 27 public void run() { 28 synchronized (lock) { 29 System.out.println(Thread.currentThread().getName() 30 + " : Start notify."); 31 lock.notify(); 32 System.out.println(Thread.currentThread().getName() 33 + " : notify ends, start to execute again."); 34 } 35 } 36 public static void main(String[] args) { 37 try { 38 Object lock = new Object(); 39 App1 app1 = new App1(lock); 40 app1.start(); 41 Thread.sleep(5000); 42 App2 app2 = new App2(lock); 43 app2.start(); 44 } catch (Exception e) {} 45 } 46 } 47
Thread.sleep(miliSec)
线程释放CPU使用权,并进入休眠状态一段时间miliSec,不会放弃线程的锁标志位,比如如果在同步代码块synchronized中调用sleep(),表示线程将一直持有当前同步代码块的锁,其他线程将一直等待。
Thread.suspend()/resume()
两个方法配套使用,suspend进入的状态必须有resume调用恢复。跟sleep()方法类似,suspend方法也不会放弃线程已经获取的object锁标志位。这对方法已经不推荐使用,因为容易造成线程自己将自己suspend起来。
Thread.yield()
表示当前线程已经获得了充分的CPU执行时间,释放CPU使用权,并重新进入队列等待执行,yield()调用不会阻塞当前线程,也不会放弃当前线程已经获取的锁标志位 ,因此yield方法仅能让跟当前线程具有同样优先级的线程有限执行。
Thread.join(miliSec)
表示当前线程需要等到join方法的调用线程执行完毕之后才能继续执行,或者是等待join方法的调用线程执行一段时间之后当前线程才能执行,内部由wait方法实现,所以线程等待开始的时候就会释放持有的对象锁。
使用synchronized关键字修饰代码块或者方法
表示这块代码为互斥区或者临界区。有两种类型的锁可以通过synchronized加到代码块或者方法上,一种是实例Object锁,一种是class锁。对于同一个ClassLoader下加载的类而言,一个类只有一把class锁,所有这个类的实例都共享一把锁;同一个类可以实现多个实例对象,也就存在多把Object锁。
synchronized是语言自带的内置独享锁(非公平锁,不管race thread排队的时间先后,通过编排字节码实现,锁为对象或者类的头标记位),而Java语言的ReentraintReadWriteLock机制是基于Abstract Queued Synchronizer的一种实现(公平/非公平锁,state加CLH队列实现),主要的实现类是ReentrantLock;Java的数据主要会在CPU、Register、Cache、Heap和Thread stack之间进行复制操作,而前面四个都是在Java Threads之间共享,因此Java的锁机制主要用于解决Racing Threads的数据一致性;
另外通过synchronized添加的锁具有可重入性,也就是只要一个线程已经获取了锁,这样只要共享同一把锁的其他synchronized修饰的代码块或者方法都可以进入,换句话说其他线程访问对其他synchronized修饰的代码块或者方法也需要等待锁的释放,因此synchronized还支持任意对象的锁,这样同一个类的不同方法可以添加不同的对象锁。
1 public class App1 { 2 private Object lock1 = new Object(); 3 private static Object lock2 = new Object(); 4 5 synchronized public void funcA() { 6 //this object lock 7 } 8 public void funcB() { 9 synchronized(this) { 10 //this object lock 11 } 12 //run something without lock 13 } 14 public void funcC(List<String> list) { 15 synchronized(list) { 16 //list object lock 17 } 18 } 19 public void funcD() { 20 synchronized(lock1) { 21 //lock1 object lock 22 } 23 } 24 public void funcE() { 25 synchronized(lock2) { 26 //lock2 static object lock 27 } 28 } 29 public void funcF() { 30 synchronized(App1.class) { 31 //App1 class lock 32 } 33 } 34 synchronized public static void funcG() { 35 //App1 class lock 36 } 37 }
使用ReentraintLock和ReentraintReadWriteLock实现线程的同步
java的lock机制基于Abstract Queued Synchronizer (AQS)的实现,AQS定义了多线程访问共享资源的同步器框架,常见的如ReentraintLock/Semaphore/CountDownLatch等都依赖于AQS的实现;
AQS通过维护一个FIFO队列,并且通过一个由volatile修饰的int状态值来实现锁的获取。FIFO队列中每一个Node表示一个排队线程,其保存着线程的引用和状态,然后通过三个方法分别对获取或者设置状态。
1 private volatile int state; 2 static final class Node { 3 int waitStatus; 4 Node prev; 5 Node next; 6 Node nextWaiter; 7 Thread thread; 8 } 9 protected final int getState() { 10 return state; 11 } 12 protected final void setState(int newState) { 13 state = newState; 14 15 protected final boolean compareAndSetState(int expect, int update) { 16 // See below for intrinsics setup to support this 17 return unsafe.compareAndSwapInt(this, stateOffset, expect, update); 18 } 19 protected boolean tryAcquire(int arg) 20 protected boolean tryRelease(int arg) 21 protected int tryAcquireShared(int arg) 22 protected boolean tryReleaseShared(int arg) 23 protected boolean isHeldExclusively()
通过对getState,setState和compareAndSetState的封装,AQS的继承类需要试下如下几个方法,前面两个表示获取和释放独占锁(如ReentraintLock),后面两个表示获取和释放共享锁(如Semaphore和CountDownLatch)。ReentrantLock初始化状态state=0,线程A访问同步代码的时候使用ReentrantLock.lock(),内部会调用tryAcquire尝试获取独占锁,状态变成state+1;其他线程调用ReentrantLock.lock()的时候就会失败,直到线程A调用unlock(内部为tryRelease)将状态编程state=0;如果线程A在持有独占锁的同时访问其他同步代码块,这时候state的值就会累加,需要调用unlock(内部为tryRelease)减少state的值。ReentrantLock也提供了类似wait/notify的方法,await/signal,同样的线程在调用这两个方法之前需要获得对象锁监视,也就是执行lock.lock()方法。
ReentrantLock是纯粹的独占锁,为了提升效率引入了ReentrantReadWriteLock.readLock/writeLock,读读共享,读写互斥,写写互斥。CLH队列中的节点模式分为shared和exclusive两种,当一个线程修改了state状态则表示成功获取了锁,如果线程的模式是shared则会执行一个传递读锁的过程,策略是从CLH队列的头到尾依次传递读锁,直到遇到一个模式为exclusive的写锁模式的节点,这个exclusive模式的节点需要等之前所有shared模式的节点对应的操作都执行完毕之后才会获取到锁,这就是读写锁的模式。
1 public class App1 extends Thread { 2 private Lock lock = new ReentrantLock(); 3 private Condition condition = lock.newCondition(); 4 public App1() { 5 super(); 6 } 7 @Override 8 public void run() { 9 try { 10 lock.lock(); 11 System.out.println(Thread.currentThread().getName() + " : start to wait."); 12 condition.await();//condition.signal(); 13 System.out.println(Thread.currentThread().getName() 14 + " : wait ends, execute again."); 15 } catch (Exception e) { 16 17 } finally { 18 lock.unlock(); 19 } 20 } 21 }