0
点赞
收藏
分享

微信扫一扫

Java 中的线程安全性问题——synchronized

青乌 2022-03-30 阅读 43
java

线程安全性

一个对象是否需要是线程安全的,取决于它是否被多个线程访问。这指的是在程序中访问对象的方式,而不是对象要实现的功能。要使得对象是线程安全的,需要采用同步机制来协同对对象的可变状态的访问。如果无法实现协同,那么可能会导致数据破坏以及其他不该出现的结果。 通过以下示例来解释:

public class ThreadDemo_线程不安全示例 {
    private static int number = 0;
    static class Counter {
        // 循环次数
        private static int MAX_COUNT = 1000000;
        // ++ 方法
        public static void incr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number++;
            }
        }
        // -- 方法
        public static void decr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number--;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            Counter.incr();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            Counter.decr();
        });
        t2.start();

        // 等待线程执行完成
        t1.join();
        t2.join();
        System.out.println("最终的结果:" + number);
    }
}

这个代码很容易看出来,过程是给定了一个循环次数,在 线程1 中对其进行 ++ 操作,在 线程2 中进行相同次数的 - - 操作。想要得到的最终结果当然是 0 了,但是以上代码的运行结果:
在这里插入图片描述
是的,每次的结果都是随机的也是不正确的,这就是因为多个线程无法实现协同,导致了数据破坏出现了不该出现的结果~

那么就引出:

当多个线程访问某个状态的变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些变量的访问。 Java 中的主要同步机制是关键字 synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括 volatile 类型的变量。

线程不安全因素

以上笼统的介绍了线程安全性的问题,那么在解决问题之前有必要先来明确一下 Java 中导致线程不安全的因素~~~

  1. 抢占式执行
    java中线程调度采用抢占式调度方法,抢占式调度模式,许多线程可能是可运行状态但只能有一个线程在运行该线程将持续运行直到它自行终止或者是由于其他的事件导致阻塞亦或者是出现高优先级线程成为可运行的则该线程失去CPU的占用权。而非其他编程语言采用有轮回式的方式。
  2. 多个线程修改同一个变量
    在这里插入图片描述
    在多线程共享的资源中,我们所定义一个变量 count :存放在堆(Heap)上的变量, 此时这个 count 是一个多个线程都能访问到的 “共享数据” 。那么就像篇头第一个示例一样,如果我们在多个线程中修改重新写入同一个类变量,那么就会出现线程不安全的问题,不能达到代码正确的结果。
  3. 非原子性操作
    对于比如 count++ 操作,它并不是一个原子性操作,它的操作是分为三步的:1. 查询 count 当前的值【load】 2.进行 count+1 操作【++】 3. 刷新 count 的最新值【save】。
    当在两个线程中同时执行时,
    在这里插入图片描述
    想要得到的结果是0但是最终的结果却不是我们想要的也不是我们可以控制的。
  4. 内存可见性问题
    可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

Java内存模型(JMM):
在这里插入图片描述

如下场景:
① 初始情况下, 两个线程的工作内存内容一致。
② 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的 工作内存的 a 的值也不一定能及时同步。

这就是内存的不可见性~

  1. 指令重排序
    指令重排序是编译器优化代码的一种操作,咱们在写的有些代码中,其实彼此的顺序(不会影响程序逻辑),谁在前谁在后无所谓~编译器会有只能的调整代码的前后顺序从而提高程序效率。 那么如果是一个多线程的程序,编译器在优化代码时出现了误判,优化了不应该更改的顺序。就也会导致线程安全问题。

volatile 解决内存可见性和指令重排序

volatile 可以解决内存可⻅性和指令重排序的问题,代码在写⼊ volatile 修饰的变量的时候:
● 改变线程⼯作内存中volatile变量副本的值
● 将改变后的副本的值从⼯作内存刷新到主内存 代码在读取 volatile 修饰的变量的时候:
● 从主内存中读取volatile变量的最新值到线程的⼯作内存中
● 从⼯作内存中读取volatile变量的副本

简单来说,他就是禁止了编译器指令重排序这一优化操作,也通过变量副本的方式保证了内存可见性~

public class TheradDemoVolatile {
    private static volatile boolean flag = false;
    public static void main(String[] args){
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程1 开始执行");
                while (!flag) {

                }
                System.out.println("终⽌执⾏");
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("在线程2 中设置 flag=true");
                flag = true;
            }
        });
        t2.start();
    }
}


如上代码,如果对 flag 变量不加 volatile 修饰,则永远执行不到System.out.println("终⽌执⾏");这行代码。因为在线程2 中的修改对线程1 不可见~ 当用 volatile 修饰之后,能够解决这个代码中的线程安全问题。
在这里插入图片描述
不足: volatile 虽然可以解决内存可⻅性和指令重排序的问题,但是解决不了原⼦性问题,因此对于 ++ 和 - - 操作的线程不安全问题依然解决不了

内置锁 synchronized

	//定义一个任意的对象
    Object myLock = new Object();

    synchronized(myLock) {
		//访问或修改由锁保护的共享状态
    }

在这里插入图片描述

synchronized 基本使用

  • ① 修饰静态⽅法
/**
 * synchronized 修饰静态方法
 */
public class ThreadSynchronized {
    private static int number = 0;

    static class Counter {

        // 循环次数
        private static int MAX_COUNT = 1000000;

        // ++ 方法
        public synchronized static void incr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number++;
            }
        }

        // -- 方法
        public synchronized static void decr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            Counter.incr();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            Counter.decr();
        });
        t2.start();

        // 等待线程执行完成
        t1.join();
        t2.join();
        System.out.println("最终的结果:" + number);
    }
}
  • ② 修饰普通⽅法
/**
 * synchronized 修饰普通方法
 */
public class ThreadSynchronized2 {
    private static int number = 0;

    static class Counter {

        // 循环次数
        private static int MAX_COUNT = 1000000;

        // ++ 方法
        public synchronized void incr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number++;
            }
        }

        // -- 方法
        public synchronized void decr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                number--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            counter.incr();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            counter.decr();
        });
        t2.start();

        // 等待线程执行完成
        t1.join();
        t2.join();
        System.out.println("最终的结果:" + number);
    }
}
  • ③ 修饰代码块
/**
 * synchronized 修饰代码
 */
public class ThreadSynchronized3 {
    private static int number = 0;

    static class Counter {

        // 循环次数
        private static int MAX_COUNT = 1000000;

        // 自定义锁对象(属性名可以自定义)
        private Object mylock = new Object();

        // ++ 方法
        public void incr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                synchronized (mylock) {
                    number++;
                }
            }
        }

        public static void test() {
            synchronized (Counter.class) {

            }
        }

        // -- 方法
        public void decr() {
            for (int i = 0; i < MAX_COUNT; i++) {
                synchronized (mylock) {
                    number--;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            counter.incr();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            counter.decr();
        });
        t2.start();

        // 等待线程执行完成
        t1.join();
        t2.join();
        System.out.println("最终的结果:" + number);
    }
}

synchronized 特性

  • 互斥(排他性)

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也 执行到同一个对象 synchronized 就会阻塞等待.

  • 刷新内存(内存不可见问题)

synchronized 的⼯作过程:

  1. 获得互斥锁
  2. 从主内存拷⻉变量的最新副本到⼯作的内存
  3. 执⾏代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁
  • 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。

/**
 * synchronized 可重入性测试
 */
public class ThreadSynchronized {
    public static void main(String[] args) {
        synchronized (ThreadSynchronized.class) {
            System.out.println("当前主线程已经得到了锁");
            synchronized (ThreadSynchronized.class) {
                System.out.println("当前主线程再次得到了锁");
            }
        }
    }
}

在这里插入图片描述

synchronized 是如何实现的?

还没理解到~ 随后尽快补上~

链接: link.

Lock

过两天写~~~~

举报

相关推荐

0 条评论