线程同步的方法:sychronized、lock、reentrantLock等。(

当使用多线程访问同一个资源时,非常容易出现线程安全问题(例如,当多个线程同时对一个数据进行修改时,会导致某些线程对数据的修改丢失)。因此,需要采用同步机制来解决这个问题。同步方法如下:

1.  Synchronized关键字

在Java语言中,每个对象都有一个对象锁与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的一段Synchronized代码时,需要首先获取这个锁,然后去执行相应的代码,执行结束后,释放锁。

Synchronized关键字主要有两种用法(Synchronized方法和Synchronized块),此外该关键字还可以作用于静态方法(此时如果调用该静态方法,将会锁住整个类。)、类或者某个实例,但这都对程序的效率有着很大的影响。

1)  Synchronized方法。该方法在声明前加入了Synchronized关键字。把需要被同步的资源放到该方法中,就能保证这个方法在同一时刻只能被一个线程所访问,从而保证了多线程访问的安全性。然而,当一个方法的方法体规模非常大时,把该方法声明为Synchronized会大大影响程序的执行效率。为了提高程序的执行效率,java提供了Synchronized块。线程在执行同步方法时是具有排它性的。当任意一个线程进入到一个对象的任意一个同步方法时,这个对象的所有同步方法都被锁定了,在此期间,其他任何线程都不能访问这个对象的任意一个同步方法,直到这个线程执行完它所调用的同步方法并从中退出,从而导致它释放了该对象的同步锁之后。在一个对象被某个线程锁定之后,其他线程是可以访问这个对象的所有非同步方法的。

2)  Synchronized块。Synchronized块既可以把任意的代码段声明为Synchronized,也可以指定上锁的对象,有非常高的灵活性。如果一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块。


2.  wait与notify

当使用Synchronized来修饰某个共享资源时,如果线程A1在执行Synchronized代码,另外一个线程A2也要同时执行同一对象的同一Synchronized代码时,线程A2将要等到线程A1执行完成后,才能继续执行。在这种情况下可以使用wait和notify方法。

在Synchronized代码被执行期间,线程可以调用对象的wait方法,释放对象锁,进入等待状态,并且可以调用notify方法或者notifyAll方法通知正在等待的其他线程。notify方法仅唤醒一个线程(等待队列中的第一个线程)并允许他去获得锁。notifyAll方法唤醒所有等待这个对象的线程并允许他们去获得锁(并不是让所有被唤醒的线程去获得锁,而是让他们去竞争)。

3.  Lock

Jdk5新增了Lock接口以及他的一个实现类ReentrantLock(重入锁),以及ReadWriteLock接口和他的唯一实现类ReentrantReadWriteLock.这个类有两个锁,一个是读操作锁,一个是写操作锁。使用读操作锁时可以允许多个线程同时访问,使用写操作锁时只允许一个线程进行。在一个线程执行写操作时,其他线程不能够执行读操作。Lock也可以用来实现多线程的同步,具体而言,他提供了如下一些方法来实现多线程的同步:

1)  lock()。以阻塞方式来获取锁,如果获取到了锁,立即返回;如果别的线程持有锁,则当前线程等待,直到获取锁后返回。

2)  tryLock()。以非阻塞的方式获取锁。只是尝试性的去获取一下锁,如果获取到锁,立即返回true,立即否则返回false。

3)  tryLock(longtimeout,TimeUnit unit)。如果获取到了锁,立即返回true,否则会等待参数给定的时间单元,在等待的过程中,如果获取到了锁,就立即返回true。如果等待超时,返回false。

4)  lockInterruptibly()。如果获取到了锁,立即返回;如果没有获取到锁,当前线程处于休眠状态,或者当前线程会被别的线程中断(会受到InterruptedException异常)。他与lock()方法的区别在于lock优先考虑获取锁,如果没有获取到锁,会一直处于阻塞状态,忽略interrupt()方法,待获取锁成功后,才响应中断。lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。

5)  ReentrantLock()。创建一个ReentrantLock实例。

6)  Unlock()。释放锁

前面讲到了可重入锁,可重入锁就是当前持有该锁的线程能够多次获取该锁,无需等待。下面介绍一下可重入锁。举个例子:村里面有一口井,村民都想到井里面打水,村里人太多,要制定一个打水的规则。井边安排一个看井人,维护打水的秩序。打水时,以家庭为单位,哪个家庭任何人先到井边,就可以先打水,而且如果一个家庭占到了打水权,其家人这时候过来打水不用排队。而那些没有抢占到打水权的人,一个一个挨着在井边排成一队,先到的排在前面。打水示意图如下:




是不是感觉很和谐,如果打水的人打完了,他会跟看井人报告,看井人会让第二个人接着打水。这样大家总都能够打到水。是不是看起来挺公平的,先到的人先打水,当然不是绝对公平的,自己看看下面这个场景 :


看着,一个有娃的父亲正在打水,他的娃也到井边了,所以女凭父贵直接排到最前面打水,羡煞旁人了。

以上这个故事模型就是所谓的公平锁模型,当一个人想到井边打水,而现在打水的人又不是自家人,这时候就得乖乖在队列后面排队。

然而总有些人不想排队,新来打水的人,他们看到有人排队打水的时候,他们不会那么乖巧的就排到最后面去排队,反之,他们会看看现在有没有人正在打水,如果有人在打水,没辄了,只好排到队列最后面,但如果这时候前面打水的人刚刚打完水,正在交接中,排在队头的人还没有完成交接工作,这时候,新来的人可以尝试抢打水权,如果抢到了,呵呵,其他人也只能睁一只眼闭一只眼,因为大家都默认这个规则了。这就是所谓的非公平锁模型。新来的人不一定总得乖乖排队,这也就造成了原来队列中排队的人可能要等很久很久。这就是所谓的非公平锁模型。

ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。我们先把故事元素转换为程序元素。


首先说说公平模型:初始化时,state=0,表示无人抢占了打水权。这时候,村民A来打水(A线程请求锁),占了打水权,把state+1,如下所示:

线程A取得了锁,把 state原子性+1,这时候state被改为1,A线程继续执行其他任务,然后来了村民B也想打水(线程B请求锁),线程B无法获取锁,生成节点进行排队,如下图所示:



初始化的时候,会生成一个空的头节点,然后才是B线程节点,这时候,如果线程A又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当A再次请求锁,就相当于是打水期间,同一家人也来打水了,是有特权的,这时候的状态如下图所示:


这就可重入锁。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程A释放了一次锁,就成这样了:


仅仅是把状态值减了,只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁。当A把锁完全释放后,state恢复为0,然后会通知队列唤醒B线程节点,使B可以再次竞争锁。当然,如果B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。

非公平锁模型:当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续乖乖休眠了。

代码如下:

ReentrantLock(CAS,AQS,java内存可见性(voliate))是可重入的独占锁或者叫排他锁。同时只能有一个线程获取该锁,其实现分为公平实现和非公平实现。读写锁解决了ReentrantLock同时只有一个线程可以获取该锁的缺点,我们实际情况下会有写少读多的场景,显然ReentrantLock满足不了要求需求,读写锁应运而生。

java5中添加了一个并发包, java.util.concurrent,里面提供了各种并发的工具类,通过此工具包,可以在java当中实现功能非常强大的多线程并发操作。

4.  使用特殊域变量(volatile)实现线程同步

在java语言编写的程序中,有时为了提高程序的运行效率,编译器会自动对其进行优化,把经常被访问的变量缓存起来,程序在读取这个变量时有可能会直接从缓存中(例如寄存器)来读取这个值,而不会去内存中读取。这样做的一个好处是提高了程序的运行效率,但当遇到多线程编程时,变量的值可能因为别的线程而改变了,而缓存里面的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。

Volatile是一个类型修饰符,它是用来修饰被不同线程访问和修改的变量。被Volatile类型定义的变量,系统每次用他的时候都是直接从对应的内存中提取,而不会利用缓存。在使用了Volatile修饰成员变量后,所有线程在任何时候所看到的的变量的值都是相同的。

由于Volatile不能保证操作的原子性,因此,一般情况下,Volatile不能代替Synchronized。此外,使用Volatile会阻止编译器对代码的优化,因此会降低程序的执行效率。除非迫不得已,一般能不用就不要用。

Volatile为什么不能保证操作的原子性:当需要使用被volatile修饰的变量时,线程会从主内存中重新获取该变量的值,但当该线程修改完该变量的值写入主内存的时候,并没有判断主内存内该变量是否已经变化,故可能出现非预期的结果。如主内存内有被volatile修饰变量 a,值为3,某线程使用该变量时,重新从主存内读取该变量的值,为3,然后对其进行+1操作,此时该线程内a变量的副本值为4。但此时该线程的时间片时间到了,等该线程再次获得时间片的时候,主存内a的值已经是另外的值,如5,但是该线程并不知道,该线程继续完成其未完成的工作,将线程内的a副本的值4写入主存,这时,主存内a的值就是4了。这样,之前修改a的值为5的操作就相当于没有发生了,a的值出现了意料之外的结果。

被synchronize修饰的变量则可以保证变量操作的原子性,因为当某线程使用变量a时,其他线程无法使用变量a,只能等该线程对a操作结束,释放a的锁后才能对a进行操作。


5.  使用局部变量实现线程同步

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。ThreadLocal 类的常用方法:

ThreadLocal() : 创建一个线程本地变量

get() : 返回此线程局部变量的当前线程副本中的值

initialValue() : 返回此线程局部变量的当前线程的"初始值"

set(T value) : 将此线程局部变量的当前线程副本中的值设置为value


6.  使用阻塞队列实现线程同步

前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。使用java5版本中新增的java.util.concurrent包将有助于简化开发。使用LinkedBlockingQueue<E>来实现线程的同步,BlockingQueue通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。LinkedBlockingQueue<E>是一个基于已连接节点的,范围任意的blockingqueue。 队列是先进先出的顺序(FIFO),LinkedBlockingQueue类常用方法 LinkedBlockingQueue() : 创建一个容量为Integer.MAX_VALUE的LinkedBlockingQueue,put(E e) : 在队尾添加一个元素,如果队列满则阻塞,size() : 返回队列中的元素个数,take() : 移除并返回队头元素,如果队列空则阻塞代码实例: 实现商家生产商品和买卖商品的同步。

7.  使用原子变量实现线程同步

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。

那么什么是原子操作呢?原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作。这几种行为要么同时完成,要么都不完成。在java的util.concurrent.atomic包中提供了创建原子类型变量的工具类,使用该类可以简化线程同步。其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),但不能用于替换Integer。 AtomicInteger类常用方法:AtomicInteger(int initialValue) : 创建具有给定初始值的变量。AtomicIntegeraddAddGet(int dalta) : 以原子方式将给定值与当前值相加。get() : 获取当前值。




  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值