Java 核心技术Java 进阶JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统

java并发编程(四)线程共享模型

2021-11-25  本文已影响0人  我犟不过你

一、什么是线程共享模型?

在前面的章节中,我们介绍了计算机的共享模型,和java的线程共享模型:

1)计算机共享模型

image.png

2)java线程共享模型

image.png

如上所示,无论是哪种模型,都有线程或cpu自己的运行时缓存或内存,同时都有主内存。

二、线程共享模型存在什么问题?

首先看下面的代码,两个线程,每个线程分别对i进行++操作,加100000次,结果会得到200000吗:

/**
 * @description: 线程共享模型问题
 * @author:weirx
 * @date:2021/11/25 9:48
 * @version:3.0
 */
public class ThreadSharedModelProblems {

    static int i = 0;

    /**
     * 两个长度的门闩
     */
    static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
            // 减少门闩数
            countDownLatch.countDown();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
            // 减少门闩数
            countDownLatch.countDown();
        });
        t2.start();
        //阻塞等待门闩数降为0
        countDownLatch.await();

        System.out.println("i = " + i);
    }

结果:

i = 143188

产生的原因呢?主要是因为i++并不是一个原子性操作。i++操作的JVM字节码如下:

getstatic     #2                  // 获取静态变量i
iconst_1                          // 定义局部变量1
iadd                              // 执行自加1操作
putstatic     #2                  // 将自加1后的值赋给静态变量i
return

那么结合上面的例子和线程共享模型就会是如下模式:

线程共享模型.png

线程t1和t2同时去主内存获取获取i的值,并进行自加1的操作,然后再将值赋回给主线程,因为这两个线程之间是没有顺序的,且没有任何的关联,势必会造成线程t1,刚写入主内存的值,被t2覆盖,而t1再次取值,就不是上次的值了。

以上呢就是共享资源所导致的问题。

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

三、解决方案

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

下文重点讲解使用synchronized解决上面的问题。

3.1 synchronized对象锁

对象锁:它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。

可以理解这个对象为一个房间,这个房间一次只能有一个人进入,代码如下:

public class ThreadSharedModelProblems {

    static int i = 0;

    /**
     * 两个长度的门闩
     */
    static CountDownLatch countDownLatch = new CountDownLatch(2);

    /**
     * 定义一个不可变的对象,此处可以理解成一个房间
     */
    static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                // 争夺进入房间的机会
                synchronized (obj){
                    i++;
                }
            }
            // 减少门闩数
            countDownLatch.countDown();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                // 争夺进入房间的机会
                synchronized (obj){
                    i++;
                }
            }
            // 减少门闩数
            countDownLatch.countDown();
        });
        t2.start();
        //阻塞等待门闩数降为0
        countDownLatch.await();

        System.out.println("i = " + i);
    }

}

synchronized 实际是用对象锁保证了临界区内代码的原子性。临界区内的代码对外是不可分割的,不会被线程切换所打断

如何理解上面这句话的后半句?cpu在运行时,会发生线程上下文的切换,假设t1正持有对象,及在房间内进行++操作,如果此时cpu时间片用完了,这个t1就会释放占用的cpu资源,但是对象锁仍然被其持有,t2仍然不能获得对象锁。只有当cpu在给t1分配时间片,并完成此次循环操作后,t2才有机会去获得对象锁。

3.2 对象锁的优化

java是一门面向对象的语言,所以像上一章节的对象锁不是好的实现方式,我们应该将其放在对象当中。

写一个Room对象,将++操作和对象锁放在其中,代码如下所示:

Room:

public class Room {

    int i = 0;

    public int getI() {
        synchronized (this) {
            return i;
        }
    }

    public void add() {
        synchronized (this) {
            i++;
        }
    }
}

main方法:

/**
 * @description: 线程共享模型问题
 * @author:weirx
 * @date:2021/11/25 9:48
 * @version:3.0
 */
public class ThreadSharedModelProblems {

    /**
     * 两个长度的门闩
     */
    static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();

        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                room.add();
            }
            // 减少门闩数
            countDownLatch.countDown();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                room.add();
            }
            // 减少门闩数
            countDownLatch.countDown();
        });
        t2.start();
        //阻塞等待门闩数降为0
        countDownLatch.await();

        System.out.println("i = " + room.getI());
    }
}

synchronized (this)当中的this是什么呢?其实就是Room这个对象本身,如下所示:

image.png

3.3 方法上的synchronized

1)普通方法上的synchronized,等同于加在当前对象上,如下面代码,test1等同于test2

2)静态方法上的synchronized,等同于加在类上,如下面代码,test3等同于test4

public class MethodSynchronized {

    public synchronized void test1() {
        System.out.println("this is test1");
    }

    public void test2() {
        synchronized (this) {
            System.out.println("this is test2");
        }
    }

    public static synchronized void test3() {
        System.out.println("this is test3");
    }

    public void test4() {
        synchronized (MethodSynchronized.class) {
            System.out.println("this is test4");
        }
    }
}

3.4 何谓“线程八锁”?

其实就是考察 synchronized 锁住的是哪个对象,我们主要要记住以下两点:

所谓线程八锁,就是八种不同锁的情况,下面我就不举例了,但是要能够分析,基本在以下几种类型中:

四、变量的安全分析

五、常见的线程安全类

常见的线程安全类其实也分为两个方面:


关于线程共享模型以及synchronized的简单使用就介绍到这里了,有帮助的话点个赞吧。。

上一篇下一篇

猜你喜欢

热点阅读