一、死锁
1、死锁产生条件
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
在JAVA编程中,有3种典型的死锁类型:
静态的锁顺序死锁,动态的锁顺序死锁,协作对象之问发生的死锁。
2、静态的锁顺序死锁
原因:
a和b两个方法都需要获得A锁和B锁。一个线程执行a方法且已经获得了A锁,在等待B锁;另一个线程执行了b方法且已经获得了B锁,在等待A锁。这种状态,就是发生了静态的锁顺序死锁。
public class StaticLockOrderDeadLock {
private final Object lockA =new Object();
private final Object lockB =new Object();
public void a(){
synchronized (lockA){
synchronized (lockB){
System.out.println("lockA");
}
}
}
public void b(){
synchronized (lockB){
synchronized (lockA){
System.out.println("lockB");
}
}
}
}
解决办法:
解决静态的锁顺序死锁的方法就是:所有需要多个锁的线程,都要以相同的顺序来获得锁。
2、动态的锁顺序死锁
原因:
动态的锁顺序死锁是指两个线程调用同一个方法时,传入的参数颠倒造成的死锁。如下代码,
一个线程调用了transferMoney方法并传入参数accountA,accountB;另
一个线程调用了transferMoney方 法并传入参数accountB,accountA。此时就可能发生在静态的锁顺序死锁中存在的问题,即:第一个线程获得了accountA锁并等待accountB锁,第二个线程获得了accountB锁 并等待accountA锁。
1/可能发生动态锁顺序死锁的代码
class DynamicLockorderDeadLock{
public void transefMoney(Account fromAccoynt, Account toAccount,Double amount){
synchronized (fromAccount){
synchronized (toAccount){
//..
fromAccount.minus (amount) ;
toAccount. addy amount) ;
//..
}
}
}
}
解决办法:
解决动态的锁顺序死锁的方法就是:使用System.identityHashCode 来定义锁的顺序。确保所有的线程都以相同的顺序获得锁。
3、协作对象之问发生的死锁
原因:
有时,死锁并不会那么明显,比如两个相互协作的类之间的死锁,比如下面的代码:一个线程调用了 Taxi对象的setLocation方法,另一个线程调用了Dispatcher对象的getlmage 方法。此时可能会发生,第一个线程持有Taxi对象锁并等待Dispatcher对象锁,另一个线程持有Dispatcher对象锁并等待Taxi对象锁。
class Taxi{
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher){
this.dispatcher = dispatcher;
}
public synchronized point getLocation(){
return location;
}
public synchronized void setLocation(Point location){
this.location = location;
if(location.equals(destination)){
dispatcher.notifyAvailable(this);
}
}
}
class Dispatcher{
private final Set<Taxi> taxis;
private final Set<Taxi> availableTaxis;
public Dispatcher(){
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
}
public synchronized Image getImage(){
Image image = new Image();
for(Taxi t:taxis){
image.drawMarker(t.getLocation());
}
return image;
}
}
解决办法:
解決协作对象之间发生的死锁:需要使用开放调用(如果调用某个外部方法时不需要持有锁,我们称之为开放调用),即避免在持有锁的情況下调用外部的方法。
二、线程同步问题的产生和解决方案
1、问题的产生:
Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之问产生冲突。
如下例:假设有一个卖票系统,一共有100张票,有4个窗口同时卖。
public class ThreadDemo {
public static void main(String[] args) {
ThreadClass threadInstance = new ThreadClass();
Thread t1 = new Thread(threadInstance);
Thread t2 = new Thread(threadInstance);
Thread t3 = new Thread(threadInstance);
Thread t4 = new Thread(threadInstance);
//开启线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class ThreadClass implements Runnable {
Object obj = new Object();
private int tickets = 100;
@Override
public void run() {
synchronized (obj) {
while (tickets > 0) {
try {
Thread.sleep(300);//释放执行权和执行资格
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "..." + tickets--);
}
}
}
}
上述结果对于同一张票进行了多次售出。这就是多线程情況下,出现了数据"脏读”情況。即多个线程访问余票num时,当一个线程获得余票的数量,要在此基础上进行-1的操作之前,其他线程可能已经卖出多张票,导致获得的num不是最新的,然后-1后更新的数据就会有误。这就需要线程同步的实现了。
问题的解决:
因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。
一共有两种锁,来实现线程同步问题,分别是:synchronized 和 ReentrantLock。
2、Synchronized关键字
1. 简介
- synchronized实现同步的基础:Java中每个对象都可以作为锁。当线程试图访
问同步代码时,必须先获得对象锁,退出或地出异常时必须释放锁。 - Synchronzied实现同步的表现形式分为:代码块同步和方法同步
2. synchronized原理
JVM基于进入和退出Monitor 对象来实现 代码块同步和方法同步,两者实现细
节不同。
-
代码块同步:
在编译后通过将monitorenter 指令插入到同步代码块的开始处,
将monitorexit 指令插入到方法结束处和异常处,通过反编译字节码可以观察
到。任何一个对象都有一个monitor 与之关联,线程执行 monitorenter 指令
时,会尝试获取对象对应的 monitor 的所有权,即尝试获得对象的锁。 -
方法同步:
synchronized方 法在method_info结构 有 ACC_synchronized 标
记,线程执行时会识别该标记,获取对应的锁,实现方法同步。
两者虽然实现细节不同,但本质上都是对一个对象的监视器 (monitor)的获取。任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为 BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻寒在同步队列的线程,使其重新尝试对监视器的获取。
![](https://img.haomeiwen.com/i5343604/56908d81e1d25e60.png)
3. sychronized的使用场景
- 实例方法同步
public sychronized void method1(){//TODO}
锁住的是该对象,类的其中一个实例,当该对象(仅仅是这一个对象)在不同线程中执行这个同步方法时,线程之间会形成互斥。达到同步效果,但如果不同线程同时对该类的不同对象执行这个同步方法时,则线程之间不会形成互斥,因为他们拥有的是不同的锁。
- 实例代码块同步
sychronized(this){//TODO}
锁住的是该对象,类的其中一个实例
- 静态方法同步
public sychronized static void method1(){//TODO}
锁住的是该类,当所有该类的对象(多个对象)在不同线程中调用这个static同步方法时,线程之间会形成互斥,达到同步效果。
- 类对象代码块同步
sychronized(Test.class){//TODO}
锁住的是该类
- 其他对象代码块同步
sychronized(object){//TODO}
object可以是任意一个对象,谁拥有object这个锁,谁就可以进入;
总结
1、同步代码块:锁可以是任意对象;
2、普通同步方法:锁是this对象。
3、静态同步方法:当前类的实例。如Test.class
3、ReentrantLock锁
1. ReentrantLock介绍
- 继承于Lock接口。
- 一个可重入的互斥锁
- 它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
//同步器实现了AbstractQueuedSynchronizer
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
//非公平锁实现继承Sync
static final class NonfairSync extends Sync {
...
}
//公平锁实现继承Sync
static final class FairSync extends Sync {
...
}
//默认非公平
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//调用的是构造方法初始化的同步器的方法
public void lock() {
sync.lock();
}
}
2. Lock接口
Lock:锁对象,在Java中锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但有的锁可以允许多个线程并发访问共享资源,比如读写锁)。
在Lock接口出现之前,Java程序是靠 synchronized 关键字实现锁功能的,java5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁的功能,它提供了与synchronized 关键宇类似的同步功能。
优点:拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等多种 synchronized 关键字所不具备的同步特性。
缺点:缺少像 synchronized 那样隐式获取释放锁的便捷性。
Lock接口的主要方法:
public interface Lock {
//执行此方法时,如果锁处于空闲状态,当前线程将获取到锁。相反,如果锁已经被其他线程持有,将禁用当前线程,直到当前线程获取到锁。
void lock();
void lockInterruptibly() throws InterruptedException;
//如果锁可用,则获取锁,并立即返回true,否则返回false. 该方法和lock(的区别在于,tryLock(只是”试图”获取锁,如果锁不可用,不会导致当前线程被禁用,当前线程仍然继续往下执行代码。而lock()方则是一定要获取到锁,如果锁不可用,就一直等待,在未获得锁之前.当前线種并不继续向下执行.
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//执行此方法时,当前线程将释放持有的锁.锁只能由持有者释放,如果线程并不持有锁,却执行该方法,可能导致异常的发生。
void unlock();
//条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await0方法,而调用后,当前线程将缩放锁。
Condition newCondition();
}
3. ReentrantLock的使用
ReentrantLock lock = new ReentrantLock();//参数默认false,不公平锁
Condition condition = lock. newCondition();
lock. lock();//如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
try {
//操作
while(条件判断表达式){
condition.wait():
}
} finally {
lock .unlock();//释放锁
}
4. ReentrantLock非公平锁的实现
在非公平锁中,每当线程执行lock方法时,都尝试利用CAS把state从0设置为1。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
下面我们用线程A和线程B来描述非公平锁的竞争过程:
- 线程A和B同时执行CAS指令,假设线程A成功,线程B失败,则表明线
获取锁,并把同步器中的exclusiveOwnerThread设置为线程A; - 竞争失败的线程B,在nonfairTryAcquire方法中,会再次尝试获取锁;
5. ReentrantLock公平锁的实现
在公平锁中,每当线程执行lock方法时,如果同步器的队列中有线程在等待,则直接加入到队列中。
- 持有锁的线程A正在running,对列中有线程BCDEF被挂起并等待被唤醒;
- 线程G执行lock,队列中有线程BCDEF在等待,线程G直接加入到队列的队尾。
所以每个线程获取锁的过程是公平的,等待时间最长的会最先被唤醒获取锁。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
6. ReentrantLock重入锁的实现
重入锁,即线程可以重复获取已经持有的锁。在非公平和公平锁中,都对重入锁进行了实现。
if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
7. 条件变量Condition
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
private transient Node firstWaiter;
private transient Node lastWaiter;
public ConditionObject() { }
private Node addConditionWaiter() {}
private void doSignal(Node first) {}
private void doSignalAll(Node first) {}
private void unlinkCancelledWaiters() {}
public final void signal() {}
public final void signalAll() {}
public final void awaitUninterruptibly() {}
private int checkInterruptWhileWaiting(Node node) {}
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {}
public final void await() throws InterruptedException {}
public final long awaitNanos(long nanosTimeout)
throws InterruptedException {}
public final boolean awaitUntil(Date deadline)
throws InterruptedException {}
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {}
final boolean isOwnedBy(AbstractQueuedSynchronizer sync) {}
protected final boolean hasWaiters() {}
protected final int getWaitQueueLength() { }
protected final Collection<Thread> getWaitingThreads() {}
}
- Synchronized中,所有的线程都在同一个obiect的条件队列上等待。而
ReentrantLock中,每个condition都维护了一个条件队列。 - 每一个Lock可以有任意数据的condition对象,Condition是与Lock鄉定的,
所以就有Lock的公平性特性:如果是公平锁,线程为按照FIFO的顺序从
Condition.await中释放,如果是非公平锁,那么后续的锁充争就不保证FIFO顺
序了。 - Condition接口定义的方法,await对应于Object.wait,signal对应于
Object.notify,signalAll对应于Object.notifyAll。特别说明的是Condition的
接口改变名称就是为了避免与Object中的waitnotiiy/notityAll的语义和使用上混
淆。
await实现逻辑:
- 将线程A加入到条件等待队列中,如果最后一个节点是取消状态,则从对列中
删除。 - 线程A释放锁,实质上是线程A修改AQS的状态state 为0,并唤醒AQS等待队列中的线程B,钱程B被唤醒后,尝试获取锁,接下去的过程就不重复说明了。
- 线程A释放锁并唤醒线程B之后,如果线程A不在AQS的同步队列中,线程A将
通过LockSupport.park进行挂起操作。随后,线程A等待被唤醒,当线程A被唤醒时,会通过acquireQueued方法充争锁,如果失败,继续挂起。如果成功,线程A从await位置恢复。
假设线程B获取锁之后,执行了take操作和条件变量的signal,signal通过某种实现唤醒了线程A,具体实现如下:
signal实现逻辑:
- 接着上述场景,线程B执行了signal方法,取出条件队列中的第一个非
CANCELLED节点线程,即线程A。另外,signalAll就是唤醒条件队列中所有非
CANCELLED节点线程。遇到CANCELLED线程就需要将其从队列中删除 - 通过CAS修改线程A的waitStatus,表示该节点已经不是等待条件状态,并将线程A插入到AQS的等待队列中
- 唤醒线程A,线程A和别的线程进行锁的竞争。
8. ReentrantLock总结
- ReentrantLock提供了内置锁类似的功能和内存语义。
- 此外,ReetrantLock还提供了其它功能,包括定时的锁等待、可中断的锁等
待、公平性、以及实现非块结构的加锁、 Condition,对线程的等待和唤醒等操
作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展
性,不过ReetrantLock需要显示的获取锁,并在finally中释放锁,否则后果很
严重。 - ReentrantLock在性能上似乎优于Synchronized,其中在idk1.6中略有胜出,在1.5中是远远胜出。那么为什么不放弃内置锁,并在新代码中都使用ReetrantLock?
- 在java1.5中,内置锁与ReentrantLock相比有例外一个优点:在线程转储中能
给出在哪些调用帧中获得了哪些锁,并能够检测和识别发生死锁的线程。
Reentrant的 非块状特性仍然意味着,获取锁的操作不能与特定的栈帧关联起
来,而内置锁却可以。 - 因为内置锁时JVM的内置属性,所以未来更可能提升synchronized而不是
ReentrantLock的性能。例如对线程封闭的锁对象消除优化,通过增加锁粒度
来消除内置锁的同步。
4、synchronized和ReentrantLock的 比较
1.区别:
- Lock是一个接口,而synchronized是Java中的关键宇,synchronized是内置的语言实现;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized 却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized 却无法办到。
- Lock可以提高多个线程进行读操作的效率。
2.两者在锁的相关概念上区别:
- 可中断锁
- 顾名恩义,就是可以响应中断的锁。
- 在Java中,synchronized就不是可中断锁,而Lock是可中断锁。如果某一线程A
正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。 - lockInterruptibly()的用法体现了Lock的可中断性。
- 公平锁
- 公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时问最久的线程(最先请求的线程)会获得该锁(并不是绝对的,大体上是这种顺序),这种就是公平锁。
- 非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
- 在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。ReentrantLock可以设置成公平锁。
- 读写锁
- 读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
- 正因为有了读写锁,才使得多个线程之间的读操作可以并发进行,不需要同步,而写操作需要同步进行,提高了效率。
- ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
- 可以通过readlockQ)获取读锁,通过writeLock(获取写锁。
- 绑定多个条件
- 一个ReentrantLock对象可以同时鄉定 多个Condition对象,而在synchronized中锁对象的wait()和notity()或notifyAll()方法可以实现一个隐含的条件,如果要和多余一个条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这么做,只需要多次调用new Condition()方法即可。
3.性能比较:
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当充争资源非常激烈时(即有大量线程同时充争),此时ReentrantLock的性能要远远优于
synchronized。所以说,在具体使用时要根据适当情況选择。
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的ReentrankLock对象,性能更高一些。到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6 上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。
5、锁的分类
1、重入锁
当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的。
具体概念就是:自己可以再次获取自己的内部锁。
Java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的。
2、公平锁
CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的 (synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取CPU的执行权,优先级高的线程会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较低,因为要实现顺序执行,需要维护一个有序队列。
ReentrantLock就是公平锁。
3、悲观锁
-
悲观锁的实现:
我们都知道,cpu是时分复用的,也就是把cpu的时问片,分配给不同的
thread/process轮流执行,时间片与时问片之间,需要进行Cpu切换,也就是会发生进程的切换。切换涉及到清空寄存器,缓存数据。然后重新加载新的thread所需数据。当一个线程被挂起时,加入到阻塞队列,在一定的时间或条件下,在通过notity(),notifyAll0唤醒回来。在某个资源不可用的时候,就将cpui上出,把当前等待线程切换为阻塞状态。等到资源(比如一个共享数据)可用了,那么就将线程唤醒,让他进入runnable状态等待cpu调度。 -
独占锁是一种悲观锁,synchronized就是一种独占锁。
它假设最坏的情况,认为一个线程修改共享数据的时侯其他线程也会修改该数据,因此只在确保其它线程不会造成干扰的情況下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
缺点:由于在进程挂起和恢复执行过程中存在着很大的开销。当一个线程正在等待锁时,它不能做任何事。
4、乐观锁
-
乐观锁的实现:
每次不加锁而是假设修改数据之前其他线程一定不会修改,如果因为修改过产生冲突就失败就重试,直到成功为止。 -
适用:
当数据争用不严重时;CAS就是一种乐观锁思想的应用。
6、CAS
CAS 操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
1、JAVA中通过锁和循环CAS的方式来实现原子操作。
JAVA中java.util.concurrent.atomic包相关类就是CAS的实现。
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = this.getIntVolatile(o, offset);
} while(!this.weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
循环不停调用weakCompareAndSetInt进行比较,weakCompareAndSetInt里面调用JAVA本地方法,内部借助C来调用CPU的底层指令来保证在硬件层上实现原子操作的。
在intel处理器中,CAS是通过调用cmpxchg指令完成的。这就是我们常说的CAS操作 (compare and swap)。
2、CAS的问题:
1、ABA问题;
2、循环时间长开销大;
3、只能保证一个共享变量的原子操作。
7、AbstractQueuedSynchronizer
1. AbstractQueuedSynchronizer介绍
AbstractQueuedSynchronizer提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架。该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础。使用的方法是继承,子类通过继承同步器并需要实现它的方法来管理其状态,管理的方式就是通过类似acauire和release的方式来操纵状态。然而多线程环境中对状态的操纵必须确保原子性,因此子类对于状态的把握,需耍使用这个同步器提供的以下三个方法对状态进行澡作
java.til. concurrent.locks.AbstractQueuedSynchronizer.getState()
java.util.concurrent.locks.AbstractQueuedSynchronizer.setState(int)
java.util.concurrent.locks.AbstractQueuedSynchronizer.compareAndSetState(int,int)
子类推荐被定义为自定义同步装置的内部类,同步器自身没有实现任何同步接口;它仅仅是定义了若千acquire之类的方法来供使用。该同步器即可以作为排他模式也可以作为共享模式,当它被定义为一个排他模式时,其他线程对其的获取就被阻止,而共享模式对于多个线程获取都可以成功。
2. AbstractQueuedSynchronizer用处
![](https://img.haomeiwen.com/i5343604/a6119aed1c00cf34.png)
3. 同步器和锁
同步器是实现锁的关键,利用同步器将锁的语义实现,然后在锁的实现中聚合同步器。
4. 独占模式和共享模式
5. 总结
AQS核心是通过一个共享变量来同步状态,变量的状态由子类去维护,而AQS框架做的是:
- 线程阻寒队列的维护
- 线程阻寒和唤醒
共享变量的修改都是通过Unsafe类提供的CAS操作完成的。
AbstractQueuedSynchronizer类的主要方法是acquire和release,典型的模板方
法,下面这4个方法由子类去实现:
protected boolean tryAcquire (int arg)
protected boolean tryRelease (int arg)
protected it tryAcquireShared (int arg)
protected boolean tryReleaseShared(int arg)
acquire方法用来获取锁,返回true说明线程获取成功继续执行,一旦返回false则线程加入到等待队列中,等待被唤醒;
release方法用来释放锁。
一般来说实现的时侯这两个方法被封装为lock和unlock方法。
三、线程间通信
1、线程间通信有两种方式:
1. wait/notify实现通信
Object类中相关的方法有notity方法和wait方法。因为wait和notify方法定义在Object类中,因此会被所有的类所继承。这些方法都是final的,即它们都是不能被重写的,不能通过子类覆写去改变它们的行为。
- wait()方法:让当前线程进入等待,并释放锁。
- wait(long)方法:让当前线程进入等待,并释放锁,不过等待时间为long,超过这个时间没有对当前线程进行唤醒,将自动唤醒。
- notify()方法:让当前线程通知那些处于等待状态的线程,当前线程执行完牛后释放锁,并从其他线程中唤醒其中一个继续执行。
- notifyAll()方法:让当前线程通知那些处于等待状态的线程,当前线程执行完半后释放锁,将唤醒所有等待状态的线程。
wait()方法使用注意事项
1、当前的线程必须拥有当前对象的monitor,也即lock,就是锁,才能调用wait()方法,否则将抛出异常jiava.lang.lllegalMonitorStateException。
2、线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notity(或者notifyAll方法),这样它才能重新获得锁的拥有权和恢复执行。
3、要确保调用wait()方 法的时候拥有锁,即,wait(方法的调用必须放在
synchronized方法或synchronized块中。
4、wait()与sleep()比较:当线程调用了wait方法时,它会释放掉对象的锁。
Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。
notify()方法使用注意事项:
1、如果多个线程在等待,它们中的一个将会选择被唤醒。这种选择是随意的,和具体实现有关。(线程等待一个对象的锁是由于调用了wait0方法)
2、被唤醒的线程是不能被执行的,需要等到当前线程放弃这个对象的锁,当前线程会在方法执行完毕后释放锁。
wait()/notify()协作的两个注意事项:
1、通知过早,如果通知过早,则会打乱程序的运行逻辑
2、等待wait的条件发生变化
2. Condition实现通信
关键宇synchronized与wait0和notityQ/notifyAll0方法相结合可以实现等待/通知模式,类似ReentrantLock也可以实现同样的功能,但需要借助于Condition对象。
关于Condition实现等待/通知就不详细介绍了,可以完全类比waitO/notify0,基本使用和注意事项完全一致。
就只简单介绍下类比情況:
condition.await()->lock.wait()
condition.signal()->lock.notify()
condition.signaAlI()->lock.notifyAlI()
2、生产者与消费者
public class Produce {
public Object object;
public ArrayList<Integer> list;//用list存放生产之后的数据,最大容量为1
public Produce(Object object,ArrayList<Integer> list ){
this.object = object;
this.list = list;
}
public void produce() {
synchronized (object) {
/*只有list为空时才会去进行生产操作*/
try {
while(!list.isEmpty()){
System.out.println("生产者"+Thread.currentThread().getName()+" waiting");
object.wait();
}
int value = 9999;
list.add(value);
System.out.println("生产者"+Thread.currentThread().getName()+" Runnable");
object.notifyAll();//然后去唤醒因object调用wait方法处于阻塞状态的线程
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Consumer {
public Object object;
public ArrayList<Integer> list;//用list存放生产之后的数据,最大容量为1
public Consumer(Object object,ArrayList<Integer> list ){
this.object = object;
this.list = list;
}
public void consmer() {
synchronized (object) {
try {
/*只有list不为空时才会去进行消费操作*/
while(list.isEmpty()){
System.out.println("消费者"+Thread.currentThread().getName()+" waiting");
object.wait();
}
list.clear();
System.out.println("消费者"+Thread.currentThread().getName()+" Runnable");
object.notifyAll();//然后去唤醒因object调用wait方法处于阻塞状态的线程
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ProduceThread extends Thread {
private Produce p;
public ProduceThread(Produce p){
this.p = p;
}
@Override
public void run() {
while (true) {
p.produce();
}
}
}
public class ConsumeThread extends Thread {
private Consumer c;
public ConsumeThread(Consumer c){
this.c = c;
}
@Override
public void run() {
while (true) {
c.consmer();
}
}
}
public class Main {
public static void main(String[] args) {
Object object = new Object();
ArrayList<Integer> list = new ArrayList<Integer>();
Produce p = new Produce(object, list);
Consumer c = new Consumer(object, list);
ProduceThread[] pt = new ProduceThread[2];
ConsumeThread[] ct = new ConsumeThread[2];
for(int i=0;i<2;i++){
pt[i] = new ProduceThread(p);
pt[i].setName("生产者 "+(i+1));
ct[i] = new ConsumeThread(c);
ct[i].setName("消费者"+(i+1));
pt[i].start();
ct[i].start();
}
}
}
如果按以上代码通过创建多个生产者和消费者线程实现多生产和多消费的情况,将会出现假死。因为notify的可能是同类型线程,即消费者唤醒的是消费者。
解决办法:将notify或signal改为nitifyAll或signalAll;
四、volitile关键字
理解volitile确保可见性需要先理解JAVA内存模型;
1、JAVA内存模型
Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之问也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
![](https://img.haomeiwen.com/i5343604/d264ce75ed8c7e23.png)
2、并发编程三大概念
1. 原子性
- 原子性定义:
即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行。
- JAVA中原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
注意:只有简单的读取、賦值(而且必须是将数字赋值给某个变量,变量之间
的相互赋值不是原子操作)才是原子操作。
x=10; //语句1
y = x;//语句2
x++;//语句3
x= x+1;//语句4
只有语句1是原子性;
语句2:包含两个操作:读取x的值;将x的值写入工作内存;
语句3和语句4:包含三个操作:读取x的值;将x的值加1;写入新值;
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于
synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就保证了原子性。
2. 可见性
-
可见性定义:
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 -
JAVA可见性:
- 对于可见性,Java提供了volatile 关键字来保证可见性。
- 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
- 普通共享变量无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能 天证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3. 有序性
- 有序性定义:
程序执行的顺序按照代码的先后顺序执行。
原因:1、指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
2、指令重排序不会影响单个线程的执行,但会影响到线程并发执行的正确性。
- JAVA有序性:
在Java里面,可以通过volatile关键字来保证一定的"有序性”。另外可以通过
synchronized 和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性虚拟机可以随意地对它们进行重排序。
happens-before原则(先行发生原则):
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
- 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
- happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
3、volatile的作用
两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(即可见性)
- 禁止进行指令重排序。(即有序性)
1. volatile保证可见性
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,
2. volatile不能保证原子性
通过锁来保证原子性,或者通过Atomic
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基木叛据类型的自增(加1操作),自減(減1操作)、以及加法操作(加一个数)
,减法操作(減一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的 (Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
3. volatile保证有序性
volatile关键字能够禁止指令重排序保证了有序性
volatile关键字禁止指令重排序有两层意思:
- 当程序执行到volatile变 量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将在对volatile变量的读操作或者写操作的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
4、volatile的原理
1. 可见性
处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。
如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条LOck前缀的指令,将这个变量所在缓存行的数据写会到系统内存。这一步确保了如果有其他线種对声明了volatile变量进行修改,则立即更新主内存中数据。
但这时候其他处理器的缓存还是日的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过噢探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的、
2. 有序性
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到內存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
5、volatile的应用场景
1、终止线程的状态标记量
2、单例模式double check
public class App {
private static volatile App instance = null;
private App() {
}
private static App getInstance() {
if (instance == null) {
synchronized (App.class) {
if (instance == null) {
instance = new App();
}
}
}
return instance;
}
}
注意点:
1、第一次判空:避免进入锁,减少性能开销;
2、第二次判空:如果实例为空,则进行创建;
3、volatile关键字:保证有序性;
因为 instance = new App();不是原子操作共三步:
第一步:是给 singleton 分配内存空间;
第二步:开始调用 Singleton 的构造函数等,来初始化 singleton;
第三步:将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。
指令重排序的优化可能导致执行顺序为132步骤,所以返回的是null导致报错。
五、JAVA并发集合
1、ArrayBlockingQueue
- 大小固定的BlockingQueue;
- 底层是数组维护;
- 队列元素按照FIFO规则;
- 一旦创建,大小不可更改;
- 通过ReentrantLock公平锁实现公平策略;
1. 并发实现原理
底层通过ReentrantLock和Condition完成并发控制;
2. 继承关系
ArrayBlockingQueue实现了BlockingQueue;
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, Serializable {
}
BlockingQueue继承Queue;
public interface BlockingQueue<E> extends Queue<E> {
// 尝试往队列尾部添加元素,添加成功则返回true,添加失败则抛出IllegalStateException异常
boolean add(E e);
// 尝试往队列尾部添加元素,添加成功则返回true,添加失败则返回false
boolean offer(E e);
// 尝试往队列尾部添加元素,如果队列满了,则阻塞当前线程,直到其能够添加成功为止
void put(E e) throws InterruptedException;
// 尝试往队列尾部添加元素,如果队列满了,则最多等待指定时间,
// 如果在等待过程中还是未添加成功,则返回false,如果在等待
// 过程中被中断,则抛出InterruptedException异常
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
// 尝试从队列头部取出元素,如果队列为空,则一直等待队列中有元素
E take() throws InterruptedException;
// 尝试从队列头部拉取元素,如果队列为空,则最多等待指定时间,
// 如果等待过程中拉取到了新元素,则返回该元素,
// 如果等待过程被中断,则抛出InterruptedException异常
E poll(long timeout, TimeUnit unit) throws InterruptedException;
// 获取当前队列剩余可存储元素的数量
int remainingCapacity();
// 从队列中移除指定对象
boolean remove(Object o);
// 判断队列中是否存在指定对象
boolean contains(Object o);
// 将队列中的元素转移到指定的集合中
int drainTo(Collection<? super E> c);
// 从队列中最多转移maxElements个元素到指定集合中
int drainTo(Collection<? super E> c, int maxElements);
}
Queue的源码:
public interface Queue<E> extends Collection<E> {
boolean add(E var1);
boolean offer(E var1);
E remove();
E poll();
E element();
E peek();
}
方法 | 描述 |
---|---|
add | 插入元素到队尾,队列满则报错; |
offer | 插入元素到队尾,队列满则返回false; |
remove | 获取头部元素并删除,队列空则报错; |
poll | 获取头部元素并删除,队列空则返回null; |
element | 获取第一个元素不删除,队列空则报错; |
peek | 获取头部元素不删除,队列空则返回null; |
2、LinkedBlockingQueue
- 大小可由用户指定也可无界的BlockingQueue;
- 底层是链表维护;
- 通过ReentrantLock公平锁实现公平策略;
1. 并发实现原理
底层通过ReentrantLock和Condition完成并发控制;
2. ArrayBlockingQueue和LinkedBlockingQueue
- ArrayBlockingQueue
- 底层基于数组,创建即指定存储大小,是有界的阻塞队列;
- 入队和出队使用的是一个lock,读取和插入操作无法并行;
- ArrayBlockingQueue
- 底层基于链表,可指定大小,不指定则是Integer.MAX_VALUE,是无界的阻塞队列;
- 读取和插入使用的是两个锁,读取和插入操作可以并行;
3、ConcurrentHashMap
HashMap非线程安全,多线程并发可能死循环。
多线程使用Map解决方案:
1、HashTable:对读写进行加锁操作,效率低
2、Collections.synchronizedMap(hashMap):对读写进行加锁操作,效率低
3、ConcurrentHashMap:依赖JAVA内存模型;
1、ConcurrentHashMap实现原理
底层采用数组-链表-红黑树存储结构;
利用CAS+Synchronized保证并发更新的安全;
2、ConcurrentHashMap特点
ConcurrentHashMap是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新;
实例化时根据参数调整table大小;
table初始化在第一次put的时候;
table扩容分两部分,创建一个2倍大的新tabel,将table数据复制到新table;
网友评论