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

最近在复习、整理之前学习的多线程的知识,本着燃烧自己,照亮他人的想法,把自己整理的一些关于多线程的学习笔记、心得分享给大家.

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

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

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

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

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

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

 

 

线程安全

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

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

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

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

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

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

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

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();
    }

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

如果大家有任何疑问或者对博主有什么建议,欢迎大家留下评论.楼主一定会尽快回复.本期分享就到这里,我们下期再见吧!

             

public class Employees {
//程序员的等级
private int level;
//技能库
public Map<String,String> skills;

//工资
private int sal;

public void updateSal(String multithreading){
// 如果有会多线程这个技术
if (multithreading.equals(skills.get(multithreading))){
//根据你的等级升职加薪操作..
sal = level * sal;
}else{
//如果不会多线程,学习多线程,更改等级为中级
skills.put(multithreading,multithreading);
level = 2;
//根据等级加薪,..
updateSal(multithreading);
}
}
}

猜你喜欢

转载自www.cnblogs.com/xisuo/p/9761217.html