2、共享模型之管程

该栏目讲叙多线程基础、共享模型的内存、管程、无锁、不可变设计和多线程并发工具


多线程共享同一资源问题

1、问题简述

  • :多个线程同时共享一个成员变量,且都对该变量进行非原子性的写操作,由于分时调度系统,引发线程上下文切换,无法保证非原子性的写操作一定将结果写入内存,所以其他线程操作该变量时不一定正确的问题

2、问题举例

/**
 * 线程安全问题:由于分时系统,引发线程上下文切换,导致线程安全问题
 */
public class SafeThreadQuestion {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count--;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("count = " + count);
    }

}

3、问题分析

  • 从字节码层面理解 ++i(java 对静态变量的自增、自减不是原子操作)
getstatic i // 获取静态变量的值i
iconst_1    // 准备常量1
iadd        // 自增
putstatic i // 将修改后的值存入静态变量i
  • java 的内存模型理解:完成静态变量的自增、自减需要在主存和工作内存中进行数据交换
    内存模型理解

4、相关概念

  • 临界区:存在多线程共享问题的代码块,称这段代码块为临界区
static int count = 0;

static void increment()
// 临界区
{
    count++;
}

static void decrement()
// 临界区
{
    count--;
}
  • 竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

5、线程安全分析

  • 成员变量
    • 成员变量没有被共享,则线程安全
    • 成员变量被共享,但只有读操作,则线程安全;如果读写操作都有,则考虑线程安全
  • 静态变量
    • 静态变量没有被共享,则线程安全
    • 静态变量被共享,但只有读操作,则线程安全;如果读写操作都有,则考虑线程安全
  • 局部变量
    • 在调用方法中时,局部变量在每个线程的栈帧内存中被创建,所以是线程安全的。但局部变量的引用对象则未必
      • 对象没有离开方法作用范围,则线程安全
      • 对象离开方法作用范围,则考虑线程安全(子类重写方法,并启动新的线程;将引用对象返回)

6、解析方案

  • 阻塞式:synchronized、Lock
  • 非阻塞式:原子变量

7、synchronized 解决方案

  • 概述:它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程想获取这个对象锁就会被阻塞,这样就保证了临界区代码的原子性,不用担心线程上下文切换
  • 注意
    • 互斥是保证临界区的竞态条件不发生,同一时刻只有一个线程执行临界区代码
    • 同步量由于线程执行的先后,顺序不同,需要一个线程等待其他线程运行到这个点
  • 语法

对象头

  • 普通对象:Mark Word(32bit) + Klass Word(32bit)
    对象头
  • 数组对象:Mark Word(32bit) + Klass Word(32bit)+ 数组长度(32bit)

Monitor

  • 概述:即管程,是由系统所创建的。每个 Java 对象都可以关联一个 Monitor 对象,当使用 synchronized 给对象上锁之后,该锁对象的 Mark Word 就会指向该 Monitor 对象
  • 工作原理
    Monitor工作原理
  • 当 Thread-2 线程执行到 synchronized 就会将 Monitor 的 Owner 置为 Thread-2
  • 在 Thread-2 上锁的过程中,如果 Thread-3、Thread-4、Thread-5 也来执行 synchronized,就会进入 EntryList,线程状态为 BLOCKED
  • 在 Thread-2 执行完同步代码块的内容后,恢复对象锁的 Mark Word 并释放锁,唤醒 EntryList 中等待的线程来竞争锁
  • Thread-0 和 Thread-1 是之前获得过锁,但条件不满足进入WaitSet,线程状态为 WAITING

synchronized 优化原理

1、轻量级锁

  • 场景:锁对象虽然被多个线程访问,但多线程之间访问的时间是错开的(锁之间没有竞争),那么可以使用轻量级锁来优化
  • 举例
static final Object obj = new Object();
public static void method1(){
    synchronized(obj){
        method2();
    }
}

public static void method2(){
    synchronized(obj){
       
    }
}
  • 原理
    • 当执行到 synchronized 时,每个线程的栈帧都会创建一个锁记录,其内部可以存储锁对象的 Mark Word
      轻量级锁原理1
    • 让锁记录中的 Ojbect reference 指向锁对象,并尝试用CAS替换锁对象的 Mark Word,将 Mark Word 的值存入锁记录
      轻量级锁原理2
    • 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00,表示由该线程给对象加锁
      在这里插入图片描述
    • 如果 CAS 失败,有两种情况
      • 如果是其他线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
      • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
        轻量级锁原理3
    • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
    • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级解锁流程

2、锁膨胀

  • 场景:在尝试加轻量级锁过程中,如果 CAS 操作无法成功,这时有一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进入锁膨胀,将轻量级锁变为重量级锁
  • 过程
    • 当 Thread-1 进入轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
      锁膨胀过程1
    • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
      • 即为 Ojbect 对象申请 Monitor 锁,让 Ojbect 指向重量级锁地址
      • 然后自己进入 Monito r的 EntryList
        在这里插入图片描述
    • 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,失败,这时会进入重量级锁解锁流程,即按 照Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中的 BLOCKED 线程

3、自旋优化

  • 概述:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当线程自旋成功(即这时候持锁对象已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
  • 注意
    • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
    • 在 Java 6 之后自旋是自适应的,Java 7 之后不能控制是否开户自旋功能

4、偏向锁

  • 概述:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS,以后只要不发生竞争,这个对象就归该线程所有
  • 偏向状态
    • 开启偏向锁,对象创建后,其 Mark Word 的最后 3 位为 101,这时它的 thread、epoch、age 为 0
    • 如果没有开启偏向锁,那么创建对象后,其 Mark Word 的最后 3 位为 001,这时它的hashcode,age都为0,第一次用到 hashcode 时才会赋值
    • 偏向锁默认是延迟的,如果想禁用延迟,可以加 VM 参数 -xx:BiasedLockingStartupDelay = 0
  • 撤销
    • 调用对象的 hashcode
    • 其他线程使用偏向锁对象
    • 调用 wait / notify
  • 批量重偏向:如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID,当撤销偏向锁阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢,于是会给这些对象加锁时重新偏向到加锁线程
  • 批量撤销:当撤销偏向锁阈值超过40次后,JVM会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都为变为不可偏向的,新建的对象也是不可偏向的

5、锁清除

  • :JIT(即时编程器)会对Java的字节码文件进行进一步的优化。如果加锁的对象在方法内,且没有离开方法范围,则会将锁清除,提高效率

等待唤醒机制

1、相关 API

描述方法
持锁线程会释放对象的锁,并进入 waitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到调用 notify() 为止wait()
有时限的等待, 到 n 毫秒后结束等待,或是被 notify() 为止wait(long n)
唤醒 waitSet 等待中的一个线程notify()
唤醒 waitSet 等待中的所有线程notifyAll()

2、wait & sleep 的区别

  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要强制与 synchronized 配合使用,但 wait 需要
  • sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
  • 它们状态都是 TIMED_WAITING

3、使用方式

synchronized(lock) {    
	while(条件不成立) {        
    	lock.wait();    
	}
    // 干活 
}
 
// 另一个线程 
synchronized(lock) {    
    lock.notifyAll(); 
}

4、wait & notify 原理

wait & notify 原理

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

5、保护性暂停模式

  • 描述:一个线程等待另一个线程的结果
  • 实现
/**
 * 资源实体
 */
@Getter
public class Resource<T> {

    private final int resId;

    private final T content;

    public Resource(int resId, T content) {
        this.resId = resId;
        this.content = content;
    }
}

///

/**
 * 资源警卫
 */
public class ResourceGuard<T> {

    private Resource<T> resource;

    /**
     * 获取资源
     *
     * @return 资源
     */
    public Resource<T> getResource() {
        synchronized (this) {
            while (resource == null) {
                try {
                    System.out.println("资源耗尽,等待资源生产...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return resource;
        }
    }

    /**
     * 获取资源,带超时
     *
     * @param waitTime 超时时间
     * @return 资源
     */
    public Resource<T> getResource(long waitTime) {
        synchronized (this) {
            final long startTime = System.currentTimeMillis();
            long spendTime = 0;
            while (true) {
                waitTime = waitTime - spendTime;
                if (waitTime <= 0) {
                    System.out.println("等待超时...");
                    break;
                }
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                spendTime = System.currentTimeMillis() - startTime;
            }
            return resource;
        }
    }

    /**
     * 生产资源
     *
     * @param content 内容
     */
    public void produceResource(T content) {
        synchronized (this) {
            System.out.println("生产资源...");
            this.resource = new Resource<>((int) (Math.random() * 100), content);
            this.notifyAll();
        }
    }

}

///

/**
 * 测试类
 */
public class TestDemo {

    public static void main(String[] args) {
        ResourceGuard<String> resourceGuard = new ResourceGuard<>();
        new Thread(() -> {
            final Resource<String> resource = resourceGuard.getResource(3000);
            if (resource != null) {
                System.out.println(resource.getResId() + "==" + resource.getContent());
            }
        }, "T1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resourceGuard.produceResource("Hello Java");
        }, "T2").start();
    }

}

6、生产者消费者模式

  • 描述
    • 消费队列可以用来平衡生产和消费的线程资源
    • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
    • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • 实现
/**
 * 消息实体
 */
@Getter
public class Message {

    private final int msgId;

    private final String content;

    public Message(int msgId, String content) {
        this.msgId = msgId;
        this.content = content;
    }
}

///

/**
 * 消息队列
 */
@Setter
public class MessageQueue {

    private final LinkedList<Message> queue;

    private int capacity;

    public MessageQueue(int capacity) {
        this.capacity = capacity;
        this.queue = new LinkedList<>();
    }

    public Message take() {
        synchronized (queue) {
            if (queue.isEmpty()) {
                try {
                    System.out.println("消息队列没有消息,等待生产...");
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            final Message message = queue.removeFirst();
            queue.notifyAll();
            return message;
        }
    }

    public void putMessage(Message message) {
        synchronized (queue) {
            if (queue.size() == capacity) {
                try {
                    System.out.println("消息队列已满了,等待消费");
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            queue.add(message);
            queue.notifyAll();
        }
    }
}

///

/**
 * 测试类
 */
public class TestDemo {

    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Message message = new Message(i, "Hello " + i);
                queue.putMessage(message);
            }
        }, "producer").start();

        new Thread(() -> {
            while (true) {
                final Message message = queue.take();
                System.out.println(message.getMsgId() + "==" + message.getContent());
            }
        }, "consumer").start();
    }

}

park & unpark

1、简介

  • wait & notify 和 notifyAll 必须配合 Object Monitor 一起使用,而park&unpark不必
  • park & unpark 是以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么精确
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

2、原理

  • 先 park 后 unpark
    park & unpark 原理1
    park & unpark 原理2

  • 先 unpark 后 park
    park & unpark 原理3

3、实现

public class ParkAndUnParkDemo {

    public static void main(String[] args) {
        final Thread t1 = new Thread(() -> {
            System.out.println("开始执行...");
            LockSupport.park();
            System.out.println("执行结束...");
        }, "T1");

        final Thread t2 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
                LockSupport.unpark(t1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2");

        t1.start();
        t2.start();
    }

}

活跃性

1、死锁

  • 概述:两个线程互相等待对方锁释放
  • 定位死锁:jstack 进程 ID 命令或 jconsole 工具
  • 解决死锁:顺序加锁、使用超时锁
  • 举例
public class DeadLockDemo {

    private static final Object objA = new Object();
    private static final Object objB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (objA) {
                try {
                    System.out.println("lock A");
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objB) {
                    System.out.println("lock B");
                }
            }
        }, "T1");

        Thread t2 = new Thread(() -> {
            synchronized (objB) {
                try {
                    System.out.println("lock B");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objA) {
                    System.out.println("lock B");
                }
            }
        }, "T2");

        t1.start();
        t2.start();
    }

}

2、活锁

  • 概述:两个线程互相改变对方的结束条件,最后谁也无法结束
  • 举例
public class LiveLock {

    private static volatile int count = 10;

    private static final Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            while (count > 0) {
                synchronized (obj) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(200);
                        count--;
                        System.out.println("count=" + count);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(() -> {
            while (count < 20) {
                synchronized (obj) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(200);
                        count++;
                        System.out.println("count=" + count);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

    }

}

3、饥饿

  • :一个线程的优先级太低,始终得不到 CPU 调度执行,也不能够结束

ReentrantLock

1、特性

  • 可重入:指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 可中断:reentrantLock.lockInterruptibly()
  • 可以设置超时时间:reentrantLock.tryLock(1, TimeUnit.SECONDS)
  • 可以设置为公平锁
  • 支持多个条件变量: Condition waitCigaretteQueue = lock.newCondition(); waitCigaretteQueue.await()

2、常用 API

public class ReentrantLockDemo {

    private static final ReentrantLock lock = new ReentrantLock();

    private static final Condition condition = lock.newCondition();

    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        // reentrant1();
        // interrupt();
        // lockTimeout();
        condition();
    }

    /**
     * 可重入
     */
    private static void reentrant1() {
        lock.lock();
        try {
            System.out.println("reentrant 1");
            reentrant2();
        } finally {
            lock.unlock();
        }
    }

    private static void reentrant2() {
        lock.lock();
        try {
            System.out.println("reentrant 2");
        } finally {
            lock.unlock();
        }
    }

    /**
     * 可中断
     */
    private static void interrupt() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                try {
                    lock.lockInterruptibly();
                } catch (InterruptedException e) {
                    System.out.println("线程中断...");
                    e.printStackTrace();
                }

                try {
                    System.out.println("执行1...");
                } finally {
                    lock.unlock();
                }
            }
        });
        t1.start();

        TimeUnit.SECONDS.sleep(5);
        t1.interrupt();
    }

    /**
     * 锁超时
     */
    private static void lockTimeout() {
        Thread t1 = new Thread(() -> {
            try {
                final boolean flag = lock.tryLock(3, TimeUnit.SECONDS);
                if (!flag) {
                    System.out.println("获取锁超时...");
                    return;
                }
                try {
                    System.out.println("获取锁成功...");
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
    }

    /**
     * 条件变量
     */
    private static void condition() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                lock.lock();
                while (!flag) {
                    try {
                        System.out.println("条件不满足,进入等待...");
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("执行...");
            } finally {
                lock.unlock();
            }
        });

        t1.start();

        TimeUnit.SECONDS.sleep(5);
        lock.lock();
        flag = true;
        condition.signalAll();
        lock.unlock();
    }

}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值