三、线程安全

发布时间:2024年01月24日

一、共享带来的问题

多线程访问共享资源,容易导致结果出现错误,如果线程1获取共享资源v= 1,然后对v进行自增操作,变成了2

但是还没有写入共享资源,这时候发生了上下文切换

线程2, 获取了共享资源 v = 1, 然后对v进行自减操作,变成了0,然后写入了共享资源,这时候v = 0

但是线程2执行完之后,时间片就分配回线程1,线程1执行写入操作,最后v = 2

1)临界区

  • 一个程序运行多个线程本身是没有问题的。
  • 问题出在多个线程访问共享资源
    • 多个线程读 共享资源 其实也没有问题
    • 在多个线程对 共享资源 读写操作时发生了指令交错,就会出现问题
  • 一段代码块内,如果存在对 共享资源 的多线程读写操作,称这段代码块为临界区

2)竞态条件 Race Condition

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

也就是上面的例子中,发生上下文切换类似的操作,就是发生了竞态条件

二、synchronized 解决方案

1)语法:

synchronized(对象)
{
	//临界区
}

2)解决:

3)思考

synchronized 实际是用 对象锁保证了 临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换打断。

4)面向对象改进

把需要保护的共享变量放入一个类

class Room {
    private int value = 0;

    public void increment() {
        synchronized (this) {
            value ++;
        }
    }
    
    public void decrement() {
        synchronized (this) {
            value --;
        }
    }

    public int getValue() {
        synchronized (this) {
        }
    }
}

三、方法上的 synchronized

1)语法:

成员方法上的synchronized

class Test {
    public synchronized void test() {
        
    }
}
等价于   锁住当前类this对象
class Test{
    public void test() {
        synchronized(this) {
            
        }
    }
}

静态方法上的synchronized

class Test {
    public synchronized static void test() {
    }
}
等价于  锁住类对象
class  Test{
    public static void test() {
        synchronized(Test.class) {
            
        }
    }
}

不加synchronized 的方法

不加synchronized 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去) 不能保证原子性

2)线程八锁

锁住的是同一个对象,所以时间片先分配给谁,先输出谁,所以1 跟 2 都有可能

sleep()不会释放锁,所以情况跟情况1差不多,只是中间多了一个1s的等待

四、线程安全分析

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

  • 静态变量不用说肯定是会存在线程安全问题的。
  • 成员变量,如果只有一个实例对象,然后存在多线程的情况下,是会有多线程安全的问题。
  • 如果他们没有共享,则线程安全
  • 如果存在共享
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全问题。

2)局部变量是否线程安全?

  • 局部变量是线程安全的。
  • 但是局部变量引用的对象则不一定
    • 如果对象没有逃离方法的作用范围,那么是线程安全的。
    • 如果对象逃离了方法的作用范围,需要考虑是不是线程安全。 例如:引用了堆中的对象,会被共享。

3)局部变量线程安全分析?

1. 普通局部变量

public static void test1() {
	int i = 10;
    i ++;
}

这个代码不会出现线程安全问题

每一个线程会有自己对应的栈空间,每个线程调用test1()方案时局部变量i,会在每个线程自己的栈空间调用,进行压栈,所以各自调用自己的,互不干扰,不存在共享。

2. 局部变量引用

public static void main(String[] args) {
    ThreadUnsafe tes = new ThreadUnsafe();
    for (int i = 0; i < 2; i ++ ) {
        new Thread(() -> {
            test.method1(200);
        }, "Thread" + (i + 1)).start();
}

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

    private void methdo2() {
        list.add("1");
    }
    
    private void methdo3() {
        list.remove(0);
    }
}

上面的代码中,list是成员变量,所以是共享资源,也就是临界区,临界区中的代码如果不加以限制,多线程情况下,会造成执行顺序不可预测,发生竞态条件

所以上面的代码会有线程安全的问题,会导致发生数组下表越界异常(同时执行remove操作,这时候就会发生错误)

解决方法

需要确保只能有一个线程能够执行,或者将成员变量 变成 局部变量。

3. 暴露引用对象

如果创建一个子类 继承 ThreadUnsafe类,然后子类对 method2 或者 method3 进行重写,创建一个新的线程执行

这时候list这个局部变量就暴露了, 也就是在子类中的一个新的线程中被引用到了,这时候list就是一个共享资源,也就是临界区,那么就会发生线程安全问题

public static void main(String[] args) {
    ThreadUnsafe tes = new ThreadUnsafe();
    for (int i = 0; i < 2; i ++ ) {
        new Thread(() -> {
            test.method1(200);
        }, "Thread" + (i + 1)).start();
}

class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++ ) {
            method2();
            method3();
        }
    }
	//private
    public void methdo2() {
        list.add("1");
    }
    //private
    public void methdo3() {
        list.remove(0);
    }
}
    
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

解决方案:

可以通过将父类中的方法权限修饰符进行修改,变成private或者final等,子类就不能够进行重写,这样就不会导致线程安全问题。

4. 常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的

Hashtable table = new Hashtable();

new Thread(() -> {
    table.put("key", "value1");
}).start();

new Thread(() -> {
    table.put("key", "value2");
}).start();

可以看源码,是添加了synchronized锁,保证了原子性

但是线程安全是调用单一方法

如果多个方法组合调用 ,那么将就不是线程安全的了

4.1. 线程安全类方法的组合
Hashtable table = newHashtable();

if (table.get("key") == null) {
    table.put("key", value);
}

线程1跟线程2同时访问上面的代码

单独访问put跟get方法是有原子性的,但是两个组合起来就不是了

4.2. 不可变类线程安全性

String、Integer是不可变类,所以其内部状态是不可修改的,因此他们的方法都是线程安全的

疑问:String 中有 substring等方法不是可以修改他的值吗

substring是创建一个新的值,所以不会对原本字符串进行修改。

4.3. 案例分析

继承了HttpServlet,Servlet是Tomcat中的,只能有一个实例,所以omcat中的多线程调用的时候共享使用,就会发生线程安全得问题

例1:

例2:

最好对count有一些保护,防止称为临界区。

例3:

例4:

例5:如果将例4中的Connection 写成成员变量,不是局部变量,那么就会有线程安全问题

因为 servlet 只有一份,导致 userservice只有一份,所以UserDao也只有一份,所以多线程访问的时候,就会导致可能第二个线程close链接,第一个线程就拿不到了。

例6:

所以平时书写的时候,不想往外暴露的就写成final,或者private私有的,可以增强安全性。

String 类是不可变的,但是他也是写成final,防止发生继承之后覆盖行为,修改了。这也是很经典的 闭合原则

五、Monitor 概念

1)Java 对象头

例子:

int 占用 4个字节

Integer 占用 8个对象头 + 4 个int值字节 12字节

2)Monitor (锁)

Monitor 被翻译成 监视器 或 管程(操作系统层面)

  • 刚开始的时候Monitor 为null
  • 当 第一个线程 执行到 synchronized(obj)的时候,因为是第一个,所以obj对象的 对象头中的 MarkWord就会通过 指针 的形式关联一个 Monitor ,然后将当前线程 设置成 Monitor中的Owner,表示现在的所有者,只能有一个Owner
  • 然后第二个线程来执行的时候,执行到 synchronized(obj)的时候,发现obj 关联的 Monitor 已经有 Owner 了,这时候就会将第二个线程放到 EntryList 中阻塞等待,相当于放到一个阻塞队列中,进入BLOCKED状态
  • 然后第三个线程来执行也是相同,进入EntryList 中阻塞等待,以此类推。。。
  • 然后等第一个线程的同步代码执行完毕之后,就会唤醒 EntryList中等待的线程来竞争锁,这是一种不公平的挑选,不是先来先出的。

注意:

  • synchronized 必须是进入同一个对象的monitor才能有上述的效果
  • 不加synchronized 的对象不会关联监视器,不遵从上述规则

3)synchronized 优化原理

1. 轻量级锁

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

轻量级锁 使用者是没有感知的,语法仍然是 synchronized

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
        //同步块 A
        method2();
    }
}
public static void method2() {
	synchronized( obj ) {
        //同步块B
    }
}

原理:

2. 锁膨胀

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

static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
        //同步块
    }
}

所谓的锁膨胀,也就是在将来的解锁操作 进入一个重量级锁的解锁操作

根据上图进行解释分析:

  • 当线程1 加轻量级锁失败,进入了锁膨胀流程
  • 这时候Object 对象就会申请一个Monitor锁,并且让Object 的对象头修改,指向重量级锁的地址
  • 然后线程1 就进入Monitor的 EntryList 阻塞队列 BLOCKED,这样就不会让线程1干耗着

  • 当线程0 退出同步代码块进行解锁时,使用CAS将Mark Word 的值恢复给对象头,这时候就恢复失败了,因为Monitor的地址已经不是所记录的地址了
  • 这时候经过了锁膨胀,已经是重量级锁的地址了,所以需要进行一个重量级锁的解锁操作,通过对象头地址找到Monitor, 将Monitor的Owner置为 空, 然后唤醒 EntryList 中的线程

3. 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,也就是会进行几次循环重试。

如果当前线程自旋成功(即这时候持锁线程已经推出了同步块,释放了锁),这时候当前线程就可以避免阻塞了。

因为阻塞会导致上下文切换,性能影响比较大。

自旋只有在多核cpu的情况下才有用,如果单核就没有意义。一个cpu执行同步代码块,另一个线程都没有cpu执行循环,所以没有意义

4. 偏向锁

轻量级锁在没有竞争时(就自己当前线程在运行),每次重入仍然需要执行CAS操作,CAS肯定是执行失败,但是知道是自己线程,所以会保留下来,有性能损耗

Java 6 中引入了偏向锁来做进一步优化: 只有第一次使用CAS 将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。

以后只要不发生竞争,这个对象就归该线程所有。

4.1. 偏向状态

貌似对象的hashcode是懒生成的,当调用hashcode()方法获取 hashcode 值的时候才对对象头里面写入hashcode值,一旦hashcode已经写入,无法使用偏向锁进而使用轻量级锁等

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建之后,markword的值为 0x05 也就是 最后 3 位 101,这时它的thread、 epoch、age 都为0
  • 偏向锁默认是延迟开启的,不会再程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -xx:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果对象在一开始创建的,那么就是没有开启偏向锁,markword最后3位为001,这时候在延迟之后输出他的markword也还是之前的001,需要重新创建的才有效
  • 如果没有开启偏向锁,那么对象创建之后,markword值为0x01 也就是001,这时它的 hashcode、age都为0,第一次用到hashcode才会赋值。
  • 如果一个对象调用开启了偏向锁,调用synchronized之后使用的是偏向锁,那么前面54位地址操作系统会默认分配一个线程id,表示这个对象就给当前线程用了,锁代码同步块执行完毕之后,那54位地址还是执行当前线程,除非有其他线程来竞争,不然一直表示给当前线程使用。但是轻量级锁释放之后就会恢复。
  • 如果禁用偏向锁 ,添加 VM 参数 -xx:-UseBiasedLocking(禁用)/ +UseBiasedLocking(启用)

锁使用优先级:

偏向锁 > 轻量级锁 > 重量级锁

4.2. 撤销 - 调用对象 hashCode

线程调用hashcode() 方法之后,根据对象头格式,偏向锁有54位存储线程id,没有多余的地方存储31位的hashcode码,所以会将thread、epoch清空,转成正常Normal对象。

敲黑板:偏向锁和hashcode是互斥存在的;

  • 轻量级锁的hashcode存储在线程栈帧的锁记录中;
  • 重量级锁的hashcode存储在Monitor对象中!

4.3. 撤销 - 其他线程使用对象

当一个线程使用了当前对象,如果使用的是偏向锁,那么会在thread中记录当前线程的id,表示这个对象给当前线程用

这时候如果有另一个线程来访问这个对象,这时候发现上面有偏向锁,偏向了某一个线程,这时候会撤销偏向锁,改成轻量级锁,然后记录锁地址。

最后解锁之后,无锁状态。

4.4. 撤销 - 调用 wait / notify
  • 想要使用wait / notify 这种机制, 只有重量级锁才有
  • 所以偏向锁,轻量级锁都会升级成重量级锁。

4.5. 批量重偏向
  • 如果对象被多线程访问,但是没有竞争,这时偏向了线程 T1 的对象仍然有机会重新偏向 T2,重偏向会重置对象的 Thread ID,这是一个批量重偏向优化。
  • 比如说对象已经偏向了 线程 T1 ,记录了 T1的Thread ID,然后等执行结束之后,线程 T2 来执行,会发现已经被T1用了偏向锁,这时候JVM会撤销偏向锁,改成轻量级锁
  • 但是当撤销阈值达到超过20次之后,JVM觉得是不是偏向错了,于是会给这些对象加锁时给予重新偏向至加锁线程的能力。

4.6. 批量撤销
  • 当撤销偏向锁操作达到阈值40次之后,JVM会觉得是真的偏向错了,这时候会将整个类的所有对象都设置成不可偏向,新创建的对象也是。

4.7. 锁消除

我们知道加锁,不管了怎么优化,偏向锁,轻量级锁,都会对性能有锁损耗,但是为什么执行代码耗时一样。

这时候就涉及到了JVM了对象逃逸,

我们Java程序是对字节码通过解释 + 编译的方式来执行的,但是对于其中的一些热点代码,会进行一个优化

这时候就涉及 JIT即时编译器 ,会将热点代码进一步翻译成机器码,缓存起来,以后执行就不用 通过编译了

另外他的一个优化手段就是去分析这个局部变量是不是可以优化,发现根本不会逃离方法作用范围,那就不会共享,那么加锁就没有意义,所以Jit 即时编译器会直接将synchronized优化去掉,只是执行了锁中的代码块的代码。

锁消除参数默认开启,如果需要关闭可以使用功能下面 VM 参数

六、wait notify

1) 原理

2)API介绍

wait()、notify()、 notifyAll() 方法都是属于Object 对象的方法,需要获取此对象的锁 之后才能够使用

wait() 对象调用wait() 方法之后,线程会 Owner中释放锁,然后进入WaitSet中等待唤醒

wait( time ) 对象调用带参数的wait()方法之后,线程会 Owner中释放锁,然后进入WaitSet中等待 指定时间,然后如果期间没有被唤醒,指定之间之后就会自动唤醒,然后进入EntryList再次尝试获取锁,竞争锁

notify () 对象调用notify()方法之后,会随机挑选一个 WaitSet 中的一个线程唤醒,然后进入EntryList 竞争锁

notifyAll() 对象调用notifyAll()方法之后,会 唤醒 WaitSet 中所有线程,然后进入EntryList中竞争锁。

wait(long n) 进入 TIMED_WAITING 状态

wait( ) 进入 WAITING状态

3)wait notify 的正确使用姿势

1. sleep vs wait

  • sleep是Thread的静态方法, wait是 Object 的方法
  • sleep 可以随意使用不需要配合synchronized使用, wait需要配合synchronized使用
  • sleep 如果是在synchronized中使用,也是不会释放锁的,wait会释放锁
  • 共同点: 都是进入TIMED_WAITING 状态, 有时间的等待

2. 正确使用姿势

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

3. 设计模式 之 同步模式保护性暂停

3.1. 定义

3.2. 实现
class GuardedObject {
    //结果
    private Object response;
	
    //获取结果
    public Object get() {
        synchronized (this) {
            //没有结果
            while(response == null) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    //产生结果
    public void generation(Object response) {
        synchronized (this) {
            this.response = response;
            this.notifyAll();
        }
    }
}
public static void main(String[] args) {
    GuardedObject guardedObject = new GuardedObject();
    new Thread(() -> {
        System.out.println(guardedObject.get());
    }).start();

    new Thread(() -> {
        int x = 111;
        guardedObject.generation(x);
    }).start();
}

相对于join的好处

join 需要等待线程执行的结束之后,才能唤醒自己的线程,需要是全局变量 等待另一个线程的结束

保护性暂停等待 设计模式不需要完全等待线程执行结束,可以线程执行到一半的时候就响应线程,从而唤醒自己线程,继续执行,可以是局部变量 等待另一个线程的结果

3.3. 功能增强(超时)
class GuardedObject {
    //结果
    private Object response;
	
    //获取结果
    public Object get(long timeout) {
        synchronized (this) {
            //记录一个初始时间
            long begin = System.currentTimeMillis();
            //经过时间
            long access = 0;
            //没有结果
            while(response == null) {
                long waitout = timeout - access;
                if (waitout <= 0) {
                    break;
                }
                try {
                    this.wait(waitout);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                access = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    //产生结果
    public void generation(Object response) {
        synchronized (this) {
            this.response = response;
            this.notifyAll();
        }
    }
}

3.4. join 原理 源码

join 实现源码跟 超时增强是一模一样的

在java中,Thread类线程执行完run()方法后,一定会自动执行notifyAll()方法。因为线程在die的时候会释放持用的资源和锁,自动调用自身的notifyAll方法。

3.5. 扩展2

刚才思路的问题:

一个线程通过一个 GuardedObject 对象来进行通信, 通过参数的形式来传递,多个线程之间传递来传递去,非常不方便

实现解耦:

通过设计一个集合来管理多个 GuardedObject ,每个给予一个id,用于区分,然后 生产供给, 获取所需。

class Mailboxes{
    //集合
    private static Map<Integer, GuardedObject> map = new Hashtable<>();
    //id
    private static int id = 1;

    private static synchronized int geterateId() {
        return id ++;
    }

    public static GuardedObject createGuardedObject() {
        GuardedObject guardedObject = new GuardedObject(geterateId());
        map.put(guardedObject.getId(), guardedObject);
        return guardedObject;
    }

    public static GuardedObject getGuardedObject(int id) {
        return map.remove(id);
    }

    //获取所有GuardedObject
    public static Set<Integer> getIds() {
//        System.out.println(map.keySet());
        return map.keySet();
    }
}
class GuardedObject {
    //id
    private int id;
    //结果
    private Object response;

    public GuardedObject(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    //获取结果
    public Object get(long timeout) {
        synchronized (this) {
            //记录一个初始时间
            long begin = System.currentTimeMillis();
            //经过时间
            long access = 0;
            //没有结果
            while(response == null) {
                long waitout = timeout - access;
                if (waitout <= 0) {
                    break;
                }
                try {
                    this.wait(waitout);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                access = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    //产生结果
    public void generation(Object response) {
        synchronized (this) {
            this.response = response;
            this.notifyAll();
        }
    }
}

class People extends Thread {
    @Override
    public void run() {
        //准备收信
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
       System.out.println("准备收信" + guardedObject.getId());
        Object res = guardedObject.get(5000);
        //收到信
        System.out.println("收到信" + res);
    }
}


class Postman extends Thread {

    private int id;

    private String mail;

    public Postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        //开始送信
//        System.out.println("送信" + id + "内容" + mail);
        GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
        System.out.println("送信" + id + "内容" + mail);
        guardedObject.generation(mail);
    }
}
public class Main{
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i ++ ) {
            new People().start();
        }
        Thread.sleep(1);
//        System.out.println(Mailboxes.getIds());
        for (Integer id : Mailboxes.getIds()) {
//            System.out.println(id + "内容");
            new Postman(id, id + "内容").start();
        }
    }
}

特点: 产生结果线程 和 使用结果线程是一一对应的。

4. 设计模式 之 异步模式 生产者 / 消费者

4.1. 定义

为什么这里是异步,保护性暂停模式却是同步?

  • 因为保护性暂停是一一对应的,只要产生了结果,我就就能立刻拿到进行处理,所以是同步的
  • 但是生产者/消费者 产生了结果之后放入消息队列,如果前面有结果未处理,需要等待,不能立刻执行,所以称为异步

4.2. 实现

final class MessageDeque {

    private static LinkedList<Message> list = new LinkedList<>();

    private static int capacity;

    public MessageDeque(int capacity) {
        this.capacity = capacity;
    }

    //存放消息
    public void put(Message message) {
        synchronized (list) {
            //如果没有满
            while (list.size() == capacity) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("生产者线程等待结束, 没有满, 放入");
            list.addLast(message);
            list.notifyAll();
            System.out.println("放入结束");
        }
    }
    //取出消息
    public Message take() {
        synchronized (list) {
            //如果没有消息
            while(list.isEmpty()) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            Message message = list.removeFirst();
            System.out.println("消费者等待结束, 拿出消息" + message.getId());
            list.notifyAll();
            return message;
        }
    }
}

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

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

    public int getId() {
        return id;
    }

    public Object getMail() {
        return mail;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", mail=" + mail +
                '}';
    }
}
public class Main {
    public static void main(String[] args) throws InterruptedException {
        MessageDeque messageDeque = new MessageDeque(2);
        for (int i = 0; i < 3; i ++ ) {
            int id = i;
            new Thread(() -> {
                messageDeque.put(new Message(id, "消息" + id));
            }).start();
        }
        new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                messageDeque.take();
            }
        }).start();

    }
}

七、Park & Unpark

1) 基本使用

它们是 LockSupport类中的方法

// 暂停当前线程
LockSupport.park();

// 恢复当前线程的运行
LockSupport.unpark(暂停线程对象);

调用了Park方法的线程进入 Waiting的状态

特点:

与Object 的 wait & notify 相比

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

2)底层原理

每个线程都有自己的一个 Parker 对象, 由三部分组成 _counter, _cond 和 _mutex 打个比喻

1. 调用 park

2. 后调用Unpark

3. 先调用Unpark 在调用 park

八、重新理解线程状态转换

九、多把锁

一把锁本来锁住一个房间,那么一个人要睡觉另一个人要学习就是串行执行

锁的粒度细分

分两把锁,锁住房间,睡觉去睡觉的房间,学习去学习的房间

  • 好处,可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁。

十、活跃性

1) 死锁

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

t1 线程 获得 A对象 锁, 接下来想要获得 B对象 锁

t2 线程获得 B对象 锁,接下来想要获得 A对象 锁

同时阻塞住了,死锁了。

2) 哲学家就餐死锁

各自等对方筷子,也就对方锁,循环等待,所以死锁了

3) 活锁

不断改变对方结束条件,所以适当增加一些随机睡眠时间,使得交错运行,就可以避免活锁。

4)饥饿

就是两个线程互相需要对方的锁,那么就死锁了

如果让他们按照一定的顺序加锁,那么就能解决死锁问题了

但是就可能造成饥饿,也就是有些锁获取的次数比较少,概率很低,这就叫做饥饿。

十一、ReentrantLock (可重入锁)

1) 基本特点

想对于 synchronized 它具备以下特点

  • 可中断(别的线程可以破坏你未获取锁的blocking状态,而不是指获取锁之后自己的中断阻塞状态)
  • 可以设置超时时间 ( synchronized 获取不到锁,会一直阻塞等待, 不能设置超时时间)
  • 可以设置为公平锁 ( synchronized 获取到锁的几率平均一点,先进先出)
  • 支持多个条件变量 ( synchronized 进入wait 只有一个WaitSet,ReentrantLock 可以设置多种等待条件)

与 synchronized 都支持可重入。

2) 基本语法

synchronized 在关键字级别去保护临界区

ReentrantLock 在对象级别去保护临界区, 需要创建一个 ReentrantLock 对象

// 获取锁
reentrantLock.lock();
try {
    // 临界区
} finally {
    // 释放锁
    reentrantLock.unlock();
}

3)可重入

一个线程自己已经获取到锁了,第二次想要获取这把锁

  • 获取到了锁,是可重入锁。
  • 反之则不可重入锁。

4)可打断

线程 等待锁的过程中,可以使用 interrupt 方法终止线程的等待。

synchronized 和 ReentrantLock.lock 都是不可打断的

需要使用 ReentrantLock.lockInterruptibly() 才是可以调用线程的 interrupt 方法打断的。

可打断看,可以避免死等,减少死锁发生 。

5)锁超时

ReentrantLock.tryLock() 获取不到锁立刻返回false, 否则 返回 true

ReentrantLock.tryLock(long n, TimeUnit) 获得不到锁就等待时间 【n TimeUnit】 内一直重试,时间结束之后返回false, 获取到了就返回true

可以调用 interrupt 方法打断 tryLock 方法,支持可打断。

带超时的方法都是避免 无限制 的等待

可以使用tryLock 解决 哲学家就餐问题。

获取不到锁(筷子)就放下,互相谦让,最后和气吃饭。

6)公平锁

ReentrantLock 默认 不公平,通过构造方法传入 参数 true 表示公平

可以让 进入 EntryList中的线程按照先进先出的原则,避免饥饿现象发生

公平锁一般没有必要,会降低并发度,后面分析原理会讲解。

7)条件变量

直接说 条件变量 不好理解,举例讲解

synchronized 中也有条件变量, 当不满足条件时,进入WaitSet中等待。

ReentrantLock 相对于 synchronized 优势在于,支持多个条件变量,

  • synchronized 那些不满足条件的线程在一间休息室等消息
  • ReentrantLock 支持多间休息室, 有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒。

1. 基本使用

  • await 前需要先获得锁
  • await 执行后,线程就会释放锁,然后进入 conditionObject 等待
  • 调用 signal 、signalAll唤醒await(打断、或者带时间的await超时),就会被唤醒,进入 EntryList ,继续竞争锁。
  • 竞争锁成功之后,从await 后面的代码继续执行

创建条件变量

ReentrantLock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
COndition condition2 = lock.newCondition();
lock.lock();
try {
    try {
        condition1.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
} finally {
    lock.unlock();
}

new Thread(() -> {
	lock.lock();
    try {
    	condition1.signal();
    } finally {
        lock.unlock();
    }
});

8)设计模式 同步模式 之 顺序控制

1. 实现先输出2 后输出1

1.1. wait notify
static Object lock = new Object();
static boolean t2runned = false;

public static void main(String[] args) {
    new Thread(() -> {
        synchronized(lock) {
            while (!t2runned) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(1);
        }
    }, "t1").start();
    
    new Thread(() -> {
        synchronized(lock) {
            System.out.println(2);
            t2runned = true;
            lock.notify();
        }
    }, "t2").start();
}
1.2. ReentrantLock await single
public class Main {
    static ReentrantLock lock = new ReentrantLock();
    static boolean flag = false;
    static Condition condition = lock.newCondition();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                while (!flag) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(1);
            } finally {
                lock.unlock();
            }
        });

        t1.start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(2);
                flag = true;
                condition.signal();
            } finally {
                lock.unlock();
            }
        }).start();
    }
}
1.3. park & unpark
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        LockSupport.park();
        System.out.println(1);
    }, "t1");

    t1.start();
    
    Thread t2 = new Thread(() -> {
        System.out.println(2);
        LockSupport.unpark(t1);
    }, "t2").start();
}

2. 多个线程交替执行,交替输出

线程1输出a 5次,线程2 输出 b 5次, 线程3输出c 5次, 现在要求输出abc abc abc abc abc 怎么实现

文章来源:https://blog.csdn.net/yuigjjhgjh/article/details/135733309
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。