从线程交替打印的几种方法来看java的线程同步

1 题目描述

从一个经典面试题来探讨一下java中线程同步方面的问题。
编写一个程序,开启 N 个线程,这 N 个线程交替打印 1-10000 的整数,样例 Sample:
Thread1:1 Thread2:2 Thread3:3 Thread1:4 Thread2:5 Thread3:6 … Thread3:99 Thread1:10000

2 解体思路

这道题是java线程同步的题,本篇介绍7个方法:

  1. 使用synchronized+wait+notify
  2. 可重入锁
  3. 可重入锁+condition
  4. 信号量
  5. locksupport
  6. volatile+synchronized
  7. cas

以下一个个介绍

3 实现

以下是几个通用的属性

//线程数
static int threadCount = 3;
//循环输出最大值
static int maxNum = 10000;
//当前值
static int curNum = 0;

3.1 synchronized+wait+notify

使用同步块和wait、notify的方法控制三个线程的执行次序。具体方法如下:从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,例子中使用了一个全局的锁lock。当执行取模操作满足条件就使用notifyAll唤醒其他所有线程,并把全局的计数器+1,并输出内容,然后调用wait等待。
可以看到,当不满足取模的条件的情况下,始终无法触发,当一个线程调用notifyAll后,所有线程就会尝试取模的判断。所以线程就会根据全局计数器,按照既定顺序输出。

public static void test1() {
    Object lock = new Object();
    class InnerThread extends Thread {
      int modNum;

      InnerThread(String threadName, int modNum) {
        this.modNum = modNum;
        setName(threadName);
      }

      @Override
      public void run() {
        while (true) {
          if (curNum > maxNum) {
            System.out.println(getName() + " end");
            break;
          }
          synchronized (lock) {
            while (curNum % threadCount == modNum) {
              System.out.println(getName() + curNum);
              curNum++;
              lock.notifyAll();
            }
            try {
              if (curNum <= maxNum)
                lock.wait();
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }
        }
      }
    }

    for (int i = 1; i <= threadCount; i++) {
      new InnerThread(String.format("Thread %d:", i), i - 1).start();
    }
  }

3.2 可重入锁

public static void test2() {
    Lock lock = new ReentrantLock();

    class InnerThread extends Thread {
      int modNum;

      InnerThread(String threadName, int modNum) {
        this.modNum = modNum;
        setName(threadName);
      }

      @Override
      public void run() {
        while (true) {
          try {
            lock.lock();
            if (curNum > maxNum) {
              System.out.println(getName() + " end");
              break;
            }
            if (curNum % threadCount == modNum) {
              System.out.println(getName() + curNum);
              curNum++;
            }
          } finally {
            lock.unlock();
          }
        }
      }
    }

    for (int i = 1; i <= threadCount; i++) {
      InnerThread thread = new InnerThread(String.format("Thread %d:", i), i - 1);
      thread.start();
    }
  }

3.3 可重入锁+condition

public static void test3() {
    Lock lock = new ReentrantLock();

    class InnerThread extends Thread {
      Condition curCon, nextCon;
      int modNum;

      InnerThread(String threadName, int modNum, Condition curCon, Condition nextCon) {
        setName(threadName);
        this.curCon = curCon;
        this.nextCon = nextCon;
        this.modNum = modNum;
      }

      @Override
      public void run() {
        while (true) {
          try {
            lock.lock();
            if (curNum > maxNum) {
              System.out.println(getName() + " end");
              nextCon.signal();
              break;
            }
            if (curNum % threadCount == modNum) {
              System.out.println(getName() + curNum);
              curNum++;
              nextCon.signal();
            }
            curCon.await();
          } catch (InterruptedException e) {
            e.printStackTrace();
          } finally {
            lock.unlock();
          }
        }
      }
    }

    Condition a = lock.newCondition();
    Condition b = lock.newCondition();
    Condition c = lock.newCondition();
    new InnerThread(String.format("Thread %d:", 1), 0, a, b).start();
    new InnerThread(String.format("Thread %d:", 2), 1, b, c).start();
    new InnerThread(String.format("Thread %d:", 3), 2, c, a).start();
  }

3.4 信号量

Semaphore又称信号量,是操作系统中的一个概念,在Java并发编程中,信号量控制的是线程并发的数量。
Semaphore实现原理简单理解:
Semaphore是用来保护一个或者多个共享资源的访问,Semaphore信号量内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。

就好比一个厕所管理员,站在门口,只有厕所有空位,就开门允许与空侧数量等量的人进入厕所。多个人进入厕所后,相当于N个人来分配使用N个空位。为避免多个人来同时竞争同一个侧卫,在内部仍然使用锁来控制资源的同步访问。

如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
————————————————————————————————————————————————
Semaphore内部主要通过AQS(AbstractQueuedSynchronizer)实现线程的管理。Semaphore有两个构造函数,第一个参数permits表示许可数,它最后传递给了AQS的state值。线程在运行时首先获取许可,如果成功,许可数就减1,线程运行,当线程运行结束就释放许可,许可数就加1。如果许可数为0,则获取失败,线程位于AQS的等待队列中,它会被其它释放许可的线程唤醒。在创建Semaphore对象的时候还可以指定它的公平性。一般常用非公平的信号量,非公平信号量是指在获取许可时先尝试获取许可,而不必关心是否已有需要获取许可的线程位于等待队列中,如果获取失败,才会入列。而公平的信号量在获取许可时首先要查看等待队列中是否已有线程,如果有则入列。

//非公平的构造函数
public Semaphore(int permits);//permits=10,表示允许10个线程获取许可证,最大并发数是10;
通过fair参数决定公平性
public Semaphore(int permits,boolean fair)

Semaphore semaphore = new Semaphore(10,true);  
semaphore.acquire();  //线程获取许可证
//do something here  
semaphore.release();  //线程归还许可证
public static void test4() {
    class InnerThread extends Thread {
      Semaphore curSemaphore, nextSemaphore;
      int modNum;

      InnerThread(String threadName, int modNum, Semaphore curSemaphore, Semaphore nextSemaphore) {
        setName(threadName);
        this.curSemaphore = curSemaphore;
        this.nextSemaphore = nextSemaphore;
        this.modNum = modNum;
      }

      @Override
      public void run() {
        while (true) {
          try {
            curSemaphore.acquire();
            if (curNum > maxNum) {
              System.out.println(getName() + " end");
              nextSemaphore.release();
              break;
            }
            if (curNum % threadCount == modNum) {
              System.out.println(getName() + curNum);
              curNum++;
              nextSemaphore.release();
            }
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }
    Semaphore a = new Semaphore(1);
    Semaphore b = new Semaphore(0);
    Semaphore c = new Semaphore(0);
    new InnerThread(String.format("Thread %d:", 1), 0, a, b).start();
    new InnerThread(String.format("Thread %d:", 2), 1, b, c).start();
    new InnerThread(String.format("Thread %d:", 3), 2, c, a).start();
  }

3.5 locksupport

static List<Thread> threads = new ArrayList<>(3);
  static int curThreadCnt = 0;
  public static void test5() {
    class InnerThread extends Thread {
      InnerThread(String threadName) {
        setName(threadName);
      }

      @Override
      public void run() {
        while (true) {
          //线程堵塞
          LockSupport.park();
          if (curNum > maxNum) {
            System.out.println(getName() + " end");
            break;
          }
          System.out.println(getName() + curNum++);
          //唤醒下一个线程
          LockSupport.unpark(threads.get(++curThreadCnt % threadCount));
        }
        threads.forEach(LockSupport::unpark);
      }
    }

    for (int i = 1; i <= threadCount; i++) {
      Thread t = new InnerThread(String.format("Thread %d:", i));
      threads.add(t);
      t.start();
    }
    //从第一个线程开始唤醒
    LockSupport.unpark(threads.get(0));
  }

3.6 volatile+synchronized

首先介绍一下volatile关键字很重要的两个特性:

1、保证变量在线程间可见,对volatile变量所有的写操作都能立即反应到其他线程中,换句话说,volatile变量在各个线程中是一致的(得益于java内存模型—“先行发生原则”);

2、禁止指令的重排序优化;

但是volatile也有它的问题,基于volatile变量的运算在并发下不是安全的,原因是自增操作并不是原子性的。

打个比方:
当前主内存的 count = 5,线程A 通过read、load等原子操作把 count = 5加载到本地内存中

在执行引擎执行count++操作时,会有多步操作

1:先初始化count = 5

2:对count执行计算:count = 5+1

3:把count=6 通过assign写入本地内存中

刚好在1,2,3步中间,cpu被线程B抢去了,此时线程B的count也是5,执行count+1后,刷新主内存并通知了线程A.

那么线程A原本的执行引擎中的count已经作废,会被丢弃,重新从主内存中获取最新的值,这样就平白无故丢了一次 +1 的操作,所以最后结果和预计不同

解决方案: 使用synchronized 既可以保证原子性 ,也可以保证可见性。

volatile static int ai = 0;
  public static void test6() {
    Object writeLock = new Object();

    class InnerThread extends Thread {
      int modNum;

      InnerThread(String threadName, int modNum) {
        setName(threadName);
        this.modNum = modNum;
      }

      @Override
      public void run() {
        while (ai <= maxNum) {
          if (ai % threadCount == modNum) {
            System.out.println(getName() + ai);
            //不是原子操作可能会出问题,加锁保证
            synchronized (writeLock) {
              ai++;
            }
          }
        }
        System.out.println(getName() + " end");
      }
    }

    for (int i = 1; i <= threadCount; i++) {
      new InnerThread(String.format("Thread %d:", i), i - 1).start();
    }
  }

3.7 cas

volatile操作是不具备原子性,所以之前的例子中必须要加上synchronized同步,另外有一种方法是使用juc中的原子操作类比如AtomicInteger。
这些原子操作类都使用了cas无锁算法,cas即compare and swap,是cpu指令。CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS比较与交换的伪代码可以表示为:

do{  
       备份旧数据; 
       基于旧数据构造新数据; 
}while(!CAS( 内存地址,备份的旧数据,新数据 ))  

在这里插入图片描述
(上图的解释:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。)

就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。
AtomicInteger.incrementAndGet的实现用了乐观锁技术,调用了sun.misc.Unsafe类库里面的 CAS算法,用CPU指令来实现无锁自增。所以,AtomicInteger.incrementAndGet的自增比用synchronized的锁效率倍增。
究其原因,其实就是乐观锁和悲观锁的区分。
独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。就是cas算法。

public static void test7() {
    AtomicInteger ai = new AtomicInteger(0);
    class InnerThread extends Thread {
      int modNum;

      InnerThread(String threadName, int modNum) {
        setName(threadName);
        this.modNum = modNum;
      }

      @Override
      public void run() {
        int curVal;
        while ((curVal=ai.get()) <= maxNum) {
          if (curVal % threadCount == modNum) {
            System.out.println(getName() + curVal);
            ai.getAndIncrement();
          }
        }
        System.out.println(getName() + " end");
      }
    }

    for (int i = 1; i <= threadCount; i++) {
      new InnerThread(String.format("Thread %d:", i), i - 1).start();
    }
  }

4 参考

非阻塞同步算法与CAS(Compare and Swap)无锁算法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值