Java多线程学习之基础知识篇(线程的安全性)

本系列博客基于<Java并发编程实战>一书,感兴趣的同学可以购买纸质书籍进行学习.

多线程是什么?

  • 多线程就是多个线程,那线程又是什么,但在我们一口气弄清楚线程是什么之前还需要弄清楚进程,所以不急,我们从进程来了解起
  • 进程

进程的精简定义:一段程序的执行过程.

进程的官方定义:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
[使用通俗的话来解释]:我们写的一个类(Demo.class)就可以看成一个程序,里面写了main方法执行一句输出Hello World,这个Demo就是一个进程,而我们运行的软件也是进程,什么软件管家,游戏,都是进程.这些进程运行在cup上.

  • 线程

线程的官方定义:
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
[通俗的说法]:一个进程可以实现很多功能,这些功能就是一个个线程来完成.

第一章.线程的安全性

1.1 什么是线程的安全性

  • 概念:

线程安全性的最核心概念就是:正确性,就是某个类的行为与其规范完全一致

  • 定义:

所见即所知,当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的

  • 总结:

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的

  • 案例:书写一个无状态的Servlet程序代码:
//前台输入数值,返回一个因数分解结果
public class SafeServlet implements Servlet{
	public void service(ServletRequest req,ServletResponse resp){
		BigInteger i=extractFromRequest(req);
		BigInteger[] factors=factors(i);
		encodeIntoResponse(resp,factors);
	}

}
  • 上面该SafeServlet是无状态的,它既不包含任何域,也不包含任何对其他类中域的引用,计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能由正在执行的线程访问.通俗来说就是:访问该Servlet的线程不会影响另一个访问同一个Servlet的线程的计算结构,类似等同与访问了两个不同的实例

无状态的对象一定是线程安全的

1.2 原子性

  • 上面说到,无状态对象一定是线程安全的,那么如果我们给无状态对象增加一个状态会怎么样?这里我们改进上面Servlet程序,增加一个统计处理请求的次数.
public class UnSafeServlet implements Servlet{
	private long count=0;//定义一个类变量
	public long getCount(){return count;}
	public void service(ServletRequest req,ServletResponse resp){
		BigInteger i=extractFromRequest(req);
		BigInteger[] factors=factors(i);
		++count;
		encodeIntoResponse(resp,factors);
	}

}
  • UnSafeServlet是线程不安全的,因为虽然++count看似是一步执行,但这个操作并非原子的,因此并不会作为一个不可分割的操作执行.++count分成了三步:1.读取count的值 2.加一 3.将新结果写入count.这三步:"读取-修改-写入"的操作序列,其结果状态依赖于之前的状态.
  • 当我们给出两个没有同步的线程时,假设一开始count为1,此时线程A执行访问Servlet操作,此时count读取为1,然后开始加一,还没写人count时,线程B也开始访问了,此时读取的count也为1,因为线程A还没把加一后的结果写入到count里面,最终导致的结果就是count为2,出现错误.
  • 原子性的定义:

一组语句作为一个不可分割的单元被执行

1.2.1 竞态条件

  • 定义

在并发编程中,由于不恰当的执行时序而出现不正确的结果

  • 最常见的竞态条件类型就是"先检查后执行",即通过一个可能失效的观测结果来决定下一步的动作.

1.2.2 复合操作

  • 我们发现一个问题就是统计访问次数的Servlet程序里面对count加一的操作不是一次性执行结束,在这个修改的过程中会有其他线程来对count操作,也就是说它们需要一组包含以原子方式执行的操作.
  • 要避免竞态条件问题,我们就必须在某个线程修改变量时,通过某种方式防止其他线程使用这个变量,从而实现读取的数据是其他线程修改后的准确数据.
  • 什么是原子方式执行操作

假定有两个操作A和B,如果从执行A的线程来看,当另外一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说就是原子的.

  • 复合操作的定义:

我们将"先检查后执行","读取-修改-写入"等操作称为复合操作,包含了一组必须以原子方式执行的操作以确保线程安全性.

1.3 加锁机制

前言:要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量

1.3.1 内置锁

  • 同步代码块的概念:

Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)
以关键字Synchronized来修饰的方法就是一种横跨整个方法体的同步代码块

  • 同步代码块的两部分:
作为锁的对象引用作为由这个锁保护的代码块
  • 静态的Synchronized方法以Class对象作为锁:
Synchronized (lock){
//访问或者修改由锁保护的共享状态
}
  • 内置锁的定义

每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或者是监视器锁(Monitor Lock)

  • 内置锁的概念:

1.线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时自动释放锁.
2.获取内置锁的唯一途径就是进入这个锁保护的同步代码块或者方法
3.Java的内置锁相当于一种互斥锁,这意味着最多只有一个线程能持有这种锁,当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁
4.由于每次只能由一个线程执行内置锁保护的代码块,因此这个代码块会以原子方式执行,这样多个线程执行代码就不会产生干扰

1.3.2 重入

  • 引言:

前面我们说道,当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会被阻塞,那如果这个线程试图获取一个自己已经持有的锁时,那么这个请求就会成功,不会阻塞.原因就是因为内置锁是可重入的.

  • 内置锁重入的实现方式:

1.为每个锁关联一个获取计数值和一个所有者线程,如果计数值为0,代表这个锁是没有被任何线程持有的.
2.当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并将获取的计数值设置为1.
3.如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会对应递减
4.当计数值为0时,这个锁被释放.

1.4 用锁来保护状态

  • 概念:

由于锁能使其保护的代码路径以串行形式来访问,因此可以通过锁来构造一些协议用来实现对共享状态的独占访问,只要始终遵循这些协议,就可以确保状态的一致性.

  • 具体的概述:

对于可能被多个线程同时访问的可变状态变量,在访问它的使用都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的.
例如之前的案例,Servlet中的一个统计访问次数的变量count,如果我们只是使用同步代码块是不够的,因为在访问这个变量的所以位置上都需要同步

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值