并发--共享模型管程

一、共享问题

安全问题,不同线程对同一数据进行操作,可能数据会被覆盖出现错误结构。字节码层面,线程读取内存中变量,操作时候,将数据存入内存。代码块存在对共享资源的多线程读写操作,此代码块为临界区,多个线程在临界区执行,代码执行序列不同导致结果无法预测,发生竞态条件

二、synchronized

关键字,同步锁。修饰对象:代码块、类、方法、静态方法。对象锁,同一时刻最多只有一个线程可以获得对象锁,其他线程处于阻塞状态,即使当前线程时间片用完,锁不会被接触,只有代码被执行完,才轮到接下来的线程。

synchronized用对象锁保证了临界区内代码的原子性,临界区内代码不会被打断,是不可分割的整体。

synchronized(对象){ 临界区}

a) 修饰方法:作用方法是调用这个方法的对象。

public synchronized void method(){
//修饰方法
}
//synchronized不能被继承,子类必须添加关键字
class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public synchronized void method() { }
}
//子类方法可以调用父类的同步方法
class Parent {
   public synchronized void method() {   }
}
class Child extends Parent {
   public void method() { super.method();   }
} 

子类不同步的方法调用父类方法,子类方法也相当于同步。接口方法不能使用synchronized关键字,构造方法不能使用。

等价于public void method(){ synchronized(this){ }}

b) 修饰代码块:作用对象是调用代码块的对象。

public void method(){
   synchronized(this){ }//对同一个对象加锁
}
//给某个对象加锁
public void method(SomeObject obj)
{
   //obj 锁定的对象
   synchronized(obj){ }
}

c) 修饰静态方法:作用对象是类的所有对象

public synchronized static void method() {
   // todo
}

d) 修饰类:作用范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

class ClassName {
   public void method() {
      synchronized(ClassName.class) { }
   }
}

线程八锁,判断synchronized锁住的是哪儿个对象,只有锁住同一个对象,才互斥。

三、线程安全分析

成员变量和静态变量:没有共享,安全。共享,读,安全;读写,需要考虑。

局部变量:线程安全,但局部变量引用的对象未必,对象没有逃离方法的作用访问,安全,逃离方法的作用范围,需要考虑。

局部变量被线程调用时,会在线程的栈帧中被创建多份,因此不存在共享。

当子类继承了父类并重写了方法,需要考虑安全问题 

线程安全类:String,Integer,StringBuffer,Random,Vector,Hashtable,java.util.concurrent包下的类,多个线程调用类的同一个实例的方法时,是线程安全的,但方法组合使用时,可能会有问题if(table.get(k)==null){ table.put(k,v);}

String,Integer是不可变类,线程安全。String的substring方法等是创建新的对象。

四、Monitor

对象头,对于普通的对象由对象头和成员变量等组成。对象头包括klass word(类的指针)和Mark word。

monitor管程、监视器,synchronized给对象上锁后,对象和monitor关联,markword中存储monitor的地址,monitor中有owner,锁当前的拥有者,当线程执行时,owner会置为相应线程。EntryList,阻塞等待队列。

五、轻量级锁

5.1 轻量级锁:当多线程时间是错开的,会使用轻量级锁,节省时间。语法仍然是synchronized, 会先优先考虑能否使用轻量级锁,不能则使用重量级锁。线程在栈帧中创建锁记录对象,包括两部分,对象指针和对象的markword。对象中mark word01表示无锁,锁记录中00表示轻量级

替换失败的情况,其他线程已经持有该对象的轻量级锁,或者自己执行了synchronized锁重入,此时会再创建一个锁记录,但锁记录中不再存储mark word,而是null。

解锁时,如果锁记录为null,清掉,如果不为null,对象头mark word和锁对象中存储值交换,恢复对象头。

5.2 锁膨胀

当存在竞争时,Thread0已经将对象的mark work置为00,轻量级锁,此时对象申请Monitor,变为重量级锁,mark word记录指向Monitor的地址,后两位变为10。Thread0解锁时,由于mark word中记录的时Monitor的地址,而非锁记录的地址,因此解锁时,进入重量级解锁的流程.

5.3 自旋优化

重量级锁竞争时,线程会先使用自旋优化,若自旋重试时,owner为空,可以避免阻塞。Java6后自旋锁是自适应的,7之后不能控制是否开启自旋,自旋会占用CPU,多核才有优势。

六、偏向锁

轻量级锁在没有竞争时,每次重入都要执行CAS,偏向锁进行优化,第一次将线程ID设置到对象MarkWord,之后发现线程ID是自己的就不会重新CAS。

如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,偏向锁是默认是延迟的。正常状态对象一开始是没有 hashCode 的,第一次调用才生成,调用了 hashCode() 后会撤销该对象的偏向锁,调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销,轻量级锁会在锁记录中记录 hashCode,重量级锁会在 Monitor 中记录 hashCode

线程1使用偏向锁对象,当有其他线程也要使用该偏向锁对象时,偏向锁会升级为轻量级锁。调用wait/notify会变为重量级锁。

6.1 批量重偏向,如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID,当撤销偏向锁阈值超过 20 次后,会在给对象加锁时重新偏向至新的加锁线程。
6.2 批量撤销(偏向)当撤销偏向锁阈值超过 40 次后,整个类的所有对象都会变为不可偏向的,新建的该类型对象也是不可偏向的.

6.3 锁消除 当偏向锁对象是局部变量且不加锁并无影响时,计时编译器会对字节码进一步优化,会自动消除锁。

七、wait、notify

当前线程条件不满足,但一直阻塞,效率太低,调用wait方法,释放锁,知道调用notify方法,线程停止等待,重新竞争锁。

Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态,BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片,BLOCKED 线程会在 Owner 线程释放锁时唤醒,WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争。

obj.wait() 让进入 object 监视器的线程到 waitSet 等待,notify() 在 object 上正在 waitSet 等待的线程中随机挑一个唤醒,notifyAll() 让正在 waitSet 等待的线程全部唤醒。它们都属于 Object 对象的方法,必须获得此对象的锁。

7.1 sleep和wait区别

sleep 是 Thread 的静态方法,而 wait 是 Object 的方法, sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用,sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁,状态为 TIMED_WAITING

notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,发生虚假唤醒,改为 notifyAll,用while+wait

while (!hasCigarette) {
    //
    try {
        room.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

7.2 同步模式之保护性暂停

Guarded Suspension,一个线程等待另一个线程的执行结果。有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject,如果有结果不断从一个线程到另一个线程那么可以使用消息队列。JDK 中,join 的实现、Future 的实现,采用的就是此模式,因为要等待另一方的结果,因此同步模式

public class Test {
    //线程1等待线程2的下载结果
    public static void main(String[] args){
        GuardeObject guardeObject = new GuardeObject();
        new Thread(()->{
            List<String> list=(List<String>)guardeObject.get();
        },"t1").start();
        new Thread(()->{
            List<String> list=Downloader.download();
            guardeObject.complete(list);
        },"t2").start();
    }
}
class GuardeObject {
    private Object response;
    //获取结果
    public Object get(){
        synchronized (this){
            while (response==null){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            return response;
        }
    }
    //产生结果
    public void complete(Object response){
        synchronized (this){
            this.response=response;
            this.notify();
        }
    }
}

多任务,设计一个用来解耦的中间类

join原理:应用保护性暂停。

7.3 异步模式之生产者、消费者

不需要产生结果和消费结果的线程一一对应,消费队列可以用来平衡生产和消费的线程资源,生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据,消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据,阻塞队列,采用这种模式

八、park,Unpark

park  unpark 是以线程为单位来阻塞和唤醒线程。

unpark在park前后都可以唤醒。

和wait/notify的区别:wait必须配合object monitor(锁)使用,park不需要;park以线程为单位阻塞唤醒unpark(Thread t),notify随机唤醒;unpark可以在park前后。

线程有一个Parker对象,由_counter,_mutex,_cond组成,先park后un,检查 _counter ,本情况为 0,需要休息,获得 _mutex 互斥锁,等待队列,线程进入 _cond 条件变量阻塞,设置 _counter = 0;unpark(Thread) ,设置 _counter 为 1,唤醒阻塞的线程。

先un后park,设置 _counter 为 1,当前线程调用 park() ,检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行,设置 _counter 为 0。

九、状态转换

九、多锁、活跃性

多锁,增加并发性,但容易发生死锁

class BigRoom {
    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();
    public void sleep() {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }
    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }
}

线程没有按预期结束,执行不下去的情况,为活跃性问题,除了死锁以外,还有活锁和饥饿者两种情况。活锁,两个线程互相改变对方的结束条件,最后谁也无法结束。饥饿,线程由于优先级太低,得不到 CPU 调度执行,不能结束。

十、ReentrantLock

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

ReentrantLock 里面有一个内部类 SyncSync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。

公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。

非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁

  • 对比synchronized

两者都是可重入锁,线程可以再次获取自己的内部锁。

区别:synchronized 是依赖于 JVM 实现的,ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)

ReentrantLock可中断等待,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。可中断锁:获取锁的过程可以中断,不可中断锁:线程申请了锁,只有拿到锁后才能进行其他逻辑。

ReentrantLock可以设置为公平锁,而synchronized只能是非公平锁

可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。多条件变量,await 前需要获得锁, 执行后,会释放锁,进入 conditionObject 等待,await 的线程被唤醒(或打断、或超时)去重新竞争 lock 锁,竞争 lock 锁成功后,从 await 后继续执行

Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”,synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而Condition实例的signalAll()方法,只会唤醒注册在该Condition实例中的所有等待线程

十一、ReentrantReadWriteLock

既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。

实现了 ReadWriteLock ,是一个可重入的读写锁,ReentrantReadWriteLock 其实是两把锁,一把是 WriteLock (写锁),一把是 ReadLock(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。

  • 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
  • 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

读锁不能升级为写锁:写锁位独占锁,升级会引起线程的争夺,还可能会有死锁的问题,例如两个线程都想升级写锁,需要对方都释放自己的锁。

十二、stampedLock

JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 Condition

不同于一般的 Lock 类,StampedLock 并不是直接实现 LockReadWriteLock接口,而是基于 CLH 锁 独立实现的。

StampedLock 提供了三种模式的读写控制模式:写锁:独占锁,读锁(悲观锁):共享锁。乐观锁:允许多个线程获取乐观锁和读锁。同时允许一个写线程获取写锁。

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值