线程安全问题的解决---加锁解锁;Volative关键字;对象等待集

本文探讨了线程不安全的定义、常见原因,如非原子操作、多线程共享变量和指令重排序,以及如何通过锁机制(synchronized)、Volatile和对象等待集(wait/notify)来确保代码的线程安全性。实例演示了加锁、内存可见性和并发控制在实际编程中的应用。
摘要由CSDN通过智能技术生成
  • 线程不安全:多线程并发执行某个代码时,产生了逻辑上的错误,称为线程不安全。
  • 线程不安全的原因:
  1. 线程是抢占式执行的(万恶之源)。
  2. 非原子性的操作:例如自增,每次自增都能拆分为三个部分:(1)把内存中的数据读取到cpu。(2)在cpu中把数据+1。(3)把计算结束的数据写回内存中。当cpu执行到上面三个步骤中任意一个时,都可能会被调度器调度走,让给其他线程来执行。
  3. 多个线程尝试修改同一个变量。
  4. 内存可见性导致的线程安全问题。
  5. 指令重排序:java的编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,保证原有逻辑不变的情况下,提高程序的运行效率。优化后的代码速度会提升很多。
  • 如何解决? 
  1.  抢占式执行:无法解决。
  2. 非原子性:可以解决,给自增操作加上锁(使用范围最广)。
  3. 多个线程尝试修改同一个变量:具体问题具体分析。
  4. 内存可见性:使用Volatile关键字。
  5. 指令重排序:对象等待集。
  • 加锁解锁,都是Synchronized关键字,避免忘记解锁。
  1. 锁的特点是:互斥,同一时刻只有一个线程能获取到锁,其他的线程如果也尝试获取锁,救会发生阻塞等待,一直等到刚才的线程释放锁,此时剩下的线程再重新竞争锁。
  2. 基本操作:加锁  lock,  解锁  unlock 。尝试加锁时不一定立刻成功,如果发现当前的锁已经被占用,改代码就会阻塞等待,一直等到之前的线程释放锁,才可能会获取到这个锁。
  3. 一旦使用锁,这个程序基本上就和高性能无缘了。因为等待时间不可控。
  4. synchronized的具体使用:可以灵活的指定某个对象来加锁,而不仅是把锁加到某个方法。(1)加到普通方法前:表示锁this(2)加到静态方法前:表示锁当前类的类对象(3)加到某个代码块之前:显式指定给某个对象加锁。 

//线程不安全示例
public class ThreadDemo2 {
    static class Counter {
        public int count = 0;
//进入increase方法之前会先尝试加锁,increase方法执行完之后会自动解锁
        synchronized public void increase() {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0;i < 30000;i++) {
                    counter.increase();
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0;i < 30000;i++) {
                   counter.increase();
                }
            }
        };
        t2.start();

        t1.join();
        t2.join();
//两个线程,各自自增30000次,最终预期结果应该是60000次
        System.out.println(counter.count);
    }
}

 上面的代码中,如果increase方法前不加synchronized,就是线程不安全的示例,预期结果可能会小于60000,加了synchronized,结果一定是60000.

  • Volative关键字:保持内存可见性。禁止编译器对读操作的优化(一个线程读,一个线程写,读线程操作太频繁,导致读操作被优化掉了,优化成直接从cpu的寄存器取上次结果,就会导致修改对于读线程来说可能没生效),牺牲了性能,换来了结果的正确性。 

//内存可见性示例
    //预期结果:t1先进入循环,t2会读取一个整数,若非零,此时t1循环会终止
public class ThreadDemo3 {
    static class Counter {
        public volatile int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread() {
            @Override
            public void run() {
                while(counter.flag == 0) {

                }
                System.out.println("循环结束");
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入一个整数: ");
                counter.flag = scanner.nextInt();
            }
        };
        t2.start();
    }
    //实际效果:t2输入非零整数时t1循环并未结束(此现象涉及到了编译器优化的bug,不能联合多个线程来进行优化)
}

 上面的代码中public volatile int flag = 0;如果不加volatile则线程不安全。

  • 对象等待集:本质是让程序员有一定的手段来干预线程的调度。 
  1. wait方法:当操作条件不成熟就等待。工作原理是:(1)释放锁;(2)等待通知(WAITING状态);(3)当收到通知后,尝试重新获取锁,继续往下执行。其中(1)(2)是原子性的,避免由于抢占式执行引起的竞态条件问题。
  2. notify方法:当条件成熟时,通知指定的线程来工作。 
  3. notifyAll方法:唤醒所有等待的线程,不建议使用。
  4. wait方法和notify方法都必须在synchronized代码块内部使用。

//对象等待集
public class ThreadDemo4 {
    public static void main(String[] args) {
        Object locker = new Object();

        Thread t1 = new Thread(){
            @Override
            public void run() {
                synchronized (locker) {
                    while (true) {
                        try {
                            System.out.println("wait 开始");
                            locker.wait();
                            System.out.println("wait 结束");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                Scanner scanner = new Scanner(System.in);
                System.out.println("请输入任意一个整数,继续执行notify");
                int num = scanner.nextInt();
                synchronized (locker) {
                    System.out.println("notify开始");
                    locker.notify();
                    System.out.println("notify结束");
                }
            }
        };
         t2.start();
    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值