1.synchronnized
2. Lock3.reentrantLock
6.readwritelock
7.countDownLatch
8.cyclicBarrier
synchronnized
synchronized的局限性
synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要释放锁,非常方便。然而synchronized也有一定的局限性,例如:
1.当线程尝试获取锁的时候,如果获取不到锁一直堵塞。
2.如果获取锁的线程进入休眠或者堵塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。
synchronized的使用
synchronized是java中的关键字,是一种同步锁,它修饰的对象有一下几种:
1.修饰一个代码块。被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码的对象。
2.修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3.修饰一个静态的方法 ,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
4.修饰一个类,其作用的范围是synchronized后面括号起来的部分,作用的对象是这个类的所有对象
修饰一个代码块
1.一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被堵塞。
demo:synchronized的用法:
/**
* 同步线程
*/
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
}
SyncThread的调用:
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();
结果如下:
SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受堵塞,必须等待当前线程执行完成这个代码以后才能执行该代码块。Thread1和Thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码才能释放该对象锁,下一个线程才能执行并锁定该对象。
对上述代码修改后
Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
thread1.start();
thread2.start();
结果如下:
SyncThread1:0
SyncThread2:1
SyncThread1:2
SyncThread2:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread1:7
SyncThread1:8
SyncThread2:9
不是说一个线程执行synchronized代码块时其他线程受阻塞吗?为什么上面的例子中thread1和thread2同时在执行。这是因为synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联,而这两把 锁是想不干扰的,不形成互斥,所以两个线程可以同时执行。
2.当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
多线程访问synchronzied和非synchronized代码块
class Counter implements Runnable{
private int count;
public Counter() {
count = 0;
}
public void countAdd() {
synchronized(this) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//非synchronized代码块,未对count进行写操作,所以可以不用synchronized
public void printCount() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + " count:" + count);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("A")) {
countAdd();
} else if (threadName.equals("B")) {
printCount();
}
}
}
调用代码
Counter counter = new Counter();
Thread thread1 = new Thread(counter, "A");
Thread thread2 = new Thread(counter, "B");
thread1.start();
thread2.start();
结果如下
A:0
B count:1
A:1
B count:2
A:2
B count:3
A:3
B count:4
A:4
B count:5
上面代码中countAdd是一个synchronized的,printCount是非synchrozed的。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的synchronized的代码块而不受阻塞。
指定对象加锁
/**
* 银行账户类
*/
class Account {
String name;
float amount;
public Account(String name, float amount) {
this.name = name;
this.amount = amount;
}
//存钱
public void deposit(float amt) {
amount += amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取钱
public void withdraw(float amt) {
amount -= amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public float getBalance() {
return amount;
}
}
/**
* 账户操作类
*/
class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account) {
this.account = account;
}
public void run() {
synchronized (account) {
account.deposit(500);
account.withdraw(500);
System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
}
}
}
调用代码
Account account = new Account("zhang san", 10000.0f);
AccountOperator accountOperator = new AccountOperator(account);
final int THREAD_NUM = 5;
Thread threads[] = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i ++) {
threads[i] = new Thread(accountOperator, "Thread" + i);
threads[i].start();
}
结果如下:
Thread3:10000.0
Thread2:10000.0
Thread1:10000.0
Thread4:10000.0
Thread0:10000.0
在AccountOperator类中的run方法里,我们用synchronized给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到这个锁谁就可以运行它所控制的的那段代码。
当有一个明确对象作为锁时,就可以用类似下面这样的方式写程序
public void method3(SomeObject obj)
{
//obj 锁定的对象
synchronized(obj)
{
// todo
}
}
修饰一个方法
synchronized修饰一个方法很简单,就是在方法的前面加谁又能吃融资额度,public synchronized void menthod(){//todo};sychronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。
Synchronized作用于整个方法的写法。
写法一:
public synchronized void method()
{
// todo
}
写法二:
public void method()
{
synchronized(this) {
// todo
}
}
写法一修饰的是一个方法,写法二修饰的是一个代码块,但写法一和写法二是等价的,都是锁定了整个方法时的内容。
在用synchronized关键字修饰方法要注意一下几点:
1.synchronized关键字不能继承。
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类的这个方法默认情况下并不是同步的,而必须在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
两种方式的例子代码如下:
在子类方法汇总加上synchronized关键字:
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public synchronized void method() { }
}
在子类方法中调用父类的同步方法:
class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }
}
1.在定义接口方法时不能使用synchronized关键字。
2.构造方法不能使用synchronized关键字,但可以使用synchronized代码来进行同步。
修饰一个静态的方法
synchronized也可以修饰一个静态方法,用法如下
public synchronized static void method() {
// todo
}
我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。我们对Demo1进行一些修改如下:
【Demo5】:synchronized修饰静态方法
/**
* 同步线程
*/
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public synchronized static void method() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void run() {
method();
}
}
调用代码:
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();
结果如下:
SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。这与Demo1是不同的。
我们把Demo5再作一些修改。
【Demo6】:修饰一个类
/**
* 同步线程
*/
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public static void method() {
synchronized(SyncThread.class) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public synchronized void run() {
method();
}
}
其效果和【Demo5】是一样的,synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。4
synchronized和lock
synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁是由jvm实现,用户不需要显示的释放锁,非常方便。然而synchronized也有一定的局限性,
1.当线程尝试获取锁的时候,如果获取不到锁会一会堵塞。
2.如果获取锁的线程进入休眠或者堵塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。
JDK1.5之后发布,加入Doug Lea实现的concurrent包。包内提供Lock类,用来提供更多扩展的加锁功能。Lock弥补了synchrinized的局限,提供了更细粒度的加锁功能。
Lock
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
其中最常用的就是Lock和unLock操作了,因为使用Lock时,需要手动的释放锁,所以需要使用try...catch来包住业务代码,并且在finally中释放锁
private Lock lock = new ReentrantLock();
public void test(){
lock.lock();
try{
doSomeThing();
}catch (Exception e){
// ignored
}finally {
lock.unlock();
}
}
AQS
abstractQueuedSynchronized简称AQS,是一个用于重构锁和同步容器的框架,事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock。FutureTash等。AQS解决了在实现同步容器时设计的大量细节问题。
AQS使用一个FIFO的列队表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的几点与等待线程关联,每个节点维护一个等待状态。
AQS中还有一个表示状态的字段state,例如ReentrantLock用他表示线程重入锁的次数。semaphonre用它表示剩余的许可数量,FutureTask用它表示任务的状态,对state变量值的更新都采用CAS操作保证更新操作的原子性。
AbstractQueuedSynchronizer继承了AbstractOwnableSynchronizer,这个类只有一个变量:exclusiveOwnerThread,表示当前占用该锁的线程,并且提供了相应的get,set方法。
理解AQS可以帮助我们更好的理解JCU包中的同步容器。
Lock()和unLok()实现原理
reentrantLock是lock默认实现之一。那么lock()和unlock()是怎么实现的呢?首先我们要弄清楚几个概念
可重入锁:可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
可中断锁:可中断锁是指线程尝试获取锁的过程中,是否可以响应中断,synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
公平锁与非公平锁:公平锁是指多个线程同时尝试获取同一把锁时,获取线程的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
ReentrantLock内部结构
reentrantLock提供了两个构造器,分别是:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认构造器初始化为NonfairSync对象,即非公平锁,而带参数的构造器可以指定使用公平锁和非公平锁。由Lock()和unlock的源码可以看到,他们只是分别调用了sync对象的lock和release的方法
Sync是ReentrantLock的内部类,它的结构如下
可以看到Sync扩展了AbstractQueuedSynchronizer。
我们从源代码出发,分析非公平锁获取锁和释放锁的过程。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用一个锁时,cas操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就能“插队”了。
若当前三个线程去竞争锁,假设线程A的CAS操作成功了,拿到了锁开开心心的返回了,那么线程B和C则设置state失败,走到了else里面。我们往下看acquire。
acquire(arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。
tryAcquire(arg)
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state变量值
int c = getState();
if (c == 0) { //没有线程占用锁
if (compareAndSetState(0, acquires)) {
//占用锁成功,设置独占线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新state值为新的重入次数
setState(nextc);
return true;
}
//获取锁失败
return false;
}
非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁为被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。
2.第二步,入队。由于上文中提到线程A已经占用了锁,所以B和C执行tryAcquire失败,并且等待队列。如果线程A拿着锁不放,那么B和C就会被挂起。
先看一下入队的过程。
先看addWaiter
/**
* 将新节点和当前线程关联并且入队列
* @param mode 独占/共享
* @return 新节点
*/
private Node addWaiter(Node mode) {
//初始化节点,设置关联线程和模式(独占 or 共享)
Node node = new Node(Thread.currentThread(), mode);
// 获取尾节点引用
Node pred = tail;
// 尾节点不为空,说明队列已经初始化过
if (pred != null) {
node.prev = pred;
// 设置新节点为尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
enq(node);
return node;
}
B、C线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到enq(node)。我们假设同时走到了enq(node)里。
/**
* 初始化队列并且入队新节点
*/
private Node enq(final Node node) {
//开始自旋
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 如果tail为空,则新建一个head节点,并且tail指向head
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// tail不为空,将新节点入队
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B,C开始第二轮循环,此时tail已经不为空,二个线程都走到else里面、假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可成功 入队。
当B,C入等待队列后,此时AQS队列如下:
3.第三步,挂起。B和C相继执行acquireQueued(final Node node,int arg)。这个方法已经入队的线程尝试获取锁,若失败则会被挂起。
/**
* 已经入队的线程尝试获取锁
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //标记是否成功获取锁
try {
boolean interrupted = false; //标记线程是否被中断过
for (;;) {
final Node p = node.predecessor(); //获取前驱节点
//如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node); // 获取成功,将当前节点设置为head节点
p.next = null; // 原head节点出队,在某个时间点被GC回收
failed = false; //获取成功
return interrupted; //返回是否被中断过
}
// 判断获取失败后是否可以挂起,若可以则挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程若被中断,设置interrupted为true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
code里的注释已经很清晰的说明了acquireQueued的执行流程。假设B和C在竞争锁的过程中A一直持有锁,那么它们的tryAcquire操作都会失败,因此会走到第2个if语句中。我们再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事吧。
/**
* 判断当前线程获取锁失败之后是否需要挂起.
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱节点状态为signal,返回true
return true;
// 前驱节点状态为CANCELLED
if (ws > 0) {
// 从队尾向前寻找第一个状态不为CANCELLED的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前驱节点的状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* 挂起当前线程,返回线程中断状态并重置
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。
最终队列可能会如下图所示
线程B和C都已经入队,并且都被挂起。当线程A释放锁的时候,就会去唤醒线程B去获取锁啦。
各种版本控制工具的使用
Condition
condition将object监视器方法(wait,notify和notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用,为每个对象提供多个等待set(wait-set)。其中,lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。
条件(也称为条件队列或条件变量)为线程提供 了一个含义,以便在某个状态条件现在可能为true的另一线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,多以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait
做的那样
Condition实例实质上被绑定在一个锁上。要为特定Lock实例获得Condition实例,请使用其newCondition()方法。
作为一个示例,假定有一个绑定的缓存区,它支持put和 take方法。如果试图在空的缓存区上执行take操作,则在某一项变得可用之前,线程将一直阻塞;如果试图在满的缓存区上执行put操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待set中保存put线程和take线程,这样就可以在缓存区中的顶或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个condition实例来做到这一点。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
Condition实现可以提供不同于Object监视器方法的行为和语义,比如受保证的通知排序,或者在执行通知时不需要保持一个锁。如果某个实现提供了这样特殊的语义,则该实现必须记录这些语音。
注意:condition实例只是一些普通的对象,它们自身可以用作synchronized语句中的目标,并且可以调用自己的wait和notification监视器方法。获取condition实例的监视器锁或者使用其监视器方法,与获取和该condition相关的lock或使用其waiting和signalling方法没有什么特定的关系。为了避免混淆,建议除了在其自身的实现中之外,切勿以这种方式使用 Condition
实例。
实现注意事项
在等待condition时,允许发生“虚假唤醒”,这通常作为基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,因为condition应该总是在一个循环中被等待,并测试正被等待的状态声明。某个实现可以随意移除可能的虚假唤醒,但建议应用程序总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待。
三中形式的条件等待(可中断,不可中断和超时)在一些平台上的实现以及他们的性能特征可能会有所不同。
Condition方法详解
1.void await() throws interruptedException 造成当前线程在接到信号或被中断之前一直处在等待状态。
与此condition相关的锁以原子方式释放,并且出于线程调度的目的,将禁用当前线程,且在发生以下 四种情况之一以前,当前线程将一直处于休眠状态。
(1)其他某个线程调用此Condition的signal()方法,并且碰巧将当前线程选为被唤醒的线程:
(2)其他某个线程调用此Condition的signalAll()方法;
(3)其他某个线程中断当前线程,且支持中断线程挂起;
(4)发生“虚假唤醒”
在所有情况下,在此方法可以返回当前线程之前,都必须重新获取与此条件有关的锁。在线程返回时,可以保证 它保持此锁。
如果当前线程:
- 在进入此方法时已经设置了该线程的中断状态;或者
- 在支持等待和中断线程挂起时,线程被中断,
InterruptedException
,并清除当前线程的中断状态。在第一种情况下,没有指定是否在释放锁之前发生中断测试。
void signal() 唤醒一个等待线程。
如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await
返回之前,该线程必须重新获取锁
void signalAll() 唤醒所有等待线程。
如果所有的线程都在等待此条件,则唤醒所有线程。在从 await
返回之前,每个线程都必须重新获取锁。
SemaPhore方法详解
semaphore又称信号量,是操作系统中的一个概念,在java并发编程中,信号量控制的是线程并发的数量。
public Semaphore(int permits)
其中参数permits就是允许同时运行的线程数目;
package concurrent.semaphore;
import java.util.concurrent.Semaphore;
public class Driver {
// 控制线程的数目为1,也就是单线程
private Semaphore semaphore = new Semaphore(1);
public void driveCar() {
try {
// 从信号量中获取一个允许机会
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " start at " + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " stop at " + System.currentTimeMillis());
// 释放允许,将占有的信号量归还
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package concurrent.semaphore;
public class Car extends Thread{
private Driver driver;
public Car(Driver driver) {
super();
this.driver = driver;
}
public void run() {
driver.driveCar();
}
}
package concurrent.semaphore;
public class Car extends Thread{
private Driver driver;
public Car(Driver driver) {
super();
this.driver = driver;
}
public void run() {
driver.driveCar();
}
}
public class Run {
public static void main(String[] args) {
Driver driver = new Driver();
for (int i = 0; i < 5; i++) {
(new Car(driver)).start();
}
}
}
运行结果
Thread-0 start at 1482664517179
Thread-0 stop at 1482664518179
Thread-3 start at 1482664518179
Thread-3 stop at 1482664519179
Thread-1 start at 1482664519179
Thread-1 stop at 1482664520179
Thread-4 start at 1482664520179
Thread-4 stop at 1482664521180
Thread-2 start at 1482664521180
Thread-2 stop at 1482664522180
从输出可以看出,改输出与单线程是一样的,执行完一个线程,在执行另一线程。
如果信号量大于1呢,我们将信号量设为3:
public class Driver {
// 将信号量设为3
private Semaphore semaphore = new Semaphore(3);
public void driveCar() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " start at " + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " stop at " + System.currentTimeMillis());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
Thread-0 start at 1482665412515
Thread-3 start at 1482665412517
Thread-1 start at 1482665412517
Thread-3 stop at 1482665413517
Thread-0 stop at 1482665413517
Thread-4 start at 1482665413517
Thread-2 start at 1482665413517
Thread-1 stop at 1482665413518
Thread-4 stop at 1482665414517
Thread-2 stop at 1482665414517
从输出的前三行可以看出,有3个线程可以同时进行,三个线程同时运行的时候,第四个线程必须等待前面一个要完成,才能执行第四个线程启动。
当然也可以好用acquire动态地添加permits数量,他表示的是一次性获取许可的数量,比如:
public class Driver {
// 信号量共10个
private Semaphore semaphore = new Semaphore(10);
public void driveCar() {
try {
// 每次获取3个
semaphore.acquire(3);
System.out.println(Thread.currentThread().getName() + " start at " + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " stop at " + System.currentTimeMillis());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
就是说可以允许3个线程一起运行。
我们可以用 public int availablePemits()查看现在可用的信号量:
public class SemaphoreAvaliablePermits {
public static void main(String[] args) {
try{
Semaphore semaphore = new Semaphore(10);
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
semaphore.acquire();
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
semaphore.acquire(2);
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
semaphore.acquire(3);
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
semaphore.acquire(4);
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
semaphore.release();
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
semaphore.release(2);
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
semaphore.release(3);
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
semaphore.release(4);
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
输出:
Semaphore available permits: 10
Semaphore available permits: 9
Semaphore available permits: 7
Semaphore available permits: 4
Semaphore available permits: 0
Semaphore available permits: 1
Semaphore available permits: 3
Semaphore available permits: 6
Semaphore available permits: 10
还有一个方法 public int drainPermits(),这个方法返回即可所有的许可数目,并将许可置为0:
public class SemaphoreDrainPermits {
public static void main(String[] args) {
try{
Semaphore semaphore = new Semaphore(10);
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
semaphore.acquire();
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
System.out.println("Semaphore drain permits" + semaphore.drainPermits());
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
System.out.println("Semaphore drain permits" + semaphore.drainPermits());
System.out.println("Semaphore available permits: " + semaphore.availablePermits());
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
输出:
Semaphore available permits: 10
Semaphore available permits: 9
Semaphore drain permits9
Semaphore available permits: 0
Semaphore drain permits0
Semaphore available permits: 0
ReadWriteLock方法详解
lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现互斥的效果。它们必须用同一个Lock对象。
读写锁:分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,这是由jvm自己控制的,我们只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时些,那就上读锁;如果你的代码修改数据,只能有一个在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
读写锁接口:ReadWriteLock,它的具体实现类:ReentrantReadWriteLock
在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。比如在一个线程读取数据的时候,另外一个线程在写数据,而导致前后数据的不一致性;一个线程在写数据的时候,另外一个线程也在写,同样也会导致线程前后看到的数据的不一致性。
这时候可以在读写方法中加互斥锁,任何时候只能允许一个线程的一个读或写操作,而不允许其他线程的读或写操作,这样是可以解决这样以上的问题,但是效率却大折扣了。因为在真实的业务场景中,一份数据,读取数据的操作次数通常高于写入数据的操作,而线程与线程之间的读读操作是不涉及到线程安全问题的,没有必要加入互斥锁,只要在读-写,写-写期间上锁就行了。
对于以上这种情况,读写锁是最好的解决方案!其中它的实现类:ReentrantReadWriteLock--顾名思义是可重入的读写锁,允许多个线程获取ReadLock,但只允许一个写线程获取WriteLock
读与锁的机制
“读-读”不互斥
“读-写”互斥
“写-写”互斥
ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁。
线程进入读锁的前提条件:
1.没有其他线程的写锁
2.没有写请求,或者有写请求但是调用线程和持有线程是同一线程
进入写锁的前提条件:
1.没有其他线程的读锁
2.没有其他线程的写锁
需要提前了解的概念:
锁降级:从写锁变成读锁;
锁升级:从读锁变成写锁;
ReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.writeLock().lock();
System.out.println("writeLock");
rtLock.readLock().lock();
System.out.println("get read lock");
以上代码虽然没有产生死锁,但没有正确释放锁。从写锁降级成读锁,并不一会释放当前线程获取的写锁,仍然需要显示释放,否则别的线程永远也获取不到写锁。
以下我会通过一个真实场景下的缓存机制来讲解 ReentrantReadWriteLock 实际应用
首先来看看ReentrantReadWriteLock的javaodoc文档中提供给我们的一个很好的Cache实例代码案例:
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have,acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 在释放写锁之前通过获取读锁降级写锁(注意此时还没有释放写锁)
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // 释放写锁而此时已经持有读锁
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
以上代码加锁的顺序为:
1. rwl.readLock().lock();
2. rwl.readLock().unlock();
3. rwl.writeLock().lock();
4. rwl.readLock().lock();
5. rwl.writeLock().unlock();
6. rwl.readLock().unlock();
以上过程整体讲解:
1. 多个线程同时访问该缓存对象时,都加上当前对象的读锁,之后其中某个线程优先查看data数据是否为空。【加锁顺序序号:1 】
2. 当前查看的线程发现没有值则释放读锁立即加上写锁,准备写入缓存数据。(不明白为什么释放读锁的话可以查看上面讲解进入写锁的前提条件)【加锁顺序序号:2和3 】
3. 为什么还会再次判断是否为空值(!cacheValid)是因为第二个、第三个线程获得读的权利时也是需要判断是否为空,否则会重复写入数据。
4. 写入数据后先进行读锁的降级后再释放写锁。【加锁顺序序号:4和5 】
5. 最后数据数据返回前释放最终的读锁。【加锁顺序序号:6 】
如果不使用锁降级功能,如先释放写锁,然后获得读锁,在这个get过程中,可能会有其他线程竞争到写锁 或者是更新数据 则获得的数据是其他线程更新的数据,可能会造成数据的污染,即产生脏读的问题。
下面,让我们来实现真正趋于实际生产环境中的缓存案例:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CacheDemo {
/**
* 缓存器,这里假设需要存储1000左右个缓存对象,按照默认的负载因子0.75,则容量=750,大概估计每一个节点链表长度为5个
* 那么数组长度大概为:150,又有雨设置map大小一般为2的指数,则最近的数字为:128
*/
private Map<String, Object> map = new HashMap<>(128);
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
}
public Object get(String id){
Object value = null;
rwl.readLock().lock();//首先开启读锁,从缓存中去取
try{
value = map.get(id);
if(value == null){ //如果缓存中没有释放读锁,上写锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try{
if(value == null){ //防止多写线程重复查询赋值
value = "redis-value"; //此时可以去数据库中查找,这里简单的模拟一下
}
rwl.readLock().lock(); //加读锁降级写锁,不明白的可以查看上面锁降级的原理与保持读取数据原子性的讲解
}finally{
rwl.writeLock().unlock(); //释放写锁
}
}
}finally{
rwl.readLock().unlock(); //最后释放读锁
}
return value;
}
}
提示:读写锁之后有一个与它配合使用的有条件的阻塞,可以实现线程间的通信,它就是Condition。
CountDownLatch
允许一个或者多个线程等待其他线程完成操作。
应用场景
假如有这样一个需求,当我们需要解析一个Excel里多个sheet的数据时,可以考虑使用多线程,每个线程解析一个sheet里的数据,等到所有的sheet都解析完后,程序需要提示解析完成。
在这个需求中,要实现主线程等待所有线程完成sheet的解析操作,最简单的方法就是加入join。
代码如下:
public class SheetTest {
public static void main(String[] args) throws InterruptedException {
Thread thread01 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.err.println("线程一");
}
});
Thread thread02 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.err.println("线程二");
}
});
thread01.start();
thread02.start();
thread01.join();
thread02.join();
System.err.println("所有线程已完成");
}
}
join用于让当前执行线程,等待join线程执行结束。其原理是不停检查join线程是否存活,如果join线程存活,则让当前线程永远wait,代码片段如下,wait(0)表示永远等待下去。
while (isAlive()) {
wait(0);
}
直到join线程中止后,线程的this.notifyAll会被调用,调用的notifyAll是在jvm里实现的,所以jdk里看不到。jdk不推荐在线程实例上使用wait,notify,notifyAll方法。
在jdk1.5之后的并发包中提供了countDownLatch也可以实现join这个功能,并且比join的功能多。
public class CountDownLatchTest {
static CountDownLatch cdl=new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.err.println("1");
cdl.countDown();
System.err.println("2");
cdl.countDown();
}
}).start();
cdl.await();
System.err.println("3s");
}
}
CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待n个点完成,这里就输入n。
当我们调用一次CountDownLatch的countDown方法时,N就会减一,CountDownLatch的await会阻塞当前线程,直到n位零,由于countDown方法可以用在任何地方,所以这里说n个点,也可以是n个线程,也可以是一个线程n个步骤。用多个线程时,只需要把CountDownLatch的引用传递到线程里。
sheet
如果有某个解析sheet的线程处理的比较慢,我们不可能让主线程一直等待。所以我们可以使用另外一个带指定时间的await方法,await(long time,TimeUnit unit):这个方法等待特定时间后,就会不在阻塞当前线程。join也有类似的功能。
注意:计数器必须大于等于零,只是计数器等于零时,调用await方法就不会阻塞当前线程,CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。
CyclicBarrier
循环栅栏,栅栏就是一个障碍物。假如我们将计数器设置为10,那么凑齐第一批10个线程后,计数器就会归零,然后接着凑齐下一个十个线程,这就是循环栅栏的意义。