三、了解 Java 多线程 (入门篇)

今天来聊聊 Java 中的锁:

举个例子:

两个线程,依次 start 后,执行同一个对象的同一段方法,会出现什么样的后果?

我们写一段代码看看:

public class Demo {

    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo(10, "小明");
        Thread thread = new Thread() {
            @Override
            public void run() {
                userInfo.printUserName(currentThread().getName());
            }
        };
        Thread a = new Thread(thread, "A");
        Thread b = new Thread(thread, "B");
        a.start();
        b.start();
    }
}

class UserInfo {


    private Integer userId;

    private String userName;

    public UserInfo(Integer userId, String userName) {
        this.userId = userId;
        this.userName = userName;
    }

    public Integer getUserId() {
        return userId;
    }

    public String getUserName() {
        return userName;
    }
    @Override
    public String toString() {
        return "UserInfo{" +
                "userId=" + userId +
                ", userName='" + userName + '\'' +
                '}';
    }
    public void printUserName(String name){
        if (name != null && "A".equals(name)){
            try {
                userId = 111111;
                System.out.println("My name is A");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            userId = 222222;
            System.out.println("My name is B");
        }
        System.out.println(name + ":" + getUserId());
    }
}

运行上面的代码,控制台的会打印出如下内容:

结果中,A线程的 userId 被设置成了 222222 这个值,但是我们在代码中,当 name 为 "A" 值时,我们设置的 userId 是为 111111 这个值的。

为什么会出现这种情况呢? 我们看代码,代码中,printUserName(String name) 方法中,当 name 为 “A”时,A线程修改完 userId 的值后,会调用 sleep() 方法,当我们运行 main 函数时,A线程修改完 userId 的值后,进入睡眠,这时,B线程进来修改 userId 的值,接着同时打印 name 和 userId 的值,A线程苏醒,打印 name 和 userId 的值;因为,A,B两个线程公用的是同一个 UserInfo 对象,在A线程睡眠期间,B线程修改 userId的值,实际上是在A线程修改以后的基础上进行修改的,所以,A线程苏醒后,同时打印 name 和 userId 时,其中 userId 的值其实是最后一次被修改的值(最后一次被修改是B线程做的修改)。

像这种情况:A线程读取到了B线程中修改的值,我们一般管他叫做 脏读。

要解决这种情况,需要给方法加一个 java 中的 synchronized关键字,就可以了。

下图为增加关键字后,运行结果:

 

synchronized 关键字的作用呢,就是被其修饰的方法,或者代码段,一次只能有一个线程执行,只有当一个线程执行完被 synchronized 修饰的方法或者代码段时,才允许另一个线程进入。

synchronized 的机制就是,多个线程在准备进入被其修饰的代码段或者方法,会进行去竞争获取锁;一个对象只有一把锁,争抢到锁的线程进入被其修饰的代码段或者方法中运行,其他线程等待;当争抢到锁的线程运行完被修饰的代码块或者方法时,会释放掉锁,这时,等待的线程再次竞争锁。

synchronized 的注意点:

1.当其修饰静态方法时,其加锁对象为 当前类的 class 对象。

2.当其修饰普通成员方法时,加锁对象为 当前类的实例。

3.当其修饰代码块时,加锁对象为 参数对象实例,如下:

synchronized (name){
                    userId = 111111;
                }

其加锁对象为 name 实例。

 

另外,再补充一点,关于 原子性与可见性的问题,synchronized 修饰的方法可代码块,是可以保证方法或者代码块执行的原子性,同时又可以保证执行过程中,方法或者代码块以外声明的变量被修改后的可见性。

保证原子性:一次只能有一个线程进入被其修改的方法或者代码块,在这期间,其他线程无法进入该方法或代码块执行,执行线程不受其他线程的干扰,成功则成功,失败则抛出异常,退出同步方法或代码块,并释放锁。

保证可见性:synchronized 修饰的方法或者代码块保证可见性,是通过保证原子性从而实现可见性的。

为什么我这么说呢,因为,synchronized 保证原子性,确保了一次只有一个线程去修改成员变量,也就是说同一时刻,没有其他的线程在执行该段代码,不会对数据造成影响,持有锁的当前线程运行完后,退出同步代码块或方法,释放锁;等待获取锁的任意一个线程得到锁,进入同步代码块进行数据操作,这是,它再去获取数据时,已经是上一个线程修改完后的值,故此,synchronized保证了可见性。

能保证原子性,则必然能保证可见性。

 

在 Java 中,volatile 关键字修饰的变量,也可以保证可见性,但是,请永远牢记:volatile 能保证可见性,不能保证原子性。

先看下图:

再配合一段代码:

public class Demo {

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadTest.setaBoolean(false);
    }
}

class ThreadTest extends Thread {
    private boolean aBoolean = true;

    public void setaBoolean(boolean aBoolean) {
        this.aBoolean = aBoolean;
    }

    @Override
    public void run() {
        System.out.println("进入 Run 方法");
        while (aBoolean){

        }
        System.out.println("退出 Run 方法");
    }
}

运行上面的代码,永远不会打印 "退出 Run 方法";

因为,main()函数中创建 ThreadTest 实例,其实例是存储在上图的 主内存 位置,当在 main()函数中调用 start() 方法,线程运行 run() 函数时,会将 主内存里的 ThreadTest 实例中所有 run()函数中用到的变量和对象加载到工作内存中,然后进行数据的操作,当修改某一变量时,先修改工作内存中的值,然后在从工作内存中写入到主内存,完成修改,请注意,这一修改为原子性操作。

而上面代码中,run()函数只有将数据写入工作内存的动作,main()函数的赋值操作,只是改变了主内存中的值,并未修改线程工作内存中的值,故此,run()函数将会陷入死循环。

接下来,我们回到 volatile 关键字上来,是通过使线程在运行 run() 函数时,不将数据加载到工作内存,当 run() 函数中需要使用时,直接去主内存中读取,修改时,直接写入主内存;以此来保证可见性的。

 

 

好了,今天就先唠这些,关于Java中的锁,这个只是个开头,后续可能还会再写几篇,Emmm  作为懒癌晚期病人,但愿我能早日研究明白

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值