Java多线程【状态与安全问题】(下)

简介: Java多线程【状态与安全问题】(下)

🍇2.3解决线程不安全方案


这个时候我们应该怎么解决这个问题呢? 上锁!(synchronized)

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到

同一个对象 synchronized 就会阻塞等待.

进入 synchronized 修饰的代码块, 相当于 加锁

退出 synchronized 修饰的代码块, 相当于 解锁


e0a740c49dbc4cb6981709f27c005fdb.png

public class Test {
   static class Count{
        public int count = 0;
        //对方法上锁
        synchronized public void sum(){
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        Thread thread1 = new Thread(()->{
            //对该线程上锁
            synchronized (count) {
                for (int i = 0; i < 50000; i++) {
                    count.sum();
                }
            }
        });
        Thread thread2 = new Thread(()->{
            //对该线程上锁
            synchronized (count) {
                for (int i = 0; i < 50000; i++) {
                    count.sum();
                }
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count.count);
    }
}


f4dcff251c41474eadfcd90a881b7d05.png

🍇2.4Java 标准库中线程安全类与不安全类


Java 标准库中线程不安全类: 使用了一些锁机制来控制


  1. Vector (不推荐使用)
  2. HashTable (不推荐使用)
  3. ConcurrentHashMap
  4. StringBuffer(StringBuffer 的核心方法都带有 synchronized )
    特例:String:还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的


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


  1. .ArrayList
  2. LinkedList
  3. TreeMap
  4. HashSet
  5. TreeSet
  6. StringBuilder


🍎三.多线程常见关键字


🍇3.1synchronized(锁)


🎈3.1.1互斥


synchronized的底层是使用操作系统的mutex lock实现的.


我们在介绍线程安全问题解决时说过 synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.

进入 synchronized 修饰的代码块, 相当于 加锁

退出 synchronized 修饰的代码块, 相当于 解锁


可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).

如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.

如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队


f3b6540efc90430fb024862451e09da7.png




理解 “阻塞等待”

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.


注意:

上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这

也就是操作系统线程调度的一部分工作.

假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B

和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能

获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

注意:synchronized是非公平锁


🎈3.1.2刷新内存


synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁
    所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.


🎈3.1.3可重入

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

理解 “把自己锁死”:

一个线程没有释放锁, 然后又尝试再次加锁.

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第

二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无

法进行解锁操作. 这时候就会 死锁

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.


🎈3.1.4synchronized使用案例


1.直接修饰普通方法: 锁的 SynchronizedDemo 对象


public class SynchronizedDemo {
public synchronized void methond() {
}
}


2. 修饰静态方法: 锁的 SynchronizedDemo 类的对象


public class SynchronizedDemo {
public synchronized static void method() {
}
}


3. 修饰代码块: 明确指定锁哪个对象


public class SynchronizedDemo {
public void method() {
}
}


4.锁类对象


public class SynchronizedDemo {
public void method() {
}
}



🍇3.2volatile(可见性)

🎈3.2.1volatile与synchronized区别


关键字 相同作用 不同作用
synchronized 可以保证内容可见性 能够保证线程执行的原子性
volatile 保证线程的可见性 并不能保证线程执行的原子性


🎈3.2.2volatile使用的场景


我峨嵋你来执行一个停止线程的代码程序


public class Test2 {
    private static int isQuit;
    private static Object okject = new Object();
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Thread thread = new Thread(()->{
            while(isQuit==0){
            }
            System.out.println("该线程执行结束");
        });
         thread.start();
        System.out.println("输入一个不为0的的数停止该线程");
        isQuit = scanner.nextInt();
        System.out.println("mian结束运行");
    }
}

690cf85e38384b3e8ed16bdaaeb93444.png


我们发现main结束时并没有执行到Thread内的输出"该线程结束",这是因为在在编译器执行时,因为每次快速执行线程的while循环时会被编译器优化掉while判断isQuit忽视这个值导致线程一直在执行并没有在输入非0时的结束操作而停止该线程结束

那我们应该怎样去做呢?

没错就是要用到我们刚刚认识到的volatile关键字(synchronized也可以)


volatile优化版本:8f8d4f89d15041ad9e087ffcc46462bb.png

public class Test2 {
    //volatile 在需要在循环判断的isQuit中加入volatile来防止编译器优化忽略判断isQuit
    volatile private static int isQuit;
    private static Object okject = new Object();
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Thread thread = new Thread(()->{
            while(isQuit==0){
            }
            System.out.println("该线程执行结束");
        });
        thread.start();
        System.out.println("输入一个不为0的的数停止该线程");
        isQuit = scanner.nextInt();
        System.out.println("mian结束运行");
    }
}

8f8d4f89d15041ad9e087ffcc46462bb.png


synchronized优化版本:

public class Test2 {
    private static int isQuit;
    private static final Object okject = new Object();
    public static void main(String[] args) throws InterruptedException {
        Scanner scanner = new Scanner(System.in);
        Thread thread = new Thread(()->{
            synchronized (okject) {
                while (isQuit == 0) {
                }
                System.out.println("该线程执行结束");
            }
        });
        thread.start();
        System.out.println("输入一个不为0的的数停止该线程");
        isQuit = scanner.nextInt();
        System.out.println("mian结束运行");
    }
}

9ba738d21a744497867f71b45733d2ec.png


我们发现该程序并没有结束

这也是编译器优化的问题,编译器的优化不仅可以产生内存可见性问题,也会产生重排序问题,就是保证在原有的逻辑下,调整代码的执行顺序,从而提高线程的运行效率从而引起线程的不安全,这个问题我们了解即可.


🍇3.3wait丶notify(等待丶唤醒)


🎈3.3.1wait


wait 做的事情:


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


注意:

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常


wait 结束等待的条件:

其他线程调用该对象的 notify 方法.wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.


代码示例: 观察wait()方法搭配synchronized使用

public class Testified {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        Thread thread = new Thread(()->{
            synchronized (o){
            }
           //执行一些任务
        });
        thread.start();
        System.out.println("开始等待");
        thread.wait();
        System.out.println("等待结束");
    }
}


dc0a0ac839ba441ba12462083fdaeb85.png


抛出错误这是因为我们需要搭配notify来进行等待和唤醒


🎈3.3.2notify


notify 方法是唤醒等待的线程

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


代码示例: 使用notify()方法唤醒线程

我们来设置一个通过线程2来等待后被线程1唤醒的操作

class Tack{
    public int i ;
    public void tack(int i){
        System.out.println("第"+i+"任务完成了!");
    }
}
public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        Object ob = new Object();
        Thread thread1 = new Thread(()->{
           synchronized (ob){
               //开始执行并且执行完成后将wait唤醒
               System.out.println("notify开始执行了!");
               ob.notify();
               Tack tack1 = new Tack();
               System.out.println("开始执行第一个任务!");
               tack1.tack(1);
           }
        });
        Thread thread2 = new Thread(()->{
            synchronized (ob){
                Tack tack2 = new Tack();
                System.out.println("开始执行第二个任务!");
                System.out.println("wait开始执行!");
                try {
                    //开始等待,等待被notify唤醒
                    ob.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tack2.tack(2);
            }
        });
        //先开始执行线程2
        thread2.start();
        //线阻塞0.1s
        Thread.sleep(100);
        //开始执行线程1
        thread1.start();
    }
}

182d452669e24f29877ae76cbacc9f8d.png

🍎四.总结


🍇4.1线程不安全情况总结(重点)


1.线程是抢占行执行,线程充满了随机性[这就是线程不安全的万恶之源!!!]

2.多线程对同一个变量进行修改操作

3.针对的变量操作不是原子性的

4.内存可见性被编译器优化

5.重拍序


这样问题所围绕的解决方法就是加锁!!在进行搭配volatile,wait,notify来进行解决问题和优化

相关文章
|
3天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
9天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
32 9
|
6天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
12天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
9天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
12天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
26 3
|
10天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
11天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
23 1
|
12天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
下一篇
无影云桌面