目录

一、多线程带来的风险 - 线程安全(重点)

1、了解线程不安全

2、线程安全的概念

3、线程不安全的原因

     3.1线程调度是随机的

     3.2修改共享数据

     3.3操作原子性

     3.4内存可见性

     3.5指令重排序

二、synchronized 关键字 - 监视器锁

1、synchronized 的特性

     1.1 互斥

     1.2 可重入

2、synchronized的使用

3、Java标准库中的线程安全类

三、volatile关键字

四、wait 和 notify

1、wait()方法

2、notify()方法

3、notifyAll()方法

4、wait 和 sleep 的区别

五、单例模式

1、饿汉式

2、懒汉式


一、多线程带来的风险 - 线程安全(重点)

1、了解线程不安全

public 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的结果应该为100000,但实际结果并不是


2、线程安全的概念

       线程安全是指当多个线程同时访问一个共享资源(如变量、对象、文件等)时,无论这些线程的执行调度顺序如何,都能保证程序的执行结果始终正确,且不会出现数据损坏、逻辑错误或不可预期的行为。

       如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。


3、线程不安全的原因

     3.1线程调度是随机的

         开发者无法保证线程对共享资源的访问顺序,随机调度使一个程序在多线程环境下,执行顺序存在很多变数。


     3.2修改共享数据

          多个线程修改同一个变量

          上面的线程不安全的代码中,涉及到多个线程对 count 变量进行修改

          此时这个 count 是一个多个线程都能访问到的 “ 共享数据 ”,若仅为 “ 只读操作 ”,则不会出现线程不安全问题


     3.3操作原子性

          原子性是指一个操作或一组操作要么全部执行完成,要么完全不执行,中间不会被其他线程打断。

一条Java语句不一定是原子的,也不一定是一条指令,比如 count++,其实是由三步操作组成的:

1、从内存把数据读到 CPU

2、进行数据更新

3、把数据写回到CPU

不保证原子性会给多线程带来什么问题

若线程 A 执行到步骤 2 时被暂停,线程 B 同时执行步骤 1 (读取到相同的5),后续两个线程都会写回 6 ,最终结果比预期少 1 ,引发数据错误。 


     3.4内存可见性

          内存可见性是指当一个线程修改了共享变量的值后,其它线程能够立即看到这个修改后的最新值的特性

Java内存模型(JMM)

Java虚拟机规范中定义了Java内存模型,目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下能达到一致的并发效果

  • 线程之间的共享变量存在主内存(Main Memory)
  • 每一个线程都有自己的 “ 工作内存 ” (Working Memory)
  • 当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据
  • 当线程要修改一个共享变量的时候,会先修改工作内存中的副本,再同步回主内存

  由于每个线程有自己的工作内存,这些工作内存中的内容相当于同一个共享变量的 “ 副本 ” ,此时修改线程1的工作内存中的值,线程2的工作内存不一定会及时变化


     3.5指令重排序

     假设早上做早餐有三个步骤:

  1. 煮开水
  2. 用开水泡麦片
  3. 把泡好的麦片端到餐桌

正常顺序是1->2->3,但为了节省时间,可能会调整顺序:1->3->2

    先煮开水,不等水开,先把麦片碗拿到餐桌,等水开后再泡麦片

    这种就叫做指令重排序,可能在多线程下变量的读写顺序与代码逻辑顺序不一致,进而引发线程安全问题


二、synchronized 关键字 - 监视器锁

1、synchronized 的特性

     1.1 互斥

synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一对象的 synchronized 就会阻塞等待

  • 进入 synchronized 修饰的代码块,相当于加锁
  • 退出 synchronized 修饰的代码块,相当于解锁


     1.2 可重入

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

Java 的 synchronized 是可重入锁

    public static int count=0;
    public static void main(String[] args) {
        Object locker=new Object();
        Thread t1=new Thread(() ->{
            synchronized (locker){
                synchronized (locker){
                    for (int i = 0; i < 50000; i++) {
                        count++;
                    }
                }
            }
        });
    }
  • 如果某个线程加锁的时候,发现锁已经被占用,但是恰好占用的是自己,那么仍然可以获取到锁,并让计数器自增


2、synchronized的使用

1、修饰代码块

      锁任意对象

        Object locker=new Object();
        public void fun(){
            synchronized (locker){
                
            }
        }

      锁当前对象

        public void fun(){
            synchronized (this){

            }
        }

2、直接修饰普通方法

        public synchronized void fun(){
           
            
        }

3、修饰静态方法

        public static synchronized void fun(){


        }

两个线程分别尝试获取两把不同的锁,不会产生锁竞争


3、Java标准库中的线程安全类

线程不安全:ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder

线程安全的:Vector(不建议使用)、HashTable(不建议)、ConcurrentHashMap、StringBuffer、String


三、volatile关键字

      volatile用于保证变量在多线程环境下的可见性、禁止指令重排序,但不保证原子性

代码在写入volatile修饰的变量时:

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取volatile修饰的变量时:

  • 从主内存中读取volatile的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

代码表示:

import java.util.Scanner;
public class Demo6 {
        public static volatile int flag=0;//如果没有volatile这个关键字,下面的输入不是0循环也不会结束
        public static void main(String[] args) {
                Thread t1=new Thread(() ->{
                   while (flag==0){

                   }
                        System.out.println("循环结束");
                });
                Thread t2=new Thread(() -> {
                        Scanner scanner=new Scanner(System.in);
                        System.out.println("请输入一个数:");
                         flag=scanner.nextInt();
                });
                t1.start();
                t2.start();
        }
}


四、wait 和 notify

1、wait()方法

wait的执行逻辑:

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

wait的结束条件:

  • 其它线程调用该对象的 notify 方法
  • wait 的超时等待方法
  • 其它线程调用该等待线程的interrupted方法,导致 wait 抛出异常

注意:wait 要搭配 synchronized 来使用,否则会抛出异常

代码展示:

public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        synchronized (locker){
            System.out.println("wait 之前");
            locker.wait();//如果没有唤醒,那么wait就会一直等待,不会继续往下执行
            System.out.println("wait 等待结束");
        }
    }
}


2、notify()方法

关键特性和使用规则:

  • notify( ) 必须在同步代码块(synchronized 块)或同步方法中调用,否则会抛出异常
  • 它会随机唤醒一个正在该对象上等待(通过 wait( ) 方法进入等待状态)的线程,而非所有等待线程
  • 调用 notify( ) 后,当前线程不会立即释放持有的锁,而是会继续执行完同步代码块或同步方法后,才会释放锁,此时被唤醒的线程才能尝试获取到锁并继续执行

代码展示:

public class Demo8 {
    public static void main(String[] args) {
        Object locker=new Object();
        Thread t1=new Thread(() ->{
           synchronized (locker){
               System.out.println("t1线程的wait之前");
               try {
                   locker.wait();
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               System.out.println("t1线程结束");
           }
        });
        Thread t2=new Thread(() ->{
           synchronized (locker){
               System.out.println("t2线程notify之前");
               try {
                   Thread.sleep(10000);//为了让效果更明显
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               locker.notify();
               System.out.println("t2线程notify之后");
           }
        });
        t1.start();
        t2.start();
    }
}


3、notifyAll()方法

使用 notifyAll方法可以一次性唤醒所有等待的线程

代码展示:

import java.util.Scanner;
public class Demo9 {
    private static void wait(Object locker)  {
        try {
            locker.wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1=new Thread(() ->{
           synchronized (locker) {
               System.out.println("t1 wait执行前");
               wait(locker);
           }
            System.out.println("t1 线程结束");
        });
        Thread t2=new Thread(() ->{
            synchronized (locker) {
                System.out.println("t2 wait执行前");
                wait(locker);
            }
            System.out.println("t2 线程结束");
        });
        Thread t3=new Thread(() ->{
            synchronized (locker) {
                System.out.println("t3 wait执行前");
                wait(locker);
            }
            System.out.println("t3 线程结束");
        });
        Thread t4=new Thread(() ->{
            synchronized (locker) {
                System.out.println("t4 wait执行前");
                wait(locker);
            }
            System.out.println("t4 线程结束");
        });

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        Thread t5=new Thread(() ->{
            synchronized (locker) {
                System.out.println("输入任意内容唤醒");
                Scanner scanner=new Scanner(System.in);
                scanner.next();
                locker.notifyAll();
            }
            System.out.println("t5 线程结束");
        });
        t5.start();
    }
}


4、wait 和 sleep 的区别

都可以让线程阻塞,都可以指定阻塞时间

不同点:

  1. wait 的设计是为了被 notify ,超时时间只是 “ 后手 ”;sleep 的设计就是为了按照一定时间阻塞
  2. wait 必须搭配锁使用;sleep 不需要
  3. wait 一进来会先释放锁,再获取到锁;sleep 放到锁内部,休眠时不会释放锁
  4. wait 虽然也能够通过 interrupt 唤醒,实际上更希望通过 notify 唤醒,notify 唤醒后,还可以随时再 wait ,再 notify;sleep 在 interrupt 之后可能把线程终止掉 


五、单例模式

确保在多线程环境下,一个类仅有一个实例

1、饿汉式

线程安全,但可能浪费资源

class Singleton{
    private static Singleton instance=new Singleton();
    private Singleton(){
        
    }
    public static Singleton getInstance(){
        return instance;
    }
}


2、懒汉式

首次调用 getInstance( ) 方法时创建实例,双重 if 判断加上 volatile 关键字解决线程安全问题

class SingletonLazy{
    private static SingletonLazy instance=null;//如果加上volatile则线程安全
    private static Object locker=new Object();
    private SingletonLazy(){

    }
    public static SingletonLazy getInstance(){
        //双重if判断
        //判断是否加锁
        if(instance==null) {
            synchronized (locker) {
        //判断实例是否存在
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐