并发编程专题-02共享模型-管程(悲观锁)

1.共享问题

1.1 Java共享问题演示

以下的结果可能是正数、负数、零。因为 Java 中对静态变量的自增,自减并不是原子操作。单线程情况下,不会出现指令交错的现象。但是在多线程环境下,可能出现指令交错运行。

//两个线程对共享的值进行修改,一个进行++,一个进行--,查看最后的结果是否为0
public class TestSecurity {

    static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 3000; i++) {
                    count++;
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 3000; i++) {
                    count--;
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count); //不一定为0
    }
}

1.2 临界区

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

上面程序代码的临界区为对资源的共享部分。

static int count = 0;
//...
// 临界区
{
 counter++;
}
//...
// 临界区
{
 counter--;
}

1.3 竞态条件

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

2. synchronized

  • synchronized 采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
  • synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

2.1 synchronized语法

synchronized(对象) 
{
 临界区
}
  • 上述问题代码的解决方案

另外准备对象,用于加锁使用。当某个线程持有该对象锁后,其他线程会被阻塞,无法访问临界资源。

public class TestSecurity {

    static int count = 0;
    //对象锁
    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 3000; i++) {
                    //临界区加锁
                    synchronized (lock){
                        count++;
                    }
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 3000; i++) {
                    //临界区加锁
                    synchronized (lock){
                        count--;
                    }
                }
            }
        };
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

2.2 方法上的 synchronized

在成员方法上面加synchronized表示对this对象加锁,在静态方法上面加synchronized表示对类加锁

class Test{
     public synchronized void test() {

     }
}
//等价于
class Test{
     public void test() {
         synchronized(this) {

         }
     }
}
class Test{
     public synchronized static void test() {
         
     }
}
//等价于
class Test{
     public static void test() {
         synchronized(Test.class) {

         }
     }
}

3.变量和线程安全分析

3.1 成员变量和静态变量的线程安全问题

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

3.2 局部变量的线程安全问题

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

注: private 或 final 提供安全的意义在于,在一定程度上防止了子类重写父类方法,引入线程安全问题。(在重写的方法中,采用多线程的方式对共享资源进行读写——非线程安全)

3.3 常见线程安全类

String,Integer,StringBuffer,Random,Vector,HashTable,java.util.concurrent包下的类。

  • 它们的每个方法是原子的
  • 但它们多个方法的组合不是原子的

如:下面代码使用多个方法的组合,非线程安全的

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}
  • String、Integer 等都是不可变类(共享的变量只读),因为其内部的状态不可以改变,因此它们的方法都是线程安全的

4 Monitor 管程

4.1 Java对象头

以32位虚拟机为例

# 普通对象 对象头包括 标记字段和Class字段(标识对象的Class类型)
|--------------------------------------------------------------|
| Object Header (64 bits)                                   |
|------------------------------------|-------------------------|
| Mark Word (32 bits)               | Klass Word (32 bits)    |
|------------------------------------|-------------------------|
# Mark Word 结构
# 01 正常状态;  01 biased_lock:1 偏向锁;   00 轻量级锁;    10 重量级锁;    11 GC
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits)                                                    | State             |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0         | 01      | Normal            |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01      | Biased            |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30                                 | 00      | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30                      | 10      | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                                                | 11       | Marked for GC      |
|-------------------------------------------------------|--------------------|

4.2 Monitor原理

Monitor管程,每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针

Monitor的结构:

image-20210615183300471.png
  • Monitor中只能有一个Owner,表示获得锁的线程
  • 未获得锁的其他线程进入EntryList中等待
  • Owner线程执行完同步代码块中的内容后,会唤醒EntryList中等待的线程来竞争锁,非公平的
  • 如果Owner线程某些条件不满足,可以调用wait()方法,进入WaitSet 等待notify()唤醒,将资源所有权释放,供其他线程访问

注:synchronized必须进入同一个对象的Monitor才有上述功能;不加synchronized的对象不会关联Monitor管程。

4.3 synchronized原理

4.3.1 轻量级锁

使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(没有竞争),则可以使用轻量级锁优化。轻量级锁对使用者是透明的,语法还是synchronized

轻量级锁的加锁过程

  1. 创建锁记录对象,每个线程的栈帧中包含一个锁记录的结构
image-20210615184749305.png
  1. 让锁记录中的Object reference指向锁对象,尝试使用CAS,将lock record地址和Object的Mark Word替换
image-20210615184951286.png
  • 替换成功,则对象头中保存锁记录地址和状态(00 轻量级锁),表示由该线程给对象加锁。(相当于不锁门了,改成在门口放书包,进门前先翻阅书包,看看是哪个线程)
image-20210615185230003.png
  • 替换失败,分两种情况

    • 如果其他线程已经持有该Object的轻量级锁,表明有竞争,进入锁膨胀过程
    • 如果是自己执行的synchronized锁重入,则再添加一条Lock Record作为重入计数
    image-20210615185410308.png
  1. 退出synchronized代码块(解锁时),分两种情况
    • 如果有取值为null的锁记录,表示有重入,让重入计数减一
    • 如果锁记录的取值不为null,此时使用CAS将Mark Word的值恢复给对象头
      • 成功,解锁成功
      • 失败,说明发生竞争,轻量级锁进行了锁膨胀已经升级为重量级锁,进入重量级锁的解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,发现该对象的Mark Word中已经指向了其他线程的Lock Record,则进入锁膨胀,将轻量级锁变为重量级锁

  1. 当出现竞争线程时,该线程尝试加锁失败。发现Object对象中已经指向了其他线程的Lock Record。
image-20210615190342119.png
  1. 此时当前线程加轻量级锁失败,进入锁膨胀阶段
    • 首先为Object对象申请Monitor管程,让Object指向重量级锁地址
    • 当前线程进入该Monitor的EntryList,等待唤醒
image-20210615190554686.png
  1. 当持有锁的线程退出同步代码块时,使用CAS给Mark Word的值恢复给对象头时失败,这时进入重量级锁的解锁流程,按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的阻塞线程
4.3.2 自旋优化

使用场景:重量级锁竞争时,可以使用自旋优化。如果当前线程自旋成功(持有锁的线程已经退出同步代码块,解锁),则可以避免当前线程的阻塞。

注:自旋会占用CPU时间,单核CPU自旋浪费资源,自旋优化适用于多核CPU;Java6后自旋是自适应的,刚刚自旋成功的话,则判定这次自旋的成功率会较高,多自旋几次,相反则少自旋几次

4.3.3 偏向锁

使用场景:轻量级锁在没有竞争时,每次还需要执行CAS操作,Java6中引入偏向锁进一步优化,只有第一次使用CAS将线程ID设置到对象的 Mark Word 中,之后发现这个线程ID是自己就表示没有竞争,不用重新CAS。以后不发生竞争,则该对象就归该线程所有。

注:默认开启了偏向锁,偏向锁默认是延迟的,不会在程序启动时立即生效,避免延迟,添加VM参数:-XX:BiasedLockingStartupDelay=0 用于禁用延迟。hashcode第一次用到才会被赋值,当有hashcode时,偏向锁则消失,两者不可同存。

实例测试

//偏向锁测试
public class TestLock {

    public static void main(String[] args) {
        Dog dog = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(dog);
        new Thread(){
            @Override
            public void run() {
                System.out.println("before");
                System.out.println(classLayout.toPrintable());
                synchronized (dog){
                    System.out.println("in");
                    System.out.println(classLayout.toPrintable());
                }
                System.out.println("after");
                System.out.println(classLayout.toPrintable());
            }
        }.start();
    }
}

class Dog{}
# 结果
before
com.yqj.concurrent2.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0x2000c143
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

in
com.yqj.concurrent2.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001b426005 (biased: 0x000000000006d098; epoch: 0; age: 0)
  8   4        (object header: class)    0x2000c143
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

after
com.yqj.concurrent2.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001b426005 (biased: 0x000000000006d098; epoch: 0; age: 0)
  8   4        (object header: class)    0x2000c143
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

偏向锁的撤销

  1. 调用对象的hashCode

偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销。这是由于空间有限,两者不能共存。而轻量级锁可以将hashCode保存在锁记录Lock Record中。重量级锁可以将hashCode保存在Monitor中。

  1. 其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。

//测试实例
public class TestLock {

    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        ClassLayout classLayout = ClassLayout.parseInstance(dog);
        Thread t1 = new Thread() {
            @Override
            public void run() {
                synchronized (dog) {
                    System.out.println(classLayout.toPrintable());
                }
                //必须使用wait/nodify,因为t1线程不能结束,否则底层线程可能被jvm重用作为t2线程,底层线程的id一样,导致依旧还是偏向锁
                synchronized (TestLock.class){
                    TestLock.class.notify();
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                synchronized (TestLock.class){
                    try {
                        TestLock.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("before");
                System.out.println(classLayout.toPrintable());
                synchronized (dog) {
                    System.out.println("in");
                    System.out.println(classLayout.toPrintable());
                }
                System.out.println("after");
                System.out.println(classLayout.toPrintable());
            }
        };
        t2.start();
    }
}

class Dog{}
# 结果 原本为偏向锁,后由于其他线程也使用该对象,从而升级为轻量级锁
com.yqj.concurrent2.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001af67005 (biased: 0x000000000006bd9c; epoch: 0; age: 0)
  8   4        (object header: class)    0x2000c205
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

before
com.yqj.concurrent2.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001af67005 (biased: 0x000000000006bd9c; epoch: 0; age: 0)
  8   4        (object header: class)    0x2000c205
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

in
com.yqj.concurrent2.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000001b83f630 (thin lock: 0x000000001b83f630)
  8   4        (object header: class)    0x2000c205
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

after
com.yqj.concurrent2.Dog object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x2000c205
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
  1. 调用wait/notify

批量重偏向

使用场景:当对象被多个线程访问,但不存在竞争,这时偏向了t1线程的对象仍然有机会重新偏向t2,重偏向会重置对象的ID。(当撤销偏向锁第20次后,会给之后的这些对象加锁时重新偏向至加锁线程)

批量撤销

使用场景:当撤销偏向锁再超过40次后(批量重偏向后),整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

5 wait/notify

5.1 wait/notify 原理

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

5.2 wait/notify 使用

属于Object对象的方法,必须获得该对象锁时,才可以调用这几个方法。

public class TestWaitNotify{

    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        List<Thread> list = new ArrayList<>();
        //创建5个线程,并启动
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(() -> {
                synchronized (lock) {
                    System.out.println("run...");
                    try {
                        lock.wait(); //当前线程等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " other...");
                }
            });
            list.add(t);
            t.start();
        }

        TimeUnit.SECONDS.sleep(1);

        synchronized (lock) {
            System.out.println("wake up...");
//            lock.notify();  //唤醒lock对象上的一个线程
            lock.notifyAll(); //唤醒lock对象上的全部等待的线程
        }
    }
}

5.3 与sleep方法的区别

  • sleep是Thread方法,而wait是Object方法
  • sleep不需要和synchronized配合使用,但wait需要和synchronized一起使用
  • sleep睡眠时不会释放对象锁,但wait在等待的时候会释放对象锁

5.4 wait/notify使用的正确方式

synchronized(lock) {
     while(条件不成立) {
         lock.wait();
     }
     // 其他操作
}
//另一个线程
synchronized(lock) {
    lock.notifyAll();
}

6 park/unpark

6. 1 park/unpark原理

每个线程都有自己的一个Parker对象,该对象由三部分组成,包括 _counter, _cond, _mutex。

三种情况:

  1. 先调用 park() 阻塞线程
image-20210616083943696.png
  • 当前线程调用park方法
  • 检查 _counter,本情况为0,此时获得 _mutex互斥锁
  • 线程进入 _cond 条件变量阻塞
  • 设置 _counter = 0
  1. 线程已经处于阻塞状态,调用unpark()恢复线程
image-20210616084132819.png
  • 调用 unpark方法,设置 _counter = 1
  • 唤醒 _cond 条件变量中的线程
  • 线程恢复运行
  • 设置 _counter = 0
  1. 先调用unpark(),后调用 park()
image-20210616084240013.png
  • 调用 unpark方法,设置 _counter = 1
  • 当前线程调用 park方法
  • 检查 _counter,此时为1,线程无需阻塞,继续运行
  • 设置 _counter = 0

6.2 park/unpark使用

//测试实例
public class TestLock {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("thread start");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("park");
                LockSupport.park();
                System.out.println("continue");
            }
        };
        t1.start();

        //TimeUnit.SECONDS.sleep(1); //在线程t1 park前 先unpark
        TimeUnit.SECONDS.sleep(3); //在线程t1 park后 再unpark
        System.out.println("unpark");
        LockSupport.unpark(t1);
    }
}

6.3 与 wait/nodify 的比较

  • wait/notify必须配合 Monitor管程一起使用,而 park/unpark不需要
  • park/unpark是以线程为单位来阻塞和唤醒指定的线程,而notify只能随机唤醒等待线程,notifyAll只能唤醒全部等待的线程,不够精确
  • park/unpark可以先unpark,而 wait/notify 不能先 notify

7 线程状态

image-20210616090447421.png

几种可以实现状态转换的方法总结:

  1. New -> Runnable
  • 调用 t.start() 方法
  1. Runnable -> Waiting
  • 获取对象锁后,调用 obj.wait() 方法。调用 obj.notify(),obj.notifyAll(),t.interrupt()时,竞争锁成功则线程从 Waiting -> Runnable;竞争锁失败则线程从 Waiting -> Blocked

  • 当前线程调用 t.join()方法。当t线程运行结束或者调用了当前线程的interrupt(),让目标线程从 Waiting -> Runnable

  • 当前线程调用 LockSupport.park()方法。调用 LockSupport.unpark(目标线程) 或者调用了线程的interrupt(),则会让目标线程从 Waiting -> Runnable

  1. Runnable -> Timed_Waiting
  • 获取对象锁后,调用 obj.wait(long n) 方法。t线程等待时间超过n毫秒,或调用 obj.notify(),obj.notifyAll(),t.interrupt()时,竞争锁成功则线程从 Timed_Waiting -> Runnable;竞争锁失败则线程从 Timed_Waiting -> Blocked
  • 当前线程调用 t.join(long n)方法。t线程等待时间超过n毫秒,或当 t 线程运行结束或者调用当前线程的interrupt(),让目标线程从 Timed_Waiting -> Runnable
  • 当前线程调用 Thread.sleep(long n) 方法。当前线程等待时间超过了 n 毫秒,让目标线程从 Timed_Waiting -> Runnable
  • 当前线程调用 LockSupport.parkNanos(long nanos)等方法。当前线程等待超时,或调用 LockSupport.unpark(目标线程) 或者调用了线程的interrupt(),则会让目标线程从Timed_Waiting -> Runnable
  1. Runnable -> Blocked
  • t 线程调用 synchronized(obj) 获取了对象锁时如果竞争失败,从Runnable -> Blocked。持对象锁的线程执行完同步代码块后,会唤醒Blocked的线程重新竞争,如果t线程竞争成功,则从 Blocked -> Runnable
  1. Runnable -> Terminated
  • 当前线程执行完所有代码

8 活跃性

8.1 死锁

一个线程同时需要获取多把锁,此时便容易发生死锁

  • 避免死锁需要注意加锁顺序
  • 如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

死锁测试实例

//测试实例,t1线程获得了lockA,但无法获得lockB。t2线程获得了lockB,但无法获得lockA
public class TestLock {

    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lockA){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1已获得lockA");
                synchronized (lockB){
                    System.out.println("t1已获得lockB");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (lockB){
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2已获得lockB");
                synchronized (lockA){
                    System.out.println("t2已获得lockA");
                }
            }
        }, "t2");

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

哲学家就餐问题(死锁)

public class TestLock {

    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");

        new Philosopher("t1", c1, c2).start();
        new Philosopher("t2", c2, c3).start();
        new Philosopher("t3", c3, c4).start();
        new Philosopher("t4", c4, c5).start();
        new Philosopher("t5", c5, c1).start();
    }

}

class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Chopstick{" +
                "name='" + name + '\'' +
                '}';
    }
}

class Philosopher extends Thread {
    String name;
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        this.name = name;
        this.left = left;
        this.right = right;
    }

    private void eat() {
        System.out.println(currentThread().getName() + " eat...");
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            synchronized (left) {
                synchronized (right) {
                    eat();
                }
            }
        }
    }

    @Override
    public String toString() {
        return "Philosopher{" +
                "name='" + name + '\'' +
                ", left=" + left +
                ", right=" + right +
                '}';
    }
}

8.2 活锁

活锁出现在两个线程互相改变对付的结束条件,最后均无法结束的现象

public class TestLock {

    private static int count = 10;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {

            while (count > 0) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(count);
            }

        }, "t1");

        Thread t2 = new Thread(() -> {

            while (count < 20) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
                System.out.println(count);
            }

        }, "t2");

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

8.3 饥饿

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

9 ReentrantLock

相当于synchronized的改进版本,具有如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 两者都是可重入的
//ReentrantLock语法
// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

9.1 可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

//测试实例,由于可重入性,在一个线程执行过程中的两个方法均可以正常获取锁并执行
public class TestLock {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        method1();
    }

    private static void method1() {
        lock.lock();
        try {
            System.out.println("method1");
            method2();
        }finally {
            lock.unlock();
        }
    }

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

}

9.2 可打断

加锁等待时,可以通过异常打断

//测试实例,主线程先加锁,t1线程没有获得锁等待,当主线程发送打断后,t1线程可以被成功打断
public class TestLock {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("start");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                System.out.println("lock interrupted");
                return;
            }
            try {
                System.out.println("run...");
            }finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        System.out.println("main lock");
        t1.start();
        try {
            Thread.sleep(2000);
            t1.interrupt();
            System.out.println("interrupt t1");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

}

9.3 锁超时

  • 立刻失败,没有获得锁立刻返回false
//测试实例,主线程先加锁,t1线程没有获得锁,尝试加锁失败返回
public class TestLock {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            if (!lock.tryLock()){
                System.out.println("lock fail");
                return;
            }
            try {
                System.out.println("run...");
            }finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        System.out.println("main lock");
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
  • 超时失败,等待一段时间后,没有获得锁返回false
//测试实例,主线程先加锁,t1线程没有获得锁,等待指定时间后若依旧没有获得锁则加锁失败返回
public class TestLock {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                if (!lock.tryLock(1,TimeUnit.SECONDS)){
                    System.out.println("lock fail");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                System.out.println("run...");
            }finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        System.out.println("main lock");
        t1.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
  • 使用ReentrantLock解决哲学家就餐问题
public class TestLock {

    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");

        new Philosopher("t1", c1, c2).start();
        new Philosopher("t2", c2, c3).start();
        new Philosopher("t3", c3, c4).start();
        new Philosopher("t4", c4, c5).start();
        new Philosopher("t5", c5, c1).start();
    }

}
//继承了ReentrantLock类
class Chopstick extends ReentrantLock {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Chopstick{" +
                "name='" + name + '\'' +
                '}';
    }
}

class Philosopher extends Thread {
    String name;
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        this.name = name;
        this.left = left;
        this.right = right;
    }

    private void eat() {
        System.out.println(currentThread().getName() + " eat...");
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            //尝试加锁,加锁失败直接放弃,避免死锁
            if (left.tryLock()) {
                try {
                    if (right.tryLock()) {
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock();
                }
            }
        }
    }

    @Override
    public String toString() {
        return "Philosopher{" +
                "name='" + name + '\'' +
                ", left=" + left +
                ", right=" + right +
                '}';
    }
}

9.4 公平锁

ReentrantLock默认是不公平的,不建议使用公平锁,会降低程序的并发度

//设置为公平锁的方式
ReentrantLock lock = new ReentrantLock(true);

9.5 条件变量

  • synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
  • ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量
    • synchronized 是那些不满足条件的线程都在一间休息室等消息
    • ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
//测试实例
public class TestLock {

    private static final ReentrantLock lock = new ReentrantLock();
    private static Condition waitCigaretteQueue = lock.newCondition();
    private static Condition waitBreakfastQueue = lock.newCondition();
    private static volatile boolean hasCigarette = false;
    private static volatile boolean hasBreakfast = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("wait cigarette");
                while (!hasCigarette){
                    try {
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("has Cigarette");
                }
            }finally {
                lock.unlock();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("wait breakfast");
                while (!hasBreakfast){
                    try {
                        waitBreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("has breakfast");
                }
            }finally {
                lock.unlock();
            }
        }, "t2");

        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(1);
        sendCigarette();
        TimeUnit.SECONDS.sleep(1);
        sendBreakfast();
    }

    private static void sendBreakfast() {
        lock.lock();
        try {
            System.out.println("send breakfast");
            hasBreakfast = true;
            waitBreakfastQueue.signal();
        }finally {
            lock.unlock();
        }
    }

    private static void sendCigarette() {
        lock.lock();
        try {
            System.out.println("send cigarette");
            hasCigarette = true;
            waitCigaretteQueue.signal();
        }finally {
            lock.unlock();
        }
    }
}

问题

  1. synchronized的理解(线程八锁问题)
  • 如果把 synchronized(obj) 放在 for 循环的外面,表示对整个for循环加锁,原子性
  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2),表示分别对不同的对象加锁,无效。锁对象
  • 如果 t1 synchronized(obj) 而 t2 没有加,t2不遵守规则随便访问,锁对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城老湿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值