阶段二:5.jdk并发包

1.synchronnized

2. Lock

3.reentrantLock

4.Condition
5.semaphore

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.没有其他线程的写锁

需要提前了解的概念:

    锁降级:从写锁变成读锁;

    锁升级:从读锁变成写锁;

读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。
如下代码会产生死锁,因为在同一线程中,在没有释放读锁的情况下,申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的
ReentrantReadWriteLock支持锁降级,如下代码不会产生死锁。
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个线程后,计数器就会归零,然后接着凑齐下一个十个线程,这就是循环栅栏的意义。




























































































































评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值