& 并发编程-4-队列和公平锁

toc

锁的膨胀过程

关于偏向锁

预备知识CAS

什么是CAS?
compare and swap 比较和交换,在intel的CPU中,使用cmpxchg指令实现;在java发展初期,java语言是不能够利用硬件提供的这些遍历来提升系统性能的。而随着java不断的发展,Java本地方法(JNI)的出现,使得java程序越过JVM直接调用本地方法提供了一种便携的方式,因而java在并发的手段上也多了起来。

CAS是一个CPU的指令 不会进入内核态
CAS操作包含三个操作数-- 内存位置(V)、预期原值(A)和新值(B)。
如果内位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
无论哪种情况,它都会在CAS指令之前返回该位置的值;整个过程都是不可打断的,所以CAS是一个原子操作;主要是利用CPU的CAS指令,同事借住JNI来完成Java的非阻塞算法;

CAS的练习

假设有如下接口

package com.enjoy.cas;
public interface Account {
  //查询
  Integer query();
  //获取
  void acquire(Integer i);
}

有api针对这个Account接口的实现类的query和acquire调用

package com.enjoy.cas;
import java.util.ArrayList;
import java.util.List;
/**
* zl
*/
public class AccountTest {
  public static void main(String[] args) throws InterruptedException {
    //test(new AccountNomal(10000));
    //test(new AccountSync(10000));
    test(new AccountCas(10000));
 }
  /**
  * 传入一个具体的Account来测试
  * @param account
  */
 static void test(Account account) throws InterruptedException {
    List<Thread> ts = new ArrayList<>();
    //定义500个线程 每个线程针对account取20
    for (int i = 0; i < 500; i++) {
      ts.add(new Thread(() -> {
        account.acquire(20);
     }));
   }
    //启动
    for (Thread t : ts) {
      t.start();
   }
    //等待他们全部指向完
    for (Thread t : ts) {
      t.join();
   }
    //查询还剩多少
    System.out.println(account.query());
 }
}

如何保证acqurie的操作是线程安全呢?

1.线程不安全的实现

package com.enjoy.cas;
/**
* 没有锁没有CAS
*
*/
public class AccountNomal implements Account {
  public AccountNomal(int balance){
    this.balance=balance;
 }
  private Integer balance;
  @Override
  public Integer query() {
    return this.balance;
 }
  @Override
  public void acquire(Integer i) {
    this.balance -= i;
 }
}
运行结果:大于或者等于0;

#### 2.线程安全的加锁实现

package com.enjoy.cas;

/**
* sync
* result :0
*
*/
public class AccountSync implements Account {
  public AccountSync(int balance){
    this.balance=balance;
 }
  private Integer balance;
  @Override
  public Integer query() {
    return this.balance;
 }
  @Override
  public void acquire(Integer i) {
    synchronized (this) {
      this.balance -= i;
   }
 }
}
运行结果:始终为0

3.线程安全的无锁实现

package com.enjoy.cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* cas
* result :0
*
*/
public class AccountCas implements Account {

  public AccountCas(int balance){
    this.balance=new AtomicInteger(balance);
 }
  private AtomicInteger balance;
  @Override
  public Integer query() {
    return this.balance.get();
  }
  @Override
  public void acquire(Integer i) {
    while(true) {
      int prev = balance.get();
      int next = prev - i;
      if(balance.compareAndSet(prev, next)) {
        break;
    }
   }
 }
}

预备知识 公平锁非公平锁

理解公平锁和非公平锁

考虑如下代码:

package com.enjoy.test;
/**
* sync是否为公平锁
*/
public class TestSysn {
  //定义一把锁
  private static Object lock = new Object();
  public static void main(String[] args) throws InterruptedException {
    //线程的数量
    int N = 10;
    Thread[] threads = new Thread[N];
    for(int i = 0; i < N; ++i){
      threads[i] = new Thread(new Runnable(){
        public void run() {
          /**
          * 如果这里打印的结果是无序的则表示 非公平锁
          * 有序则公平锁
          * 结果是倒序 那么他就是公平锁吗?
          * 其实很多时候可能我们对公平和非公平都理解错了
          * 这里不管顺序打印还是倒序打印都只是对队列的特性验证
          * 并不能说明他就是公平或者非公平
          */
          synchronized(lock){//t0 t1....t9 到一个队列当中的去阻塞
            System.out.println(Thread.currentThread().getName() + "
get synch lock!");
            try {
              Thread.sleep(200);
           } catch (InterruptedException e) {

              e.printStackTrace();
           }
         }
       }
     });
   }
    //main 线程可以得到锁 持有了锁
    synchronized(lock){
      for(int i = 0; i < N; ++i){
        //t0
        threads[i].start();
        Thread.sleep(200);
     }
   }
//
//    for(int i = 0; i < N; ++i)
//      threads[i].join();
 }
}

结果分析:synchronized实现的锁,如果有多个线程阻塞,最先阻塞的最后执行;最后阻塞的最先执行

然后看看Lock实现的锁

package com.enjoy.test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 这是一把公平锁
* 公平锁非公平锁理解本身就错了
*
*
* 非公平锁他们首先会在lock方法调用加锁的时候去抢锁(公平锁调用lock不会上来就拿锁);
* 如果加锁失败
* 则去看为什么失败(是否锁被人持有了),他在判断的时候如果锁没有被人持有
* 非公平锁有会去直接加锁(不会判断是否有人排队),成功则进入同步块;失败则进入队列
* 进入队列后如果前面那个人是head则再次尝试加锁,成功则执行同步代码块,失败则park(真正的排
队)
*
* 公平锁:第一次加锁的时候,他不会去尝试加锁,他会去看一下我前面有没有人排队,如果有人排队,我
则进入
* 队列(并不等于排队),然后还不死心,再次看一下我有没有拿锁的资格(前面那个人是否为head),
* 如果有资格(前面那个人刚好为head)则继续拿锁,成功则执行同步块;失败则park(排队)
*
*
* 一朝排队,永远排队
*
*/
public class TestLock {
  private static Lock lock = new ReentrantLock();
  public static void main(String[] args) throws InterruptedException {
    int N = 10;
    Thread[] threads = new Thread[N];
    for(int i = 0; i < N; ++i){
      threads[i] = new Thread(new Runnable(){
        public void run() {

          /**
          *
          * 独占锁---顾名思义
          * t1------t9全部在这里阻塞,并且进入到队列
          * 等待持有锁的线程释放锁之后才从队列当中把这些阻塞的线程唤醒
          *
          */
          lock.lock();
          System.out.println(Thread.currentThread().getName() + "
lock!");
          try {
            Thread.sleep(20);
         } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
         }
          lock.unlock();
       }
     });
   }
    //synchronized ()
    lock.lock();
    for(int i = 0; i < N; ++i){
      threads[i].start();
      //Thread.sleep(200);
   }
   // lock.unlock();
    for(int i = 0; i < N; ++i)
      threads[i].join();
 }
}

结果分析:lock实现的同步如果有多个线程阻塞,唤醒的时候是按阻塞顺序唤醒的;关于公平锁和非公平锁可以参数我写的注释

关于公平锁和非公平锁的加锁流程和区别

好好看懂这幅图

关于队列如何设计和形成

1、AQS类的设计主要属性

private transient volatile Node head; //队首
private transient volatile Node tail;//尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0

2、Node类的设计

public class Node{  
  volatile Node prev;  
  volatile Node next;  
  volatile Thread thread;
}

最后关于synchronized是否公平锁

synchronized是非公平锁
其实前面那个小例子(倒序和顺序)不足以说明什么问题;synchronized是否公平锁得看线程在没有入队之前是否抢锁(C++代码);入队之后就和公平不公平无关了;一朝排队;永远排队;
然后关于队列,关于双向链表好好理解一下就行

公平锁什么时候用?

当后一个线程依赖于前一个线程结果的时候 需用公平锁

原文地址:https://www.cnblogs.com/doagain/p/15018634.html