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

作者: 我犟不过你 | 来源:发表于2021-11-25 14:05 被阅读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,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这个对象本身,如下所示:

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

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

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

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

    四、变量的安全分析

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

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

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

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

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

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

    五、常见的线程安全类

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

    • 使用锁(synchronized,Lock,CAS)

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

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

    • 不可变类(final)

      String
      Integer

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


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

    相关文章

      网友评论

        本文标题:java并发编程(四)线程共享模型

        本文链接:https://www.haomeiwen.com/subject/mfjatrtx.html