Synchronized 浅析

来源:互联网 发布:60cj数据库 副手 编辑:程序博客网 时间:2024/05/16 23:48

本文目录

1 应用情景

2 同步(Synchronized)

3 原子性

4 锁

5 相关内容



1 应用情景

若多个线程,执行各自的任务操作,互不干扰,则不会出现问题。

但是,若多个线程,在某个阶段或操作中,需要处理同一个共享的数据,就会存在并发问题。

例如:当某个线程正在处理某个数据,另一个进程,也开始对这个数据进行处理(或修改),这样,就可能导致数据错误。

为了避免这种情况的发生,我们可以在线程操作同一个共享数据的地方,进行同步限制。

要求,同一时间,只有获得同步锁(执行权)的线程可以对该共享数据进行操作,其它线程必须等待。

例如:在排队过安检的时候,可能有多个排,但是,到了安检口的时候,只能一个一个的通过。



2 同步(Synchronized)

在Java中,可以通过Synchronized来指定,哪个部分需要进行同步限制。

Synchronized标记的部分,在同一时间,只允许获得同步锁(执行权)的线程来执行这部分的代码,其它线程必须等待。


2.1 同步块(Synchronized Block)

通过synchronized对某一部分代码进行同步限制,代码示例如下:

synchronized(Object obj) {    // 同步限制的代码}

表示在大括号内部的代码,在同一时间,只允许获得同步锁(执行权)的线程执行。

其中,obj参数表示同步锁,任何线程必须获得同步锁,然后才可以执行该部分代码。而同步锁只要一个,即同一时间,只有一个线程可以获得该同步锁,因此,保证了同一时间内,只有一个线程可以执行该部分代码。当线程执行完这部分代码后,将同步锁释放。

类比情景:一个仓库,只有一张门锁卡,任何想进入仓库的人,需要先获得门锁卡,然后才能进入仓库(此时,门锁卡随身携带),出仓库后,将门锁卡归还。任何人来到仓库时,都需要先检查门锁卡是否已经被其它人拿走。若已经被其它人拿走,就需要等待,直到那个人把门锁卡归还。


2.2 同步方法(Synchronized Method)

通过synchronized标记某个方法,表示该方法是同步方法,代码示例如下:

public synchronized int count() {    // 同步限制的代码}

等效代码如下,即使用当前对象this作为同步锁:

public int count() {    synchronized (this) {        // 同步限制的代码    }}

由于同步方法都是使用this作为同步锁,由此可以得出,同一个对象里的所有同步方法共用一个同步锁,即同一时间内,只有一个线程(获得同步锁的线程)可以执行这些同步方法。

同理可知,对于同一个类的多个对象(实例),同步机制互不干扰,因为每个对象都是使用自身(this)作为同步锁。

若希望在同一个类的所有对象(实例)实现同步限制,则需要定义一个静态全局变量作为同步锁。



3 原子性

在使用同步机制的时候,我们经常会想到一个问题 —— “某些操作很简单,很快就执行完,是否还需要加锁?”

这个问题的答案在于 “该操作是否为原子性操作” 。


在Java中,只有少部分操作是原子性操作,例如:

num = 1;

某些操作,看似原子性,实则不然,例如:

num++;    // 相当于 num = num + 1;num += 1;    // 相当于 num = num + 1;

在低并发量的情况下(非压力测试时),类似于 "num++;" 的操作,可能不会发生异常情况(因为几率太小)。

但当处于高并发量的情况时,问题就会暴漏出来。

因此,在对于共享数据进行任何操作时,都需要认真思考是否需要加锁,需要把锁加在哪里?



4 锁


4.1 基本概念

为了实现多线程的同步机制,会在同步限制的部分设置一个锁。

第一个执行到同步限制代码的线程,会获得这个锁,然后,该线程开始执行这部分代码。

此时,若其它线程也执行到这部分代码,发现锁已经被其它线程获取,则开始等待。

当第一个线程执行完这部分代码后,或者主动释放锁,正在等待的其它线程,开始争夺这个锁,得到锁的那一个线程,会开始执行这部分代码,其它线程继续等待。

这样,就保证了,在同一时间,同步限制的代码只被一个线程执行。


4.2 释放锁

obj.wait()

释放锁,并进入暂停状态,直到通过obj.notify/notifyAll被唤醒。


obj.wait(long timeout)

释放锁,并进入暂停状态一段时间后被唤醒,暂停期间也可以通过obj.notify/notifyAll被提前唤醒。


Thread.sleep 与 obj.wait 的区别:

正在执行同步限制代码的线程,若调用Thread.sleep方法,将会暂停执行一段时间,然后,继续执行。暂停的这段时间,该线程依然持有锁,其它线程依然处于等待中。

若该线程调用obj.wait方法,将会释放锁,然后处于暂停状态,直到被唤醒。

由于二者都会使线程暂停,因此,常常被混淆。但实际上,二者是不同领域的概念,一个是线程操作,一个是锁操作。


4.3 激活线程

obj.notify()

随机激活一个处于暂停状态的线程。


obj.notifyAll()

激活所有处于暂停状态的线程。


注意1:obj1.notify/notifyAll 与 obj2.notify/notifyAll 激活的是不同线程。obj1.notify/notifyAll 激活的是通过 obj1.wait 方法处于暂停状态的线程,而obj2.notify/notifyAll 激活的是通过 obj2.wait 方法处于暂停状态的线程。


注意2:被激活的线程,并不是马上恢复执行,而是同其它等待状态中的线程一样,需要等待拥有锁的线程释放锁,然后,开始争夺锁。


4.4 注意事项

obj.wait(), obj.notify(), obj.notifyAll()三个方法都需要在synchronized方法或synchronized块中使用,即获得了某锁(obj)后,才能对该锁进行相关的操作。



5 相关内容


5.1 java.util.concurrent.Semaphore

Semaphore被译为信号量,这里姑且也当成锁来看待,为便于理解。

在创建 Semaphore 对象的时候,可以指定锁的数量,代码如下:

Semaphore semaphore = new Semaphore(3);

每次调用semaphore.acquire()获得一个锁时,可用锁的数量减一;每次调用semaphore.release()释放一个锁时,可用锁的数量加一。

当可用锁的数量为0时,semaphore.acquire()将阻塞当前调用线程,直到其它线程调用semaphore.release()释放了一个锁之后,该线程获得锁并继续执行。


5.2 java.util.concurrent.locks.ReentrantLock

ReentrantLock 与 synchronized 有着相似的作用,但内部原理却不尽相同。从功能上来看, ReentrantLock 功能更为强大些,但使用起来会相对复杂一些。

ReentrantLock 对象通过 lock() 和 unlock() 进行加锁与解锁操作,需要注意的是,unlock()通常放在finally块中执行,以保证锁最终一定会被释放,代码如下:

ReentrantLock reentrantlock = new ReentrantLock();reentrantlock.lock();try {    // 代码} finally {    reentrantlock.unlock();}

公平锁:多个线程竞争同一个锁时,若希望按照申请锁的顺序来依次获得锁,则需要通过构造方法参数指定,代码如下:

ReentrantLock reentrantlock = new ReentrantLock(boolean fair);

设置等待时间:当申请一个锁时,可以指定等待时间,若该锁已被其它线程获取,且一直不释放,到达指定时间后,将放弃申请该锁,代码如下:

reentrantlock.tryLock(long timeout, TimeUnit unit);


5.3 集合的同步机制

在Java中,List、Set和Map也可以实现synchronized的功能,使其相关操作具有synchronized特性。

List<string> list = Collections.synchronizedList(new ArrayList<string>());  Set<string> set = Collections.synchronizedSet(new HashSet<string>());  Map<Integer, string> map = Collections.synchronizedMap(new HashMap<Integer, string>()); 

通过Collections.synchronizedXxxx方法,使标准集合对象具有synchronized特性,所有同步方法共用同一个锁,从而,使这些对象的增删改查等方法,在同一时间,只允许一个线程调用。



0 0