【JAVA多线程03】共享模型之管程

共享问题

问题举例

多个线程共享一个变量,对变量的值进行读取和修改,会出现这个变量的最终值不符合预期的结果。这是由分时机制导致的。举例以下代码:

@Slf4j
public class Test01 {
    static int j = 9;
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Runnable bolWatter = ()->{
            for(int i=0; i<5000; i++){
                j++;
            }
        };
        Runnable mulAction = ()->{
            for(int i=0; i<5000; i++){
                j--;
            }
        };
        Thread t2 = new Thread(bolWatter, "t2");
        Thread t3 = new Thread(mulAction, "t3");

        t2.start();
        t3.start();

        t2.join();
        t3.join();

        log.info("j: {}", j);
    }
}

实际上在字节码中,他是做如下操作,不是一步到位,cpu时间分片随时都有可能停止,导致指令交错的问题

临界区

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源,虽然多个线程读取共享资源没有问题,但是如果多个线程对共享资源进行读写操作时发生了指令交错,就很可能会出现意想不到的情况
  • 一段代码块如果存在对共享资源的多线程读写操作,称这段代码为临界区。
  • 举例,对共享资源进行读写的方法块

竞态条件

多个线程在临界区执行,由于代码的执行序列不同而导致无法预测的结果,称之为发生了竞态条件

synchronized

应用之互斥

为了解决上面提到的竞态条件的发生,有以下几点方法

  • 阻塞式的方案:synchronized,Lock(加锁)
  • 非阻塞式的方案:原子变量

相关语法

synchronized(对象) // 线程1, 线程2(blocked)
{
 临界区
}

使用示例

针对刚刚所提及的线程安全问题的代码进行修改

@Slf4j
public class Test01 {
    static int j = 0;


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Runnable bolWatter = ()->{
            for(int i=0; i<50000; i++){
                synchronized (Test01.class){
                    j++;
                }
            }
        };
        Runnable mulAction = ()->{
            for(int i=0; i<50000; i++){
                synchronized (Test01.class){
                    j--;
                }
            }
        };
        Thread t2 = new Thread(bolWatter, "t2");
        Thread t3 = new Thread(mulAction, "t3");

        t2.start();
        t3.start();

        t2.join();
        t3.join();

        log.info("j: {}", j);
    }
}

发现运行代码之后已经没有错误了。

你可以做这样的类比:
synchronized( 对象 ) 中的对象,可以想象为一个房间( room ),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1 t2 想象成两个人
当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了
这中间即使 t1 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙, t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count -- 代码

 案例分析

如果是静态方法,那么锁就是类对象,不是this ,这里不会互斥

线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

可以给类加上final增加类的线程安全性,防止被继承,重写方法,然后开启多线程

class ThreadSafe {
 public final void method1(int loopNumber) {
     ArrayList<String> list = new ArrayList<>();
     for (int i = 0; i < loopNumber; i++) {
         method2(list);
         method3(list);
     }
 }

 private void method2(ArrayList<String> list) {
     list.add("1");
 }

 private void method3(ArrayList<String> list) {
     list.remove(0);
 }

}


class ThreadSafeSubClass extends ThreadSafe{
 @Override
 public void method3(ArrayList<String> list) {
     new Thread(() -> {
         list.remove(0);
     }).start();
     }
}

java中 arrayList的线程代替是vector

Monitor

Java对象头

以32位虚拟机为例

普通对象的对象头在内存中的结构如下图所示,klass word是指向所从属的class,可以找到他的类对象。

 mark word结构如下图所示

 举例:int占4字节,Integer加上对象头8加上值4字节,一共12字节

Monitor(锁)

Monitor被翻译为监视器或者管程,每个java对象都可以 关联Monitor对象。

如果使用synchronized给对象上锁(重量级)之后,该对象的Mark word中就被设置指向Monitor对象的指针

 

 原理之synchronized

 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但是多线程访问时间是错开的,也就是没有竞争,那么可以用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是synchronized

首先解释下以下代码块:

 

 

 

感觉不错的一篇文章:synchronized中重量级锁、偏向锁和轻量级锁的区别_叫我猴哥的博客-CSDN博客_轻量级锁和重量级锁的区别Java1.5之前synchronized是一个重量级锁,Java 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。我们先介绍重量级锁,然后介绍优化后的轻量级锁和偏向锁。0.对象头以及Mark Word1重量级锁重量级的synchronized有三个用法:普通同步方法,锁的是当前实例对象。静态同步方法,锁的是当前类的clas...https://blog.csdn.net/qq_24598059/article/details/98475865

 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

下图所示,线程1发现锁对象的状态为已经时轻量级锁

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果线程自旋成功,即这时候持有锁的线程已经退出了同步块,释放了锁,这时当前线程就可以避免阻塞。自旋可以理解为竞争资源的时候,线程不马上进行阻塞,而是原地观察,如果这段时间还没能等待锁,才进入阻塞状态

 偏向锁

 轻量级锁在没有竞争时(就只有自己这个线程),每次重入仍然需要执行CAS操作,这样仍然比较耗时,java 6中引用偏向锁来进一步优化:只有第一次使用CAS将线程ID设置到对象的mark word头,之后发现这个线程ID是自己的就表示没有经常,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

重入:在加了锁的代码块中调用其他方法,其他方法同样加了这个对象的锁,如下图所示

 

偏向锁的失效

1.偏向锁通过对象头状态区分,有无偏向锁是取决于倒数第三个字节

2.调用对象hashCode方法会撤销对象的偏向锁,因为偏向锁会让对象头失去hashCode

3.多个线程访问同一个对象锁也会让偏向锁失效。一个线程当他第一次获得对象锁时,对象锁会成为偏向锁,当他运行期间有其它线程来竞争,会升级为重量锁。如果运行期间没有其他线程来竞争,会维持偏向锁(线程Id有存储),如果在这期间有其它线程来到临界区,就会把偏向锁升级为轻量锁

4.调用wait/notify也会使偏向锁失效,因为这两个操作是存在于重量级锁

批量重偏向

 多个线程访问同一个锁对象,但没有竞争,这是偏向了线程1的对象仍有机会重新偏向T2,重偏向会重置对象的ThreadID 。

当撤销偏向锁阈值超过20次以后,jvm会觉得我是不是偏向错了呢?于是就会在给这些对象加锁时重新偏向至加锁线程

批量撤销

当撤销偏向锁阈值超过40次后,jvm会觉得偏向锁撤销太频繁了,根本就不应该偏向,就会将这个类的所有对象变为不可偏向,就算是新建的对象也是不可偏向的

锁消除

JIT是即时编译器会对反复执行的代码进行优化,如果一个锁对象不会被共享,那么会被优化成没有加锁,这就是锁消除

wait/notify

为了防止一个线程长时间占用锁(这个线程在等待其他资源),导致其他竞争相同锁对象的线程一直在阻塞,我们可以使用wait和notify,可以让线程等待,让其他线程notify唤醒。

wait/notify原理

waiting和blocked的区别是。waiting是获得了锁,但是又释放了锁的使用权,blocked是没有获得过锁,在等待锁。

API介绍

obj.wait()让进入object监视器的线程到waitSet等待

obj.notify()在object上正在witSet等待的线程挑一个唤醒

obj.notifyAll()让object上正在waitSet等待的线程全部唤醒

 以上方法都是Obejct对象的方法,必须获得此对象的锁,才能调用这几个方法

wait和notify的正确使用姿势

  • 首先来看看sleep和wait的区别
    • sleep是Thread方法,而wait是Object方法
    • sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
    • sleep在睡眠的同时,不会释放对象锁的,但wait在等待的时候会释放对象锁
    • 他们的共同点就是状态都是TIMED_WAITING
  • 如果多个线程在wait,那么notify可能会错误唤醒线程(虚假唤醒),可以改成notifyAll,然后让代码块改成循坏,如果被唤醒就判断条件是否成立,不成立就重新进入等待状态
  • synchronized(lock) {
     while(条件不成立) {
         lock.wait();
     }
     // 干活
    }
    
    //另一个线程
    synchronized(lock) {
     lock.notifyAll();
    }

模式之保护性暂停 

同步模式之保护性暂停

  • 即guarded suspension,用在一个线程等待另一个线程的执行结果
  • 要点
    • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
    • 如果有结果不断从一个线程到另一个线程,那么可以视同消息队列
    • JDK,join的实现,future的实现,采用的就是此模式
    • 因为要等待另一方的结果,因此归类到同步模式

     以下代码和用join方法相比,消费线程不需要等待生产线程结束,只要被唤醒了就可以执行

    import lombok.extern.slf4j.Slf4j;
    @Slf4j
    public class GuarderObject {
        private Object response;
    
        public Object getResponse(){
            synchronized (this){
                while(response == null){
                    try {
                        log.info("等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            log.info("获取结果");
            return response;
        }
    
        public void setResponse(Object response){
            synchronized (this){
                log.info("赋值");
                this.response = response;
                log.info("唤醒");
                this.notifyAll();
            }
        }
    }
    
    class Test{
        public static void main(String[] args) {
            GuarderObject guarderObject = new GuarderObject();
    
            Runnable r1 = guarderObject::getResponse;
            Runnable r2 = () -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                guarderObject.setResponse("213");
            };
    
            Thread t1 = new Thread(r1, "t1");
            Thread t2 = new Thread(r2, "t2");
    
            t1.start();
            t2.start();
        }
    }
    

原理之join

 扩展

Future

 异步模式之生产者/消费者

@Slf4j
public class Test02 {
    public static void main(String[] args) {
        MessageQueue messageQueue = new MessageQueue(1);
        for(int i=0; i<3; i++){
            int finalI = i;
            new Thread(() -> {
                messageQueue.put(new Message(finalI, "消息"+finalI));
            }, "生成者"+i).start();
        }
        new Thread(() -> {
            while(true){
                try {
                    Thread.sleep(1000);
                    messageQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者").start();
    }
}

@Slf4j
class MessageQueue{
    private final LinkedList<Message> messageQueue = new LinkedList<>();
    private int capacity;

    MessageQueue(int capacity) {
        this.capacity = capacity;
    }


    //获取消息
    public Message take(){
        synchronized (messageQueue){
            while(messageQueue.isEmpty()){
                try {
                    log.info("队列为空");
                    messageQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Message message = messageQueue.removeFirst();
            log.info("take message: {}", message.getMessage());
            messageQueue.notifyAll();
            return message;
        }
    }

    public void put(Message message){
        synchronized (messageQueue){
            while(capacity == messageQueue.size()) {
                try {
                    log.info("队列已满");
                    messageQueue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            messageQueue.addLast(message);
            log.info("put message: {}", message.getMessage());
            messageQueue.notifyAll();
        }
    }
}

final class Message{
    private int id;
    private Object message;

    public Message(int id, Object message) {
        this.id = id;
        this.message = message;
    }

    public int getId() {
        return id;
    }

    public Object getMessage() {
        return message;
    }

}

park&Unparl

他们是LockSupport类中的方法

 wait/notify和park/unpark区别

park/unpark原理

 

 park底层原理

unpark底层原理 

线程状态转换

重新理解线程状态转换 

假设有线程 Thread t
情况 1 NEW -- > RUNNABLE
当调用 t.start() 方法时,由 NEW -- > RUNNABLE
情况 2 RUNNABLE < -- > WAITING
t 线程 synchronized(obj) 获取了对象锁后
调用 obj.wait() 方法时, t 线程 RUNNABLE -- > WAITING
调用 obj.notify() obj.notifyAll() t.interrupt()
竞争锁成功, t 线程 WAITING -- > RUNNABLE
竞争锁失败, t 线程 WAITING -- > BLOCKED
情况 3 RUNNABLE < -- > WAITING
当前线程 调用 t.join() 方法时, 当前线程 RUNNABLE -- > WAITING
注意是 当前线程 t 线程对象 的监视器上等待
t 线程 运行结束,或调用了 当前线程 interrupt() 时, 当前线程 WAITING -- > RUNNABLE
情况 4 RUNNABLE < -- > WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE -- > WAITING
调用 LockSupport.unpark( 目标线程 ) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -- >
RUNNABLE
情况 5 RUNNABLE < -- > TIMED_WAITING
t 线程 synchronized(obj) 获取了对象锁后
调用 obj.wait(long n) 方法时, t 线程 RUNNABLE -- > TIMED_WAITING
t 线程 等待时间超过了 n 毫秒,或调用 obj.notify() obj.notifyAll() t.interrupt()
竞争锁成功, t 线程 TIMED_WAITING -- > RUNNABLE
竞争锁失败, t 线程 TIMED_WAITING -- > BLOCKED
情况 6 RUNNABLE < -- > TIMED_WAITING
当前线程 调用 t.join(long n) 方法时, 当前线程 RUNNABLE -- > TIMED_WAITING
注意是 当前线程 t 线程对象 的监视器上等待
当前线程 等待时间超过了 n 毫秒,或 t 线程 运行结束,或调用了 当前线程 interrupt() 时, 当前线程
TIMED_WAITING -- > RUNNABLE
情况 7 RUNNABLE < -- > TIMED_WAITING
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE -- > TIMED_WAITING
当前线程 等待时间超过了 n 毫秒, 当前线程 TIMED_WAITING -- > RUNNABLE
情况 8 RUNNABLE < -- > TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos) LockSupport.parkUntil(long millis) 时, 当前线
RUNNABLE -- > TIMED_WAITING
调用 LockSupport.unpark( 目标线程 ) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从
TIMED_WAITING -- > RUNNABLE
情况 9 RUNNABLE < -- > BLOCKED
t 线程 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE -- > BLOCKED
obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程 竞争
成功,从 BLOCKED -- > RUNNABLE ,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE < -- > TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED

多把锁

如果有几个操作其实是互不相干的,但是如果只用一个对象锁的话,那么他们就是类似于串行,并发度很低,我们的解决办法就是使用多把锁

活跃性

死锁

情况:有一个线程需要同时获取多把锁,这时就容易发生死锁

 活锁

两个线程共享了一个变量,这个变量决定了结束条件,如果两个线程相互让共享变量往两个不同的方向改变,那么线程就会一直运行,停止不了

饥饿

一个线程运行优先级太低,始终的不到CPU时间片,也不能结束。

顺序加锁可以避免前面说的死锁现象,两个线程获取锁的顺序是一致的

ReentrantLock

相比较于synchronized它具备以下特点:

  1. 可中断,不像其他说拿到了锁就没办法放开
  2. 可设置超时时间,可以设置等待锁的时间
  3. 可设置为公平锁,等待队列先进先出
  4. 支持多个条件变量,你不满足什么条件就去那个条件队列等待

与synchronized一样,都支持可重入

可重入

可重入指的是一个线程获取了锁之后仍然有权利在此获取这把锁,如果是不可重入锁,在第二次获得锁的时候,自己也会被锁挡住

可打断

当使用ReentrantLock的lockInterruptibly的时候,他在等待锁的时候可以被其它线程打断,就会抛出异常。防止无限制打断下去,也是一种避免死锁的方法

public class ReentrantLockTest {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            try {
                lock.lockInterruptibly();
                try {

                }finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                log.info("打断");
                e.printStackTrace();
                return;
            }
        }, "t1");

        lock.lock();
        try {
            t1.start();
            Thread.sleep(500);
            t1.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

可超时

立即失败:

@Slf4j
public class ReentrantLockTest {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            if(!lock.tryLock()){
               log.info("获取不到锁");
               return;
            }
            try {
                log.info("获取到锁");
            }finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        try {
            t1.start();
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

公平锁

ReentrantLock默认是不公平的,公平锁一般是没有必要的,会降低并发度。

条件变量

就是线程等待的时候(wait)进入了休息室,之前都是进入同一个休息室,唤醒的话会唤醒整个休息室的所有线程,但是ReetrantLock是可以做到多个休息室的。

 

@Slf4j
public class ConditionTest {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Condition condition = lock.newCondition();

        Thread t1 = new Thread(()->{
            lock.lock();
            try {
                log.info("拿到锁t1");
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                log.info("醒了");
                lock.unlock();
            }
        }, "t1");

        t1.start();
        Thread.sleep(1000);
        log.info("拿到锁main");
        lock.lock();
        try {
            log.info("唤醒");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

同步模式之顺序控制

交替打印abc

@Slf4j
public class Test03 {
    static boolean turnToTwo = false;

    public static void main(String[] args) throws InterruptedException {
        Bag bag = new Bag(5);

        Condition c1 = bag.newCondition();
        Condition c2 = bag.newCondition();
        Condition c3 = bag.newCondition();


        new Thread(()->{
            bag.print("a", c1, c2);
        },"t1").start();

        new Thread(()->{
            bag.print("b", c2, c3);

        },"t2").start();

        new Thread(()->{
            bag.print("c", c3, c1);

        },"t3").start();

        Thread.sleep(2000);
        bag.lock();
        try {
            c1.signal();
        }finally {
            bag.unlock();
        }
    }

    static class Bag extends ReentrantLock {
        private int loopNumber;

        public Bag(int loopNumber){
            this.loopNumber = loopNumber;
        }

        public void print(String str, Condition condition, Condition next){
            for (int i=0; i<loopNumber; i++){
                lock();
                try {
                    try {
                        condition.await();
                        log.info(str);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }finally {
                    next.signal();
                    unlock();
                }
            }
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值