JUC之线程安全

线程安全

多线程下同时对共享数据进行读写操作造成的数据混乱,称之为线程安全问题。

当多线程并发访问临界资源时,如果破坏其可见性、原子性、有序性时可能会造成数据不一致。

临界资源:共享资源,同一时刻只允许只有一个线程进行读写,才能保证数据一致性。

可见性:

当多个线程修改一个共享变量时候,其中一个线程修改了共享变量的值,其他线程可以感知到变量的值的变化,这叫可见性。

其实可见性与内存模型有关系,简单一点描述,就是,每个线程会拷贝一份共享变量到自己的线程的缓存中,当每个线程修改变量也就是修改的是当前线程缓存中的变量的值,如果想要其他线程也要感知到变量的变化的话,就需要当前线程将修改过的变量的值同步到主内存中,其他线程再从主内存中重新获取一次新的值。

java中的关键字volatile可以保证共享变量的可见性,另外,synchronized和lock锁也可以保证共享变量的可见性,那是因为锁可以保证只能有一个线程在同一时刻操作共享变量,操作完以后会将线程缓存和主内存中的变量进行同步。

volatile的两个作用就是可见性和禁止指令重排序。单例模式中静态属性声明为volatile的作用就是禁止指令重排序,防止对象半初始化。

原子性:

同一时刻允许有且只有一个线程进行操作。单一不可分割的操作。

比如:i++就不是原子性的操作,其实i++可以分为3步走

第一步:获取 i 的本身的值;

第二步:i+1

第三步:将+1之后的新值赋值给 i;

所以要想保证原子性,可以给大的方法加锁来实现。CAS(CompareAndSet)也是通过锁来保证原子性的。

有序性:

有序性是指让CPU按照代码的顺序依次执行。

造成不是有序性的原因是因为编译器和CPU为了提高运行效率,会对执行进行重排序,不会按照代码的顺序依次执行。

可以对操作进行synchronized和lock加锁来保证有序性。也可以利用happens-before原则来保证程序的有序性。

happens-before 表达的并不是说前面一个操作发生在后面一个操作的前面,尽管从程序员编程角度来看也并不会出错,但它其实表达的是,前一个操作的结果对后续操作是可见的

如何解决线程不安全的问题

1、取消共享

共享资源取消共享,局部变量代替

共享资源只读

2、ThreadLocal

ThreadLocal其实是利用Map实现的,key是当前的线程,value是存放的值。

ThreadLocal用完以后为了防止内存泄漏需要清除保存的数据。因为ThreadLocal中的key是弱引用,value是强引用,很容易造成内存泄露。

JMM内存模型

内存模型

JMM在线程缓存和主内存之间数据的同步操作:

保证可见性的原理:

内存屏障

内存屏障就是防止指令重排序。

Java中规范定义的内存屏障。

原子类

一个操作不可中断,不可分割;

原子类的作用和锁的作用相同;

通常,原子类比加锁粒度更细,效率更高

java提供了一些原子类,在java.util.concurrent.atomic包下;

private static AtomicInteger value = new AtomicInteger(10);
private static int[] origin = new int[]{1,2,3};
private static AtomicIntegerArray aia = new AtomicIntegerArray(origin);

    
    public static void main(String[] args) throws Exception {
        
        value.compareAndSet(value.get(),10);
        value.decrementAndGet();
        value.getAndDecrement();
        value.incrementAndGet();
        value.getAndIncrement();
        value.set(10);
        value.lazySet(10);
        value.getAndAdd(5);
        value.addAndGet(5);

        //原子数组比原子类多了一个下标
        //创建原子数组的时候是根据原数组拷贝了一个新的数组,所以修改原子数组的时候,不会影响到原数组origin
        aia.getAndAdd(1,5);
    }

累加器

LongAdder longAdder = new LongAdder();
        longAdder.add(5);
        longAdder.increment();
        System.out.println(longAdder.longValue());

        LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,1);
        System.out.println(longAccumulator.get());
        longAccumulator.accumulate(1);
        System.out.println(longAccumulator.get());
        longAccumulator.accumulate(2);
        System.out.println(longAccumulator.get());

乐观锁和悲观锁

悲观锁

乐观锁

CAS就是乐观锁的例子。

CAS

CAS缺点

如果一直设置失败,会一直尝试,这样就会造成很大的资源开销。

自旋锁的核心思想就是CAS。

Sleep和wait方法的区别

wait()方法如果不指定时间,则需要被动唤醒,如果指定了等待时间,则不需要唤醒。

锁升级

JDK6之前,java重量级锁(也就是常见的synchronized锁)效率比较低,在用户态和内核态之间切换来进行锁的互斥等操作。synchronized是基于Monitor(管程)实现的。JDK6之后对锁进行了升级。对synchronized的锁状态分为了4种。因为synchronized锁是对象锁,所以在对象的堆内存上有固定的位置标志对应的锁状态。

无锁:

未上锁的状态。

偏向锁:

没有线程竞争的情况下,第一个获得锁的线程会把线程id写入堆内存中,同时锁会升级为偏向锁。如果同一个线程重复上锁的话,依旧是偏向锁。jvm中在jvm启动4s之后创建的对象才会启动偏向锁。

轻量级锁:

当两个以上的线程串行(交替,不是并发)的情况下获取锁,锁会升级为轻量级锁。基于CAS自旋锁。

重量级锁:

多个线程并发的获取锁的情况下锁会升级为重量级锁。用户态切换为内核态。

可重入锁(可递归锁)

ReentrantLock
ReentrantLock锁手动显示的上锁和释放锁,更灵活

    //ReentrantLock锁手动显示的上锁和释放锁,更灵活
    private ReentrantLock reentrantLock = new ReentrantLock();

    new Thread(() -> {
            try {
                reentrantLock.lock();
                //do something
            } finally {
                if (reentrantLock.isHeldByCurrentThread()) {
                    reentrantLock.unlock();
                }
            }
        }).start();

tryLock()

ReentrantLock.tryLock()方法如果锁被别的线程占用,则获取锁失败。

tryLock(time)传入参数表示获取锁最大等待时间,在这个时间内锁被释放了,也是可以获取到的。

锁中断

ReentrantLock.lockInterruptibly()

如果当前线程已经持有此锁,则持有计数增加1,并且该方法立即返回。如果锁由另一个线程持有,那么当前线程将出于线程调度目的而被禁用,并处于休眠状态,直到发生以下两种情况之一:

当前线程获得锁;

或者其他线程中断了当前线程。

如果锁是由当前线程获取的,则锁持有计数设置为1。如果当前线程在进入此方法时设置了中断状态;或者在获取锁时被中断,则抛出InterruptedException,并清除当前线程的中断状态。

公平锁和非公平锁

ReentrantLock锁是可以设置公平还是非公平锁的,默认是false非公平锁,设置为true就是公平锁。

ReentrantLock reentrantLock = new ReentrantLock(true);

共享锁和排他锁

synchronized和lock的区别

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值