高并发与多线程网络学习笔记(八)并发包

Exchanger

功能描述

两个线程通过exchanger()交换数据

要点描述

  • 参数是传给另一个线程的值,返回值是另一个线程返回给当前线程的值
  • 线程会在exchange的语句wait()住等待另一个线程返回值
  • 必须成对出现,而且无法保证出现的顺序,如果存在落单的exchange线程则会永久进入阻塞状态,因为没有人给他返回值
  • exchange返回的是同一个对象,所以存在线程安全问题
  • exchange可以循环成对利用

示例

package com.linux.huhx.concurreny;

import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExchangerTest {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        final Exchanger exchanger = new Exchanger();
        executor.execute(new Runnable() {
            String data1 = "Ling";

            @Override
            public void run() {
                doExchangeWork(data1, exchanger);
            }
        });

        executor.execute(new Runnable() {
            String data1 = "huhx";

            @Override
            public void run() {
                doExchangeWork(data1, exchanger);
            }
        });
        executor.shutdown();
    }

    private static void doExchangeWork(String data1, Exchanger exchanger) {
        try {
            System.out.println(Thread.currentThread().getName() + "正在把数据 " + data1 + " 交换出去");
            Thread.sleep((long) (Math.random() * 1000));

            String data2 = (String) exchanger.exchange(data1);
            System.out.println(Thread.currentThread().getName() + "交换数据 到  " + data2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Semaphore信号量

功能描述

Each {@link #acquire} blocks if necessary until a permit is
available, and then takes it. Each {@link #release} adds a permit,
potentially releasing a blocking acquirer.

要点描述

  • semaphore.acquire():获取permit,等待permit is available
  • semaphore.release():增加permit available
  • 它与synchronized区别
    • 当限制permit=1的时候相当于synchronized
    • semaphore可以允许多个同步线程,synchronized只允许一个(主要使用点)
    • 它比synchronized提供了更多的功能,更好用

API说明

Semaphore(int permits) //设定perimit的个数,限制了同步线程个数,若为1等于synchronzied
acquire(int permits) //从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。
drainPermits() //获取并返回立即可用的所有许可,它没有releaseAll这种方法
getQueuedThreads() //返回一个 collection,包含可能等待获取的线程。
release(int permits) // 释放给定数目的许可,将其返回到信号量。
getQueueLength() //返回正在等待获取的线程的估计数目。
tryAcquire(int permits, long timeout, TimeUnit unit)
//一般使用应该搭配超时,非阻塞方法,拿到就返回true,拿不到返回false,继续进行。

示例

class Employee implements Runnable {
    private String id;
    private Semaphore semaphore;
    private static Random rand= new Random(47);

    public Employee(String id, Semaphore semaphore) {
        this.id = id;
        this.semaphore = semaphore;
    }

    public void run() {
            try {
                semaphore.acquire();
                System.out.println(this.id + "is using the toilet");
                TimeUnit.MILLISECONDS.sleep(rand.nextInt(2000));
                semaphore.release();
                System.out.println(this.id + "is leaving");
            } catch (InterruptedException e) {
            }
    }
}

public class ToiletRace {
    private static final int THREAD_COUNT = 30;

    private static ExecutorService threadPool = Executors
            .newFixedThreadPool(THREAD_COUNT);

    private static Semaphore s = new Semaphore(10);

    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            threadPool.execute(new Employee(String.valueOf(i), s));
        }

        threadPool.shutdown();
    }
}

ReentranLock 可重入锁

功能描述

老师表示他非常喜欢这个锁,推荐大家使用。

重入性:表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞,跟synchronzied类似,实现原理要看AQS比较复杂没有展开。

要点描述

  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  • ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

 

CSDN-专业IT技术社区-登录​blog.csdn.net

API

//常用api
lock()  //获取锁
unlock() //释放锁
tryLock(long timeout, TimeUnit unit)//如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
ReentrantLock(boolean fair)//设定是否公平,默认非公平
getQueuedThreads()//返回等待线程队列

Condition

跟我们使用synchronized的监视器一样,它必须搭配lock类使用

condition = lock.newCondition();
condition.await();
condition.signal();

ReentranReadWriteLock

JUC提供了读写锁,有readLock和writeLock,不需要手写了,但是可能会存在问题,因为读锁和写锁不能共存(又称为排它锁),他们有一个争夺的过程,如果读锁99个,写锁1个,可能会导致写时饥饿,它抢不到锁。

StampedLock

可以替代ReadWriteLock

  • 写锁writeLock,是个排它锁或者叫独占锁,同时只有一个线程可以获取该锁,请求该锁成功后会返回一个stamp票据变量用来表示该锁的版本,当释放该锁时候需要unlockWrite并传递参数stamp。

 

  • 悲观读锁readLock,是个共享锁,在没有线程获取独占写锁的情况下,同时多个线程可以获取该锁,如果已经有线程持有写锁,其他线程请求获取该读锁会被阻塞。这里讲的悲观其实是参考数据库中的乐观悲观锁的,这里说的悲观是说在具体操作数据前悲观的认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在读少写多的情况下的一种考虑,请求该锁成功后会返回一个stamp票据变量用来表示该锁的版本,当释放该锁时候需要unlockRead并传递参数stamp。

 

  • 乐观读锁tryOptimisticRead,通过名字来记忆很简单,try代表尝试,说明它是无阻塞的。Optimistic乐观的,Read代表读锁。乐观锁认为数据不会轻易的被修改,因此在操作数据前并没有加锁(使用cas方式更新锁的状态),而是采用试探的方式,只要当前没有写锁就可以获得一个非0的stamp,如果已经存在写锁则返回一个为0的stamp。
    tryOptimisticRead与validate一定要紧紧挨着使用,否则在获取和验证之间很可能数据被修改。如果这期间锁发生变化则validate返回false,否则返回true。
    原理很简单,就是我先尝试获取,这时候没有写锁我就拿到了一个锁,在我真正要使用的时候我再验证一下是否发生了改变,如果没有发生改变就可以安心使用。

示例

 class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();

   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();  //获取写锁
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp); //释放写锁
     }
   }

   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //乐观读
     double currentX = x, currentY = y;
     if (!sl.validate(stamp)) { //判断共享变量是否已经被其他线程写过
        stamp = sl.readLock();  //如果被写过则升级为悲观读锁
        try {
          currentX = x;
          currentY = y;
        } finally {
           sl.unlockRead(stamp); //释放悲观读锁
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }

   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock(); //获取读锁
     try {
       while (x == 0.0 && y == 0.0) {
         long ws = sl.tryConvertToWriteLock(stamp);  //升级为写锁
         if (ws != 0L) {
           stamp = ws;
           x = newX;
           y = newY;
           break;
         }
         else {
           sl.unlockRead(stamp);
           stamp = sl.writeLock();
         }
       }
     } finally {
       sl.unlock(stamp);
     }
   }
 }

如果有写操作修改了共享变量则升级乐观读为悲观读锁,这样避免乐观读反复的循环等待写锁的释放,避免浪费CPU资源。所以在我们的使用StampedLock的时候,建议这样操作。

那是不是可以不要readwriteLock了呢?

StampedLock(读锁和写锁都是)是不可重入锁,所以不支持重入,并且StampedLock不支持条件变量,也就是没Condition。如果是线程使用writeLock()或者readLock()获得锁之后,线程还没执行完就被interrupt()的话会导致CPU飙升.如果要使用中断功能就得用readLockInterruptibly()或者writeLockInterruptibly()来获得锁。

面试官:知道Java1.8中新加的StampedLock吗?

并发编程 | StampedLock工具类之乐观读锁 tryOptimisticRead

 

性能比较

这里的stamped是使用stampedlock的悲观读写锁,optimistic是使用乐观读锁。

5 readers vs. 5 writers

10 readers vs. 10 writers

19 readers vs. 1 writer

https://blog.overops.com/java-8-stampedlocks-vs-readwritelocks-and-synchronized/​blog.overops.com

 

 

posted @ 2021-01-17 15:03  王者之剑KO  阅读(107)  评论(0)    收藏  举报