浅谈利用同步机制解决Java中的线程安全问题
我们知道大多数程序都不会是单线程程序,单线程程序的功能非常有限,我们假设一下所有的程序都是单线程程序,那么会带来怎样的结果呢?假如淘宝是单线程程序,一直都只能一个一个用户去访问,你要在网上买东西还得等着前面千百万人挑选购买,最后心仪的商品下架或者售空......假如饿了吗是单线程程序,那么一个用户得等前面全国千万个用户点完之后才能进行点餐,那饿了吗就该倒闭了不是吗?以上两个简单的例子,就说明一个程序能进行多线程并发访问的重要性,今天就让我们去了解一下Java中多线程并发访问这个方向吧。
**第一步:理解多线程的概念**
很多初学者,并不知道什么是多线程,那么在此我将简单介绍一下多线程。线程是指在一个进程中的一个顺序执行流(也就是一段执行的代码),多线程则是在一个进程中存在多个顺序执行流,它们相互独立,共享进程中的所有资源(进程中的代码段,进程的内存空间等)。
**第二步:多线程的并发访问**
那么多个线程又是如何进行并发访问的呢?我们直接上代码:
public class MyThread extends Thread{
public void run(){
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i=0;i<5;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==3){
new MyThread().start();
new MyThread().start();
}
}
}
上面就是一段最简单的多线程并发执行的例子,这段代码中一共有三个线程,main、Thread-0、Thread-1。它们并发执行上面的程序。我们来看看运行的结果:
main 2
main 3
Thread-0 0
Thread-1 0
main 4
Thread-1 1
Thread-1 2
Thread-0 1
Thread-0 2
以上是我截取的部分运行结果,由运行结果可以看出main、 Thread-0、Thread-1三个线程都对i进行了取值,而且三个线程相对独立,各自取各自的值,相互不影响。同时应该注意到,三个线程取的值是不连续的,这是因为我所创建的i是一个实例变量而不是一个局部变量,每个线程去执行线程执行体的时候都会重新对i进行取值,所以此处对i的取值不是连续的。
对于上述代码和运行结果可知,多线程并发访问的特点是:线程之间相互独立,不受其他线程的干扰。
**第三步:多线程并发访问时同步安全问题**
从第二步的叙述中,我们知道了多个线程可以对一个对象进行同时访问,那么一些问题也随之出现,那就是多线程并发访问一个对象时的安全问题。我们由一个经典题目来慢慢去剖析多线程并发安全问题,并尝试去解决这个问题。
银行取钱问题:银行取钱的流程我们大概可以分为这么几步:
*1. 用户输入账户、密码,系统去判断用户的账户密码是否正确。
*2. 用户输入取款金额。
*3. 系统判断用户的余额是否大于用户的取款金额。
*4. 如果用户的余额大于取款金额,则取款成功,如果用户的余额小于取款金额,则取款失败。
上面的操作结果好像是有理有据的,那么我们就继续上代码去完成上面的需求吧!
class Account{
//封装用户的账户、密码
private String account;
private double balance;
public Account(String account,double balance){
this.balance = balance;
this.account = account;
}
public void setAccount(String account) {
this.account = account;
}
public String getAccount() {
return account;
}
public void setBalance(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
public int hashcode(){
return account.hashCode();
}
@Override
public boolean equals(Object obj) {
if(this == obj){
return true;
}
if(this != obj && obj.getClass()== Account.class){
Account a = (Account)obj;
return a.getAccount().equals(account);
}
return false;
}
}
class DrawAccount extends Thread{
//模拟用户的账户
private Account account;
//获取当前希望取的钱数
private double drawAccount;
private String name;
public DrawAccount( Account account,double drawAccount) {
this.account = account;
this.drawAccount = drawAccount;
}
@Override
public void run() {
//余额大于取钱的数目
if(account.getBalance()>=drawAccount){
System.out.println("您的名字是"+getName()+" "+"您要提取的现金为:"+drawAccount+"元");
account.setBalance(account.getBalance()-drawAccount);
System.out.println("您的余额为:"+account.getBalance()+"元");
}
else{
System.out.println("您输入的金额有误,取钱失败");
}
}
}
public class Drawtext{
public static void main(String[] args) {
Account a = new Account("12345", 1500);
//现在就模拟两个线程去对同一个账户同时取钱
new DrawAccount(a,1000).start();
new DrawAccount(a,1000).start();
}
}
上面的代码我开启了两个线程同时取钱,并且完全符合我前面所述的银行取钱流程,那么现在我们运行这个程序:
您的名字是 Thread-0您要提取的现金为:1000.0元
您的余额为:500.0
您的名字是 Thread-1您要提取的现金为:1000.0元
您的余额为:-500.0
由上面的运行结果可知,当两个用户(线程)同时取钱的时候,程序就会出现差错,这是与银行系统的需求不匹配的,所以我们要对程序的bug作出分析,并作出相应的修改。
通过分析可知,我们必须控制在相同的时刻只能有一个用户取钱(也就是说,只能有一个线程对余额进行访问),这个时候,我们就提出了线程安全问题,解决银行多客户对同一账户并发取钱问题,就是要去解决线程安全问题。
线程安全问题的感念:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题的常用解决办法:
1、利用同步机制去解决多线程并发访问而造成的线程安全问题:同步代码块、同步监视器、同步锁。
2、创建不可变类(对象、方法等)。
今天我们主要讲解利用同步机制去解决Java中的线程安全问题
我们还是从概念出发:
同步:同步指两个或两个以上随时间变化的量在变化过程中保持一定的相对关系。同步(英语:Synchronization),指在一个系统中所发生的事件,之间进行协调,在时间上出现一致性与统一化的现象。在系统中进行同步,也被称为及时、同步化的。
同步代码块:同步代码块是利用了同步监视器来解决线程同步问题。同步代码块的格式如下:
Synchronized(obj){
.......
//此处就是同步代码块
}
上面的格式中:obj是同步监视器,通常由可能被并发访问的共享资源来充当同步监视器。
那么如果我们要用同步代码快去解决上面银行取钱问题,怎么去做呢?很简单,我们继续上代码:
class DrawAccount extends Thread{
//模拟用户的账户
private Account account;
//获取当前希望取的钱数
private double drawAccount;
private String name;
public DrawAccount( Account account,double drawAccount) {
this.account = account;
this.drawAccount = drawAccount;
}
@Override
public void run() {
//这里我们必须让account来充当同步监视器,任何线程在执行同步代码快之前都要将同步监视器进行锁定,被锁定的同步监视器,只能由这一个线程去访问,其他线程无法访问,只有当该线程释放了对同步监视器的锁定之后,其他的线程才拥有访问同步监视器的资格。
Synchronized(account){
//余额大于取钱的数目
if(account.getBalance()>=drawAccount){
System.out.println("您的名字是"+getName()+" "+"您要提取的现金为:"+drawAccount+"元");
account.setBalance(account.getBalance()-drawAccount);
System.out.println("您的余额为:"+account.getBalance()+"元");
}
else{
System.out.println("您输入的金额有误,取钱失败");
}
}
//同步代码块结束,线程释放对同步监视器的锁定。
}
}
使用同步代码块去解决问题之后,我们运行上面的代码,看看效果如何?
您的名字是 Thread-0您要提取的现金为:1000.0元
您的余额为:500.0
您输入的金额有误,取钱失败
果不其然,再利用同步代码快对程序进行修改之后,我们的问题也迎刃而解!
下面,我们再用同步方法去解决问题。
同步方法其实很简单,就是使用synchronized去修饰一个方法,格式如下:
public synchronized void draw(){}
上面就是同步方法的标准格式,现在我们用同步方法去解决银行取钱问题。上代码:
public synchronized void draw(double drawAmount) {
//余额大于取钱的数目
if(account.getBalance()>=drawAccount){
System.out.println("您的名字是"+getName()+" "+"您要提取的现金为:"+drawAccount+"元");
account.setBalance(account.getBalance()-drawAccount);
System.out.println("您的余额为:"+account.getBalance()+"元");
}
else{
System.out.println("您输入的金额有误,取钱失败");
}
}
}
我们将涉及到余额修改的方法改成同步方法,运行程序:
您的名字是 Thread-0您要提取的现金为:1000.0元
您的余额为:500.0
您输入的金额有误,取钱失败
我们发现,用同步方法去修改,也能解决问题。那么最后一个方法能否行得通呢?我们来试试吧!
同步锁:我们这里写的同步锁,是ReentrantLock(可重入锁),使用该锁对象,可以显示的加锁,释放锁。通常使用ReentrantLock的格式如下:
class A{
private final ReentrantLock lock = new ReentrantLock();
//...
//定义需要保证线程安全的方法
public void M(){
//加锁
lock.lock();
try{
//需要保证安全的代码....
}
//使用finally块来释放锁
finally{
lock.unlock();
}
}
}
这里出现了finally块,通常我建议大家用finally块来保证锁的释放。
现在我们用同步锁来修改程序。上代码:
public class Account{
private final ReentrantLock lock = new ReentrantLock();
//模拟用户的账户
private Account account;
//获取当前希望取的钱数
private double drawAccount;
private String name;
public DrawAccount( Account account,double drawAccount) {
this.account = account;
this.drawAccount = drawAccount;
}
public void setAccount(String account) {
this.account = account;
}
public String getAccount() {
return account;
}
//因为不允许余额随便更改,所以我们只设定了balance的get方法
public double getBalance() {
return balance;
}
//提供一个线程安全的draw()方法去完成取钱的操作
public void draw(double drawAmount){
//加锁
lock.lock();
try{
if(balance>=drawAmount){
System.out.println(Thread.currenThread().getName()+"您要提取的现金为:"+drawAccount+"元");
//修改余额
balance-=drawAmount;
System.out.println("您的余额为:"+balance+"元");
}
else{
System.out.println("您输入的金额有误,取钱失败");
}
finally{//使用finally块来释放锁
lock.unlock();
}
//省略hashcode()和equals()方法
此处我们使用了一个同步锁来对取钱的相关的代码进行锁定,运行结果:
Thread-0您要提取的现金为:1000.0元
您的余额为:500.0元
您输入的金额有误,取钱失败
从上面的运行结果可以出,使用同步锁也能防止多线程并发访问而造成的线程安全问题。
好啦,今天向大伙儿通过银行取钱案例介绍了三种同步方式去解决线程安全问题,相信大家都对三种方法有了以一定的了解,希望我的博客能对大家有所收获。加油啦!
(备注:因本人能力有限,在写博客的时候难免有所疏漏,如有缺漏之处,恳请各位读者谅解,并欢迎大家给我指出问题,让我能向大家学到更多知识!)
鸡年第一更!祝大家鸡年快乐!
浅谈利用同步机制解决Java中的线程安全问题的更多相关文章
- 逐步理解Java中的线程安全问题
什么是Java的线程安全问题? 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读/写完,其他线程才可使用.不会出现数据不一致或者数据 ...
- Java中的线程同步
Java 中的线程同步问题: 1. 线程同步: 对于访问同一份资源的多个线程之间, 来进行协调的这个东西. 2. 同步方法: 当某个对象调用了同步方法时, 该对象上的其它同步方法必须等待该同步方法执行 ...
- 【ASP.NET MVC系列】浅谈jqGrid 在ASP.NET MVC中增删改查
ASP.NET MVC系列文章 [01]浅谈Google Chrome浏览器(理论篇) [02]浅谈Google Chrome浏览器(操作篇)(上) [03]浅谈Google Chrome浏览器(操作 ...
- java设计模式--解决单例设计模式中懒汉式线程安全问题
首先写个单例,懒汉模式: public class SingleDemo { private static SingleDemo s = null; private SingleDemo(){} pu ...
- 浅谈JVM线程调度机制及主要策略
在之前有说过线程,应该都知道,所谓线程就是进程中的一个子任务,一个进程有多个线程.今天的话主要就是谈一谈JVM线程调度机制.我们结合线程来说,当我们在做多线程的案例时,如一个经典案例,火车站卖票. * ...
- 关于Java中的线程安全(线程同步)
java中的线程安全是什么: 就是线程同步的意思,就是当一个程序对一个线程安全的方法或者语句进行访问的时候,其他的不能再对他进行操作了,必须等到这次访问结束以后才能对这个线程安全的方法进行访问 什么叫 ...
- 浅议Grpc传输机制和WCF中的回调机制的代码迁移
浅议Grpc传输机制和WCF中的回调机制的代码迁移 一.引子 如您所知,gRPC是目前比较常见的rpc框架,可以方便的作为服务与服务之间的通信基础设施,为构建微服务体系提供非常强有力的支持. 而基于. ...
- 【转】利用匿名namespace解决C++中重复定义的问题
目录 利用匿名namespace解决C++中重复定义的问题 原文:https://blog.csdn.net/pi9nc/article/details/11267031 利用匿名namespace解 ...
- Java中的线程到底有哪些安全策略
摘要:Java中的线程到底有哪些安全策略呢?本文就为你彻底分析下! 本文分享自华为云社区<[高并发]线程安全策略>,作者:冰 河 . 一.不可变对象 不可变对象需要满足的条件 (1)对象创 ...
随机推荐
- 解决 EDAS:Upload failed: The right margin is 0.535 in on page 1 问题
参考: IEEETran page margins 解决 EDAS:Upload failed: The right margin is 0.535 in on page 1 问题 在 EDAS 上上 ...
- 剑指offer 13:调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变. 法一: public clas ...
- golang协程踩坑记录
1.主线程等待多个协程执行完毕后,再执行下面的程序.golang提供了一个很好用的工具. sync.WaitGroup下面是个简单的例子. 执行结果: 2.主线程主动去结束已经启动了的多个协程.执行结 ...
- 使用TLS证书保护Docker
使用TLS证书保护Docker 当我们使用远程调用docker时,未设置TLS的docker,将可以被任何人调用,这是极其危险的. 在阿里云上跑的docker,这次就被不怀好意的人扫描到了默认端口,2 ...
- stylus入门学习笔记
title: stylus入门学习笔记 date: 2018-09-06 17:35:28 tags: [stylus] description: 学习到 vue, 有人推荐使用 stylus 这个 ...
- Git复习步骤
1.首先肯定是安装与配置了 首先要下载Git,然后设置用户名/邮箱 https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c ...
- java笔记 -- java字符串
概念: Java字符串就是Unicode字符序列, Java没有内置的字符串类型, 而是在标准Java类库中提供了一个预定义类. 每个用双引号括起来的字符串都是String类的一个实例.String ...
- 如何让写得html页面自动刷新
一:新建一个文件夹用vscode打开 二:终端输入npm init 然后一路回车会在文件夹中生成一个package.json文件 三:新建个html,在终端中输入cnpm install -g liv ...
- java中全角半角字符的相互转换的代码
如下内容是关于java中全角半角字符的相互转换的内容.package com.whatycms.common.util; import org.apache.commons.lang.StringUt ...
- mysql、nginx、php-fpm的启动与关闭
mysql 一.启动方式 1.使用 service 启动:service mysqld start 2.使用 mysqld 脚本启动:/etc/inint.d/mysqld start 3.使用 sa ...