• Java多线程(3)


    1.线程状态回顾

    1. NEW:Thread对象有了,内核中还没有该线程
    2. TERMINATED:内核中的线程没了,Thread对象还在
    3. RUNNABLE:就绪状态(和运行)
    4. TIMED_WAITING:因为sleep进入阻塞状态
    5. BLOCKED:因为等待锁引入了阻塞状态
    6. WAITING:因为wait进入了阻塞状态

    2.Java 标准库中的线程安全类

    Java 标准库中很多都是线程不安全的.这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施.

    对于这些类,在多线程中使用的时候要注意安全问题,合理运用synchronized,和良好的代码逻辑来避免线程安全问题.

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

    还有一些是线程安全的. 使用了一些锁机制来控制.

    虽然这些类是线程安全的,但是有的是不推荐使用的,因为这些类的底层虽然加了synchronized来保证安全,但是他们属于无脑加synchronized锁(每个方法都给加上锁).所以会大大降低效率,在工作中建议使用不安全的类,自己按照逻辑对不安全的代码加锁就好.

    • Vector (不推荐使用)
    • HashTable (不推荐使用)
    • ConcurrentHashMap
    • StringBuffer

    3.wait 和 notify

    由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知.

    但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.这时候就要用到我们的 waitnotify 方法了

    首先需要知道的是: wait, notify, notifyAll 都是 Object 类的方法.所以每个对象都有该方法.

    • wait() / wait(long timeout): 让当前线程进入等待状态.
    • notify() / notifyAll(): 唤醒在当前对象上等待的线程.

    wait

    先观察代码

    public class Test {
        public static void main(String[] args) throws InterruptedException {
            Object object = new Object();
            synchronized (object) {
                System.out.println("等待中");
                object.wait();
                System.out.println("等待结束");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
    • wait 会使当前执行代码的线程进行等待. (把线程放到等待队列中)
    • wait 还会释放当前线程占用的锁(synchronized的锁)
    • 满足一定条件时被唤醒后, 重新尝试获取这个锁(synchronized).

    notify

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

    观察 wait 和 notify 的配合使用

    public class Test {
        private static Object locker;
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                synchronized(locker){
                    System.out.println("wait:前");
                    try {
                        locker.wait();
                        //调用等待方法,等待notify方法唤醒.
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("wait:后");
                }
            });
            t1.start();
    
            Thread.sleep(3000);
    
            Thread t2 = new Thread(() -> {
                synchronized(locker){
                    System.out.println("notify:前");
                    locker.notify();
                    System.out.println("notify:后");
                }
            });
            t2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    notify 和 notifyAll 的区别

    当有多个线程对同一个对象wait的时候

    • 在代码中调用notify后,就是随机唤醒一个线程.等待notify的synchronized代码块执行完之后释放锁.让唤醒的线程获取锁.
    • 如果调用的是notifyAll,就是将等待在该锁对象的锁全部唤醒.等待notifyAll的synchronized代码块执行完之后释放锁,让他们重新去竞争锁资源.

    可以看出notify和notifyAll的最终结果都是使一个线程获取到锁资源.所以一般都是使用notify方法.

    死锁

    所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进.

    例如如下代码.

    class Counter {//死锁
        public int count = 0;
        
        synchronized void increase() {//死锁
            synchronized (this) {
                count++;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果不是在Java环境中,调用了上述的increase方法后,就会发生死锁.因为synchronized首先在increase方法上加了锁.锁对象也是this.在进入到里面synchronized代码块的时候,就会阻塞等待(this)锁的释放.这种情况下就会发生死锁.

    Java中这里不会发生死锁的原因是Java中的特殊锁机制–>可重入锁(这就属于一种外力).

    类似于这种死锁,还有很多情况.最经典的问题就是哲学家进餐问题.

    死锁的原因和解决方法

    死锁的四个必要条件

    1. 互斥使用 一个锁被线程占用之后,其他线程不可占用
    2. 不可抢占 一个锁被占用之后,其他线程不可以抢走这个锁
    3. 请求和保持 当一个线程占据了多把锁之后,除非显示的释放锁,否则这些琐事中都是被该线程持有的
    4. 环路等待 等待的关系构成了环路, 比如:a等b,b等c,c又等a

    而前三种都是锁本身的特点.所以开发中要避免死锁就要从第四点入手.

    • 加锁的时候对获取的资源进行排序,使得获取资源有固定的顺序,所有线程都遵守同样的规则顺序,就不会出现环路等待.
  • 相关阅读:
    mysql修改字符集
    Day42-HttpServletRequest、Cookie
    关于vagrant up的一个终结之谜(Markdown版本)
    ZipInputStream解压报错java.lang.IllegalArgumentException: MALFORMED
    【向题看齐】408之计算机组成原理概念记忆总结
    C#程序发布时,一定要好好地保护,不然你会后悔的
    主数据管理平台产品功能组成架构
    移动端微信浏览器调试工具整理eruda,微信x5调试工具无法使用,推荐新工具eruda和debugxweb
    11.新增class
    C++基础与深度解析 | 输入与输出 | 文件与内存操作 | 流的状态、定位与同步
  • 原文地址:https://blog.csdn.net/m0_58154870/article/details/127565523