这几天看了《实战java高并发程序设计》这本书,在此做个简短的记录。首先是Java并发中的同步控制组件。
1.volatile
当你用 volatile去申明一个变量时,就等于告诉了虚拟机,这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内的所有线程都能够“看到”这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。但是volitile并不能保证一些复合操作的原子性。
2.synchronized
关键字 synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每次,只能有一个线程进入同步块,从而保证线程间的安全性。
关键字 synchronized可以有多种用法。这里做一个简单的整理。
- 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run(){
for(int j=0;j<1000000;j++){
synchronized(instance){
i++;
}
}
}
}
- 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run(){
for(int j=0;j<1000000;j++){
increase();
}
}
private synchronized void increase(){
i++;
}
public static void main(String[] args){
AccountingSync instance=new AccountingSync();
Thread thread1=new Thread(instance);
Thread thread2=new Thread(instance);
thread1.start();
thread2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
此时需要注意的是两个线程必须同时指向同一个Runnable对象,若是指向同一个类的两个不同对象便不能够达到加锁的效果,因为此时锁的粒度是针对一个对象的。
- 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
当两个线程指向同一个Runnable接口的两个不同对象时,如果要达到加锁的效果,只要将加锁的方法改成static静态方法便可以了,此时锁是针对这个类,而不是单个对象。
public static synchronized void increase(){
i++;
}
3.重入锁--ReentrantLock
与 synchronized相比,重入锁有着显示的操作过程。开发人员必须手动指定何时加锁,何时释放锁,因此重入锁的灵活性要高于synchronized,但是在这要声明,synchronized也是支持重入的。
public class ReentrantLockDemo implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo=new ReentrantLockDemo();
Thread thread1=new Thread(demo);
Thread thread2=new Thread(demo);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(i);
}
}
4.Condition--条件变量
如果大家理解了 Object.wait()和 Object.notify()方法的话,那么就能很容易地理解 Condition对象了。它和 wait()和 notify()方法的作用是大致相同的。但是 wait()和notify()方法是和synchronized关键字合作使用的,而Condtion是与重入锁相关联的。
5.信号量--semaphore
信号量为多线程协作提供了更为强大的控制方法。广义上说,信号量是对锁的扩展。无论是内部锁 synchronized还是重入锁 Reentrantlock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。
6.读写锁--ReadWriteLock
ReadWriteLock是JDK5中提供的读写分离锁。读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。读写锁允许多个线程同时读,,但是,考虑到数据完整性,写写操作和读写操作间依然是需要相互等待和持有锁的。
- 读-读不互斥:读读之间不阻塞
- 读-写互斥:读阻塞写,写也会阻塞读
写-写互斥:写写阻塞
7.倒计时器--CountDownLatch
Countdownlatch是一个非常实用的多线程控制工具类。“ Count Down”在英文中意为倒计数, Latch为门栓的意思。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
CountDownLatch只提供了一个构造器:
public CountDownLatch(int count){}
然后下面这3个方法是CountDownLatch类中最重要的方法:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };
下面的例子能够比较清晰的介绍CountDownLatch的用法:
public class CountDownLatchTest {
public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(2);
new Thread(){
public void run() {
try {
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new Thread(){
public void run() {
try {
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
try {
System.out.println("等待2个子线程执行完毕...");
latch.await();
System.out.println("2个子线程已经执行完毕");
System.out.println("继续执行主线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
8.循环栅栏--CyclicBarrier
CyclicBarrier是另外一种多线程并发控制实用工具。和 CountDownLatch非常类似,它也可以实现线程间的计数等待,但它的功能比 CountDownLatch更加复杂且强大。CyclicBarrier可以理解为循环栅栏。前面 Cyclic意为循环,也就是说这个计数器可以反复使用。比如,假设我们将计数器设置为10,那么凑齐第一批10个线程后,计数器就会归零,然后接着凑齐下一批10个线程,这就是循环栅栏内在的含义。
CyclicBarrier提供2个构造器:
public CyclicBarrier(int parties, Runnable barrierAction) {}
public CyclicBarrier(int parties) {}
参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。然后CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:
public int await() throws InterruptedException, BrokenBarrierException {}
public int await(long timeout, TimeUnit unit)throws
InterruptedException,BrokenBarrierException,TimeoutException {}
下面举一个例子来详细介绍 CyclicBarrier的详细用法:
public class Test {
public static void main(String[] args) {
int N = 4;
CyclicBarrier barrier = new CyclicBarrier(N);
for(int i=0;i<N;i++)
new Writer(barrier).start();
}
static class Writer extends Thread{
private CyclicBarrier cyclicBarrier;
public Writer(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
try {
Thread.sleep(5000); //以睡眠来模拟写入数据操作
System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务...");
}
}
}
9.线程工具阻塞类--LockSupport
LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。和Thread.suspend()相比,它弥补了由于 resume在前发生,导致线程无法继续执行的情况。和Object.wait()相比,它不需要先获得某个对象的锁,也不会抛出 InterruptedException异常。