Java—多线程之同步机制

新的同步机制(新的锁类型)

一、ReentrantLock

ReentrantLock可以用于替代synchronized

1、用法

Lock lock = new ReentrantLock();

void m1() {
   lock.lock(); //synchronized(this)
   try {
      // 业务代码   
   } catch (InterruptedException e) {
      e.printStackTrace();
   } finally {
       //ReentrantLock必须在finally中进行主动释放锁。synchronized代码块执行结束后会自动释放,抛出异常时也会释放锁
      lock.unlock();
   }
}

 

2、特点

  • 可以进行“尝试锁定”lock.tryLock(5, TimeUnit.SECONDS);这样无法锁定,或者在指定时间内无法锁定,线程可以决定是否继续等待。
  • 可以调用lock.lockInterruptibly()方法,可以对线程interrupt方法做出响应, 在一个线程等待锁的过程中,可以被打断。
  • 可以指定为公平锁。所谓公平锁是指其他线程进行锁竞争时,是否直接上来就竞争,还是进行排队(检查锁的线程等待队列,进入队列中排队)。如果直接竞争就是非公平锁,排队就是公平锁
public class ReentrantLockTest extends Thread {
      
    private static ReentrantLock lock=new ReentrantLock(true); //参数为true表示为公平锁
    public void run() {
        for(int i=0; i<100; i++) {
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"获得锁");
            }finally{
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        ReentrantLockTest rl=new ReentrantLockTest();
        Thread th1=new Thread(rl);
        Thread th2=new Thread(rl);
        th1.start();
        th2.start();
    }
}

new ReentrantLock(true); //参数为true表示为公平锁

这时程序的输出结果为:线程1和线程2能够轮流执行,交替输出,并不是严格的1、2、1、2的结果。

new ReentrantLock();

这时程序要等到线程1执行完,线程2才会执行

 

二、CountDownLatch

1、概念

它是一个同步工具类,允许一个或多个线程一直等待,直到其他线程运行完成后再执行。

/**
 * A synchronization aid that allows one or more threads to wait until
 * a set of operations being performed in other threads completes.
 *
 * @since 1.5
 * @author Doug Lea
 */
public class CountDownLatch {
}

2、使用场景:

场景1 让多个线程等待:模拟并发,让并发线程一起执行

为了模拟高并发,让一组线程在指定时刻(秒杀时间)执行抢购,这些线程在准备就绪后,进行等待(CountDownLatch.await()),直到秒杀时刻的到来,然后一拥而上;

这也是本地测试接口并发的一个简易实现。

在这个场景中,CountDownLatch充当的是一个发令枪的角色;

就像田径赛跑时,运动员会在起跑线做准备动作,等到发令枪一声响,运动员就会奋力奔跑。和上面的秒杀场景类似,代码实现如下:

CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            //准备完毕……运动员都阻塞在这,等待号令
            countDownLatch.await();
            String parter = "【" + Thread.currentThread().getName() + "】";
            System.out.println(parter + "开始执行……");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}
Thread.sleep(2000);// 裁判准备发令
countDownLatch.countDown();// 发令枪:执行发令

运行结果:

【Thread-0】开始执行……

【Thread-1】开始执行……

【Thread-4】开始执行……

【Thread-3】开始执行……

【Thread-2】开始执行……

我们通过CountDownLatch.await(),让多个参与者线程启动后阻塞等待,然后在主线程 调用CountDownLatch.countdown(1) 将计数减为0,让所有线程一起往下执行;

以此实现了多个线程在同一时刻并发执行,来模拟并发请求的目的。

场景2 让单个线程等待:多个线程(任务)完成后,进行汇总合并

很多时候,我们的并发任务,存在前后依赖关系;比如数据详情页需要同时调用多个接口获取数据,并发请求获取到数据后、需要进行结果合并

这其实都是:在多个线程(任务)完成后,进行汇总合并的场景。

代码实现如下:

CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
    final int index = i;
    new Thread(() -> {
        try {
            Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
            System.out.println("finish" + index + Thread.currentThread().getName());
            countDownLatch.countDown();//将计数器减1。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

countDownLatch.await();// 主线程在阻塞,Latch门闩/栅栏上;当计数器==0,就唤醒主线程往下执行。
System.out.println("主线程:在所有任务运行完成后,进行结果汇总");

运行结果:

finish4Thread-4

finish1Thread-1

finish2Thread-2

finish3Thread-3

finish0Thread-0

主线程:在所有任务运行完成后,进行结果汇总

在每个线程(任务) 完成的最后一行加上CountDownLatch.countDown(),让计数器-1;

当所有线程完成-1,计数器减到0后,主线程往下执行汇总任务。

 

3、CountDownLatch与Thread.join

CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API。

CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。

而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。

 

三、CyclicBarrier

概念

一个同步工具类,是为了允许多个线程在一个障碍(Barrier)点上互相等待,当大家都达到这个障碍的时候,才继续往下执行。为什么叫重复障碍(Cyclic Barrier)呢?因为当所有的线程都到达这个点之后,还可以再重复使用它。

/**
 * A synchronization aid that allows a set of threads to all wait for
 * each other to reach a common barrier point.  CyclicBarriers are
 * useful in programs involving a fixed sized party of threads that
 * must occasionally wait for each other. The barrier is called
 * <em>cyclic</em> because it can be re-used after the waiting threads
 * are released.
*/
public class CyclicBarrier {    
}

在下面的代码中,我们首先创建了一个计数器初始值为 2 的 CyclicBarrier,你需要注意的是创建 CyclicBarrier 的时候,我们还传入了一个回调函数,当计数器减到 0 的时候,会调用这个回调函数。

线程 T1 负责查询订单,当查出一条时,调用 barrier.await() 来将计数器减 1,同时等待计数器变成 0;线程 T2 负责查询派送单,当查出一条时,也调用 barrier.await() 来将计数器减 1,同时等待计数器变成 0;当 T1 和 T2 都调用 barrier.await() 的时候,计数器会减到 0,此时 T1 和 T2 就可以执行下一条语句了,同时会调用 barrier 的回调函数来执行对账操作。

非常值得一提的是,CyclicBarrier 的计数器有自动重置的功能,当减到 0 的时候,会自动重置你设置的初始值。这个功能用起来实在是太方便了。

// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池
Executor executor =
        Executors.newFixedThreadPool(1);
final CyclicBarrier barrier =
        new CyclicBarrier(2, ()->{
            executor.execute(()->check());
        });

void check(){
    P p = pos.remove(0);
    D d = dos.remove(0);
    // 执行对账操作
    diff = check(p, d);
    // 差异写入差异库
    save(diff);
}

void checkAll(){
    // 循环查询订单库
    Thread T1 = new Thread(()->{
        while(存在未对账订单){
            // 查询订单库
            pos.add(getPOrders());
            // 等待
            barrier.await();
        }
    });
    T1.start();
    // 循环查询运单库
    Thread T2 = new Thread(()->{
        while(存在未对账订单){
            // 查询运单库
            dos.add(getDOrders());
            // 等待
            barrier.await();
        }
    });
    T2.start();
}

四、CountDownLatch与CyclicBarrier

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

  • CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,才执行(CountDownLatch中侧重的是一个和一组线程之间的依赖关系。)
  • CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程在同时执行(CyclicBarrier更强调一组线程之间的关系,这些线程间互相协调等待,强调同时。)

另外,CountDownLatch是减计数,计数减为0后不能重用;而CyclicBarrier是加计数,可置0后复用。

内容来自:

CountDownLatch的两种常用场景:https://mp.weixin.qq.com/s/RhK_BrYrooGGYbN5OvyXow

CountDownLatch和CyclicBarrier让多线程步调一致:

https://mp.weixin.qq.com/s?__biz=MzAwNTE3MzgwNA==&mid=2247483867&idx=1&sn=e7e8684850f1fcf9734c3567beb06089&chksm=9b21e698ac566f8ec0691b365a32c9aaac2e004758bcbb461343eba316e55a86652dbb5b988b&scene=21#wechat_redirect

 

五、Phaser

可以看作是CyclicBarrier的升级版,可以将锁分为不同的阶段。每个阶段等一组线程集结完毕,调用onAdvance方法,并进入下一阶段。

线程到达某个阶段的时候,可以根据条件判断,该线程是否需要继续前进。

// 从Phaser继承,重写onAdvance方法
class TestPhaser extends Phaser {
    // 该方法在某阶段的所有线程都到达后,自动调用(即phaser.arriveAndAwaitAdvance()阻塞结束之后)
    @Override
    protected boolean onAdvance(int phase, int registeredParties) {
        switch (phase) {
            case 0:
                // TODO
                return false;
            case 1:
                // TODO
                return false;
            case 2:
                // TODO
                return false;
            case 3:
                // TODO
                return true;
            default:
                return true;
        }
    }
}

TestPhaser phaser = new TestPhaser ();

// 7代表进入下一阶段所需要的线程数量
phaser.bulkRegister(7);
// 定义某一个阶段
private void test() {
    if(满足某些条件) {
       //TODO
       // 等待线程集结完毕后
        phaser.arriveAndAwaitAdvance();
    } else {
        // 取消注册,取消注册后,之后的阶段该线程都不参与了,注册数-1
        phaser.arriveAndDeregister();
        // 增加注册线程
        //phaser.register()
    }
}

 

六、ReadWriteLock 读写锁

场景

有很多需求是读可以同时读,但是有人在写的时候,别人就不能读了(避免产生脏读)。而很多业务的大部分需求的是读多写少(经常查询,很少修改)。这时如果不分情况一律加排他锁,效率就会比较低,必须等别人读完才能读(串行)

概念

共享锁:又称为读锁。一个线程获取读锁之后,其他线程也能同时获取这个锁,即允许多个线程同时获取一个锁。

 排他锁,又称为写锁。也称作独占锁或互斥锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。

实例

ReentrantLock就是一种排他锁。CountDownLatch是一种共享锁。这两类都是单纯的一类,即,要么是排他锁,要么是共享锁。

ReentrantReadWriteLock是同时包含排它锁和共享锁特性的一种锁,这里主要以ReentrantReadWriteLock为例来进行分析学习。

使用ReentrantReadWriteLock的读锁时,使用的便是共享锁的特性。

使用ReentrantReadWriteLock的写锁时,使用的便是排他锁的特性;

 

  • 代码
static Lock lock = new ReentrantLock();
    private static int value;

    static CountDownLatch latch = new CountDownLatch(20);
    static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    static Lock readLock = readWriteLock.readLock();
    static Lock writeLock = readWriteLock.writeLock();

    public static void read(Lock lock) {
        lock.lock();
        try {            
            Thread.sleep(1000);
            System.out.println("read over!");
            //模拟读取操作
            latch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void write(Lock lock, int v) {
        lock.lock();
        try {            
            Thread.sleep(1000);
            value = v;
            System.out.println("write over!");
            //模拟写操作
            latch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        System.out.println(new Date());
        Runnable readR = ()-> read(lock);
        Runnable writeR = ()->write(lock, new Random().nextInt());
//        Runnable readR = ()-> read(readLock);
//        Runnable writeR = ()->write(writeLock, new Random().nextInt());

        for(int i=0; i<18; i++) new Thread(readR).start();
        for(int i=0; i<2; i++) new Thread(writeR).start();

        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(new Date());
    }

 

  • 执行结果

当使用ReentrantLock的时候,因为加的是排他锁,所以read()方法需要等18个读线程逐个读取完毕,因此需要18s,write()方法同样了2s

而使用ReentrantReadWriteLock的readLock读锁时,因为时共享锁,因此18个线程调用read()大概在1s内即可执行完毕。write()方法同样用了2s

 

小结:

读写锁类似于 ReentrantLock,也支持公平模式和非公平模式。读锁和写锁都实现了 java.util.concurrent.locks.Lock 接口,所以除了支持 lock() 方法外,tryLock()、lockInterruptibly() 等方法也都是支持的。但是有一点需要注意,那就是只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常。

ReentrantReadWriteLock的实现能满足绝大多数的多线程环境,有如下特点:

  • 支持两种优先级模式,以时间顺序获取锁和以读、写交替优先获取锁的模式;
  • 当获得了读锁或写锁后,还可重复获取读锁或写锁,即ReentrantLock(可重入锁);
  • 获得写锁后还可获得读锁,但获得读锁后不可获得写锁;

支持将写锁降级为读锁,但反之不行;

支持在等待锁的过程中中断;

对写锁支持Condition(用于取代wait,notify和notifyAll);

支持锁的状态检测,但仅仅用于监控系统状态而并非同步控制;

扩展文章:https://mp.weixin.qq.com/s?__biz=MzAwNTE3MzgwNA==&mid=2247483859&idx=1&sn=27e83f909e2e1b8743d708957ca9ae77&chksm=9b21e690ac566f8657ba1265a05ad438d5ef3c68e1989f5d98a140658d5c08cfdff31449d45f&scene=21#wechat_redirect

 

七、Semaphore信号量

场景

用于限流,例如收费站和买票口,同时行进的有4条车道,但是过收费站的时候只有两个收费通道,同时只能有两辆车通过。

  • 代码
public static void main(String[] args) {
    //允许两个线程同时执行
    Semaphore s = new Semaphore(2);
   // Semaphore s = new Semaphore(1);
    //Semaphore s = new Semaphore(2, true);

    new Thread(()->{
        try {
            // 获取锁
            s.acquire();

            System.out.println("T1 running...");
            Thread.sleep(200);
            System.out.println("T1 running...");

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            s.release();
        }
    }).start();

    new Thread(()->{
        try {
            s.acquire();

            System.out.println("T2 running...");
            Thread.sleep(200);
            System.out.println("T2 running...");

            s.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}
  • 执行结果

当参数为2时,代表允许两个线程同时执行

Semaphore s = new Semaphore(2);

结果:T1、T2同时执行

当参数为1时,代表只允许已一个线程执行

Semaphore s = new Semaphore(1);

结果:T1执行完T2才执行

 

八、Exchanger

可以用于两个线程之间交换数据,两个线程都调用exchange()方法

static Exchanger<String> exchanger = new Exchanger<>();

public static void main(String[] args) {
    new Thread(()->{
        String s = "T1";
        try {
            s = exchanger.exchange(s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " " + s);

    }, "t1").start();


    new Thread(()->{
        String s = "T2";
        try {
            s = exchanger.exchange(s);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " " + s);

    }, "t2").start();
}

相当于两个线程分别把要交换的东向放到一个要交换的位置,当两个位置都有东西的时候,进行交换。每个线程取走各自位置上的东西。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值