实现线程同步的几种方式总结

实现线程同步的几种方式总结在多线程中线程的执行顺序是依靠哪个线程先获得到CUP的执行权谁就先执行,虽然说可以通过线程的优先权进行设置,但是他只是获取CUP执行权的概率高点,但是也不一定必须先执行。在这种情况下如何保证线程按照一定的顺序进行执行,今天就来一个大总结,分别介绍一下几种方式。通过Object的wait和notify 通过Condition的awiat和signal 通过一个阻塞队列 通过两个阻塞队列 …

大家好,又见面了,我是你们的朋友全栈君。

在多线程中线程的执行顺序是依靠哪个线程先获得到CUP的执行权谁就先执行,虽然说可以通过线程的优先权进行设置,但是他只是获取CUP执行权的概率高点,但是也不一定必须先执行。在这种情况下如何保证线程按照一定的顺序进行执行,今天就来一个大总结,分别介绍一下几种方式。

  1. 通过Object的wait和notify
  2. 通过Condition的awiat和signal
  3. 通过一个阻塞队列
  4. 通过两个阻塞队列
  5. 通过SynchronousQueue 
  6. 通过线程池的Callback回调
  7. 通过同步辅助类CountDownLatch
  8. 通过同步辅助类CyclicBarrier

一、通过Object的wait和notify

之前写过一篇文章介绍生产者与消费者模式就是用这个机制实现的,现在来一个简单的写法。写一个测试了Test,加上main方法,在写一个内部类Man进行测试。main方法如下,他进行创建两个线程,传进去Runnable对象。

    public static boolean flag = false;

    public static int num = 0;

    public static void main(String[] args) {
        Man man = new Man();

        new Thread(() -> {
            man.getRunnable1();
        }).start();
        new Thread(() -> {
            man.getRunnable2();
        }).start();
    }

getRunnable1和getRunnable2分别表示两个需要执行的任务,在两个线程中进行,方法1用于数据的生产,方法二用于数据的获取,数据的初始值为num = 0,为了保证生产和获取平衡需要使用wait和notify方法,这两个方法的使用必须是要加锁的,因此使用synchronized进行加锁使用,为了演示这个效果,我们加上一个sleep方法模拟处理时间,如下:

    public static class Man {
        
        public synchronized void getRunnable1() {
            for (int i = 0; i < 20; i++) {
                while (flag) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("生产出:" + (++num) + "个");
                flag = true;
                notify();
            }
        }
        
        public synchronized void getRunnable2() {
            for (int i = 0; i < 20; i++) {
                while (!flag) {
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                //模拟加载时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("取出出:" + (num--) + "个");
                System.out.println("------------------");

                flag = false;
                notify();
            }
        }
    }

分析它的加载流程,从方法1进行分析,由于flag的初始条件为false,所以方法1不进入等待,直接进行生产,生产完成成之后,更新flag的值为true,同时notify下一个方法2的wait方法,使其变为唤醒状态。这时候由于方法1加锁了,无法执行方法1其他部分,当方法1执行完毕,方法1才有可能执行,但是方法1的flag已经为true,进入到wait里面又处于阻塞状态,所以这时候只能执行方法2了。由于方法2被唤醒了,阻塞解除,接下来就获取数据,当获取完毕又再次让flag变为false,notify方法1解除阻塞,再次执行方法1,就这样不断的循环,保证了不同线程的有序执行,直到程序终止。

运行效果如下:

实现线程同步的几种方式总结

二、通过Condition的awiat和signal

上面第一个的实现是一个阻塞,一个等待的方式保证线程有序的执行,但是不能进行两个线程之间进行通信,而接下来介绍的Condition就具备这样的功能。要获取Condition对象首先先得获取Lock对象,他是在jdk1.5之后增加的,比synchronized性能更好的一种锁机制。和上面的类似,拷贝一份代码,看看main方法:

    public static boolean flag = false;

    public static int num = 0;

    public static void main(String[] args) {
        Man man = new Man();

        new Thread(() -> {
            man.getRunnable1();
        }).start();
        new Thread(() -> {
            man.getRunnable2();
        }).start();
    }

情况和第一个实现方法分析一致,这里不重复了。主要看内部类Man中的方法1和方法2。先手创建锁对象,把synchronized改为使用Lock加锁,其次通过Lock创建Condition对象,替换掉Object类的wait方法为Condition的await方法,最后换掉notify方法为signal方法即可,执行原理和上面分析一致,代码如下:

    public static class Man {
        public static ReentrantLock lock = new ReentrantLock();
        public static Condition condition = lock.newCondition();

        public void getRunnable1() {
            lock.lock();
            try {
                for (int i = 0; i < 20; i++) {
                    while (flag) {
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("生产出:" + (++num) + "个");
                    flag = true;
                    condition.signal();
                }
            } finally {
                lock.lock();
            }
        }

        public void getRunnable2() {
            lock.lock();
            try {
                for (int i = 0; i < 20; i++) {
                    while (!flag) {
                        try {
                            condition.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("取出出:" + (num--) + "个");
                    System.out.println("------------------");
                    flag = false;
                    condition.signal();
                }
            } finally {
                lock.unlock();
            }
        }
    }

执行结果如下:

实现线程同步的几种方式总结

三、通过一个阻塞队列

  上面的两个方法实现起来代码比较繁琐,如果通过阻塞队列来实现会更加简洁,这里采用常用的容量为64的ArrayBlockingQueue来实现。main方法如下:

    public static void main(String[] args) {
        Man man = new Man();

        new Thread(() -> {
            man.getRunnable1();
        }).start();
        new Thread(() -> {
            man.getRunnable2();
        }).start();
    }

主要来看Man中的方法1和方法2,方法1中生产数据,这里把生产的数据存进队列里面,同时方法2进行取数据,如果方法1放满了或者方法2取完了就会被阻塞住,等待方法1生产好了或者方法2取出了,然后再进行。代码如下:

    public static class Man {

        ArrayBlockingQueue queue = new ArrayBlockingQueue<Integer>(64);

        public void getRunnable1() {
            for (int i = 0; i < 8; i++) {
                System.out.println("生产出:" + i + "个");
                try {
                    queue.put(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("---------------生产完毕-----------------");
        }

        public void getRunnable2() {
            for (int i = 0; i < 8; i++) {
                try {
                    int num = (int) queue.take();
                    System.out.println("取出出:" + num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

很明显使用阻塞队列代码精炼了很多,在这还可以发现这个阻塞队列是具有缓存功能的,想很多Android中网络访问框架内部就是使用这个进行缓存的,例如Volley、Okhttp等等。

运行效果如下:

实现线程同步的几种方式总结

四、通过两个阻塞队列

使用一个阻塞队列能够实现线程同步的功能,两个阻塞队列也可以实现线程同步。原理是ArrayBlockingQueue他是具有容量的,如果把他的容量定位1则意味着他只能放进去一个元素,第二个方进行就会就会被阻塞。按照这个原理进行来实现,定义两个容量为1的阻塞队列ArrayBlockingQueue,一个存放数据,另一个用于控制次序。main方法和上面一致,主要来看看Man类中的两个方法:

    static class Man {
        //数据的存放
        ArrayBlockingQueue queue1 = new ArrayBlockingQueue<Integer>(1);
        //用于控制程序的执行
        ArrayBlockingQueue queue2 = new ArrayBlockingQueue<Integer>(1);

        {
            try {
                //queue2放进去一个元素,getRunnable2阻塞
                queue2.put(22222);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public void getRunnable1() {
            new Thread(() -> {
                for (int j = 0; j < 20; j++) {
                    try {
                        //queue1放进一个元素,getRunnable1阻塞

                        queue1.put(j);
                        System.out.println("存放   线程名称:" + Thread.currentThread().getName() + "-数据为-" + j);

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    try {
                        //queue2取出元素,getRunnable2进入
                        queue2.take();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        public void getRunnable2() {
            new Thread(() -> {
                for (int j = 0; j < 20; j++) {
                    try {
                        //queue2放进一个元素,getRunnable2阻塞
                        queue2.put(22222);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    try {
                        //queue1放进一个元素,getRunnable1进入

                        int i = (int) queue1.take();
                        System.out.println("获取   线程名称:" + Thread.currentThread().getName() + "-数据为-" + i);

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

再次提醒queue2用于控制程序的执行次序,并无实际含义。最后看看运行效果,存一个、取一个很清晰,如下:

实现线程同步的几种方式总结

五、通过SynchronousQueue

SynchronousQueue不同于一般的数据等线程,而是线程等待数据,他是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。通过这一特性来实现一个多线程同步问题的解决方案,代码如下:

    /**
     * 使用阻塞队列SynchronousQueue
     * offer将数据插入队尾
     * take取出数据,如果没有则阻塞,直到有数据在获取到
     */
    public static void test() {
        SynchronousQueue queue = new SynchronousQueue();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    queue.offer(9);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        try {
            int take = (int) queue.take();
            System.out.println(take);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

子线程中进行设置数据,而主线程获取数据,如果子线程没执行完毕,子线程没有执行完毕主线程就会被阻塞住不能执行下一步。

六、通过线程池的Callback回调

在线程的创建中,有一种创建方法可以返回线程结果,就是callback,他能返回线程的执行结果,通过子线程返回的结果进而在主线程中进行操作,也是一种同步方法,这种同步在Android中特别适用,例如Android中的AsyncTask源码中任务的创建部分。代码如下:

    private static void test() {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        Future<Boolean> submit = executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                return false;
            }
        });
        try {
            if (submit.get()) {
                System.out.println(true);
            } else {
                System.out.println(false);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

7、通过同步辅助类CountDownLatch

CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行。他类实际上是使用计数器的方式去控制的,在创建的时候传入一个int数值每当我们调用countDownt()方法的时候就使得这个变量的值减1,而对于await()方法则去判断这个int的变量的值是否为0,是则表示所有的操作都已经完成,否则继续等待。可以理解成倒计时锁。

public class Test7 {
    public static void main(String[] args) {
        //启动两个线程,分别执行完毕之后再执行主线程
        CountDownLatch countDownLatch = new CountDownLatch(2);

        //线程1执行
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            countDownLatch.countDown();
        });
        //线程2执行
        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            countDownLatch.countDown();
        });


        thread1.start();
        thread2.start();
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //执行主线程
        System.out.println("主线程执行完毕");
    }
}

结果如下:

实现线程同步的几种方式总结

8、通过同步辅助类CyclicBarrier

CyclicBarrier是一个同步的辅助类,和上面的CountDownLatch比较类似,不同的是他允许一组线程相互之间等待,达到一个共同点,再继续执行。可看成是个障碍,所有的线程必须到齐后才能一起通过这个障碍。

public class Test8 {
    public static void main(String[] args) {
        //启动两个线程,分别执行完毕之后再执行主线程
        CyclicBarrier barrier  = new CyclicBarrier(2, () -> {
            //执行主线程
            System.out.println("主线程执行完毕");

        });

        //线程1执行
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "线程执行完毕");

            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        });

        //线程2执行
        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "线程执行完毕");
            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        });


        thread1.start();
        thread2.start();
    }
}

运行结果:

实现线程同步的几种方式总结

至此八大方法介绍完毕!

 

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/159700.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • TransactionScope事务级别

    TransactionScope事务级别在TransactionScope中默认的事务级别是Serializable,即在事务过程中,完全性锁表。别的进程不能查询,修改,新增,删除。这样会导致效率大大降低,虽然数据完整性很高。通常我们不需要那么高的数据完整性。所以需要修改默认的事务级别 所有的事务级别如下Chaos无法改写隔离级别更高的事务中的挂起的更改。ReadCommitted不可以在事务期间读取可变

  • 解析为什么hashmap是线程不安全的?「建议收藏」

    解析为什么hashmap是线程不安全的?「建议收藏」扩容一般我们声明HashMap时,使用的都是默认的构造方法:HashMap<K,V>,看了代码你会发现,它还有其它的构造方法:HashMap(intinitialCapacity,floatloadFactor),其中参数initialCapacity为初始容量,loadFactor为加载因子,扩容就是在put加入元素的个数超过initialCapacity*loa…

    2022年10月11日
  • Linux内核的整体架构简介

    Linux内核的整体架构简介1.前言本文是“Linux内核分析”系列文章的第一篇,会以内核的核心功能为出发点,描述Linux内核的整体架构,以及架构之下主要的软件子系统。之后,会介绍Linux内核源文件的目录结构,并和各个软件子系统对应。注:本文和其它的“Linux内核分析”文章都基于如下约定:  a)内核版本为Linux3.10.29(该版本是一个longterm的版本,会被Linux社区持续维

  • java 死链检测_网站死链检测工具/网站地图生成工具「建议收藏」

    java 死链检测_网站死链检测工具/网站地图生成工具「建议收藏」转载自http://www.yshjava.cn/post/483.html今天在谷歌站长工具上看到谷歌爬虫在笔者的个人博客网站上找到了3个无效的404链接,稍微有一点SEO常识的人都知道,404是搜索引擎爬虫非常讨厌的页面,会直接降低网站在搜索引擎中的权重和排名,这是广大站长都不愿意看到的事情。如果自己手动的去寻找这些404页面,或许很难:404存在于哪些页面中?出现一次还是多次?偶然还是必然…

  • vue 键盘事件keyup/keydoen

    vue 键盘事件keyup/keydoen使用:当你按下键盘,键盘的值为13的时候,将会执行show函数方法以上两种keyup和keyup.13方法是一样的,点击回车按键,就会执行alert方法一些便捷方式:@keyup.13回

  • [CF] 986 A. Fair

    [CF] 986 A. Fair

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号