你真的了解线程安全吗?

冰冻三尺,非一日之寒

前言

线程安全,这个名词如同一座大山,经常出现在我们的工作,以及面试中。也许,关于线程安全的面试题你早已就倒背如流,但是,你真的了解线程安全吗?

今天,我们回到最初的原点,从最基础的角度来看线程安全。

线程的一些概念

线程安全,好像听起来好高大上啊,但是这是啥意思呢,根本不懂啊!没事,语文老师教过我们,对于无法理解的概念,我们可以进行拆词。线程安全,即 “线程” + “安全”,先得有线程,才能实现所谓的 “安全”,那什么是线程呢?

线程是独立调度的基本单位,是一个基本的 CPU 执行单元,也是程序执行流的最小单元。线程是一个 “轻量级进程” ,一个进程中可以有多个线程。

哦,懂了,线程原来是进程的一部分啊,早说嘛,那现在问题来了,什么是进程呢?

进程是操作系统进行资源分配的基本单位。我们举个例子,电脑运行时会有很多单独运行的程序,例如谷歌浏览器,例如网易云音乐,这些程序其实都是一个进程,且它们是互相独立的。

了解了进程和线程的概念之后,我们再来看一下多线程的概念,提起多线程,不得不提起它的两个朋友 – 串行与并行。只有搞清楚这两个概念,我们才能更好学习多线程。

  1. 串行:单线程下,执行多个任务,必须先做完前一个任务,才能做下一个任务,在时间上不可重叠
  2. 并行:多线程下,多个任务同时被执行,在时间上是重叠的

了解了串行与并行,多线程的概念就变得简单了。我们举个例子,我们使用网易云音乐的时候,上面提到过这是一个进程,我们可以在线听歌,也可以下载音乐,还可以看看评论。如果是单线程的话,我们必须先做完前一个任务,才能做下一个任务,不能同一时间做多件事情。但是多线程的话就彻底放飞自我了,你可以在在线听歌的同时,还可以下载音乐,看评论,这些任务是同时执行的,在时间上是重叠的。

一个线程不安全的例子

我们想象一个场景,要设计一个计数器,其功能为每调用一次方法,计数器加一。有同学可能会说,简单!然后信心满满写下以下代码:

package test;

class Demo {
	
	private int num = 0;
	
	public void add() {
		num++;
	}
	
	public void print() {
		System.out.println(num);
	}
}

public class HelloWorld {

	public static void main(String[] args) {
		Demo demo = new Demo();
		for(int i = 0;i < 10;i++) {
			demo.add();
			demo.print();
		}
    }
}

执行结果如下:
在这里插入图片描述
从结果来看,这个代码是 “没有问题” 的。但是,需要注意,这个没有问题单指在单线程环境下没有问题,但是在多线程环境下可能就是另外一种结果了。不信?我们来测试一下,有代码有真相。

package test;

class Demo {
	
	private int num = 0;
	
	public void add() {
		num++;
		System.out.println(num);
	}
}

public class HelloWorld {

	public static void main(String[] args) {
		Demo demo = new Demo();
		new Thread(() -> {
			for(int i = 0;i < 100;i++) {
				demo.add();
			}
		}).start();
		
		new Thread(() -> {
			for(int i = 0;i < 100;i++) {
				demo.add();
			}
		}).start();
    }
}

测试结果如下:
在这里插入图片描述
我们惊奇地发现,这个打印的顺序居然是不同的。显然,在多个线程访问的情况下,这段代码没有按照我们预期的行为去执行,所以这段代码不是线程安全的。

究竟什么才是线程安全?

我们上面的例子为什么会发生错误?其实出现问题的原因并不少,我们这里介绍一种很常见的原因,当第一个线程刚刚拿到 num 的值时,第二个线程就改变了 num 的值,从而导致第一个线程得到的 num 值是不正确的。

我们再来分析一下为什么这段代码并不是线程安全的?其实,之所以这段代码线程不安全,是因为两个线程都需要操作一个共享的变量 num。如果不需要操作共享变量,其自然是线程安全的。

经过上面的分析,在这里我们可以给线程安全下一个定义。所谓线程安全,就是在多线程环境下,无论使用何种调用方式,都不要在主程序中做任何同步,其结果就是我们所需要的结果,那么我们说这个类是线程安全的。

如何确保线程安全

现在,我们已经知道什么是线程安全了,那么,我们应该如何确保线程安全呢?

方法一:无状态

如果一段代码没有任何状态,也就是说,这段代码既没有任何作用域,也没有去引用其它类中的域,它所执行的作用范围与执行结果只存在它这条线程的局部变量中,并且只能由正在执行的线程进行访问,那么它自然是线程安全的。为啥?因为当前线程对本方法的访问并不会对别的线程访问本方法产生影响。

无状态的对象,由于没有共享数据,多个线程同时访问并不会影响彼此,显然是线程安全的。下面是一个无状态对象的例子:

class Demo {
		
	public void add() {
		for(int i = 0;i < 10;i++) {
			System.out.println(i);
		}
	}
}

方法二:synchronized

synchronized 可以用来控制线程同步,它可以为我们指定的对象加锁,例子如下:

package test;

class Demo {
	
	private int num = 0;
	
	public synchronized void add() {
		num++;
		System.out.println(num);
	}
}

public class HelloWorld {

	public static void main(String[] args) {
		Demo demo = new Demo();
		new Thread(() -> {
			for(int i = 0;i < 100;i++) {
				demo.add();
			}
		}).start();
		
		new Thread(() -> {
			for(int i = 0;i < 100;i++) {
				demo.add();
			}
		}).start();
    }
}

我们选择为整个方法加锁,事实上锁住的是对象本身。当 synchronized 为一个对象加锁的时候,别的线程若想获取锁对象,必须得等该线程释放锁对象才行,否则会一直等待。

需要注意的是,随意使用 synchronized 会影响程序的性能,我们要正确合适地使用 synchronized。

方法三:Lock

Lock 与 synchronized 相比,让锁具备了可操作性,例如我们需要手动去获取锁与释放锁。对于更多的 Lock 特性在这里不再阐述,大家可以搜索 Lock 相关的文章对 Lock 进行深入的了解。

package test;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Demo {
	
	private Lock lock = new ReentrantLock();
	
	private int num = 0;
	
	public void add() {
		lock.lock();//获取锁
		try {
			num++;
			System.out.println(num);
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			lock.unlock();//释放锁
		}
	}
}

public class HelloWorld {

	public static void main(String[] args) {
		Demo demo = new Demo();
		new Thread(() -> {
			for(int i = 0;i < 100;i++) {
				demo.add();
			}
		}).start();
		
		new Thread(() -> {
			for(int i = 0;i < 100;i++) {
				demo.add();
			}
		}).start();
    }
}

进入方法,首先我们需要获取锁,再执行业务逻辑,最后我们需要将 Lock 的锁释放,为防止代码异常,我们需要将释放锁的操作放在 finally 中,因为 finally 中的代码必然会被执行。

结语

看完这篇博客,大家了解什么是线程安全,以及如何确保线程安全吗?其实,真正了解线程安全的概念,对于我们日常开发有很大帮助,大家需要在多线程这方面多下功夫啊!

发布了146 篇原创文章 · 获赞 300 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Geffin/article/details/105429424