深入多线程十一:超全synchronized的两种用法:同步方法与同步块(解决线程不安全问题+举栗子)

在学习Java的道路上,是否路过多线程时总让你很迷惘;很不巧,我也是,而使我们感到很迷惘主要原因都源于没有对概念的深深的理解和实践。所以我决定漫步Java多线程,同你一起会会多线程。

深入多线程系列

深入多线程一:理解多线程在于深深地理解了多任务、进程、多线程、线程
深入多线程二:手撕多线程,从会三种创建多线程方式开始:除了常见的两种,你是否了解Callable接口方式?
深入多线程三:初遇并发问题:从一个小故事开始,从一行行代码开始
深入多线程四:停止多线程,你不会还以为是用stop和destroy吧?
深入多线程五:多线程为何要使用休眠?
深入多线程六:线程礼让与强制执行
深入多线程七:纯手绘图解多线程状态+代码示例,就问你怕了吗?
深入多线程八:多线程的优先级
深入多线程九:守护线程
深入多线程十:通过案例体会多线程的不安全
深入多线程十一: 超全synchronized的两种用法:同步方法与同步块
深入多线程十二:什么是死锁?
深入多线程十三:什么是Lock锁,比起synchronized锁有什么区别?
深入多线程十四:经典生产者与消费者问题,本质是线程通信问题
深入多线程十五:管程方式解决生产者与消费者问题
深入多线程十六:信号灯方式解决生产者与消费者问题
深入多线程十七:什么是池?线程池方式解决生产者与消费者问题

Java中我们是通过private关键字来保证数据对象只能被方法访问,也就是 setget 方法,那么面对同步问题,就只要针对方法提出一个机制就可以。

通过synchronized关键字,锁机制,只允许独占对应代码段,其他线程必须等待使用后才释放锁,避免多线程同时修改数据。

synchronized 共有两种用法:

synchronized方法synchronized块

同步方法

public synchronized void method(int args){}

每个对象对应一把锁,synchronized方法控制"对象"的访问,但synchronized必须获得调用该方法的对象锁才能执行。

同步方法的缺点:

同步方法锁的范围大,同步的范围越大,相应的性能就越差;所以大的方法体申明synchronized会影响效率。

可以看到下面如图,只读是统一的数据不会出错,不需要同步;而修改面对的是不同对应不同用户数据就容易出错,所以在方法里面只修改的代码才需要锁,需要同步。

对应上面,如果你同步方法范围过大,把不需要的的代码也锁一起,自然效率就低。

在这里插入图片描述

同步块

synchronized(obj){}

同步块的synchronized里的obj可以锁任何对象,而同步方法是this,是锁对象本身或class。

同步块可以锁任何对象意味着可以更有针对性。

简单的说同步块可以更准确的控制需要的同步代码块。

只有有效的缩小同步范围,才可以保证在并发安全的前提下也能提高性能。

实战

嘿嘿,我们终于可以把之前文章里挖的坑填喽

深入浅出多线程系列十一:只有从不同案例中,才能深刻体会多线程的不安全,从而才能更好的解决!

上篇文章里,第一个抢票的线程不安全案例

代码的运行结果

卖超票与重复抢票。
在这里插入图片描述
那么怎么解决呢?

上面说过很简单,声明 synchronized

在这里插入图片描述
然后我们把ticket 赋值10张,让结果更可观。

这里我们使用的方式是synchronized 方法的方式。

public class ThreadDemo implements Runnable{

    private Integer ticket = 10;
    private boolean sign = true;

    @Override
    public void run() {
        while (sign) {
            try {
                buyTickets();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    private synchronized void buyTickets() throws InterruptedException {
        if (ticket <= 0) {
            sign = false;
            return;
        }
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName()+"抢到了第"+ticket--+"张票");
    }

    public static void main(String[] args) {
        ThreadDemo thread = new ThreadDemo();
        new Thread(thread,"小白").start();
        new Thread(thread,"小蓝").start();
        new Thread(thread,"一颗剽悍的种子").start();
    }
}

可以看到声明了 synchronized ,结果是有序的输出,也不会有卖超和重复的线程不安全问题。

在这里插入图片描述

第二个是ArrayList集合线程不安全案例

可以看到集合的长度添加的数量并不是我们预期的长度。

而是可能会出现覆盖。
在这里插入图片描述
那么我们继续来解决,同样很简单

但这次我们使用我们synchronized 代码块的方式。

public class ThreadDemo {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

在这里插入图片描述
这里有一个需要注意的点:

锁的对象是锁需要增删改的对象,锁的就是变化的量。

同步块扩展点:

synchronized(obj)里的obj可以称为 同步监视器

同步监视器可以理解成一个机制、一个过程

一个什么样的过程呢?

1. 当第一个线程访问,就会锁定同步监视器,执行其中代码。
2. 当第二个线程访问,发现同步监视器被锁定,禁止访问。
3. 这时第一个线程访问完,解锁同步监视器。
4. 然后第二个线程访问,发现同步监视器没有锁,然后锁定并访问。

以此类推…

有没有发现跟我们生活中很多栗子很相似

假设你买东西,如果不排队,这时人又很多,一窝蜂的往收银台冲,争着抢着,那么收银员就不仅手忙脚乱,还很烦躁,那么一系列问题就出现了,例如你给了钱,但是看你挺面生,好像没给呢!,还有多找钱、少找钱、还有更可恶的,逃单!!!

但是你规定了一个规则,一个机制,一个买单流程

那么事情就解决了

排队

一个个有序的排列,一个买完,才接着下一个买,就可以避免因为混乱而造成的一系列问题。

最后的话:

许多机制,许多流程都是相似且相通,希望你能举出你自己的栗子;因为只有你思考过,沉淀过的知识,才是属于你的知识!

最后的最后,为了更好的阅读体验,我把想说的话都放在了下面,嘿嘿。

我是一颗剽悍的种子 把我会的,认真的分享 是我写博客一直不变的信条。
如果你能看到这篇博文,说明咱们还是很有缘的;希望能带给你一些许帮助,创作的不易,
把我文章的知识带走,你的三连留下,点赞,评论,关注,是我最大的动力。

猜你喜欢

转载自blog.csdn.net/A_hxy/article/details/108045073