阅读 3299

Java—线程同步

几个小概念

临界资源:当多线程访问同一个对象时, 这个对象叫做临界资源

原子操作:在临界资源中不可分割的操作叫原子操作

线程不安全:多线程同时访问同一个对象, 破坏了不可分割的操作, 就可能发生数据不一致

“弱肉强食”的线程世界

大家好,我叫王大锤,我的目标是当上CEO...额 不好意思拿错剧本了。大家好,我叫0x7575,是一个线程,我的线生理想是永远最快拿到CPU。

先给大家介绍一下线程世界,线程世界是一个弱肉强食的世界,资源永远稀缺,什么东西都要抢,这几个纳秒我有幸拿到CPU,对int a = 20进行一次加1操作,当我从内存中取出a,进行加1后就失去了CPU,休息结束之后准备写入内存的时候,我惊奇的发现:内存中的a这时候已经变成了22。

一定有线程趁我不在修改了数据,我左右为难,很多线程也都劝我不要写入,但是迫于指令,我只能把21写入内存覆盖掉不符合我的运算逻辑的22。

以上只是一个微小的事故,类似的事情在线程世界层出不穷,所以虽然我们每一个线程都尽职尽责,但是在人类看来我们是引起数据不安全的祸首。

这是何等的冤枉啊,线程世界一直都是竞争激烈的世界,尤其是对于一些共享变量,共享资源(临界资源),同时有多个线程进行争夺使用时再正常不过的事情了。除非消除共享的资源,但是这又是不可能的,于是事情就开始僵持了。

线程世界出现了一把锁

幸好还是又聪明人的,有人想到了一个解决问题的好方法。虽然不知道谁想到的注意,但是这个注意确实解决了一部分问题,解决的方案是加锁

你想要进行对一组加锁的代码进行操作吗?想的话就先去抢到锁,拿到锁之后就可以对被加锁的代码为所欲为了,倘若拿不到锁的话就只能在代码块门口等着,因为等的线程太多了,这还成为了一种社会现象(状态),该社会现象被命名为线程的阻塞。

听上去很简单,但是实际上加锁有很多详细的规定的,详情政府发布了《关于synchronzied使用的若干规定》以及后来发布的《关于Lock使用的若干规定》。

线程和线程之间是共享内存的,当多线程对共享内存进行操作的时候有几个问题是难以避免的,竞态条件(race condition)和内存可见性。

**竞态条件:**当多线程访问和操作同一对象的时候,最终结果和执行时序有关,正确性是不能够人为控制的,可能正确也可能不正确。(如上文例子)

上文中说到的加锁就是为了解决这个问题,常见的解决方案有:

  • 使用synchronized关键字
  • 使用显式锁(Lock)
  • 使用原子变量

**内存可见性:**关于内存可见性问题要先从内存和cpu的配合谈起,内存是一个硬件,执行速度比CPU慢几百倍,所以在计算机中,CPU在执行运算的时候,不会每次运算都和内存进行数据交互,而是先把一些数据写入CPU中的缓存区(寄存器和各级缓存),在结束之后写入内存。这个过程是及其快的,单线程下并没有任何问题。

但是在多线程下就出现了问题,一个线程对内存中的一个数据做出了修改,但是并没有及时写入内存(暂时存放在缓存中);这时候另一个线程对同样的数据进行修改的时候拿到的就是内存中还没有被修改的数据,也就是说一个线程对一个共享变量的修改,另一个线程不能马上看到,甚至永远看不到。

这就是内存的可见性问题。

解决这个问题的常见方法是:

  • 使用volatile关键字
  • 使用synchronized关键字或显式锁同步

线程同步

传统的锁 synchronzied

同步代码块

每个java对象都有一个互斥锁标记,用来分配给线程,synchronized(o){ } 对o加锁的同步代码块,只有拿到锁标记的线程才能够进入对o加锁的同步代码块。

同步方法

synchronized作为方法修饰符修饰的方法被称为同步方法,表示对this加锁的同步代码块(整个方法都是一个代码块)。

JDK1.5的锁 Lock

ReentrantLock

ReentrantLock具有和synchronized相似的作用,但是更加的灵活和强大。

它是一个重入锁(synchronized也是),所谓重入就是可以重复进入同一个函数,这有什么用呢?

假设一种场景,一个递归函数,如果一个函数的锁只允许进入一次,那么线程在需要递归调用函数的时候,应该怎么办?退无可退,有不能重复进入加锁的函数,也就形成了一种新的死锁。

重入锁的出现就解决了这个问题,实现重入的方法也很简单,就是给锁添加一个计数器,一个线程拿到锁之后,每次拿锁都会计数器加1,每次释放减1,如果等于0那么就是真正的释放了锁。

//创建一个锁对象
Lock lock = new ReentrantLock();

//上锁(进入同步代码块)
lock.lock();

//解锁(出同步代码块)
lock.unlock();

//尝试拿到锁,如果有锁就拿到,没有拿到不会阻塞,返回false
tryLock();
复制代码

ReadWriteLock

读写锁,读写分离。分为readLock和writeLock两把锁。对于readLock来说,是一把共享锁,可以多次分配;但是当readLock锁上的时候,调用writeLock是会阻塞的,反之亦然,另,写锁是一把普通的互斥锁,只可以分配一次。

synchronized和ReentrantLock的区别
  1. 两者都是互斥锁,所谓互斥锁:同一时间只有一个拿到锁的线程才能够去访问加锁的共享资源,其他的线程只能阻塞
  2. 都是重入锁,用计数器实现
  3. ReentrantLock独有特点
    1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁
    2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
    3. ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制
volatile关键字

volatile 修饰符 用来保证可见性

当一个共享变量被volatile修饰的时候,他会保证变量被修改之后立马在内存中更新,另一线程在取值的时候需要去内存中读取新的值。

注意:尽管volatile 可以保证变量的内存可见性,但是不能够保存原子性,对于b++这个操作来说,并不是一步到位的,而是分为好几步的,读取变量,定义常量1,变量b加1,结果同步到内存。虽然在每一步中获取的都是变量的最新值,但是没有保证b++的原子性,自然无法做到线程安全

文章分类
后端