并发编程04-线程安全解决方案之如何正确使用synchroized关键字

正确使用synchroized关键字

synchroized是Java用来解决线程安全问题的一个关键字,在最开始的时候,我对他的理解就是一个锁,用来锁住一个方法或者一个代码块,其实这种想法是完全错误的,如果站在这个角度理解他,是不能写出线程安全的代码的。synchroized在英文中做动词用的意思是:使同步;做形容词用的意思是:同步的。因此他的作用是使一个方法或者一个代码块成为同步的,也就是同一时间只能有一个线程访问。那么如何实现呢?他的思路是找一把锁,把需要同步的代码锁起来。

如下图所示,我们找了一把锁将同步代码块锁了起来,当有三个线程同时访问同步代码块时,他们首先需要找到钥匙打开锁,线程1很找到了这把唯一的钥匙,打开锁,进入同步代码块运行,线程2和线程3只能眼巴巴看这,排队等候。当线程1执行完之后,他会将钥匙物归原主,那么线程2和线程3开始有机会去找到钥匙,打开锁。

在这里插入图片描述

因此,使用synchroized关键字的核心是什么呢?是找一把唯一的锁将同步代码块锁起来。好的,那让我们一起来看看如何找这样一把锁。

:理解这篇笔记必须先理解:什么是线程安全以及Java虚拟机中哪些数据是线程共享的,那些是线程私有的

synchroized用在代码块上

我们看这样一个例子吧。在sellTicket方法中,对成员变量totalTicket做减一操作,我们创建了5个线程去执行这个方法,我们希望的是当有一个线程将totalTicket减为0后,其他线程就不能再做减法操作,实际上呢,五个线程都成功对totalTicket做了减法。为什么呢?前面我们说过成员变量是放在方法区、所有线程共享的,因此这里存在一个线程安全的问题。

public class Main {

    private static int totalTicket = 1;

    public static void main(String[] args) {
        Main main = new Main();
        for (int i = 0; i < 20; i++) {
            new Thread(main::sellTicket).start();
        }

    }

    public void sellTicket() {
        if (totalTicket > 0) {
            System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            totalTicket--;
        }
    }
}

// 执行结果:
totalTicket :1 被线程: Thread-01
totalTicket :1 被线程: Thread-21
totalTicket :1 被线程: Thread-11
totalTicket :1 被线程: Thread-31
totalTicket :1 被线程: Thread-41

如何解决呢?我们可以用上面说的synchroized关键字,给他找一把锁,让他把将会出现线程安全的代码锁起来,变成同步代码快。这里我们找的锁是:this,this什么意思,当前对象的引用。执行一下这段代码,我们发现是可以解决线程安全问题的。

public class Main {

    private static int totalTicket = 1;

    public static void main(String[] args) {
        Main main = new Main();
        for (int i = 0; i < 5; i++) {
            new Thread(main::sellTicket).start();
        }

    }

    public void sellTicket() {
        synchronized (this) {
            if (totalTicket > 0) {
                System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                totalTicket--;
            }
        }
    }
}

// 执行结果
totalTicket :1 被线程: Thread-01

那么我们在看下面这段代码:我们将main方法稍作改动,在执行发现,totalTicket又被两个线程做了减法操作,实际上当他被一个线程减到0之后,就不应该在执行。那么问题出在了哪里?我们已经加了synchronized关键字做了同步。。

public class Main {

    private static int totalTicket = 1;

    public static void main(String[] args) {
        Main main1 = new Main();
        for (int i = 0; i < 5; i++) {
            new Thread(main1::sellTicket).start();
        }

        Main main2 = new Main();
        for (int i = 0; i < 5; i++) {
            new Thread(main2::sellTicket).start();
        }

    }

    public void sellTicket() {
        synchronized (this) {
            if (totalTicket > 0) {
                System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                totalTicket--;
            }
        }
    }
}

// 执行结果
totalTicket :1 被线程: Thread-01
totalTicket :1 被线程: Thread-51

我么分析一下,this是什么?当前对象的引用,我们在main方法里new了几个对象呢?两个:main1和main2。因此当从第8行调用到sellTicket的时候,当前对象是谁?是main1,this就是指向main1这个对象的一个引用。当从第13行调用到sellTicket的时候,当前对象是谁?是main2,那么this就是指向main2这个对象的一个引用。所以说,在这个代码中,我们一共找了两把锁,同时可以放两个线程进来,那么自然是线程不安全的。
在这里插入图片描述

知道了问题的所在,我们如何解决呢?这里看下正确的代码吧(限于篇幅,省去部分代码):

这个代码中,我们创建了一个static修饰的常量对象,用它去做锁,那我们知道static修饰的变量、方法等是和类的实例无关的,是属于类的,不管你创建多少个实例,static修饰的都只有一个,因此用它去做锁是可以的。

public class Main {

    private static final Object object = new Object();
    
    public void sellTicket() {
        synchronized (object) {
            if (totalTicket > 0) {
                System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                totalTicket--;
            }
        }
    }
}

当然我们也可以用Main.class去做锁,他当然也是唯一的。

public class Main {

    public void sellTicket() {
        synchronized (Main.class) {
            if (totalTicket > 0) {
                System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                totalTicket--;
            }
        }
    }
}

synchroized用在普通方法上

synchroized关键字也可以用在方法上,当他用在一个普通的方法上时候,相当于是我们前面说的synchroized (this)这种情况,因此当只有一个对象的时候,就像下面这段代码,他是线程安全的,可是如果在有一个Main的实例main2,那么这段代码同样是线程不安全的,为什么,可以参考上面的解释。

public class Main {

    private static int totalTicket = 1;

    public static void main(String[] args) {
        Main main1 = new Main();
        for (int i = 0; i < 5; i++) {
            new Thread(main1::sellTicket).start();
        }
    }

    public synchronized void sellTicket() {
        if (totalTicket > 0) {
            System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            totalTicket--;
        }
    }
}

synchroized用在静态方法上

前面说了static修饰的方法和变量都是属于类的,所有类的实例共享的,因此当synchrozide用在static修饰的方法上的时候,他的锁就是当前类,类似于synchroized (Main.class)的效果。

public class Main {

    private static int totalTicket = 1;

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(Main::sellTicket).start();
        }
    }

    public synchronized static void sellTicket() {
        if (totalTicket > 0) {
            System.out.println("totalTicket :" + totalTicket + " 被线程: " + Thread.currentThread().getName() + " 减1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            totalTicket--;
        }
    }
}

:当在方法上加锁的时候要注意,我们是不是真的需要同步整个方法,如果这个方法有50行,只有两行涉及到共享变量的修改,那么我们应该做的是将这两行用同步代码块包起来,尽量减小锁的粒度。而不是蛮横的将整个方法都锁起来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半__夏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值