首頁 >Java >java教程 >一起聊聊Java多執行緒之線程安全問題

一起聊聊Java多執行緒之線程安全問題

WBOY
WBOY轉載
2022-04-21 18:17:512018瀏覽

這篇文章為大家帶來了關於java的相關知識,其中主要介紹了關於多線程的相關問題,包括了線程安裝、線程加鎖與線程不安全的原因、線程安全的標準類等等內容,希望對大家有幫助。

一起聊聊Java多執行緒之線程安全問題

推薦學習:《java影片教學

本篇文章介紹的內容為Java多執行緒中的執行緒安全問題,此處的安全性問題並不是指的像駭客入侵造成的安全問題,執行緒安全問題是指因多執行緒搶佔式執行而導致程式出現bug的問題。

1.線程安全概述

1.1什麼是線程安全問題

首先我們需要明白作業系統中執行緒的調度是搶佔式執行的,或者說是隨機的,這就造成執行緒調度執行時執行緒的執行順序是不確定的,有一些程式碼執行順序不同不影響程式運行的結果,但也有一些程式碼執行順序發生改變了重寫的運行結果會受影響,這就造成程式會出現bug,對於多執行緒並發時會使程式出現bug的程式碼稱作執行緒不安全的程式碼,這就是執行緒安全性問題。

下面,將介紹一個典型的執行緒安全性問題實例,整數自增問題。

1.2一個存在線程安全問題的程式

有一天,老師佈置了這樣一個問題:使用兩個執行緒將變數count自增10萬次,每個執行緒承擔5萬次的自增任務,變數count的初始值為0
這個問題很簡單,最終的結果我們也能夠口算出來,答案就是10萬。
小明同學做事非常迅速,很快就寫出了下面的一段程式碼:

class Counter {
    private int count;
    public void increase() {
        ++this.count;
    }
    public int getCount() {
        return this.count;
    }}public class Main11 {
    private static final int CNT = 50000;
    private static final Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i  {
            for (int j = 0; j <p>按理來說,結果應該是<code>10</code>萬,我們來看看運行結果: <br><img src="https://img.php.cn/upload/article/000/000/067/5a3746e6458021f78f91292b5a885487-1.png" alt="一起聊聊Java多執行緒之線程安全問題"><br> 運行的結果比<code>10</code>萬要小,你可以試著執行程式你會發現每次運行的結果都不一樣,但絕大部分情況,結果都會比預期的數值小,下面我們就來分析分析為什麼會這樣。 </p><h2><strong>2.線程加鎖與線程不安全的原因</strong></h2><h2>2.1案例分析</h2><p>上面我們使用多線程運行了一個程序,將一個變數值為0的變數自增10萬次,但是最終實際結果比我們預期結果要小,原因就是執行緒調度的順序是隨機的,造成執行緒間自增的指令集交叉,導致執行時出現兩次自增但值只自增一次的情況,所以得到的結果會偏小。 </p><p>我們知道一次自增操作可以包含以下幾個指令:</p><ol>
<li>將記憶體中變數的值載入到暫存器,不妨將該運算元為<code>load</code>。 </li>
<li>在暫存器中執行自增操作,不妨將該運算記為<code>add</code>。 </li>
<li>將暫存器的值儲存至記憶體中,不妨將該運算記為<code>save</code>。 </li>
</ol><p>我們來畫一條時間軸,來總結常見的幾種情況:</p><p><strong>情況1:</strong> 執行緒間指令集,無交叉,運行結果與預期相同,圖中暫存器A表示執行緒1所使用的暫存器,暫存器B表示執行緒2所使用的暫存器,後續情況同理。 <br><img src="https://img.php.cn/upload/article/000/000/067/1a7f0bb1b7c2552a80983e04b900dc15-2.png" alt="一起聊聊Java多執行緒之線程安全問題"><br><strong>情況2:</strong> 執行緒間指令集存在交叉,運行結果低於預期結果。 <br><img src="https://img.php.cn/upload/article/000/000/067/1a7f0bb1b7c2552a80983e04b900dc15-3.png" alt="一起聊聊Java多執行緒之線程安全問題"><br><strong>情況3:</strong> 執行緒間指令集完全交叉,實際結果低於預期。 <br><img src="https://img.php.cn/upload/article/000/000/067/4e482e48ea6191128516ca82417af3cf-4.png" alt="一起聊聊Java多執行緒之線程安全問題"><br> 根據上面我們所列舉的情況,發現執行緒執行時沒有交叉指令的時候運行結果是正常的,但是一旦有了交叉會導致自增操作的結果會少<code>1 </code>,綜上可以得到一個結論,那就是由於自增操作不是原子性的,多個執行緒並發執行時很可能會導致執行的指令交叉,導致執行緒安全問題。 </p><p>那要如何解決上述執行緒不安全的問題呢?當然有,那就是對物件加鎖。 </p><h2>2.2執行緒加上鎖定</h2><h3><strong>2.2.1什麼是加鎖</strong></h3><p>為了解決「搶佔式執行」所導致的執行緒安全性問題,我們可以對操作的物件進行加鎖,當一個執行緒拿到該物件的鎖後,會將該物件鎖起來,其他執行緒如果需要執行該物件的任務時,需要等待該執行緒執行完該物件的任務後才能執行。 </p><p>举个例子,假设要你去银行的ATM机存钱或者取款,每台ATM机一般都在一间单独的小房子里面,这个小房子有一扇门一把锁,你进去使用ATM机时,门会自动的锁上,这个时候如果有人要来取款,那它得等你使用完并出来它才能进去使用ATM,那么这里的“你”相当于线程,ATM相当于一个对象,小房子相当于一把锁,其他的人相当于其他的线程。<br><img src="https://img.php.cn/upload/article/000/000/067/4e482e48ea6191128516ca82417af3cf-5.png" alt="一起聊聊Java多執行緒之線程安全問題"><br><img src="https://img.php.cn/upload/article/000/000/067/6e6c80338fcb268f141a907ecdcadb51-6.png" alt="一起聊聊Java多執行緒之線程安全問題"><br> 在java中最常用的一起聊聊Java多執行緒之線程安全問題操作就是使用<code>synchronized</code>关键字进行一起聊聊Java多執行緒之線程安全問題。</p><h3>2.2.2如何一起聊聊Java多執行緒之線程安全問題</h3><p>synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。<br> 线程进入 synchronized 修饰的代码块, 相当于<code>一起聊聊Java多執行緒之線程安全問題</code>,退出 synchronized 修饰的代码块, 相当于 <code>解锁</code>。</p><p>java中的一起聊聊Java多執行緒之線程安全問題操作可以使用<code>synchronized</code>关键字来实现,它的常见使用方式如下:</p><p><strong>方式1:</strong> 使用<code>synchronized</code>关键字修饰普通方法,这样会使方法所在的对象加上一把锁。<br> 例如,就以上面自增的程序为例,尝试使用<code>synchronized</code>关键字进行一起聊聊Java多執行緒之線程安全問題,如下我对<code>increase</code>方法进行了一起聊聊Java多執行緒之線程安全問題,实际上是对某个对象一起聊聊Java多執行緒之線程安全問題,此锁的对象就是<code>this</code>,本质上一起聊聊Java多執行緒之線程安全問題操作就是修改<code>this</code>对象头的标记位。</p><pre class="brush:php;toolbar:false">class Counter {
    private int count;
    synchronized public void increase() {
        ++this.count;
    }
    public int getCount() {
        return this.count;
    }}

多线程自增的main方法如下,后面会以相同的栗子介绍synchronized的其他用法,后面就不在列出这段代码了。

public class Main11 {
    private static final int CNT = 50000;
    private static final Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i  {
            for (int j = 0; j <p>看看一起聊聊Java多執行緒之線程安全問題:<br><img src="https://img.php.cn/upload/article/000/000/067/6e6c80338fcb268f141a907ecdcadb51-7.png" alt="一起聊聊Java多執行緒之線程安全問題"><br><strong>方式2:</strong> 使用<code>synchronized</code>关键字对代码段进行一起聊聊Java多執行緒之線程安全問題,但是需要显式指定一起聊聊Java多執行緒之線程安全問題的对象。<br> 例如:</p><pre class="brush:php;toolbar:false">class Counter {
    private int count;
    public void increase() {
        synchronized (this){
            ++this.count;
        }
    }
    public int getCount() {
        return this.count;
    }}

一起聊聊Java多執行緒之線程安全問題:
一起聊聊Java多執行緒之線程安全問題
方式3: 使用synchronized关键字修饰静态方法,相当于对当前类的类对象进行一起聊聊Java多執行緒之線程安全問題。

class Counter {
    private static int count;
    synchronized public static void increase() {
        ++count;
    }
    public int getCount() {
        return this.count;
    }}

一起聊聊Java多執行緒之線程安全問題:
一起聊聊Java多執行緒之線程安全問題
常见的用法差不多就是这些,对于线程一起聊聊Java多執行緒之線程安全問題(线程拿锁),如果两个线程同时拿一个对象的锁,就会产生锁竞争,两个线程同时拿两个不同对象的锁不会产生锁竞争。
对于synchronized这个关键字,它的英文意思是同步,但是同步在计算机中是存在多种意思的,比如在多线程中,这里同步的意思是“互斥”;而在IO或网络编程中同步指的是“异步”,与多线程没有半点的关系。

synchronized 的工作过程:

  1. 获得互斥锁lock
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁unlock

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题,即死锁问题,关于死锁后续文章再做介绍。

综上,synchronized关键字一起聊聊Java多執行緒之線程安全問題有如下性质:互斥性,刷新内存性,可重入性。

synchronized关键字也相当于一把监视器锁monitor lock,如果不一起聊聊Java多執行緒之線程安全問題,直接使用wait方法(一种线程等待的方法,后面细说),会抛出一起聊聊Java多執行緒之線程安全問題,引发这个异常的原因就是没有一起聊聊Java多執行緒之線程安全問題。

2.2.3再析案例

对自增那个代码上锁后,我们再来分析一下为什么加上了所就线程安全了,先列代码:

class Counter {
    private int count;
    synchronized public void increase() {
        ++this.count;
    }
    public int getCount() {
        return this.count;
    }}public class Main11 {
    private static final int CNT = 50000;
    private static final Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i  {
            for (int j = 0; j <p>多线程并发执行时,上一次就分析过没有指令集交叉就不会出现问题,因此这里我们只讨论指令交叉后,一起聊聊Java多執行緒之線程安全問題操作是如何保证线程安全的,不妨记一起聊聊Java多執行緒之線程安全問題为<code>lock</code>,解锁为<code>unlock</code>,两个线程运行过程如下:<br> 线程1首先拿到目标对象的锁,对对象进行一起聊聊Java多執行緒之線程安全問題,处于<code>lock</code>状态,当线程2来执行自增操作时会发生阻塞,直到线程1的自增操作完毕,处于<code>unlock</code>状态,线程2才会就绪取执行线程2的自增操作。<br><img src="https://img.php.cn/upload/article/000/000/067/aacb0e52e264f65d74e598d37852834f-10.png" alt="一起聊聊Java多執行緒之線程安全問題"><br> 一起聊聊Java多執行緒之線程安全問題后线程就是串行执行,与单线程其实没有很大的区别,那多线程是不是没有用了呢?但是对方法一起聊聊Java多執行緒之線程安全問題后,线程运行该方法才会一起聊聊Java多執行緒之線程安全問題,运行完该方法就会自动解锁,况且大部分操作并发执行是不会造成线程安全的,只有少部分的修改操作才会有可能导致线程安全问题,因此整体上多线程运行效率还是比单线程高得多。</p><h2>2.3线程不安全的原因</h2><p>首先,线程不安全根源是线程间的调度充满随机性,导致原有的逻辑被改变,造成线程不安全,这个问题无法解决,无可奈何。</p><p>多个线程针对同一资源进行写(修改)操作,并且针对资源的修改操作不是原子性的,可能会导致线程不安全问题,类似于数据库的事务。</p><p>由于编译器的优化,内存可见性无法保证,就是当线程频繁地对同一个变量进行读操作时,会直接从寄存器上读值,不会从内存上读值,这样内存的值修改时,线程就感知不到该变量已经修改,会导致线程安全问题(这是编译器优化的结果,现代的编译器都有类似的优化不止于Java),因为相比于寄存器,从内容中读取数据的效率要小的多,所以编译器会尽可能地在逻辑不变的情况下对代码进行优化,单线程情况下是不会翻车的,但是多线程就不一定了,比如下面一段代码:</p><pre class="brush:php;toolbar:false">import java.util.Scanner;public class Main12 {
    private static int isQuit;
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (isQuit == 0) {

            }
            System.out.println("线程thread执行完毕!");
        });
        thread.start();

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入isQuit的值,不为0线程thread停止执行!");
        isQuit = sc.nextInt();
        System.out.println("main线程执行完毕!");
    }}

一起聊聊Java多執行緒之線程安全問題:
一起聊聊Java多執行緒之線程安全問題
我们从一起聊聊Java多執行緒之線程安全問題可以知道,输入isQuit后,线程thread没有停止,这就是编译器优化导致线程感知不到内存可见性,从而导致线程不安全。
我们可以使用一起聊聊Java多執行緒之線程安全問題关键字保证内存可见性。
我们可以使用一起聊聊Java多執行緒之線程安全問題关键字修饰isQuit来保证内存可见性。

import java.util.Scanner;public class Main12 {
    一起聊聊Java多執行緒之線程安全問題 private static int isQuit;
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (isQuit == 0) {

            }
            System.out.println("线程thread执行完毕!");
        });
        thread.start();

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入isQuit的值,不为0线程thread停止执行!");
        isQuit = sc.nextInt();
        System.out.println("main线程执行完毕!");
    }}

一起聊聊Java多執行緒之線程安全問題:
一起聊聊Java多執行緒之線程安全問題

synchronized与一起聊聊Java多執行緒之線程安全問題关键字的区别:
synchronized关键字能保证原子性,但是是否能够保证内存可见性要看情况(上面这个栗子是不行的),而一起聊聊Java多執行緒之線程安全問題关键字只能保证内存可见性不能保证原子性。
保证内存可见性就是禁止编译器做出如上的优化而已。

import java.util.Scanner;public class Main12 {
    private static int isQuit;
    //锁对象
    private static final Object lock = new Object();
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
                synchronized (lock) {
                    while (isQuit == 0) {

                    }
                    System.out.println("线程thread执行完毕!");
                }
        });
        thread.start();

        Scanner sc = new Scanner(System.in);
        System.out.println("请输入isQuit的值,不为0线程thread停止执行!");
        isQuit = sc.nextInt();
        System.out.println("main线程执行完毕!");
    }}

一起聊聊Java多執行緒之線程安全問題:
一起聊聊Java多執行緒之線程安全問題

编译器优化除了导致内存可见性感知不到的问题,还有指令重排序也会导致线程安全问题,指令重排序也是编译器优化之一,就是编译器会智能地(保证原有逻辑不变的情况下)调整代码执行顺序,从而提高程序运行的效率,单线程没问题,但是多线程可能会翻车,这个原因了解即可。

3.线程安全的标准类

Java 标准库中很多都是线程不安全的。这些类可能会涉及到多线程修改共享数据, 又没有任何一起聊聊Java多執行緒之線程安全問題措施。例如,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder。
但是还有一些是线程安全的,使用了一些锁机制来控制,例如,Vector (不推荐使用),HashTable (不推荐使用),ConcurrentHashMap (推荐),StringBuffer。
还有的虽然没有一起聊聊Java多執行緒之線程安全問題, 但是不涉及 “修改”, 仍然是线程安全的,例如String。

在线程安全问题中可能你还会遇到JMM模型,在这里补充一下,JMM其实就是把操作系统中的寄存器,缓存和内存重新封装了一下,其中在JMM中寄存器和缓存称为工作内存,内存称为主内存。
其中缓存分为一级缓存L1,二级缓存L2和三级缓存L3,从L1到L3空间越来越大,最大也比内存空间小,最小也比寄存器空间大,访问速度越来越慢,最慢也比内存的访问速度快,最快也没有寄存器访问快。

4.Object类提供的线程等待方法

除了Thread类中的能够实现线程等待的方法,如join,sleep,在Object类中也提供了相关线程等待的方法。

序号 方法 说明
1 public final void wait() throws InterruptedException 释放锁并使线程进入WAITING状态
2 public final native void wait(long timeout) throws InterruptedException; 相比于方法1,多了一个最长等待时间
3 public final void wait(long timeout, int nanos) throws InterruptedException 相比于方法2,等待的最长时间精度更大
4 public final native void notify(); 唤醒一个WAITING状态的线程,并一起聊聊Java多執行緒之線程安全問題,搭配wait方法使用
5 public final native void notifyAll(); 唤醒所有处于WAITING状态的线程,并一起聊聊Java多執行緒之線程安全問題(很可能产生锁竞争),搭配wait方法使用

上面介绍synchronized关键字的时候,如果不对线程一起聊聊Java多執行緒之線程安全問題会产生非法监视异常,我们来验证一下:

public class TestDemo12 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行完毕!");
        });

        thread.start();
        System.out.println("wait前");
        thread.wait();
        System.out.println("wait后");
    }}

看看一起聊聊Java多執行緒之線程安全問題:
一起聊聊Java多執行緒之線程安全問題
果然抛出了一个IllegalMonitorStateException,因为wait方法的执行步骤为:先释放锁,再使线程等待,你现在都没有一起聊聊Java多執行緒之線程安全問題,那如何释放锁呢?所以会抛出这个异常,但是执行notify是无害的。

wait方法常常搭配notify方法搭配一起使用,前者能够释放锁,使线程等待,后者能获取锁,使线程继续执行,这套组合拳的流程图如下:
一起聊聊Java多執行緒之線程安全問題

现在有两个任务由两个线程执行,假设线程2比线程1先执行,请写出一个多线程程序使任务1在任务2前面完成,其中线程1执行任务1,线程2执行任务2。
这个需求可以使用wait/notify来实现。

class Task{
    public void task(int i) {
        System.out.println("任务" + i + "完成!");
    }}public class WiteNotify {
    //锁对象
    private static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                Task task1 = new Task();
                task1.task(1);
                //通知线程2线程1的任务完成
                System.out.println("notify前");
                lock.notify();
                System.out.println("notify后");
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                Task task2 = new Task();
                //等待线程1的任务1执行完毕
                System.out.println("wait前");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                task2.task(2);
                System.out.println("wait后");
            }
        });
        thread2.start();
        Thread.sleep(10);
        thread1.start();
    }}

一起聊聊Java多執行緒之線程安全問題:
一起聊聊Java多執行緒之線程安全問題

推荐学习:《java视频教程

以上是一起聊聊Java多執行緒之線程安全問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除