java 线程安全性_Java线程安全性问题摘要_Power Node Java Academy的组织

30aa2a98cc05e34e2cf3ecd72cbfab4c.png

在Java内存模型上

不同的平台,内存模型不同,但是jvm内存模型规范是统一的. 实际上,Java的多线程并发问题最终将反映在Java的内存模型中. 所谓的线程安全无非是控制多个线程对多个资源的有序访问或修改. 总结Java的内存模型,必须解决两个主要问题: 可见性和顺序. 我们都知道计算机具有缓存,并且处理器在每次处理数据时都不会占用内存. JVM定义了自己的内存模型,从而屏蔽了底层平台内存管理的细节. 对于Java开发人员,如果您解决了多线程的可见性和有序性,则必须根据jvm内存模型进行明确说明.

那么,可见度是什么?多个线程无法相互通信,它们之间的通信只能通过共享变量来完成. Java内存模型(JMM)指定jvm具有主内存,该内存由多个线程共享. 当对象是新对象时,它也会分配到主存储器中. 每个线程都有其自己的工作内存. 工作存储器将一些对象的副本存储在主存储器中. 当然,线程的工作内存大小是有限的. 当线程对对象进行操作时,执行顺序如下:

(1)将变量从主内存复制到当前工作内存(读取和加载)

(2)执行代码并更改共享变量值(使用和分配)

(3)使用工作存储器数据刷新与主存储器有关的内容(存储和写入)

JVM规范定义了到主内存的线程的操作指令: 读取线程安全问题代码,加载,使用,分配,存储,写入. 当一个共享变量在多个线程的工作内存中具有一个副本时,如果一个线程修改了共享变量,则其他线程应该能够看到修改后的值,这就是多线程可见性的问题.

那么,什么是订单?当线程引用变量时,无法直接从主内存中引用该变量. 如果线程在工作内存中没有该变量,则会将副本从主内存复制到工作内存中. 加载,线程将在完成后引用该副本. . 当同一线程再次引用该字段时,可以从主内存中获取变量的副本(读取加载使用),也可以直接引用原始副本(使用),即顺序读取,加载和使用可以由JVM实施系统决定来确定.

线程无法直接将值分配给主内存中的字段. 它将值分配给工作存储器中的变量副本(分配). 完成后,变量副本将同步到主存储区(store-write). 过去根据JVM实现系统的决定进行同步. 如果存在此字段,它将从主存储器分配给工作存储器. 此过程为读取加载. 完成后,线程将引用变量副本. 当同一线程重复多次时为字段分配值,例如:

Java代码

for(int i=0;i<10;i++)

a++;

线程可能仅向工作内存中的副本分配值,并且仅在最后一次分配后才同步到主存储区域,因此JVM实现系统可以确定分配,存储和更改的顺序. 假设有一个共享变量x,线程a执行x = x + 1. 从上面的描述可以知道,x = x + 1不是原子操作,其执行过程如下:

20200209092632953589.png

1从主存储器到工作存储器读取变量x的副本

2将1加到x

3将x加1的值写回主存储器

如果另一个线程b执行x = x-1,则执行过程如下:

1从主存储器到工作存储器读取变量x的副本

2从x减去1

3将x减1的值写回到主存储器中

因此,显然,x的最终值不可靠. 假设x现在为10,则线程a增加1,线程b减少1. 从表面上看,最终的x仍为10,但是在多线程情况下会发生这种情况:

1: 线程a从主内存到工作内存读取x的副本,并且工作内存中x的值为10

2: 线程b从主内存读取x的副本到工作内存,并且工作内存中的x值为10

3: 线程a将工作内存中的x加1,而工作内存中的x为11

4: 线程a将x提交到主内存,其中x为11

44ed048f65b90b716f8befb05eb37919.png

5: 线程b将工作内存中的x值减1,而工作内存中的x值为9

6: 线程b将x提交到主内存,其中x为9

类似地,x可能为11,如果x是一个银行帐户,线程存款,线程b扣除,显然这是一个严重的问题,要解决此问题,必须确保线程a和线程b顺序正确执行后,每个线程执行的递增或递减1是原子操作. 看下面的代码:

Java代码

public class Account {

private int balance;

public Account(int balance) {

this.balance = balance;

}

public int getBalance() {

return balance;

}

public void add(int num) {

balance = balance + num;

}

public void withdraw(int num) {

balance = balance - num;

}

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

Account account = new Account(1000);

Thread a = new Thread(new AddThread(account, 20), "add");

Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");

a.start();

b.start();

a.join();

b.join();

System.out.println(account.getBalance());

}

static class AddThread implements Runnable {

Account account;

int amount;

public AddThread(Account account, int amount) {

this.account = account;

this.amount = amount;

}

public void run() {

for (int i = 0; i < 200000; i++) {

account.add(amount);

}

}

}

static class WithdrawThread implements Runnable {

Account account;

int amount;

public WithdrawThread(Account account, int amount) {

this.account = account;

this.amount = amount;

}

public void run() {

for (int i = 0; i < 100000; i++) {

account.withdraw(amount);

}

}

}

}

第一个执行结果是10200,第二个执行结果是1060. 每个执行的结果都是不确定的,因为线程的执行顺序是不可预测的. 这是Java同步的根本原因. sync关键字可确保多个线程对于同步块是互斥的. 作为同步方法进行同步解决了Java多线程的执行顺序和内存可见性. volatile关键字“解决多线程内存可见性问题”. 稍后将详细描述.

同步关键字

如上所述,java使用synced关键字作为多线程并发环境的执行顺序的保证之一. 当一段代码将修改共享变量时,这段代码将成为一个互斥或关键的部分. 为了确保共享变量的正确性,synchronized指示关键部分. 典型用法如下:

Java代码

synchronized(锁){

临界区代码

}

为了确保银行帐户的安全,该帐户的操作方法如下:

Java代码

0d3d86c95ea84ecdc040eec6c6b3cc90.png

public synchronized void add(int num) {

balance = balance + num;

}

public synchronized void withdraw(int num) {

balance = balance - num;

}

您只是说过sync的用法是这样的:

Java代码

synchronized(锁){

临界区代码

}

那么这对于公共同步的void add(int num)意味着什么?实际上,在这种情况下,锁定是此方法的对象. 同样,如果该方法是公共静态同步的void add(int num),则锁是该方法所在的类.

理论上,每个对象都可以用作锁,但是当一个对象用作锁时线程安全问题代码,应该由多个线程共享,这样才有意义. 在并发环境中,未共享的对象不是锁. 有意义的. 如果有这样的代码:

Java代码

public class ThreadTest{

public void test(){

Object lock=new Object();

synchronized (lock){

//do something

}

}

}

将lock变量作为锁存在根本没有任何意义,因为它根本不是共享对象,并且每个线程都将执行Object lock = new Object();. 每个线程都有自己的锁,并且没有锁竞争.

每个锁对象都有两个队列,一个是就绪队列,另一个是阻塞队列. 就绪队列存储将获取锁的线程. 阻塞队列存储被阻塞的线程. 唤醒线程(通知)后,它将进入就绪队列并等待CPU调度. 当线程a首次执行account.add方法时,jvm将检查锁定对象帐户的就绪队列是否已经在等待线程. 如果存在,则表明帐户锁已被占用. 由于这是第一次运行,因此该帐户的就绪队列为空,因此线程a获取锁并执行account.add方法. 如果这时发生,线程b必须执行account.withdraw方法,因为线程a已经获得了锁并且还没有释放它,所以线程b必须进入帐户的就绪队列并等待锁被执行.

线程执行关键部分代码的过程如下:

1获取同步锁

76daf41567adb07de95f8918fc0b309b.png

2清除工作记忆

3将变量副本从主内存复制到工作内存

4计算这些变量

5将变量从工作存储器写入主存储器

6解除锁定

可以看出,同步不仅可以保证多个线程的并发排序,而且可以确保多个线程的内存可见性.

生产者/消费者模型

生产者/消费者模型实际上是一个非常经典的线程同步模型. 在许多情况下,仅在多个线程之间保证共享资源操作上多个线程的相互排斥是不够的. 有合作.

假设存在这样一种情况,即桌子上有一块盘子,桌子上只能放一个鸡蛋. 擅长将鸡蛋放在盘子上. 如果盘子里有鸡蛋,请等到没有鸡蛋,B专门从盘子里取出鸡蛋. 如果板上没有鸡蛋,请等待直到板上有鸡蛋. 实际上,印版是相互排斥的区域. 每次将鸡蛋放在盘子上时,它应该是互斥的. A的等待实际上是在主动放弃锁,B也必须提醒A在等待时放鸡蛋.

如何让线程主动释放锁

很简单,只需调用锁的wait()方法即可. wait方法来自Object,因此任何对象都具有此方法. 看下面的代码片段:

Java代码

Object lock=new Object();//声明了一个对象作为锁

synchronized (lock) {

balance = balance - num;

//这里放弃了同步锁,好不容易得到,又放弃了

lock.wait();

}

如果线程获取锁,进入同步块并执行lock.wait(),则该线程将进入锁的阻塞队列. 如果调用lock.notify(),则将通知阻塞队列中的线程进入就绪队列.

本文来自电脑杂谈,转载请注明本文网址:

http://www.pc-fly.com/a/jisuanjixue/article-264852-1.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值