Java多线程安全问题(原子性)

本文探讨了在多线程环境下出现线程安全问题的原因,主要聚焦于原子性问题。通过代码示例展示了在并发执行时,共享变量的非原子操作可能导致结果不一致。解决方案是使用`synchronized`关键字实现互斥锁,确保同一时刻只有一个线程执行临界区代码,从而解决原子性问题。文章还简单介绍了`synchronized`的优化和不同锁状态的概念。
摘要由CSDN通过智能技术生成

可以结合之前的博客内容观看Java多线程初识

为什么会在多线程下存在线程安全问题

随着时间的流逝,我们计算机硬件也在不断的迭代更新,CPU、内存、I/O设备这三者的速度存在差异,主要表现:
CPU增加了高速缓存,为了较为平衡与内存交互的速度
操作系统中的线程被CPU分时复用也是为了提高交互速度
代码在被编译成执行指令顺序是为了CPU更合理利用
以上其实都是硬件层面的优化,而程序最后享受着这些成果,但是线程安全确实这些优化方面造成的
例如:并发状态下多个线程被CPU来切换调度运行时间片,此时正好操作了一个共享变量,然后操作共享变量的指令多个情况下就会被切换,中断等操作。这种就体现了原子性问题,反之在操作一个或者多个在CPU中执行的指令不被中断的特性就可以称作原子性

线程安全的源头之一

原子性问题

直接上代码体现

	static int i =0;//共享变量i
    public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread(()->{
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int j = 0; j < 1000; j++) {//执行1000次
                i++;
            }
        });
        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int j = 0; j < 1000; j++) {//执行1000次
                i++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();//t1,t2 相互等待对方线程执行完成,没有完成时main线程阻塞
        System.out.println(i);//执行结果 i < 2000
    }

执行结果

1850

以上代码感官上运行结果正常是:2000
但是为什么 i < 2000呐,这个是由于两个线程并发执行同一块代码,操作了同一个共享变量,在CPU层面“i++”,被拆分成了多个执行指令

GETFIELD i : I // 访问变量i
ICONST_1 // 将整形常量1放入操作数栈
IADD // 把操作数栈中的常量1出栈并相加,将相加的结果放入操作数栈
PUTFIELD i : I // 访问类字段(类变量),复制给i这个变量
非原子操作得到逻辑执行示意图

解决原子性问题

通过上面了解,原子性问题导致的原因是线程切换,那么我们在操作共享变量时禁止线程切换不就可以解决问题了吗?
那么就产生了“同一个时刻只有一个线程执行”,其他线程等待,这种现象我们称为“互斥”,那么互斥锁的概率就可以这理解。

Java语言提供关键字:synchronized

syn是在Java其中的一种锁的实现,可以用来修饰代码块、实例方法、静态方法

class demo{
	public synchronized void method1(){
		//独行区
	}
	public synchronized  static method2(){
		//独行区
	}
	public void method3(){
		synchronized(this){//this = 实例后的 demo对象
		//独行区
		}
	}
}
class example{
	public synchronized  static method2(){
		//独行区
	}
	public void method3(){
		synchronized(example.class){
		//独行区
		}
	}
	//当修饰静态方法或example.class的时候,锁定的当前类的class对象,
}

更新原子性问题的代码

	static int i =0;
    public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread(()->{
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int j = 0; j < 1000; j++) {
                synchronized (AtomicExample.class){
                    i++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int j = 0; j < 1000; j++) {
                synchronized (AtomicExample.class){
                    i++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }

2000

synchronized原理(可以不看)

jdk1.6开始对synchronized关键字进行的许多优化,为了减少重量级锁带来的性能开销,尽可能减轻锁的级别下解决现存并发问题。锁一共有4个状态:无锁、偏向锁、轻量级锁(自旋锁)、重量级锁,从低到高逐步升级。加锁的本质就是在锁对象的对象头写入当前线程ID,查询当前线程用的什么锁状态可以通过打印对象的对象头查看,建议ClassLayout jar包,主要看对象后的第一个字节最后三位,【001】表示无锁,【000】轻量级锁,【101】偏向锁、【010】重量级锁
这块没有具体代码展示,大部分都为理论

偏向锁

默认情况下偏向锁是开启状态,如果有线程去抢占锁,会优先抢占偏向锁,如果没有线程来竞争,会偏向该线程的ID

轻量级锁

存在线程竞争的情况下,撤销偏向锁升级到轻量级锁,操作对象头用CAS操作markword设置为指向自己线程的LR指针,设置成功后表示抢占到锁,其他没有抢占到锁的线程不会直接等待,会有一定的自旋次数(可通过JVM配置自旋次数),自旋次数会跟进竞争状态调整控制自旋的时间,自旋就是一直是尝试获取锁的状态,线程是没有阻塞的

重量级锁

自旋一定次数后没有获取到锁,进行锁升级到重量级锁,想操作系统申请资源,然后线程被阻塞挂入到等待队列,等获取到锁的线程释放锁,在通过CPU唤醒其中一个,切换上下文资源,重新获取锁。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值