Java并发编程-2.线程安全

基础知识点

为什么会发生线程安全问题?

  • 当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题,读操作不会发生数据冲突问题

多线程三大特性

  • 原子性
    一个操作或者多个操作,要么全部执行完成,要不就都不执行
  • 可见性
    当多个线程访问同一个变量时,一个线程修改饿了这个变量的值,其他线程能够立刻看的到修改的值
  • 有序性
    程序执行的顺序按照代码的先后顺序执行

经典案例

抢火车票线程安全

class Demo6 implements Runnable {
    private int count = 10;

    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
            }
            sale();
        }
    }

    public void sale() {
		System.out.println(Thread.currentThread().getName() + ",出售:" + (10 - count + 1)+"张票");
    	count --;
    }
}

    public static void main(String[] args) {
        Demo6 demo6 = new Demo6();
        new Thread(demo6,"窗口1").start();
        new Thread(demo6,"窗口2").start();
    }

执行结果

窗口1,出售:1张票
窗口2,出售:2张票
窗口2,出售:3张票
窗口1,出售:3张票
窗口1,出售:5张票
窗口2,出售:5张票
窗口2,出售:7张票
窗口1,出售:7张票
窗口2,出售:9张票
窗口1,出售:10张票

由上面的执行结果可看出,窗口1和窗口2卖出了重复的火车票,因此可见多个窗口售卖同一批火车票发生了线程安全问题。

解决线程安全问题

内置锁(Synchronized)

  • 内置锁也是互斥锁
  • 保证线程原子性,当线程进入方法的时候,自动获取锁,一旦锁被其他线程获取到后,其他的线程就会等待。当程序执行完毕后,就会把锁释放
  • 会降低程序运行效率,锁的资源的竞争,重入锁
  • 使用同步的时候,锁必须是同一把锁
  • 同步代码块
    synchronized(任意全局对象){
    	//代码块
    }
    
  • 静态同步方法:锁用的是当前class字节码文件
  • 非静态同步方法:锁用的是当前this对象

解决火车票线程安全

class Demo6 implements Runnable {
    private int count = 10;

    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
            }
            sale();
        }
    }

    public synchronized void sale() {
        if (count > 0) {
            System.out.println(Thread.currentThread().getName() + ",出售:" + (10 - count + 1)+"张票");
            count --;
        }
    }
}

    public static void main(String[] args) {
        Demo6 demo6 = new Demo6();
        new Thread(demo6,"窗口1").start();
        new Thread(demo6,"窗口2").start();
    }

java内存模型(JMM)

  • jmm决定一个线程对共享变量的写入时,能否对另一个线程可见
  • 主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区
  • 工作内存:java虚拟机中每个线程都有自己的工作内存(本地内存),该内存是线程私有的为了方便理解,可以认为是虚拟机栈
    在这里插入图片描述
    在这里插入图片描述
    从图上可以看出:线程A与线程B之间通信,需要经历下面步骤:
    1.首先线程A把本地内存A中更新过的共享变量刷新到主内存中
    2.然后,线程B到主内存中去读取线程A之间的共享变量
    在这里插入图片描述
    java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

Volatile

Volatile描述

  • 可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值
  • 在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存
  • Volatile 保证了线程间共享变量的及时可见性,但不能保证原子性

案例-停止线程执行

class Demo8 extends Thread {
    public boolean flag = true;

    @Override
    public void run() {
        System.out.println("子线程执行");
        while (flag) {

        }
        System.out.println("子线程停止");
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

public class DemoThread8 {
    public static void main(String[] args) {
        Demo8 demo8 = new Demo8();
        demo8.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        demo8.setFlag(false);
        System.out.println("子线程中flag已经设置成false");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println("子线程中的flag值:" + demo8.flag);

    }
}

执行结果

子线程执行
  • 由上面的执行结果可以看出:主线程将子线程设置成false后,程序还一直运行。这是因为线程之间是不可见,子线程读取的是副本,没有及时读取到主内存结果。
  • 结果方法:使用Volatile关键字将解决线程之间可见性, 强制线程每次读取该值的时候都去“主内存”中取值
    public volatile boolean flag = true;	
    
    执行结果:
    子线程执行
    子线程中flag已经设置成false
    子线程停止
    子线程中的flag值:false
    

Volatile特性

  • 保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成
  • 禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)

volatile 性能

  • volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行

Volatile与Synchronized区别

  • 从而我们可以看出volatile虽然具有可见性但是并不能保证原子性
  • 性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized

要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值