从零开始学多线程之线程安全(一)

准备把自己关于多线程的学习笔记写成三个部分分享给大家: 基础、实战、测试&优化

这三个部分是一环扣一环的.

1.基础: 多线程操作的对象必须是线程安全的,所以构建线程安全的对象是一切的基础.这一部分讲的就是如何构建线程安全的类,和一些多线程的基础知识.

  1. 实战: 构建好了线程安全的类,我们就可以用线程/线程池,去构建我们的并发程序了,如何执行任务?如何关闭线程池?如何扩展线程池?这里都会给你答案

  2. 测试&优化: 构建好的程序会不会发生死锁? 如何优化程序? 如何知道运行的结果是否正确? 这一部分会 一 一为你解答.

好了废话不多说,本篇博客是系列的第一篇,我们来讲述一下线程安全.

线程安全
在多线程环境下,保证线程访问的数据的安全格外重要.编写线程安全的代码,本质上就管理状态的访问,而且通常是共享的、可变的状态.

状态:可以理解为对象的成员变量.

共享: 是指变量可以被多个线程访问

可变: 是指变量的值在生命周期内可以改变.

保证线程安全就是要在不可控制的并发访问中保护数据.

如果对象在多线程环境下无法保证线程安全,就会导致脏数据和其他不可预期的后果

有很多在单线程环境下运行良好的代码,在多线程环境下却有问题.例如自增操作:

public class Increment {
private int num = 0;

public void doSomething(){
    //do something
    num++;
}

}
每次调用doSomething()方法的时候,num都会执行自增操作.但是在多线程环境下,这段代码是有问题的.

原因在于num++并不是原子操作,而是由三个离散操作组合而来的:“读-改-写”,读取当前的值,加1,写入变量.

可能会出现某一时刻,两个线程同时读到num的数值,然后分别+1,分别写入.这样其中一次计数就不存在了.

有一个专门形容这类情况的名词,叫竞争条件

当计算的正确性依赖于运行时相关的时序或者多线程的交替时,会产生竞争条件.

我对竞争条件的理解就是,多个线程同时访问一段代码,因为顺序的问题,可能导致结果不正确,这就是竞争条件.

“检查-再运行”,也是一种竞争条件.

public class Singleton {
private Singleton singleton;

private Singleton() {
}
public Singleton getSingleton(){
    if(singleton == null){
        singleton = new Singleton();
    }
    return singleton;
}

}

看这个例子,我们把构造方法声明为private的这样就只能通过getSingleton()来获得这个对象的实例了,先检查这个对象是否被实例化了,如果没有,那就实例化并返回,但是可能同一时刻两个线程同时通过了条件判断,这样就产生了两个对象的实例.

问题已经很清楚了,那么如何解决问题呢?

Java提供了synchronized(同步)关键字.只要是使用了synchronized关键字修饰的方法,就被加锁了,synchronized锁是互斥锁,同一时间只能有一个线程占有锁,其他对象想要获得锁只能等到占有锁的线程释放锁.

我们来修改一下上面的代码,使它们成为线程安全的:

private int num = 0;

public synchronized void doSomething(){
    //do something
    num++;
}

public synchronized void test(){
if (state){
//做一些事
}else{
// 做另外一些事
}
}

好了,现在它们又是线程安全的了.

这种方式虽然很简单,但是由于synchronized块包住的代码都会顺序的执行,有时会导致令人无法忍受的响应速度

决定synchronized块的大小需要权衡各种设计要求,包括安全性、简单性和性能,其中安全性是绝对不能妥协的,而简单性和性能又是互相影响的(将整个方法声明为synchronized很简单,但是响应速度不太好,将同步块的代码缩小,可能很麻烦,但是性能变好了).

那么在简单性和性能之间我们要如何取舍呢? 这里有个原则: 通常简单性与性能之间是相互牵制的,实现一个同步策略时,不要过早地为了性能而牺牲简单性(这是对安全性潜在的妥协).

如有耗时长的操作(I/O啊,长时间的计算啊),切记不能放在锁里,否则可能引发活跃度(死锁)与性能(响应慢)的风险.

下面我们再看一段代码:

1 public class Employees {
2 //程序员的等级
3 private int level;
4 //技能库
5 public Map<String,String> skills;
6
7 //工资
8 private int sal;
9
10 public void updateSal(String multithreading){
11 // 如果有会多线程这个技术
12 if (multithreading.equals(skills.get(multithreading))){
13 //根据你的等级升职加薪操作…
14 sal = level * sal;
15 }else{
16 //如果不会多线程,学习多线程,更改等级为中级
17 skills.put(multithreading,multithreading);
18 level = 2;
19 //根据等级加薪,…
20 updateSal(multithreading);
21 }
22 }
23 }

员工类有个方法,根据你会不会多线程技术来提高你的薪水,如果你的技能库里有多线程技术,执行加薪操作,如果没有会让你学习,给你的技能库加上这个技能,并且提高你的等级,但是在一些极端的情况下会出现问题,线程A走到17行添加完技能又没修改等级的时候,可能有另一个线程重新调用方法,通过了12行的验证,但是等级没有改变,执行加薪操作的时候是按照等级的过期值执行的.

这里我们就要注意了,当不变约束涉及到多个变量的时候,要原子的更新它们.在这个方法上加锁就又可以保证这个方法是线程安全的了.

最后给大家介绍一下原子变量atomic,使用原子变量也可以把自增操作变为原子的.

private AtomicLong num = new AtomicLong(0);

public void doSomething(){
    //do something
    num.incrementAndGet();
}

好了关于线程安全和锁就为大家简单的介绍到这里,博主下一篇会更新关于安全发布对象的知识,这两篇结合起来就可以帮助我们构建线程安全的类了.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值