• 线程安全问题


    线程安全

    当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象时线程安全的。

    线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

    电影院卖票问题:

    100张票四个窗口同时卖

    public class Main {
        public static void main(String[] args) {
            SaleTickets st = new SaleTickets();
            Thread t1 = new Thread(st);
            Thread t2 = new Thread(st);
            Thread t3 = new Thread(st);
            Thread t4 = new Thread(st);
            //三个窗口同事卖票
            t1.start();
            t2.start();
            t3.start();
            t4.start();
    
        }
    }
    
    class SaleTickets implements Runnable {
        private int tickets = 100; //剩余十张票
    
        @Override
        public void run() {
            while (true) {
                if (tickets>0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "还剩" + tickets--);
                }
            }
        }
    }
    
    /**
     * Thread-3还剩8
     * Thread-0还剩9
     * Thread-2还剩10
     * Thread-1还剩7
     * Thread-3还剩6
     * Thread-2还剩4
     * Thread-0还剩3
     * Thread-1还剩5
     * Thread-0还剩0
     * Thread-2还剩2
     * Thread-1还剩-1
     * Thread-3还剩1
     */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    发现程序出现了个问题 各个窗口的剩余票数不同步 这种问题称为线程不安全

    当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题:Java中提供了同步机制(synchronized)来解决。

    通过加锁和解锁的操作,就能保证多条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。

    保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

    synchronized(lock) {
        n = n + 1;
    }
    
    • 1
    • 2
    • 3

    ynchronized保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized改写如下:

    class SaleTickets implements Runnable {
        private int tickets = 100; //剩余十张票
        private Object lock = new Object();
    
        @Override
        public void run() {
            while (true) {
            synchronized (lock) {
                if (tickets>0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "还剩" + tickets--);
                }
            }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    注意

    synchronized(Counter.lock) { // 获取锁
        ...
    } // 释放锁
    
    • 1
    • 2
    • 3

    它表示用Counter.lock实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized语句块结束会自动释放锁。这样一来,对Counter.count变量进行读写就不可能同时进行。

    使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。

    使用synchronized

    1. 找出修改共享变量的线程代码块;
    2. 选择一个共享实例作为锁;
    3. 使用synchronized(lockObject) { ... }

    在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁

    我们经常会用到StringBuffer和StringBuilder,StringBuilder不是线程安全的StringBuffer是线程安全的

    验证StringBuffer是线程安全的
        /**
         * 验证StringBuffer线程安全,如下,如果length==1000,则可证明
         * @throws InterruptedException
         */
        public static void testStringBuffer() throws InterruptedException {
            StringBuffer sb = new StringBuffer();
            for (int i=0; i<10; i++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j=0; j<1000; j++){
                            sb.append("a");
    
                        }
                    }
                }).start();
            }
    
            Thread.sleep(100);
            System.out.println(sb.length());
        }
    
    /**
         * 主测试方法
         * @param args
         */
      public static void main(String[] args) {
            try {
                ThreadTest.testStringBuffer();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    通过上面代码,我们开10个线程,每个线程循环1000次往StringBuffer对象里面append字符。理论上应该输出实例sb的字符串长度=10000,我们执行代码,实际输出也是10000,证明了StringBuffer是线程安全的。

    验证StringBuilder是线程不安全的
        /**
         * 验证StringBuild线程不安全,如下,如果length!=1000,则可证明
         * @throws InterruptedException
         */
        public static void  testStringBuild() throws InterruptedException {
            StringBuilder sb = new StringBuilder();
            for (int i=0; i<10; i++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j=0; j<1000; j++){
                            sb.append("a");
                        }
                    }
                }).start();
            }
    
            Thread.sleep(100);
            System.out.println(sb.length());
    
        }
    
    /**
         * 主测试方法
         * @param args
         */
      public static void main(String[] args) {
            try {
                ThreadTest.testStringBuild();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    通过上面代码,我们开10个线程,每个线程循环1000次往StringBuilder对象里面append字符。理论上应该输出实例sb的字符串长度=10000,我们执行代码,实际输出<10000,证明了StringBuilder是线程不安全的。

    StringBuilder和StringBuffer主要不同在于,StringBuffer的append、delete、replace、length等方法前都加了synchronized关键字保证线程安全,而StringBuilder没有,另外的区别在于StringBuffer有一个toStringCache的char数组,是用于记录最近一次toString()方法的缓存,任何时候只要StringBuffer被修改了这个变量会被赋值为null

    多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;

    同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;

    注意加锁对象必须是同一个实例;

    对JVM定义的单个原子操作不需要同步。

  • 相关阅读:
    总结HTTP协议和JSON相关的知识点
    hexo+github手把手教你部署个人博客
    leetcode93. 复原 IP 地址
    MySQL基础——DDL、DML、DQL、DCL语句
    了解什么是JDBC
    基于大语言模型扬长避短架构服务
    iPhone没有收到iOS16最新版的推送,如何升级系统?
    搭建docker镜像仓库
    Sketch在mac运行时崩溃,什么是安全模式以及如何启用它?
    【图像处理】使用各向异性滤波器和分割图像处理从MRI图像检测脑肿瘤(Matlab代码实现)
  • 原文地址:https://blog.csdn.net/m0_59138290/article/details/128007884