自学内容网 自学内容网

java多线程——线程安全-举例

何为线程安全

线程安全(Thread Safety)是计算机科学中的一个术语,它指的是在多线程环境下,某个操作或代码块能够正确地执行,不会产生不正确的结果或数据不一致的情况。具体来说,线程安全保证当多个线程同时访问同一个共享资源(例如变量、数据结构或对象)时,这些访问不会导致数据损坏、不一致性或其他不希望发生的行为。

要实现线程安全,通常需要采取一些同步机制来确保一次只有一个线程能够访问共享资源,或者确保在多个线程同时访问时不会发生数据冲突。这些同步机制可能包括使用锁(如Java中的synchronized关键字)、信号量、互斥量、读写锁、条件变量等。

线程安全性的一个重要方面是确保对共享资源的访问是原子的,即不可分割的,要么全部完成,要么全部不完成。这可以防止在访问过程中被其他线程打断,从而避免数据不一致性。

此外,线程安全还需要考虑内存可见性和指令重排序等问题。内存可见性是指一个线程对共享变量的修改能够及时地被其他线程所感知。指令重排序是编译器或处理器为了优化性能而可能改变指令的执行顺序,这有时可能导致线程安全问题。

在编写多线程程序时,确保线程安全是非常重要的。如果忽略了线程安全性,可能会导致难以调试和重现的错误,这些错误可能只在特定的线程调度或系统负载下才会出现。

因此,开发者需要仔细考虑并设计线程安全的代码,使用适当的同步机制,并对共享资源进行适当的访问控制。在Java中,除了synchronized关键字外,还可以使用java.util.concurrent包中提供的各种线程安全的数据结构和工具类来构建线程安全的程序。

举例


import java.util.Random;

public class OrginTest {
    static int totalTicket=10;

    static void saleTicket(String name) {
        int saleCount = 0;
        while (totalTicket > 0) {
            saleCount++;
            totalTicket--;
            System.out.println(name + "销售第" + saleCount + "张,剩余:" + totalTicket + "张");

            //模拟 没有人买票的 空闲时间
            try {
                Thread.sleep(new Random().nextInt(100,1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(() -> {
            saleTicket("售票口1");
        }).start();
        new Thread(() -> {
            saleTicket("售票口2");
        }).start();
        new Thread(() -> {
            saleTicket("售票口3");
        }).start();
        while (totalTicket > 0) {}
        System.out.println("dd");
    }
}

 实际输出

售票口2销售第1张,剩余:8张
售票口1销售第1张,剩余:9张
售票口3销售第1张,剩余:7张
售票口3销售第2张,剩余:6张
售票口2销售第2张,剩余:5张
售票口1销售第2张,剩余:4张
售票口2销售第3张,剩余:3张
售票口3销售第3张,剩余:2张
售票口1销售第3张,剩余:1张
售票口2销售第4张,剩余:0张

明显看到剩余的8,9、7的顺序没对。为什么?

我们可以这样理解:在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存),之所以会输出8、9、7,是因为进程内的所有线程都可以访问到该区域,当第一个线程已经获得9这个数了,还没来得及输出,下一个线程在这段时间的空隙获得了8这个值,故输出时会输出8、9的顺序值。

并发的三大基本特性原子性可见性以及有序性 请参考可见性、有序性、原子性,该如何理解?-CSDN博客

如何解决 

①synchronized(自动锁,锁的创建和释放都是自动的);

②lock 手动锁(手动指定锁的创建和释放)。

为什么能解决?如果可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,然后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。

synchronized


import java.util.Random;

public class synchronizedTest {

    static int totalTicket = 10;
    final static Object object = new Object();

    static void saleTicket(String name) {
        int saleCount = 0;
        if (totalTicket > 0) {
            while (totalTicket > 0) {
                synchronized (object) {
                    saleCount++;
                    totalTicket--;
                    System.out.println(name + "销售第" + saleCount + "张,剩余:" + totalTicket + "张");
                }
                //模拟 没有人买票的 空闲时间
                try {
                    Thread.sleep(new Random().nextInt(100, 1000));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }


    public static void main(String[] args) {
        new Thread(() -> {
            saleTicket("售票口1");
        }).start();
        new Thread(() -> {
            saleTicket("售票口2");
        }).start();
        new Thread(() -> {
            saleTicket("售票口3");
        }).start();
        while (totalTicket > 0) {
        }
        System.out.println("dd");
    }
}

输出

售票口1销售第1张,剩余:9张
售票口3销售第1张,剩余:8张
售票口2销售第1张,剩余:7张
售票口1销售第2张,剩余:6张
售票口2销售第2张,剩余:5张
售票口3销售第2张,剩余:4张
售票口1销售第3张,剩余:3张
售票口2销售第3张,剩余:2张
售票口3销售第3张,剩余:1张
售票口3销售第4张,剩余:0张

lock


public class LockTest {

    static int totalTicket = 10;
    static Lock lock = new ReentrantLock();

    static void saleTicket(String name) {
        int saleCount = 0;

        while (totalTicket > 0) {
            try {
                lock.lock();
                if (totalTicket > 0) {
                    saleCount++;
                    totalTicket--;
                    System.out.println(name + "销售第" + saleCount + "张,剩余:" + totalTicket + "张");
                }
            } finally {
                lock.unlock();
            }
            //模拟 没有人买票的 空闲时间
            try {
                Thread.sleep(new Random().nextInt(100, 1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(() -> {
            saleTicket("售票口1");
        }).start();
        new Thread(() -> {
            saleTicket("售票口2");
        }).start();
        new Thread(() -> {
            saleTicket("售票口3");
        }).start();
        while (totalTicket > 0) {
        }
        System.out.println("dd");
    }
}

 输出结果

售票口1销售第1张,剩余:9张
售票口3销售第1张,剩余:8张
售票口2销售第1张,剩余:7张
售票口1销售第2张,剩余:6张
售票口2销售第2张,剩余:5张
售票口3销售第2张,剩余:4张
售票口1销售第3张,剩余:3张
售票口2销售第3张,剩余:2张
售票口3销售第3张,剩余:1张
售票口3销售第4张,剩余:0张

我们在编程中如何避免

  1. 使用同步机制
    • 同步代码块:使用synchronized关键字来同步代码块,确保同一时间只有一个线程可以执行该代码块。
    • 同步方法:将方法声明为synchronized,这样每次只有一个线程可以调用该方法。
    • 显式锁:使用java.util.concurrent.locks包中的LockReentrantLock等显式锁来替代synchronized
  2. 使用原子类
    • Java的java.util.concurrent.atomic包提供了原子变量类,如AtomicIntegerAtomicLongAtomicReference等,这些类提供了在单个变量上进行原子操作的方法,而不需要使用锁。
  3. 避免共享可变状态
    • 设计无状态(或尽可能少状态)的对象,这样它们就不需要同步。
    • 使用线程局部变量(ThreadLocal)来存储每个线程独有的数据。
  4. 使用线程安全的集合
    • Java的java.util.concurrent包提供了多种线程安全的集合类,如ConcurrentHashMapCopyOnWriteArrayList等。
  5. 不可变对象
    • 使用不可变对象,因为一旦创建,它们的状态就不能改变,从而天然地是线程安全的。
  6. 使用并发工具
    • 利用java.util.concurrent包中的高级并发工具,如ExecutorServiceFutureCallableSemaphoreCountDownLatchCyclicBarrier等,来管理线程和同步。
  7. 避免死锁
    • 仔细设计锁的使用,避免嵌套锁和循环等待条件,这些可能导致死锁。
  8. 检查并修复竞争条件
    • 通过代码审查和测试来识别并修复竞争条件,这些条件可能发生在多个线程以不确定的顺序访问共享资源时。
  9. 使用高级并发模式
    • 了解并应用高级并发模式,如生产者-消费者模式、读者-写者模式、观察者模式等,这些模式有助于构建更健壮的并发系统。
  10. 避免过度同步
    • 同步会引入性能开销,因此应该避免不必要的同步。只同步那些确实需要保护的共享资源。
  11. 使用静态分析工具
    • 使用静态代码分析工具来检测潜在的线程安全问题。

原文地址:https://blog.csdn.net/oopxiajun2011/article/details/144031625

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!