Java编程基础知识之线程篇(其四)

18 篇文章 2 订阅
11 篇文章 0 订阅

内容回顾:

Java编程基础知识之线程篇(其一)

Java编程基础知识之线程篇(其二)

Java编程基础知识之线程篇(其三)

且听下回分解..........

目录

前言:

一、synchronized 关键字-监视器锁monitor lock

1.synchronized 的特性

1)互斥

2) 刷新内存

3) 可重入

2.synchronized使用示例

1) 直接修饰普通方法: 锁的 SynchronizedDemo 对象

2)修饰静态方法: 锁的 SynchronizedDemo 类的对象

3)修饰代码块: 明确指定锁哪个对象 

3.补充说明

二、volatile 关键字

1.volatile 能保证内存可见性

2.补充说明:

1)volatile不保证原子性

2)synchronized保证原子性与内存可见

三、 wait 和 notify

1.wait方法

2.notify方法

3.notifyAll

4.wait方法与sleep方法对比(面试题)


前言:

本文章内容介绍synchronized关键字和volatile关键字,只有也包括wait和notify两个线程方法的介绍。

一、synchronized 关键字-监视器锁monitor lock

1.synchronized 的特性

1)互斥

        synchronized 会起到互斥效果 , 某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到同一个对象 synchronized 就会 阻塞等待
        
  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

 注意:

  • 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 也就是操作系统线程调度的一部分工作.
  • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
  • synchronized的底层是使用操作系统的mutex lock实现的.

2) 刷新内存

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.

3) 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

理解 "把自己锁死"

        一个线程没有释放锁, 然后又尝试再次加锁 .
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待. 
lock();
        按照之前对于锁的设定, 第二次加锁的时候 , 就会阻塞等待 . 直到第一次的锁被释放 , 才能获取到第二个锁,但是由于第二个锁导致这个线程处于阻塞状态,而阻塞状态就导致第一个锁无法解开,从而使B锁也无法解开,都得互相开锁才能进行下一步,就和狗和猫打架谁也不让谁一样,这就是 死锁 .

 这样的锁被称为 不可重入锁(比较经典的死锁问题:哲学家就餐问题,可以去了解一下)

                       

死锁的四个必要条件:(前三个为锁本身特点)

  1. 互斥使用:一个锁被一个线程占用了之后,其他线程占用不了(锁本质,保证原子性)
  2. 不可抢占:一个锁被一个线程占用了之后,其他线程不能把这个锁抢走
  3. 请求和保持:当一个线程占据了多把锁之后,除非显示的释放锁,否则这些锁始终都是被该线程保持有
  4. 环路等待:等待关系成环 A等B,B等C,C等A.......

        幸好我们的synchronized是一个可重入锁,所以不会出现以上问题(但是不代表不会出现其他死锁问题)

可重入锁;

代码示例
在下面的代码中 ,
  • increase increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 对象加锁的.
  • 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁没释放, 相当于连续加两次锁)
这个代码是完全没问题的 . 因为 synchronized 是可重入锁 .
static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}

在可重入锁的内部, 包含了 "线程持有者" "计数器" 两个信息.

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

 可重入锁带来的问题:

  1. 可重入锁的意义在于降低了程序猿的负担(提高开发效率
  2. 代价,程序中需要又更高的开销(维护属于哪个线程,并且加减计数,降低了运行效率

        在实际开发中,如果需要使用嵌套锁,一定要约定好加锁的顺序。即所有锁都是按照a->b->c顺序加锁,别有的abc,有的cba.......

2.synchronized使用示例

        synchronized 本质上要修改指定对象的 " 对象头 ". 从使用角度来看 , synchronized 也势必要搭配一个具体的对象来使用

 

1) 直接修饰普通方法: 锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}

2)修饰静态方法: 锁的 SynchronizedDemo 类的对象

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

3)修饰代码块: 明确指定锁哪个对象 

锁当前对象
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}
锁类对象
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}
        我们重点要理解,synchronized 锁的是什么 . 两个线程竞争同一把锁 , 才会产生阻塞等待, 两个线程分别尝试获取两把不同的锁 , 不会产生竞争 .

3.补充说明

        String是一个线程安全的类,但是不是因为他有加锁操作,事实上它并没有加锁,安全的原因在于,String本身就是不可修改的,也就是不怕多个线程修改字符串的问题

        其他支持线程安全的类

  • Vector (不推荐使用,无脑加锁,效率低)
  • HashTable (同上)
  • ConcurrentHashMap
  • StringBuffer

        线程不安全的类

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder
  • ..........

二、volatile 关键字

1.volatile 能保证内存可见性

        volatile 修饰的变量 , 能够保证 " 内存可见性 ".
代码在写入 volatile 修饰的变量的时候 ,
  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候 ,
  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本 

        直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况. 加上 volatile , 强制读写内存. 防止出现因Java编译器的代码优化而导致的主内存和工作内存信息不一致的问题,虽然这样速度是慢了, 但是数据变的更准确了。

代码示例:

在这个代码中
  • 创建两个线程 t1 t2
  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
  • 预期当用户输入非 0 的值的时候, t1 线程结束
static class Counter {
    public int flag = 0; }
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (counter.flag == 0) {
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)

         上面代码t1 读的是自己工作内存中的内容. 当 t2 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.(这是由于代码优化的问题)

        解决办法是给flag加上volatile

static class Counter {
    public volatile int flag = 0; }
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.

2.补充说明:

1)volatile不保证原子性

static class Counter {
    volatile public int count = 0;
    void increase() {
        count++;
   }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
       }
   });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

        这段代码的结构无法保证结果为10w,所以只能加锁

2)synchronized保证原子性与内存可见

static class Counter {
    public int flag = 0; }
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (true) {
            synchronized (counter) {
                if (counter.flag != 0) {
                    break;
               }
           }
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}

三、 wait notify

        为了控制线程顺序,可以使用wait和notify,这两种方法多用在锁的代码块中,前者是等待释放当前锁,后者是通知当前对象等待的线程并使其获得对象锁wait和notify是应在在同一个对象上的

1.wait方法

wait 做的事情 :
  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.

wait 结束等待的条件 :
  • 其他线程调用该对象 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

 wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
   }
}
        这样在执行到object.wait() 之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()

2.notify方法

notify 方法是唤醒等待的线程 .
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程(并没有 "先来后到")
  • notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁

代码示例 : 使用 notify() 方法唤醒线程
  • 创建 WaitTask , 对应一个线程, run 内部循环调用 wait.
  • 创建 NotifyTask , 对应另一个线程, run 内部调用一次 notify
  • 注意, WaitTask NotifyTask 内部持有同一个 Object locker. WaitTask NotifyTask 要想配合
  • 就需要搭配同一个 Object.
static class WaitTask implements Runnable {
    private Object locker;
    public WaitTask(Object locker) {
        this.locker = locker;
}
    @Override
    public void run() {
        synchronized (locker) {
            while (true) {
                try {
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }
   }
}
static class NotifyTask implements Runnable {
    private Object locker;
    public NotifyTask(Object locker) {
        this.locker = locker;
   }
    @Override
    public void run() {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notify();
            System.out.println("notify 结束");
       }
   }
}
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    Thread.sleep(1000);
    t2.start();
}

3.notifyAll

        notify方法只是唤醒某一个等待线程 . 使用 notifyAll 方法可以一次唤醒所有的等待线程. 
        范例:使用notifyAll() 方法唤醒所有等待线程 , 在上面的代码基础上做出修改 . 创建 3 WaitTask 实例 . 1 NotifyTask 实例 .
static class WaitTask implements Runnable {
 // 代码不变
}
static class NotifyTask implements Runnable {
  private Object locker;
    public NotifyTask(Object locker) {
        this.locker = locker;
   }
    @Override
    public void run() {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notifyAll();
            System.out.println("notify 结束");
       }
   }
}
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t3 = new Thread(new WaitTask(locker));
    Thread t4 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    t3.start();
    t4.start();
    Thread.sleep(1000);
    t2.start();
}
        此时可以看到, 调用 notifyAll 能同时唤醒 3 wait 中的线程
        注意: 虽然是同时唤醒 3 个线程 , 但是这 3 个线程需要竞争锁 . 所以并不是同时执行 , 而仍然是有先有后的 执行.

        notify方法与notifyAll方法区别

        二者共同点在于都是将wait唤醒,不同在于前者一次只唤醒一个,多个线程的时候,由线程调度按照一定规则调度一个,后者是一次性唤醒全部,让那些线程去争一个锁。前者是被安排,后者是主动抢 

4.wait方法与sleep方法对比(面试题)

        其实理论上 wait sleep 完全是没有可比性的,因为一个是用于线程之间的通信的一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间
总结:
  1. wait 需要搭配 synchronized 使用. sleep 不需要.
  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法

个人笔记使用

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

摸鱼儿hzj

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

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

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

打赏作者

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

抵扣说明:

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

余额充值