草庐IT

【五】线程安全VS线程不安全

一个想打拳的程序员 2023-07-30 原文

1. Java内存模型的特征

  Java内存模型是围绕着在并发过程中如何处理原子性可见性有序性这三个特征来建立。下面逐个看下哪些操作实现这三个特性:

1.1 原子性(Atomicity)

  由Java内存模型来直接保证的原子性变量操作包括 readloadassignusestorewrite 这六个,我们可以大致认为,基本数据类型的访问、读写都是具备原子性(例外就是 longdouble 的非原子性协定),当然如果应用场景需要更大的范围来保证原子性,可以使用 synchronized 关键字,在 synchronized 块之间的操作也具备原子性。

1.2 可见性(Visibility)

  所谓的可见性就是指当一个线程修改了共享变量的值时,其他线程能够立马知道这个修改。Java内存模型是通过在变量修改后将新值台同步回主内存,在变量读取之前从主内存刷新变量值这种依赖主内存作为传递中介的方式来实现可见性!不论是普通变量还是 volatile 变量都是一样。然后 volatile 和普通变量的区别在于,volatile 变量可以保证新值能立刻同步主内存,每次使用都是拿到最新的值。
  另外 synchronizedfinal 也可以实现可见性。

1.3 有序性(Ordering)

  如果在同一个线程内,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指:线程内表现为串行的语义;后半句:是指 指令重排序工作内存与主内存同步延迟现象。

2. 线程安全、线程不安全

  • 线程安全:就是说多线程访问同一代码,不会产生不确定的结果。
  • 线程不安全:就是多线程访问同一代码,会产生不确定的结果,造成线程不安全有5个原因:
    • 抢占式执行
    • 多个线程修改同一个变量
    • 修改操作,不是原子的
    • 内存可见性,引起的线程不安全
    • 指令重排序,引起的线程不安全

3. 如何解决线程不安全

3.1 使用synchronized 关键字

先来看一段线程不安全的代码

class Counters{
    public int count = 0;

    public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class Demo22 {

    public static void main(String[] args) throws InterruptedException {
        Counters counters = new Counters();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counters.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counters.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counters.getCount());
    }
}


可以看出结果并不是我们想的 20000. 是因为当我们在开启t1线程和t2线程操作同一个变量count,但是由于 count++ 这个操作并不具备原子性,该操作是从主内存中读取值到工作内存中,然后+1,在写回到主内存中,如果在t1读取旧值和写回新值的中间,t2线程也读取了值,这个值和t1读取的是一样的,如果t2读完旧值以后进行+1,并写回主内存,此时t2再写回主内存,在这种情况下,就少了一次+1的操作!此时就造成了线程不安全!因此需要 synchronized 关键字修饰,来保证线程的安全!下面来看synchronized的使用方法。


3.1.1 修饰实例方法

作用于当前实例加锁,进入同步代码前要获得当前实例的锁

class Counter{
    public int count = 0;
    synchronized public void add(){
        count++;
    }
    public int getCount(){
        return count;
    }
}
public class Demo21 {
    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }

}

此时输出结果为20000,此时保证了,当t1线程在访问synchronzed方法时,t2线程并不能访问。这就保证了操作的原子性! 另外要注意,当一个线程正在访问一个对象的synchronized实例方法,那么其他线程并不能访问该对象的其他synchronized方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,但是其他线程可以访问该实例对象的非synchronized方法。


3.1.2 修饰静态方法的时候

相当于给类加锁,会作用于这个类的所有对象实例
当一个线程A调用实例对象的非静态 synchronized方法,而线程B调用实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥对象!
因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用锁是当前实例对象的锁

class Counter {
    public static int count = 0;

    synchronized public static void add() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Demo21 {
    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();
        Counter counter1 = new Couter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                counter.add();
                System.out.println(counter.count);
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
            	couter1.add();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(counter.getCount());
    }
}


上述代码中,虽然创建了两个实例,但是由于count是被static修饰的,所以是类成员,所以当t1和t2线程进行操作时,操作的是同一个变量!所以当synchronized修饰静态方法的时候,锁就是当前class对象锁,就是 Counter.class。所以当两个线程进行访问时,竞争的是同一把锁,会产生互斥现象!所以保证了线程的安全性!另外如果线程t1访问的是实例对象的非静态synchronized方法时,另一个线程t2需要访问实例对象所属类的静态synchronized方法时,是不会发生互斥的,因为锁对象不同!


3.1.3 修饰代码块

指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁

class Counter {
    Object locker = new Object();
    public int count = 0;

    public void add() {
        synchronized (locker) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

public class Demo21 {
    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                counter.add();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(counter.getCount());
    }
}

修饰代码块时候,可以在()里面填写任意对象!由于在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,但是需要同步的代码块又是一小部分,所以此时就可以用修改代码块的方式加锁!


3.1.4 synchronized关键字总结

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的代码块或者方法在任意时刻都只能有一个线程执行。

4. 使用volatile关键字

4.1 内存可见性

先看一段代码:

public class Demo23 {
    public static int flag = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {

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

        t1.start();
        t2.start();
    }
}


此处可以看出程序并没有停止!利用下图来解释!

所以当你输入1的时候,由于优化,所以寄存器中的值仍为0


public class Demo23 {
    volatile public static int flag = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {

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

        t1.start();
        t2.start();
    }
}


当我们在用 volatile修饰 flag之后,可以看出程序最终结果和我们预想的一样!因为 volatile 保证了内存可见性,当变量修改时,可以立即同步回内存!

总结:volatile 不保证原子性! 适用的场景,一个线程读,一个线程写!

4.2 指令重排序

可以看一段伪代码

Map configOptions;
char[] configText;
// 此变量必须定义为 volatile
volatile boolean initialized = fasle;

// 假设以下代码在线程A中执行
// 模拟读取配置信息,当读取完成后
// 将initialized设置为true,通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized  = true;

// 假设以下代码在线程B执行
// 等待initialized为true,代表线程A已经把配置信息初始化
while(!initialized){
	sleep();
}

// 使用线程A中初始化好的配置信息
doSomethingWithConfig();

可以试想一下,如果定义的 initialized 没有被 volatile修饰,就可能会因为指令重排序的优化,导致线程A中最后一行代码被提前执行,这样在线程B中使用配置信息就会可能出现错误,而volatile关键字就可以避免此类情况发生!


如果有错误,请留言指正~~

本文参考资料:
《深入理解Java虚拟机》

有关【五】线程安全VS线程不安全的更多相关文章

  1. ruby-on-rails - Railstutorial : db:populate vs. 工厂女孩 - 2

    在railstutorial中,作者为什么选择使用这个(代码list10.25):http://ruby.railstutorial.org/chapters/updating-showing-and-deleting-usersnamespace:dbdodesc"Filldatabasewithsampledata"task:populate=>:environmentdoRake::Task['db:reset'].invokeUser.create!(:name=>"ExampleUser",:email=>"example@railstutorial.org",:passwo

  2. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  3. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  4. ruby - 如何安全地删除文件? - 2

    在Ruby中是否有Gem或安全删除文件的方法?我想避免系统上可能不存在的外部程序。“安全删除”指的是覆盖文件内容。 最佳答案 如果您使用的是*nix,一个很好的方法是使用exec/open3/open4调用shred:`shred-fxuz#{filename}`http://www.gnu.org/s/coreutils/manual/html_node/shred-invocation.html检查这个类似的帖子:Writingafileshredderinpythonorruby?

  5. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

  6. ruby - 用 YAML.load 解析 json 安全吗? - 2

    我正在使用ruby2.1.0我有一个json文件。例如:test.json{"item":[{"apple":1},{"banana":2}]}用YAML.load加载这个文件安全吗?YAML.load(File.read('test.json'))我正在尝试加载一个json或yaml格式的文件。 最佳答案 YAML可以加载JSONYAML.load('{"something":"test","other":4}')=>{"something"=>"test","other"=>4}JSON将无法加载YAML。JSON.load("

  7. ruby - 如何在 ruby​​ 中运行后台线程? - 2

    我是ruby​​的新手,我认为重新构建一个我用C#编写的简单聊天程序是个好主意。我正在使用Ruby2.0.0MRI(Matz的Ruby实现)。问题是我想在服务器运行时为简单的服务器命令提供I/O。这是从示例中获取的服务器。我添加了使用gets()获取输入的命令方法。我希望此方法在后台作为线程运行,但该线程正在阻塞另一个线程。require'socket'#Getsocketsfromstdlibserver=TCPServer.open(2000)#Sockettolistenonport2000defcommandsx=1whilex==1exitProgram=gets.chomp

  8. ruby - Rails 开发服务器、PDFKit 和多线程 - 2

    我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它:

  9. ruby-on-rails - 安全地显示使用回形针 gem 上传的图像 - 2

    默认情况下:回形针gem将所有附件存储在公共(public)目录中。出于安全原因,我不想将附件存储在公共(public)目录中,所以我将它们保存在应用程序根目录的uploads目录中:classPost我没有指定url选项,因为我不希望每个图像附件都有一个url。如果指定了url:那么拥有该url的任何人都可以访问该图像。这是不安全的。在user#show页面中:我想实际显示图像。如果我使用所有回形针默认设置,那么我可以这样做,因为图像将在公共(public)目录中并且图像将具有一个url:Someimage:看来,如果我将图像附件保存在公共(public)目录之外并且不指定url(同

  10. arrays - Ruby 数组 += vs 推送 - 2

    我有一个数组数组,想将元素附加到子数组。+=做我想做的,但我想了解为什么push不做。我期望的行为(并与+=一起工作):b=Array.new(3,[])b[0]+=["apple"]b[1]+=["orange"]b[2]+=["frog"]b=>[["苹果"],["橙子"],["Frog"]]通过推送,我将推送的元素附加到每个子数组(为什么?):a=Array.new(3,[])a[0].push("apple")a[1].push("orange")a[2].push("frog")a=>[[“苹果”、“橙子”、“Frog”]、[“苹果”、“橙子”、“Frog”]、[“苹果”、“

随机推荐