【javaEE】——synchronized使用和单例模式(线程安全)03

本文深入探讨Java多线程核心技术,包括线程安全、synchronized关键字、wait和notify方法、死锁预防、线程安全的单例模式及其实现、阻塞队列的应用和生产者-消费者模型、自定义定时器的设计与实现、线程池的原理和使用等。

目录

一、线程安全

1.1synchronized的使用

死锁的四个必要条件:

1.2Java 标准库中的线程安全类

1.3volatile 关键字

1.4 wai和notify

二、多线程案例

2.1 单例模式(懒汉、饿汉模式)

2.2 线程安全的单例模式(!!!!安全的懒汉模式(重要))

2.3 阻塞队列(生产者-消费者模型)

2.4  定时器

三、线程池


一、线程安全

操作系统中,由于线程的调度是随机性的(抢占式执行),由此带来了一些安全方面的问题。

 线程不安全的原因:

  • 1.线程之间的抢占式执行(无法确定顺序,具有随机性)
  • 2.多个线程 修改 同一个变量(规避:一个线程只改一个变量)
  • 3.原子性:操作只有一个步骤,多步骤互相排斥,加锁即保证了原子性(同步互斥,不能同时进行两个操作,A作业时B不能进行打断)
  • 4.内存可见性(和原子性类似):一个线程对共享变量值的修改,能够及时地被其他线程看到.(一个线程频繁读,另一个频繁写数据,则会存在可见性问题)
  • 指令重排序:代码执行顺序也会影响线程安全

给方法直接加synchronized关键字进行加锁,进入此方法自动加锁,离开方法,自动解锁,便可保证下面代码自增结果始终为100000

当一个线程加锁成功,其他线程尝试枷锁便会出发阻塞等待(BLOCKED),持续到占用所得线程释放锁为止。

class Counter {
    public int count;
    synchronized public void increase() {
        count++;
    }
}

public class thread {
    private static Counter counter = new Counter();  //创建一个实例counter

    public static void main(String[] args) throws InterruptedException {
        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); //100000
    }
}

执行计数器++,的步骤:

  • 1.先把内存中count的值加载到cpu寄存器中
  • 2.再给寄存器中的值+1
  • 3.再把寄存器的值写回到count种

synchronized可保证原子性、可见性、有序但不能禁止指令重排;

volatile保证可见性、禁止指令重排但不保证原子性

此处的有序并不代表指令重排序


1.1synchronized的使用

Java中每个类都是继承自Object,new出来的实例包含了自己安排的属性,也包含了“对象头”,对象的一些元数据。(加锁操作也就是给对象头里设置标志位)。两个线程针对同一个变量进行加锁才具有竞争,若是不同变量则不需竞争

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

监视器锁:monitor lock

  • 1.直接修饰普通方法:(针对某个对象(this)加锁)
    synchronized public void increase() {
        count++;
    }
  • 2.修饰一个代码块:需要显式指定那个对象加锁,(java中任意对象都可作为锁对象)
    public void increase() {
        synchronized(this) {  //锁对象需要手动指定
            count++;
        }
    }
  • 3.修饰一个静态方法:对类对象进行加锁
    public static void func() {
        synchronized (Counter.class) {  //针对类对象(Counter是类名)进行加锁
 
        }
    }

连续进行两次锁,则会造成死锁的状态:

  • 外层锁:进入方法则开始枷锁,加锁成功,因为当前锁无占用
  • 内层锁:进入代码块,开始加锁,加锁失败(外层占用锁,需要其释放才可加锁)
    synchronized public void increase() {
        synchronized(this) {  //锁对象需要手动指定
            count++;
        }
    }

 为了解决死锁的问题,引入了可重入锁(记录当前锁被占用的线程,记录一个加锁次数)

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

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.;
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
    }
    synchronized void increase2() {
        increase();
    }
}

死锁的四个必要条件:

  • 互斥使用:一个锁被占用后,其他线程不能占用(原子性)
  • 不可抢占:一个所被占用后,其他线程不能抢走
  • 请求和保持:一个线程占据多把锁之后,除非进行显式的释放锁,否则锁只能被该线程持有
  • 环路等待:等待关系(实际中,要尽量避免出现循环等待,给固定的顺序,就可避免死锁)

1.2Java 标准库中的线程安全类

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

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

 线程安全的 :

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer  (StringBuffer 的核心方法都带有 synchronized)
  • String(线程安全,非synchronized,是不可变对象(没有提供public 的修改属性操作)),不可变对象和常量/final没有联系

1.3volatile 关键字

volatile 修饰的变量, 能够保证 "内存可见性".。不会引起线程阻塞。

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

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

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

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

直接访问工作内存(实际是 CPU 的寄存器或 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况。加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.(缓存是介于CPU和寄存器之间的,空间比寄存器大,速度比CPU快)

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 的变化.

1.4 wai和notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序具有随机性。

wai和notify都是 Object 对象

wait 做的事情:

  • 释放当前的锁
  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.
<
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值