JavaEE初阶-多线程2

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • [1. 线程安全](#1. 线程安全)
    • [1.1 死锁问题](#1.1 死锁问题)
      • [1.1.1 一个线程一把锁](#1.1.1 一个线程一把锁)
      • [1.1.2 两个线程两把锁](#1.1.2 两个线程两把锁)
      • [1.1.3 N个线程N个锁](#1.1.3 N个线程N个锁)
  • [2. volatile 关键字](#2. volatile 关键字)
  • [3. wait等待 和 notify通知](#3. wait等待 和 notify通知)
  • 总结

前言

1. 线程安全

sql 复制代码
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:" + count);
    }

发现结果不是10000

而且每一次执行结果都不一样

因为t1和t2是并发执行的,这就是线程安全问题,或者叫做线程不安全


这个就是正确的顺序

这是其中一个错误

所以一定小于100000

这种错误,就可能导致小于50000

产生原因

原子就是不可分割的最小单位

count++就不是原子操作,有load,add,save等等操作

基本上很多操作多不是原子得到

但是java中的a=b这种操作是原子的

这两个也是线程不安全的原因

线程安全的解决方案

第一个原因,操作系统抢占式,我们是无法干预的

第二个原因不适用于所有情况,取决于实际需求,总不能要求不同时修改同一个变量吧,,,不太合理

第三个原因,就是把非原子的修改变为原子性的修改

-----》加锁

synchronized关键字

synch ro nized

sql 复制代码
    private static int count = 0;
    private static Object object = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (object) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (object) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:" + count);
    }

大括号里面的代码,就是我们要打包成一个原子性的代码

如果一边加锁一边不加锁,还是一样的有线程安全问题

synchronized (object)加锁的是object,必须是对同一个对象加锁,所以是不同对象,还是线程不安全

锁对象必须是一个类对象

String,List这些都可以

但是int a =10;

a就不能作为锁对象

多个线程竞争锁是不可预期的

synchronized底层实现就是JVM中c++中实现的,在依靠操作系统中的api实现的加锁

,然后来自于cpu上的特殊的指令来实现的

StringBuffeer线程安全----》因为相关操作带有synchronized关键字,但是使用不当,也会有线程安全问题

Stringbuilder线程不安全

如果还定义一个解锁操作的话,那么如果中间return了,或者抛出异常;1,那么就解锁不了

但是synchronized无论是return还是异常,都是会自动解锁的


类对象就是Class,反射

sql 复制代码
                synchronized (object.getClass()) {
                    count++;
                }

这样也是可以的

Class这种类对象也是唯一的

类对象也是对象,都可以写到synchronized

synchronized还可以修饰方法

sql 复制代码
class Counter{
    public int count = 0;
    public  void add(){
        count++;
    }
}
sql 复制代码
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:" + counter.count);
    }
sql 复制代码
                synchronized (counter) {
                    counter.add();
                }

可以这样加锁

sql 复制代码
class Counter{
    public int count = 0;
     synchronized public  void add(){
        count++;
    }
}

也可以这样加锁,都是等价的效果

synchronized 的锁对象就是this,就是counter

但是不要无脑加锁-------》使用锁可能会触发阻塞----》什么时候恢复呢--》不可预期

加锁是有代价的

比如这种static方法,没有this,你加锁的话,就相当于给类对象加锁

就相当于

给Counter.class加锁

若锁的是 类对象(比如 synchronized(Counter.class) 或静态同步方法):竞争范围是「整个类的所有实例」------ 因为每个类在 JVM 中只有 一个唯一的 Class 对象(存放在方法区),所有线程无论操作哪个实例,只要竞争这把锁,都会互斥。

简单说:synchronized(Counter.class) 锁定的是 Counter 类对应的 Class 对象,这把锁是「全局唯一」的,所有 Counter 实例(甚至无实例时)都会共享这把锁。

给类对象(静态方法)加锁就会阻塞所有的实例了

1.1 死锁问题

1.1.1 一个线程一把锁

sql 复制代码
class Counter{
    public int count = 0;
     synchronized public  void add(){
        count++;
    }
}
sql 复制代码
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (counter) {
                    counter.add();
                    
                }
            }
        });

这就是死锁了

就是对一把锁,一个线程,加了两次锁

这样没有死锁,是因为java的synchronized的特殊处理

但是如果是c++的话,就可能会出问题

synchronized的特殊处理:可重入锁,这样就不会死锁了

可重入锁就是说额外记录一下,当前是哪个线程对这个线程加锁了

如果这次线程再次对这个锁进行访问,放行

synchronized就是可重入锁,然后引入一个计数器,看看真加锁几次,假加锁几次,这样就可以知道在什么地方,合适的解锁了

1.1.2 两个线程两把锁

sql 复制代码
        Thread t1 = new Thread(()->{
            synchronized (lock1) {
                System.out.println("lock1 ");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2) {
                    System.out.println("lock2 ");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock2) {
                System.out.println("lock2 ");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock1) {
                    System.out.println("lock1 ");
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

现在它们两个线程的状态都是BLACKED的

都在获取下一个锁的时候阻塞了

1.1.3 N个线程N个锁

哲学家就餐问题

死锁的必要条件,四个条件缺一不可

1.锁是互斥的,锁的基本特性,你加锁了,别人就进不来---》不可改变

2.锁是不可抢占的,线程1拿到了锁,不主动释放的话,线程2就不能拿到锁,也是锁的基本特性--》不可改变

3.请求和保持,线程1拿到锁a之后,不释放a的前提下,去拿锁b

如果拿锁之前先释放自己的锁,就不会死锁了-

4.循环等待,多个线程获取锁的过程,存在循环等待

如何避免循环等待,给锁进行编号1,2,3,4,-----

加锁的时候,必须按照顺序加锁,必须先加锁编号小的,在加锁编号大的

就是一个线程有两个锁的时候,只能先对编号小的进行加锁,然后才对大的加锁,如果编号小的被别人用的,就只能一个锁都不用,就等待--》不会循环等待了---》不会在死锁了

sql 复制代码
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

比如这两个锁,我们就约定,不管什么情况。都先对lock1 进行加锁,在对lock2进行加锁

sql 复制代码
        Thread t1 = new Thread(()->{
            synchronized (lock1) {
                System.out.println("lock1 ");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2) {
                    System.out.println("lock2 ");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock1) {
                System.out.println("lock1 ");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lock2) {
                    System.out.println("lock2 ");
                }
            }
        });

大家都这样加锁,就不会死锁了

java中的集合类,大多数都是线程不安全的

多个线程修改同一个集合类的数据---》不安全

String

Vector

Stack

HashTable

StringBuffer都是加锁了的,安全的

ArrayList,Queue,HashMap,TreeMap,LinkedList,PriorityQueue都是不安全的

2. volatile 关键字

线程安全的第四个原因:内存可见性

sql 复制代码
    private static int n = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while (n==0){

            }
            System.out.println("hello t1");
        });
        Thread t2 = new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入:");
            n = sc.nextInt();

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

但是输入了2,什么都没有变化

为什么呢

这就是内存可见性问题

就是一个线程读,一个线程写数据的时候就会出现这个问题

sql 复制代码
            while (n==0){

            }

这个循环非常快,n==0这个判断有两件事




这个就是内存可见性问题

编译器的优化我们都是不好判断的,而且这是由写java的人来决定的

sql 复制代码
    private static int n = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while (n==0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("hello t1");
        });
        Thread t2 = new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入:");
            n = sc.nextInt();

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

但是如果在循环里面加上休眠---》内存可见性问题就消失了,成功了

---》读取n内存数据的优化操作没了--》因为与读取内存相比,sleep开销更大,所以就不弄这个优化了

如果没有sleep,但还是希望能够没有bug--》volatile

sql 复制代码
    private static volatile int n = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while (n==0){
            }
            System.out.println("hello t1");
        });
        Thread t2 = new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println("请输入:");
            n = sc.nextInt();

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


但是volatile不能解决原子性问题

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原⼦性, volatile 保证的是内存可⻅性

sql 复制代码
    private static volatile int n = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                n++;
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                n++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("n=" + n);
    }

3. wait等待 和 notify通知

因为线程的调度是随机的,所以有了这两个

多个线程---》控制线程之间执行某个逻辑的先后顺序

可以让后执行的逻辑,使用wait,先执行的线程完成某些逻辑之后,用notify来唤醒对应的wait

这两个还可以解决线程饿死问题

线程饿死:就是一直等待别人解锁,等半天

或者他刚刚解锁,然后上锁,让别人一直没有机会上锁

wait() / wait(long timeout): 让当前线程进⼊等待状态.

notify() / notifyAll(): 唤醒在当前对象上等待的线程

wait和notify是Object提供的方法,所以所有对象都可以wait和notify

sql 复制代码
        Object object = new Object();
        System.out.println("wait前");
        object.wait();
        System.out.println("wait后");

IllegalMonitorStateException:表示锁状态非法

因为wait中会针对obj进行解锁,所以一定要先对obj进行加锁,才可以解锁

sql 复制代码
        Object object = new Object();
        System.out.println("wait前");
        synchronized (object) {
            object.wait();
        }
        System.out.println("wait后");

但是没有打印出wait后呢---》阻塞到object.wait();了,由于代码中没有notify,所以会一直wait等待下去

所以wait的作用就是解锁,然后一直在这个方法这里阻塞等待,等待收到通知,这两个操作同时执行,是原子的,如果不是原子的,那么刚刚解锁,还没开始等待,别人突然就notify了---》自己再去等待,就可能不会被唤醒了

notify就是通知wait的线程被唤醒,被唤醒的线程就会重新竞争锁,再去继续执行原来得到操作

wait:释放锁,阻塞等待,收到通知之后,重新获取锁,继续执行

而且锁必须是同一个对象,才可以唤醒

sql 复制代码
    private static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            synchronized (lock) {
                System.out.println("t1 wait 前 ");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 后 ");
            }
        });
        Thread t2 = new Thread(()->{
            System.out.println("t2 notify 前");
            Scanner scanner = new Scanner(System.in);
            int i = scanner.nextInt();
            lock.notify();
            System.out.println("t2 notify 后");
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }


注意notify使用的时候也要加锁

sql 复制代码
    private static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            synchronized (lock) {
                System.out.println("t1 wait 前 ");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 后 ");
            }
        });
        Thread t2 = new Thread(()->{
            System.out.println("t2 notify 前");
            Scanner scanner = new Scanner(System.in);
            int i = scanner.nextInt();
            synchronized (lock) {
                lock.notify();
            }
            System.out.println("t2 notify 后");
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }

因为如果锁都没被wait,没有解锁--》你去notify是不科学的

所以wait就相当于中途离开锁

让notify的线程进来,在notify通知前一个进来


如果有多个wait的--》notify的时候,就是会随机唤醒一个

sql 复制代码
        Thread t1 = new Thread(()->{
            synchronized (lock) {
                System.out.println("t1 wait 前 ");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 后 ");
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock) {
                System.out.println("t2 wait 前 ");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2 wait 后 ");
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (lock) {
                System.out.println("t3 wait 前 ");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t3 wait 后 ");
            }
        });
        Thread t4 = new Thread(()->{
            System.out.println("t4 notify 前");
            Scanner scanner = new Scanner(System.in);
            int i = scanner.nextInt();
            synchronized (lock) {
                lock.notify();
            }
            System.out.println("t4 notify 后");
        });
        t1.start();
        t2.start();
        t3.start();
        t4.start();

botifyAll就是唤醒所有了

sql 复制代码
        Thread t4 = new Thread(()->{
            System.out.println("t4 notify 前");
            Scanner scanner = new Scanner(System.in);
            int i = scanner.nextInt();
            synchronized (lock) {
                lock.notifyAll();
            }
            System.out.println("t4 notify 后");
        });

notify是随机唤醒一个

没人wait,多次notify---》不会咋样,就和notify一次是一样的效果

sql 复制代码
        Thread t1 = new Thread(()->{
            System.out.println("A");
            synchronized (lock1) {
                lock1.notify();
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (lock1) {
                try {
                    lock1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("B");
            synchronized (lock2) {
                lock2.notify();
            }
        });
        Thread t3 = new Thread(()->{
            synchronized (lock2) {
                try {
                    lock2.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("C");
        });
        t1.start();
        t2.start();
        t3.start();

运行的时候,t2和t3先等待----》t1在notify,没有问题

但是如果这样呢,先notify了,t2再去wait---》wait晚了,没有通知了

sql 复制代码
        Thread t1 = new Thread(()->{
            System.out.println("A");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (lock1) {
                lock1.notify();
            }
        });

这样就好了

总结

相关推荐
v***5651 小时前
Spring Cloud Gateway
android·前端·后端
Boop_wu2 小时前
[Java EE] 多线程 -- 初阶(5) [线程池和定时器]
java·开发语言
optimistic_chen2 小时前
【Java EE进阶 --- SpringBoot】Spring事务传播机制
spring boot·后端·spring·java-ee·事务·事务传播机制
雨中飘荡的记忆3 小时前
Java + Groovy计费引擎详解
java·groovy
嘟嘟w3 小时前
JVM(Java 虚拟机):核心原理、内存模型与调优实践
java·开发语言·jvm
合作小小程序员小小店3 小时前
web开发,在线%药店管理%系统,基于Idea,html,css,jQuery,java,ssm,mysql。
java·前端·mysql·jdk·html·intellij-idea
ZHE|张恒3 小时前
设计模式(八)组合模式 — 以树结构统一管理对象层级
java·设计模式·组合模式
TDengine (老段)3 小时前
TDengine 转换函数 CAST 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ3 小时前
java实现校验sql中,表字段在表里是否都存在,不存在的给删除掉
java·sql