java多线程编程:关于 synchronized 的一点理解;synchronized 的应用场景;形象举例如何正确加 synchronized

本文探讨了锁的产生原理,synchronized关键字锁定的对象,以及如何正确使用synchronized避免并发问题。通过实例讲解了为何仅锁代码块不足以解决问题,揭示了线程安全的关键在于对共享资源的同步操作。
摘要由CSDN通过智能技术生成

应该关注的问题

  • 锁是怎么产生的
  • synchronized 关键字锁的是谁
  • 需要等待的是谁
  • 怎么上锁才是对的

锁是怎么产生的

  • 这里你只需要记住一句话:
    • 每个对象产生的时候自己就带了一把锁
    • 我知道你以前可能想的是:我们需要创建一把锁,然后把。。。锁住,其实不是的,每个对象在产生的时候,自己就有一把锁,只是你看不见,只有再用 synchronized 的时候才能有用
    • 比如现在你有一个 Toilet 类,那么这个 Toiltet 创建的实例自身就带锁;你可以这么理解,每个厕所被生产出来的时候,大门上就会带一把锁。

synchronize 关键字锁的是谁

  • synchronized 可以锁三种东西:
    • synchronized 关键字加载静态方法上锁的是类对象
    • synchronized 关键字加载实例方法上锁的是这个实例(也就是 this
    • synchronized 关键字加载代码块上锁的是依然是实例对象,只不过相比于第二种方式,这样的效率会更高一些

锁非静态方法(实例方法)

  • 对一个实例对象进行限制,我们看下面的代码
    • 构造一个 Toilet 类,创建一个 Toilet 对象,名为:城南厕所;就像我们上面提到的,它在创建的时候就会有一把锁
    • 创建一个 Person 类,产生 1000 个排队上厕所的人。
    • 1000 个排队上厕所的人争夺的是这个 “城南厕所” 的对象。
    • 如果我们想让这些人在上厕所的时候不被打扰,应该给哪个代码段加 synchronized 关键字呢?当然是把对 toilet 变量 “写” 操作的代码块加上 synchronized;我们来看代码示例,毕竟语言描述太不具体:talk is cheap, show me the code

Producer 来产生排队上厕所的人

  • 每个人都是一个线程
package Test;

import static java.lang.Thread.interrupted;
import static java.lang.Thread.sleep;

public class Producer implements Runnable{
    Toilet toilet;
    public Producer(Toilet toilet){
        this.toilet = toilet;
    }

    @Override
    public void run() {
        int counter = 0;
        while (!interrupted()){
            new Thread(new Person(""+counter, toilet)).start();
            counter ++;

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

Toilet 公共厕所,构建实例“城南厕所”

package Test;

import static java.lang.Thread.interrupted;
import static java.lang.Thread.sleep;

public class Toilet implements Runnable{

    public Person getPerson() {
        return person;
    }
    public void setPerson(Person person) {
        this.person = person;
    }

    private Person person;
    String name;

    public Toilet(String toiletName){
        this.name = toiletName;
    }



    public static void main(String[] args) {

        Toilet toilet = new Toilet("城南厕所");
        new Thread(toilet).start();

        Producer producer = new Producer(toilet);
        new Thread(producer).start();

    }

    @Override
    public void run() {
        while (!interrupted()){

            if (person != null){
               
                System.out.println(person.name + "is peeing");
                System.out.println("please, wait");

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

                System.out.println(person.name + "leaves the toilet");

                person = null;
                
            }
            
        }
    }
}

Person 上厕所

package Test;



public class Person implements Runnable {
    String name;
    Toilet toilet;

    public Person(String name, Toilet toilet){
        this.name = name;
        this.toilet = toilet;
        System.out.println(name + "is creating");
    }


    public void pee(){


        this.toilet.setPerson(this);


    }

    @Override
    public void run() {
        pee();
    }

}

0is creating
0is peeing
please, wait
1is creating
2is creating
3is creating
4is creating
5is creating
6is creating
7is creating
8is creating
9is creating
9leaves the toilet
10is creating
10is peeing
please, wait
11is creating
12is creating
13is creating
14is creating
15is creating
16is creating
17is creating
18is creating
19is creating
19leaves the toilet
20is creating
20is peeing
please, wait
21is creating
22is creating

  • 根据输出的日志记录,很容易发现,厕所同时进了很多人,一个还没有完成,另外一个人就进来了;例如 0 号并没有走出 toilet ,反而是 9 号 先离开 toilet
  • 我们在很多例子中都知道这是因为线程安全问题导致的,我们要用 synchronized 关键字来实现同步,但你真的知道加在哪里么?

不易察觉的错误

  • 按照上面我们说的, toilet 之所以被很多人同时访问,肯定是因为 toiletperson 属性同时被多个人修改,那我把那段代码通过 synchronized 锁起来肯定就没事了。所以 toilet 代码如下修改:
    @Override
    public void run() {
        while (!interrupted()){
			// 在这里锁起来,保证这段代码是安全的
            synchronized (this){
                if (person != null){

                    System.out.println(person.name + "is peeing");
                    System.out.println("please, wait");

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

                    System.out.println(person.name + "leaves the toilet");

                    person = null;

                }
            }
  • 我们只修改了 run 中的内容,把修改 toilet 变量的代码上锁,结果如何呢?

0is creating
0is peeing
please, wait
1is creating
2is creating
3is creating
4is creating
5is creating
6is creating
7is creating
8is creating
9is creating
9leaves the toilet
10is creating
10is peeing
please, wait
11is creating
12is creating
13is creating
14is creating
15is creating
16is creating
17is creating
18is creating
19is creating
19leaves the toilet
20is creating
20is peeing
please, wait
21is creating
22is creating
23is creating
24is creating
25is creating
26is creating
27is creating
28is creating
29is creating
29leaves the toilet
30is creating
30is peeing
please, wait
31is creating
32is creating
33is creating
34is creating
35is creating

Process finished with exit code -1

  • 你会发现,其实问题并没有被解决。
  • 乍一看好像是 sleep 的问题。是不是因为 sleep 的时候,线程把锁释放了,然后让其他的线程访问了呢?
  • 当然不是,sleep 的时候,线程依然持有锁,这个部分大家可以去看 sleepwait 的区别,由于这块不是本文重点,所以不展开讲。
  • 那究竟是什么原因导致这块代码没有被锁住呢?

答案揭晓

在这里插入图片描述

  • 看似在下面的代码中你锁住了 person 不能被多个线程随意修改,但其实根本没有被锁住,因为上面的 setPerson 方法是线程不安全的,换句话说,没有用 synchronized 关键字限制。这样就导致了在多个 Person 的线程中,他们都可以用 setPerson 方法同时地修改 toiletperson 属性,导致 toiletperson 还是在不断地被修改。
    在这里插入图片描述
  • 所以我们要保证线程安全就要使用下面的代码,把真正关乎 toiletperson 属性修改的方法给锁住,就能够保证绝对安全了。

正确操作

package Test;

import static java.lang.Thread.interrupted;
import static java.lang.Thread.sleep;

public class Toilet implements Runnable{

    public Person getPerson() {
        return person;
    }
    public synchronized void setPerson(Person person) {
        this.person = person;
    }

    private Person person;
    String name;

    public Toilet(String toiletName){
        this.name = toiletName;
    }

    public static void main(String[] args) {

        Toilet toilet = new Toilet("城南厕所");
        new Thread(toilet).start();

        Producer producer = new Producer(toilet);
        new Thread(producer).start();

    }

    @Override
    public void run() {
        while (!interrupted()){
            synchronized (this){
                if (person != null){

                    System.out.println(person.name + "is peeing");
                    System.out.println("please, wait");

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

                    System.out.println(person.name + "leaves the toilet");

                    person = null;

                }
            }
        }
    }
}

锁静态方法

  • synchronized 关键字放在静态方法上,就是对整个类对象进行限制。
  • 通过厕所的方法进行说明的话就是:
    • 在上面的例子中,如果很多人排队在 “城南厕所” 门口,当 synchronized 加在非静态的代码上时代表一个人上城南厕所,其他人不能上城南厕所,但是如果这时候有 “城北厕所”,他们可以去 “城北厕所”
    • 但是如果 synchronized 加在了静态方法上。那么当一个人进了 “城南厕所”,“城北厕所” 也会上锁,从而导致一个厕所锁住,所有厕所都不能上。换句话说,把这个类的所有实例都锁住了。

锁代码块

  • synchronized 加在代码块上,锁的还是实例对象,但是这样做的效率比加载实例方法上效率高
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

暖仔会飞

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

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

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

打赏作者

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

抵扣说明:

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

余额充值