Java多线程之搞定最后一公里详解

目录
  • 绪论
  • 一:线程安全问题
    • 1.1 提出问题
    • 1.2 不安全的原因
      • 1.2.1 原子性
      • 1.2.2 代码“优化”
  • 二:如何解决线程不安全的问题
    • 2.1 通过synchronized关键字
    • 2.2 volatile
  • 三:wait和notify关键字
    • 3.1 wait方法
    • 3.2 notify方法
    • 3.3 wait和sleep对比(面试常考)
  • 四:多线程案例
    • 4.1 饿汉模式单线程
    • 4.2 懒汉模式单线程
    • 4.3 懒汉模式多线程低性能版
    • 4.4懒汉模式-多线程版-二次判断-性能高
  • 总结

绪论

上期介绍了多线程的概念、优势、创建方法以及几个常用的关键字。有了之前的基础过后,我们来讨论讨论线程安全问题以及其他线程进阶知识。

一:线程安全问题

1.1 提出问题

首先,给大家看一下这个代码:

public class yy1 {
    private static class Counter {
        private long n = 0;
        public void increment() {
            n++;
        }
        public void decrement() {
            n--;
        }
        public long value() {
            return n;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final int COUNT = 1000_0000;
        Counter counter = new Counter();
        Thread thread = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                counter.increment();
            }
        }, "李四");
        thread.start();
        for (int i = 0; i < COUNT; i++) {
            counter.decrement();
        }
        thread.join();
// 期望最终结果应该是 0
        System.out.println(counter.value());
    }
}

大家看结果:

大家观察下是否适用多线程的现象是否一致?同时尝试思考下为什么会有这样的现象发生呢?

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:

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

1.2 不安全的原因

1.2.1 原子性

举个简单的例子,当我i们买票的时候,如果车站剩余票数大于0,就可以买。反之,买完一张票后,车站的票数也会自动减一。假设出现这种情况,两个人同时来买票,只剩最后一张票,前面那个人把最后一张票买了,但是短时间内票数还没减一也就是清零,这时另外一个人看到还有一张票,于是提交订单,但是其实已经没有多余的票了,那么问题就来了。这时我们引入原子性:

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证, A 进入房间之后,还 没有出来; B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁, A 进去就把门锁上,其他人是不是就进不来了。这样 就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。 不保证原子性, 如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。

1.2.2 代码“优化”

一段代码是这样的: 1. 去前台取下 U 盘 2. 去教室写 10 分钟作业 3. 去前台取下快递 如果是在单线程情况下, JVM 、 CPU 指令集会对其进行优化,比如,按 1->3->2 的方式执行,也是没问题,可以少跑 一次前台。这种叫做指令重排序。 刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了,什么问题呢。可能快递是 在你写作业的10 分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的。

二:如何解决线程不安全的问题

2.1 通过synchronized关键字

synchronized 的底层是使用操作系统的 mutex lock 实现的。 当线程释放锁时, JMM 会把该线程对应的工作内存中的共享变量刷新到主内存中 当线程获取锁时, JMM 会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内 存中读取共享变量 synchronized 用的锁是存在 Java对象头里的。 synchronized 同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题; 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
public synchronized static void methond() {
}
public static void main(String[] args) {
method();
// 进入方法会锁 SynchronizedDemo.class 指向对象中的锁;出方法会释放
SynchronizedDemo.class 指向的对象中的锁
}
}

锁的 SynchronizedDemo 类的对象

public class SynchronizedDemo {
public synchronized static void methond() {
}
public static void main(String[] args) {
method();
// 进入方法会锁 SynchronizedDemo.class 指向对象中的锁;出方法会释放
SynchronizedDemo.class 指向的对象中的锁
}
}

明确锁的对象

public class SynchronizedDemo {
public synchronized static void methond() {
}
public static void main(String[] args) {
method();
// 进入方法会锁 SynchronizedDemo.class 指向对象中的锁;出方法会释放
SynchronizedDemo.class 指向的对象中的锁
}
}
public class SynchronizedDemo {
public void methond() {
// 进入代码块会锁 SynchronizedDemo.class 指向对象中的锁;出代码块会释放
SynchronizedDemo.class 指向的对象中的锁
synchronized (SynchronizedDemo.class) {
}
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.method();
}
}

2.2 volatile

这里提一下volatile:

首先,被volatile关键字修饰的变量,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步

三:wait和notify关键字

3.1 wait方法

其实 wait() 方法就是使线程停止运行。

1. 方法 wait() 的作用是使当前执行代码的线程进行等待, wait() 方法是 Object 类的方法,该方法是用来将当前线程 置入 “ 预执行队列 ” 中,并且在 wait() 所在的代码处停止执行,直到接到通知或被中断为止。

2. wait() 方法只能在同步方法中或同步块中调用。如果调用 wait() 时,没有持有适当的锁,会抛出异常。

3. wait() 方法执行后,当前线程释放锁,线程与其它线程竞争重新获取锁。

public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println(" 等待中 ...");
object.wait();
System.out.println(" 等待已过 ...");
}
System.out.println("main 方法结束 ...");
}

这样在执行到 object.wait() 之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了 另外一个方法唤醒的方法 notify() 。

3.2 notify方法

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

class MyThread implements Runnable {
    private boolean flag;
    private Object obj;
    public MyThread(boolean flag, Object obj) {
        super();
        this.flag = flag;
        this.obj = obj;
    }
    public void waitMethod() {
        synchronized (obj) {
            try {
                while (true) {
                    System.out.println("wait()方法开始.. " +
                            Thread.currentThread().getName());
                    obj.wait();
                    System.out.println("wait()方法结束.. " +
                            Thread.currentThread().getName());
                    return;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public void notifyMethod() {
        synchronized (obj) {
            try {
                System.out.println("notifyAll()方法开始.. " +
                        Thread.currentThread().getName());
                obj.notifyAll();
                System.out.println("notifyAll()方法结束.. " +
                        Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    @Override
    public void run() {
        if (flag) {
            this.waitMethod();
        } else {
            this.notifyMethod();
        }
    }
}
public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        MyThread waitThread1 = new MyThread(true, object);
        MyThread waitThread2 = new MyThread(true, object);
        MyThread waitThread3 = new MyThread(true, object);
        MyThread notifyThread = new MyThread(false, object);
        Thread thread1 = new Thread(waitThread1, "wait线程A");
        Thread thread2 = new Thread(waitThread2, "wait线程B");
        Thread thread3 = new Thread(waitThread3, "wait线程C");
        Thread thread4 = new Thread(notifyThread, "notify线程");
        thread1.start();
        thread2.start();
        thread3.start();
        Thread.sleep(1000);
        thread4.start();
        System.out.println("main方法结束!!");
    }
}

从结果上来看第一个线程执行的是一个 waitMethod 方法,该方法里面有个死循环并且使用了 wait 方法进入等待状态 将释放锁,如果这个线程不被唤醒的话将会一直等待下去,这个时候第二个线程执行的是 notifyMethod 方法,该方 法里面执行了一个唤醒线程的操作,并且一直将 notify 的同步代码块执行完毕之后才会释放锁然后继续执行 wait 结束 打印语句。 注意: wait , notify 必须使用在 synchronized 同步方法或者代码块内。

3.3 wait和sleep对比(面试常考)

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间。用生活中的例子说的话就是婚礼时会吃糖,和家里自己吃糖之间 有差别。说白了放弃线程执行只是 wait 的一小段现象。 当然为了面试的目的,我们还是总结下: 1. wait 之前需要请求锁,而 wait 执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对像上的 monitor lock 2. sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。 3. wait 是 Object 的方法 4. sleep 是 Thread 的静态方法

四:多线程案例

4.1 饿汉模式单线程

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

4.2 懒汉模式单线程

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

4.3 懒汉模式多线程低性能版

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

4.4懒汉模式-多线程版-二次判断-性能高

class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

总结

多线程的部分暂时分享到这里,但其实还有很多没有没有涉及 ,等日后深刻理解后再来分享,码文不易,多谢大家支持,感激不尽!

到此这篇关于Java多线程之搞定最后一公里详解的文章就介绍到这了,更多相关Java 多线程内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 一文彻底搞懂java多线程和线程池

    目录 什么是线程 一. Java实现线程的三种方式 1.1.继承Thread类 1.2.实现Runnable接口,并覆写run方法 二. Callable接口 2.1 Callable接口 2.2 Future接口 2.3 Future实现类是FutureTask. 三. Java线程池 3.1.背景 3.2.作用 3.3.应用范围 四. Java 线程池框架Executor 4.1.类图: 4.2 核心类ThreadPoolExecutor: 4.3 ThreadPoolExecutor逻辑结

  • Java多线程学习笔记

    目录 多任务.多线程 程序.进程.线程 学着看jdk文档 线程的创建 1.继承Thread类 2.实现Runable接口 理解并发的场景 龟兔赛跑场景 实现callable接口 理解函数式接口 理解线程的状态 线程停止 线程休眠sleep 1.网路延迟 2.倒计时等 线程礼让yield 线程强制执行 观察线程状态 线程的优先级 守护线程 线程同步机制 1.synchronized 同步方法 2.同步块synchronized(Obj){} lock synchronized与lock 多任务.多

  • Java多线程之线程状态详解

    目录 线程状态 停止线程 线程休眠 模拟网络延迟(放大问题的发生性) 模拟计时 线程礼让 插队(线程强制执行) 线程状态观测 线程优先级 守护线程 总结 线程状态 五个状态:新生.就绪.运行.死亡.阻塞 停止线程 不推荐使用JDK提供的stop().destroy()方法[已弃用] 推荐线程自己停止 建议用一个标志位进行终止变量,到flag=false,则终止线程运行 public class StopDemo implements Runnable { // 设置一个标志位 boolean f

  • Java多线程基本概念以及避坑指南

    目录 前言 1. 多线程基本概念 1.1 轻量级进程 1.2 JMM 1.3 Java中常见的线程同步方式 2. 避坑指南 2.1. 线程池打爆机器 2.2. 锁要关闭 2.3. wait要包两层 2.4. 不要覆盖锁对象 2.5. 处理循环中的异常 2.6. HashMap正确用法 2.7. 线程安全的保护范围 2.8. volatile作用有限 2.9. 日期处理要小心 2.10. 不要在构造函数中启动线程 End 前言 多核的机器,现在已经非常常见了.即使是一块手机,也都配备了强劲的多核处

  • Java基础之多线程方法状态和创建方法

    目录 Java之线程的五大状态及其常用方法(六个状态还有timed_wating超时等待) 1.线程的五大状态及其转换 2.设置或获取多线程的线程名称的方法 3.线程休眠------sleep()方法 4.线程让步------yield()方法 5. 等待线程终止------join()方法 6. 线程停止 7. 线程等待------wait()方法 8. 线程唤醒-------notify()方法 9. notifyAll()方法 JAVA多线程有哪几种实现方式? 1. 继承Thread类 2

  • Java多线程之搞定最后一公里详解

    目录 绪论 一:线程安全问题 1.1 提出问题 1.2 不安全的原因 1.2.1 原子性 1.2.2 代码"优化" 二:如何解决线程不安全的问题 2.1 通过synchronized关键字 2.2 volatile 三:wait和notify关键字 3.1 wait方法 3.2 notify方法 3.3 wait和sleep对比(面试常考) 四:多线程案例 4.1 饿汉模式单线程 4.2 懒汉模式单线程 4.3 懒汉模式多线程低性能版 4.4懒汉模式-多线程版-二次判断-性能高 总结

  • java 多线程与并发之volatile详解分析

    目录 CPU.内存.缓存的关系 CPU缓存 什么是CPU缓存 为什么要有多级CPU Cache Java内存模型(Java Memory Model,JMM) JMM导致的并发安全问题 可见性 原子性 有序性 volatile volatile特性 volatile 的实现原理 总结 CPU.内存.缓存的关系 要理解JMM,要先从计算机底层开始,下面是一份大佬的研究报告 计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的!如果我们计算一次a+b所需要的的时间: CPU读取内存获得a,1

  • Java多线程中ReentrantLock与Condition详解

    一.ReentrantLock类 1.1什么是reentrantlock java.util.concurrent.lock中的Lock框架是锁定的一个抽象,它允许把锁定的实现作为Java类,而不是作为语言的特性来实现.这就为Lock的多种实现留下了空间,各种实现可能有不同的调度算法.性能特性或者锁定语义.ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票.定时锁等候和可中断锁等候的一些特性.此外,它还提供了在激烈争用情况下更

  • java多线程关键字final和static详解

    这篇文章主要介绍了java多线程关键字final和static详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 final关键字 1.final关键字在单线程中的特点: 1)final修饰的静态成员:必须在进行显示初始化或静态代码块赋值,并且仅能赋值一次. 2)final修饰的类成员变量,可以在三个地方进行赋值:显示初始化.构造代码块和构造方法,并且仅能赋值一次. 3)final修饰的局部变量,必须在使用之前进行显示初始化(并不一定要在定义是

  • Java多线程之哲学家就餐问题详解

    一.题目 教材提供一个哲学家就餐问题的解决方案的框架.本问题要求通过pthreads 互斥锁来实现这个解决方案. 哲学家 首先创建 5 个哲学家,每个用数字 0~4 来标识.每个哲学家作为一个单独的 线程运行. 可使用 Pthreads 创建线程.哲学家在思考和吃饭之间交替.为了模拟这两种活动,请让线程休眠 1 到 3 秒钟.当哲学家想要吃饭时,他调用函数: pickup_forks(int philosopher _number) 其中,philosopher _number 为想吃饭哲学家的

  • Java多线程读写锁ReentrantReadWriteLock类详解

    目录 ReentrantReadWriteLock 读读共享 写写互斥 读写互斥 源码分析 写锁的获取与释放 读锁的获取与释放 参考文献 真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的.所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率. 读写锁表示两个锁,一个是读操作相关的锁

  • Java多线程中的Balking模式详解

    目录 1.场景 2.详细说明 3.Balking模式的本质:停止并返回 源代码如下: 总结 1.场景 自动保存功能: 为防止电脑死机,而定期将数据内容保存到文件中的功能. 2.详细说明 当数据内容被修改时,内容才会被保存.即当写入的内容与上次写入的内容一致时,其实就没有必要执行写入操作.也就是说,以”数据内容是否一致”作为守护条件.若数据内容相同,则不执行写入操作,直接返回. 3.Balking模式的本质:停止并返回 如果现在不合适执行该操作,或者没有必要执行该操作,就停止处理,直接返回—-Ba

  • Java多线程同步工具类CountDownLatch详解

    目录 简介 核心方法 CountDownLatch如何使用 CountDownLatch运行流程 运用场景 总结 简介 CountDownLatch是一个多线程同步工具类,在多线程环境中它允许多个线程处于等待状态,直到前面的线程执行结束.从类名上看CountDown既是数量递减的意思,我们可以把它理解为计数器. 核心方法 countDown():计数器递减方法. await():使调用此方法的线程进入等待状态,直到计数器计数为0时主线程才会被唤醒. await(long, TimeUnit):在

  • Java多线程模拟银行系统存钱问题详解

    目录 一.题目描述 二.解题思路 三.代码详解 多学一个知识点 一.题目描述 题目:模拟一个简单的银行系统,使用两个不同的线程向同一个账户存钱. 实现:使用特殊域变量volatile实现同步. 二.解题思路 创建一个类:SynchronizedBankFrame,继承JFrame类 写一个内部类Bank 定义一个account变量,来表示账户. deposit():一个存钱的方法 getAccount():显示账户余额的方法. 写一个内部类Transfer,实现Runnable接口 在run方法

  • Java多线程案例之阻塞队列详解

    目录 一.阻塞队列介绍 1.1阻塞队列特性 1.2阻塞队列的优点 二.生产者消费者模型 2.1阻塞队列对生产者的优化 三.标准库中的阻塞队列 3.1Java提供阻塞队列实现的标准类 3.2Blockingqueue基本使用 四.阻塞队列实现 4.1阻塞队列的代码实现 4.2阻塞队列搭配生产者与消费者的代码实现 一.阻塞队列介绍 1.1阻塞队列特性 阻塞队列特性: 一.安全性 二.产生阻塞效果 阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.阻塞队列能是一种线程安全的数据结构, 并且具有

随机推荐