java并发编程(四)线程共享模型 一、什么是线程共享模型? 二、线程共享模型存在什么问题? 三、解决方案 四、变量的安全分析 五、常见的线程安全类

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

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

1)计算机共享模型

2)java线程共享模型

如上所示,无论是哪种模型,都有线程或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

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

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

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

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

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

三、解决方案

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

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

下文重点讲解使用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这个对象本身,如下所示:

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 锁住的是哪个对象,我们主要要记住以下两点:

  • 普通方法锁住的是this(当前对象),而静态方法锁住的是类(class)
  • 同一时刻,只有一个线程能够持有锁

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

  • 同一个对象,内部无论几个非静态方法有锁,都是互斥的
  • 同一个类的不同对象,锁不互斥
  • 对象锁,即this,与类锁(class),是不互斥的
  • 同一个类的内部两个静态方法的锁,是互斥的

四、变量的安全分析

  • 成员变量与静态变量是线程安全的吗?

    如果它们没有共享,则线程安全。

    如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:

    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
  • 局部变量是线程安全的吗?
    局部变量是线程安全的。

    但局部变量引用的对象则未必

    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全。(比如由于内部类重写方法,该方法使用了修改了局部变量,且该方法被共享了,则会导致该变量的不安全,可以对这种方法时使用final,或设置为pravite)。

五、常见的线程安全类

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

  • 使用锁(synchronized,Lock,CAS)

    StringBuffer
    Random
    Vector
    Hashtable
    java.util.concurrent 包下的类

    需要注意的是,上面举例的类,他们的方法都是原子性的,但是组合使用后并不能保证原子性,需要我们自己进行控制。

  • 不可变类(final)

    String
    Integer

    String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的


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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章