我是靠谱客的博主 聪慧老虎,最近开发中收集的这篇文章主要介绍多线程与高并发(三)--Atomic类和线程同步新机制,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

一、什么是无锁操作?

  • 举个例子

模拟我们计一个数,所有的线程都要共同访问这个数count值,大家知道如果所有线程都要访问这个数的时候,如果每个线程给它往上加了10000,你这个时候是需要加锁的,不加锁会出问题。但是,你把它改成AtomicInteger之后就不用在做加锁的操作了,因为incrementAndGet内部用了cas操作,直接无锁的操作往上递增,无锁的操作效率会更高。

/**
 * 解决同样的问题的高效方法,使用AtomXXX类
 * AtomXXX类的本身方法都是原子性的,但不能保证多个方法连续调用都是原子性的
 */
public class Test {

    /*volatile*/ //int count1 = 0;
    AtomicInteger count = new AtomicInteger(0);

    /*synchronized*/void m() {
        for (int i = 0; i < 10000; i++)
            //if count1.get() < 1000
            count.incrementAndGet(); //count1++
    }

    public static void main(String[] args) {
        Test t = new Test();
        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }
        threads.forEach((o) -> o.start());
        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}
输出:100000

二、效率的比较?

  • 举个例子

模拟了很多个线程对一个数进行递增。
第一种:是一个long类型的数,递增的时候加锁;用synchronized,用一个lock,Object lock = new Object();,然后newRunnable(), 依然是一样的,在递增的时候我写的是synchronized (lock),然后计算时间;
第二种:使用AtomicLong可以让它不断的往上递增,这是第二种;每一个线程都new出来,之后每一个线程都做了十万次递增,第一种方式,打印起始时间->线程开始->所有线程结束->打印结束时间->计算最后花了多少时间;
第三种:使用LongAdder,1000个线程,每个线程十万次递增,第三种呢用的是LongAdder,这个LongAdder里面直接就是count3.increment();

public class Test {

    static long count2 = 0L;
    static AtomicLong count1 = new AtomicLong(0L);
    static LongAdder count3 = new LongAdder();

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int k = 0; k < 100000; k++) count1.incrementAndGet();
            });
        }
        long start = System.currentTimeMillis();
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        long end = System.currentTimeMillis();
        //TimeUnit.SECONDS.sleep(10);
        System.out.println("Atomic: " + count1.get() + " time " + (end - start));

        //-----------------------------------------------------------

        Object lock = new Object();
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int k = 0; k < 100000; k++)
                        synchronized (lock) {
                            count2++;
                        }
                }
            });
        }

        start = System.currentTimeMillis();
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        end = System.currentTimeMillis();

        System.out.println("Sync: " + count2 + " time " + (end - start));

        //---------------------------------------------------------

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int k = 0; k < 100000; k++) count3.increment();
            });
        }
        start = System.currentTimeMillis();
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        end = System.currentTimeMillis();
        //TimeUnit.SECONDS.sleep(10);
        System.out.println("LongAdder: " + count1.longValue() + " time " + (end - start));
    }
    
}

输出:
Atomic: 100000000 time 1137
Sync: 100000000 time 6154
LongAdder: 100000000 time 186

结论:跑起来对比LongAdder是效率最高的,但是,把线程数变小了LongAdder未必有优势,循环数量少了LongAdder也未必有优势,所以,实际当中用哪种你要考虑一下你的并发有多高。

问题一:为什么Atomic要比Sync快?

因为不加锁,synchronized是要加锁的,有可能它要去操作系统申请重量级锁,所以synchronized效率偏低,在这种情形下效率偏低。

问题二:LongAdder为什么要比Atomicx效率要高呢?

是因为LongAdder的内部做了一个分段锁,类似于分段锁的概念。在它内部的时候,会把一个值放到一个数组里,比如说数组长度是4,最开始是0,1000个线程,也就是每个数组单元就有250个线程,以此类推,每一个都往上递增算出来结果在加到一起。
在这里插入图片描述

二、ReentrantLock

  • 什么是可重入?

意思就是我锁了一下还可以对同样这把锁再锁一下,synchronized必须是可重入的,不然的话子类调用父类是没法实现的,synchronized方法是可以调用synchronized方法的。锁是可重入的。子类和父类如果是synchronized(this)就是同一把锁,同一个this当然是同一把锁。

  • 举个例子
/**
 * new Thread(rl::m2).start();时候
 * 本例中由于m1锁定this,只有m1ִ执行完毕的时候,m2才能执行
 */
public class Test {

    synchronized void m1() {
        for (int i = 0; i < 5; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
            if (i == 2) m2();
        }
    }

    synchronized void m2() {
        System.out.println("m2 ...");
    }

    public static void main(String[] args) {
        Test rl = new Test();
        new Thread(rl::m1).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //new Thread(rl::m2).start(); }

    }}
输出:
0
1
2
m2 ...
3
4
  • ReentrantLock是可以替代synchronized

原来写synchronized的地方换写lock.lock(),加完锁之后需要注意的是记得lock.unlock()解锁,由于synchronized是自动解锁的,大括号执行完就结束了。lock就不行,lock必须得手动解锁,手动解锁一定要写在try…finally里面保证最好一定要解锁,不然的话上锁之后中间执行的过程有问题了,死在那了,别人就永远也拿不到这把锁了。使用syn锁定的话如果遇到异常,jvm会手动释放锁,但是lock必须手动释放锁

public class Test {

    Lock lock = new ReentrantLock();

    void m1() {
        try {
            lock.lock(); //synchronized(this)
            for (int i = 0; i < 5; i++) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    void m2() {
        try {
            lock.lock();
            System.out.println("m2 ...");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Test rl = new Test();
        new Thread(rl::m1).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(rl::m2).start();
    }
}
输出:
0
1
2
3
4
m2 ...
  • Reentrantlock可以用tryLock进行尝试锁定

ReentrantLock有一些功能还是要比synchronized强大的,强大的地方,你可以使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行,synchronized如果搞不定的话他肯定就阻塞了,但是用ReentrantLock你自己就可以决定你到底要不要wait。

  • 举个栗子

下面程序 就是说比如5秒钟你把程序执行完就可能得到这把锁,如果得不到就不行。由于我的第一个线
程跑了10秒钟,所以你在第二个线程里申请5秒肯定是拿不到的,把循环次数减少就可以能拿到了。

public class test {

    Lock lock = new ReentrantLock();

    void m1() {
        try {
            lock.lock();
            for (int i = 0; i < 10; i++) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(i);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /*** 使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行
     *可以根据tryLock的返回值来判断是否锁定 * 也可以指定tryLock的时间 */
    void m2() { /*boolean locked = lock.tryLock(); System.out.println("m2 ..." + locked); if(locked) lock.unlock(); */
        boolean locked = false;
        try {
            locked = lock.tryLock(5, TimeUnit.SECONDS);
            System.out.println("m2 ..." + locked);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (locked) lock.unlock();
        }
    }

    public static void main(String[] args) {
        test rl = new test();
        new Thread(rl::m1).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(rl::m2).start();
    }
}
输出:
0
1
2
3
4
5
m2 ...false
6
7
8
9
  • ReentrantLock可以被打断的加锁

ReentrantLock还可以用lock.lockInterruptibly() 这个类,对interrupt()方法做出相应,可以被打断的加锁。

  • 举个栗子

线程1 上来之后加锁,加锁之后开始睡,睡的没完没了的,被线程1拿到这把锁的话,线程2如果说在想拿到这把锁不太可能,拿不到锁他就会在哪儿等着,如果我们使用原来的这种lock.lock()是打断不了它的,那么我们就可以用另外一种方式lock.lockInterruptibly() 这个类可以被打断的,当你要想停止
线程2就可以用 interrupt(),打断线程2的等待。这也是ReentrantLock比synchronized好用的一个地方。

  • 程序有点问题
public class test {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            try {
                lock.lock();
                System.out.println("t1 start");
                TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                System.out.println("t1 end");
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
            } finally {
                lock.unlock();
            }
        });

        t1.start();

        Thread t2 = new Thread(() -> {
            try {
                //lock.lock();
                lock.lockInterruptibly(); //可以对interrupt()方法做出相应
                System.out.println("t2 start");
                TimeUnit.SECONDS.sleep(5);
                System.out.println("t2 end");
            } catch (InterruptedException e) {
                System.out.println("interrupted!");
            } finally {
                lock.unlock();
            }
        });

        t2.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (
                InterruptedException e) {
            e.printStackTrace();
        }

        t2.interrupt();//打断线程2的等待

    }
}
  • ReentrantLock可以指定为公平锁,

公平锁的意思是当我们new一个ReentrantLock你可以传一个参数为true,这个true表示公平锁,公平锁的意思是谁等在前面就先让谁执行,而不是说谁后来了之后就马上让谁执行。如果说这个锁不公平,来了一个线程上来就抢,它是有可能抢到的,如果说这个锁是个公平锁,这个线程上来会先检查队列里有没有原来等着的,如果有的话他就先进队列里等着别人先运
行,这是公平锁的概念。

  • 举个栗子

ReentrantLock默认是非公平锁。公平是相对的,也有可能出现同一个线程一直拿到锁,多线程时候会有出现交替拿到锁的情况。

    private static ReentrantLock lock = new ReentrantLock(true); //参数为true表示为 公平锁,请对比输出结果

    @Override
    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) {
        test rl = new test();
        Thread th1 = new Thread(rl);
        Thread th2 = new Thread(rl);
        th1.start();
        th2.start();
    }
}
  • 总结:Reentrantlock vs synchronized

1.都是可重入锁。
2.ReentrantLock需要自己手动解锁,synchronized可以自行解锁。
3.ReentrantLock可以使用trylock,自己来控制,尝试锁定。
3.ReentrantLock可以使用lockInterruptibly:这个类,中间还可以被打断。
4.ReentrantLock可以公平和非公平的切换。
5.ReentrantLock内部是用的cas,synchronized是锁升级实现。
6.Reentrantlock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

三、CountDownLatch

CountDown叫倒数,Latch是门栓的意思(倒数的一个门栓,数到了,我这个门栓就开了)。

  • 用法1:

CountDownLatch典型用法1:某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为n new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1 countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

  • 用法2:

CountDownLatch典型用法2:实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。

  • 举个栗子1

latch.await(),它的意思是说给我看住门,给我插住不要动。CountDown()是在原来的基础上不断判断线程是否结束,比如:一直到这个数字变成0的时候门栓就会被打开,线程结束的。


    public static void main(String[] args) {
        usingJoin();
        usingCountDownLatch();
    }


    private static void usingCountDownLatch() {
        Thread[] threads = new Thread[100];
        CountDownLatch latch = new CountDownLatch(threads.length);
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                System.out.println("子线程" + Thread.currentThread().getName() + "开始执行");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程"+Thread.currentThread().getName()+"执行完成");
                latch.countDown();
            });
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
        try {
            System.out.println("主线程"+Thread.currentThread().getName()+"等待子线程执行完成...");
            //阻塞当前线程,直到计数器的值为0
            latch.await();
            System.out.println("主线程"+Thread.currentThread().getName()+"开始执行...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end latch");
    }


    private static void usingJoin() {
        Thread[] threads = new Thread[100];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                int result = 0;
                for (int j = 0; j < 10000; j++) result += j;
            });
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("end join");
    }
	}

用join实际上不太好控制,必须要你线程结束了才能控制,但是如果是一个门栓的话我在线程里不停的CountDown,在一个线程里就可以控制这个门栓什么时候往前走,用join我只能是当前线程结束了你才能自动往前走,当然用join可以,但是CountDown比它要灵活。

  • 举个栗子2

百米赛跑,4名运动员选手到达场地等待裁判口令,裁判一声口令,选手听到后同时起跑,当所有选手到达终点,裁判进行汇总排名。

public class test {

    public static void main(String[] args) {

        ExecutorService service = Executors.newCachedThreadPool();

        final CountDownLatch cdOrder = new CountDownLatch(1);
        final CountDownLatch cdAnswer = new CountDownLatch(4);

        for (int i = 0; i < 4; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("选手" + Thread.currentThread().getName() + "正在等待裁判发布口令");
                        cdOrder.await();
                        System.out.println("选手" + Thread.currentThread().getName() + "已接受裁判口令");
                        Thread.sleep((long) (Math.random() * 10000));
                        
                        System.out.println("选手" + Thread.currentThread().getName() + "到达终点");
                        cdAnswer.countDown();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            service.execute(runnable);
        }

        try {

            Thread.sleep((long) (Math.random() * 10000));
            System.out.println("裁判"+Thread.currentThread().getName()+"即将发布口令");

            cdOrder.countDown();
            System.out.println("裁判"+Thread.currentThread().getName()+"已发送口令,正在等待所有选手到达终点");
            cdAnswer.await();
            System.out.println("所有选手都到达终点");
            System.out.println("裁判"+Thread.currentThread().getName()+"汇总成绩排名");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
}
输出:
选手pool-1-thread-1正在等待裁判发布口令
选手pool-1-thread-3正在等待裁判发布口令
选手pool-1-thread-4正在等待裁判发布口令
选手pool-1-thread-2正在等待裁判发布口令
裁判main即将发布口令
裁判main已发送口令,正在等待所有选手到达终点
选手pool-1-thread-3已接受裁判口令
选手pool-1-thread-1已接受裁判口令
选手pool-1-thread-4已接受裁判口令
选手pool-1-thread-2已接受裁判口令
选手pool-1-thread-3到达终点
选手pool-1-thread-4到达终点
选手pool-1-thread-2到达终点
选手pool-1-thread-1到达终点
所有选手都到达终点
裁判main汇总成绩排名

四、CyclicBarrier

意思是循环栅栏,这有一个栅栏,什么时候人满了就把栅栏推倒,哗啦哗啦的都放出去,出去之后扎栅栏又重新起来,再来人,满了,推倒之后又继续。

  • 举个栗子

下面程序,两个参数,第二个参数不传也是可以的,就是满了之后不做任何事情。第一个参数是20,满了之后帮我调用第二个参数指定的动作,我们这个指定的动作就是一个Runnable对象,打印满人,发车。什么barrier.await()会被放倒,就是等够20个人了,后面也可以写你要做的操作 s。什么时候满了
20人了就发车。下面第一种写法是满了之后我什么也不做,第二种写法是用Labda表达式的写法。这个意思就是线程堆满了,我们才能往下继续执行。

public class test {

    public static void main(String[] args) {
        //CyclicBarrier barrier = new CyclicBarrier(20);
        CyclicBarrier barrier = new CyclicBarrier(20, () -> System.out.println("满人"));
        /*CyclicBarrier barrier = new CyclicBarrier(20, new Runnable()
        { @Override public void run() { System.out.println("满人,发车"); } });*/

        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

}
输出:5个满人
满人
满人
满人
满人
满人
  • 应用场景

CyclicBarrier的概念呢比如说一个复杂的操作,需要访问 数据库,需要访问网络,需要访问文件,有一种方式是顺序执行,挨个的都执行完,效率非常低,这是一种方式,还有一种可能性就是并发执行,原来是1、2、3顺序执行,并发执行是不同的线程去执行不同的操作,有的线程去数据库找,有
的线程去网络访问,有的线程去读文件,必须是这三个线程全部到位了我才能去进行,这个时候我们就可以用CyclicBarrier。

五、Phaser

Phaser它就更像是结合了CountDownLatch和CyclicBarrier,翻译一下叫阶段。Phaser是按照不同的阶段来对线程进行执行,就是它本身是维护着一个阶段这样的一个成员变量,当前我是执行到那个阶段,是第0个,还是第1个阶段啊等等,每个阶段不同的时候这个线程都可以往前走,有的线程走到某个阶段就停了,有的线程一直会走到结束。你的程序中如果说用到分好几个阶段执行 ,
而且有的人必须得几个人共同参与的一种情形的情况下可能会用到这个Phaser。

  • 举个栗子

模拟了一个结婚的场景,结婚是有好多人要参加的,因此,我们写了一个类Person是一个Runnable可以new出来,扔给Thread去执行,模拟我们每个人要做一些操作,有这么几种方法,arrive()到达、eat()吃、leave()离开、hug()拥抱这么几个。作为一个婚礼来说它
会分成好几个阶段,第一阶段大家好都得到齐了,第二个阶段大家开始吃饭, 三阶段大家离开,第四个阶段新郎新娘入洞房,定义了一个phaser,我这个phaser是从Phaser这个类继承,重写onAdvance方法,前进,线程抵达这个栅栏的时候,所有的线程都满足了这个第一个栅栏的条件了onAdvance会被
自动调用,目前我们有好几个阶段,这个阶段是被写死的,必须是数字0开始,onAdvance会传来两个参数phase是第几个阶段,registeredParties是目前这个阶段有几个人参加,每一个阶段都有一个打印,返回值false,一直到最后一个阶段返回true,所有线程结束,整个栅栏组,Phaser栅栏组就结束
了。
怎么才能让我的线程在一个栅栏面前给停住呢,就是调用phaser.arriveAndAwaitAdvance()这个方法,这个方法的意思是到达等待继续往前走,直到新郎新娘如洞房,其他人不在参与,调用phaser.arriveAndDeregister() 这个方法。还有可以调用方法phaser.register()往上加,不仅可以控制栅栏上的个数还可以控制栅栏上的等待数量,这个就叫做phaser。

public class test {

    static Random r = new Random();
    static MarriagePhaser phaser = new MarriagePhaser();

    static void milliSleep(int milli) {
        try {
            TimeUnit.MILLISECONDS.sleep(milli);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    

    static class MarriagePhaser extends Phaser {
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {
            switch (phase) {
                case 0:
                    System.out.println("所有人都到齐了!" + registeredParties);
                    System.out.println();
                    return false;
                case 1:
                    System.out.println("所有人都吃完了!" + registeredParties);
                    System.out.println();
                    return false;
                case 2:
                    System.out.println("所有人都离开了!" + registeredParties);
                    System.out.println();
                    return false;
                case 3:
                    System.out.println("婚礼结束!新郎新娘抱抱!" + registeredParties);
                    return true;
                default:
                    return true;
            }
        }
    }

    static class Person implements Runnable {
        String name;

        public Person(String name) {
            this.name = name;
        }

        public void arrive() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 到达现场!n", name);
            phaser.arriveAndAwaitAdvance();
        }

        public void eat() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 吃完!n", name);
            phaser.arriveAndAwaitAdvance();
        }

        public void leave() {
            milliSleep(r.nextInt(1000));
            System.out.printf("%s 离开!n", name);
            phaser.arriveAndAwaitAdvance();
        }

        private void hug() {
            if (name.equals("新郎") || name.equals("新娘")) {
                milliSleep(r.nextInt(1000));
                System.out.printf("%s 洞房!n", name);
                phaser.arriveAndAwaitAdvance();
            } else {
                phaser.arriveAndDeregister(); //phaser.register()
            }
        }

        @Override
        public void run() {
            arrive();
            eat();
            leave();
            hug();
        }
    }

    public static void main(String[] args) {
        phaser.bulkRegister(7);
        for (int i = 0; i < 5; i++) {
            new Thread(new Person("p" + i)).start();
        }
        new Thread(new Person("新郎")).start();
        new Thread(new Person("新娘")).start();
    }

}
输出:
p0 到达现场!
p3 到达现场!
p2 到达现场!
新娘 到达现场!
p1 到达现场!
p4 到达现场!
新郎 到达现场!
所有人都到齐了!7

p3 吃完!
p1 吃完!
新郎 吃完!
p2 吃完!
p4 吃完!
新娘 吃完!
p0 吃完!
所有人都吃完了!7

p1 离开!
新郎 离开!
p0 离开!
新娘 离开!
p4 离开!
p2 离开!
p3 离开!
所有人都离开了!7

新娘 洞房!
新郎 洞房!
婚礼结束!新郎新娘抱抱!2

六、ReadWriteLock

这个ReadWriteLock 是读写锁。读写锁的概念其实就是共享锁和排他锁,读锁就是共享锁,写锁就是排他锁

  • 举个栗子

第一种:直接new ReentrantLock()传进去,分析下这种方法,主程序定义了一个Runnable对象,第一个是调用read() 方法,第二个是调用write() 方法同时往里头扔一个随机的值,然后起了18个读线程,起了两个写线程,这个两个我要想执行完的话,我现在传的是一个ReentrantLock,这把锁上了之后没有其他任何人可以拿到这把锁,而这里面每一个线程执行
都需要1秒钟,在这种情况下你必须得等20秒才能干完这事儿;
第二种:我们换了锁 new ReentrantReadWriteLock() 是ReadWriteLock的一种实现,在这种实现里头我又分出两把锁来,一把叫readLock,一把叫writeLock,通过他的方法readWriteLock.readLock()来拿到readLock对象,读锁我就拿到了。通过readWriteLock.writeLock()拿到writeLock对象。这两把锁在我读的时候扔进去,因此,读线程是可以一起读的,也就是说这18个线程可以一秒钟完成工作结束。
所以使用读写锁效率会大大的提升。

public class test {

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

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

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

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

    public static void main(String[] args) {

        //Runnable readR = ()-> read(lock);
        Runnable readR = () -> read(readLock);
        //Runnable writeR = ()->write(lock, new Random().nextInt());
        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();
    }
}

问题:以后还写不写synchronized?分布式锁咋实现?

以后一般不用这些新的锁,多数都用synchronized。只有特别特别追求效率的时候才会用到这些新的
锁。现在用分布式锁很多,分布式锁也是面试必问的,它也不难比较简单,学了redis,redis有两种方
法可以实现分布式锁,还有学完 ZooKeeper也可以实现分布式锁,还有数据库也可以实现,但数据库
实现的效率就比较低了。

给大家举一个简单的例子,就说秒杀这个事情。在开始秒杀之前它会从数据库里面读某一个数据,比如
所一个电视500台,只能最对售卖500台,完成这件事得是前面的线程访问同一个数最开始是0一直涨到
500就结束,需要加锁,从0递增,如果是单机LongAdder或AtomicInteger搞定。分布式的就必须得用
分布式锁,对一个数进行上锁。redis是单线程的所以扔在一台机器上就ok了。

七、Semaphore

Semaphore,信号灯。可以往里面传一个数,permits是允许的线程数量,Semaphore的含义就是限流,比如说你在买票,Semaphore写5就是只能有5个人可以同时买票。acquire的意思叫获得这把锁,线程如果想继续往下执行,必须得从Semaphore里面获得一个许可,他一共有5个许可,5个都用到0了你就得给我等着。例如,有一个八条车道的机动车道,这里只有两个收费站,到这儿,谁acquire得到其中某一个谁执行。

s.acquire()这个方法叫阻塞方法,阻塞方法的意思是说我大概acquire不到的话我就停在这,acquire的意思就是得到。如果我 Semaphore s = new Semaphore(1) 写的是1,我取一下,acquire一下他就变成0,当变成0之后别人是acquire不到的,然后继续执行,线程结束之后注意要s.release(),执行完该执行的就把他release掉,release又把0变回去1,还原化。

  • 默认Semaphore是非公平的,new Semaphore(2, true)第二个值传true才是设置公平。
  • 举个栗子
public class test {

    public static void main(String[] args) {
        //Semaphore s = new Semaphore(2)
        //允许2个线程同时执行并且公平
        Semaphore s = new Semaphore(2, true);
        //Semaphore s = new Semaphore(1);
        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();
    }
}
输出:
T1 running...
T2 running...
T2 running...
T1 running...

八、Exchanger

Exchanger叫做交换器,线程间通信的方式非常多,这只是其中一种,就是线程之间交换数据用的,交换这个东西只能两两进行。

在这里插入图片描述

  • 举个栗子
public class test {
    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();
    }
}
输出:
t2 T1
t1 T2

九、总结

  • CountDownLatch的用法,就是倒计时,什么时候计数完了,门栓打开,程序继续往下执行;
  • CycliBarrier一个栅栏,循环使用,什么时候人满了,栅栏放倒大家冲过去;
  • Phaser分阶段的栅栏;
  • ReadWriteLock读写锁,重点掌握;
  • Semaphore限流用的;
  • Exchanger两个线程之间互相交换数据;

最后

以上就是聪慧老虎为你收集整理的多线程与高并发(三)--Atomic类和线程同步新机制的全部内容,希望文章能够帮你解决多线程与高并发(三)--Atomic类和线程同步新机制所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(33)

评论列表共有 0 条评论

立即
投稿
返回
顶部