Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线程的深入剖析。
本篇文章主要介绍Java多线程中的同步,也就是如何在Java语言中写出线程安全的程序,如何在Java语言中解决非线程安全的相关问题。多线程中的同步问题是学习多线程的重中之重,这个技术在其他的编程语言中也涉及,如C++或C#。
同步和异步:
1、概念:
同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去
异步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为没有同步机制存在,A线程仍然请求的到,A线程无需等待
2、特点:
显然,同步最最安全,最保险的。而异步不安全,容易导致死锁,这样一个线程死掉就会导致整个进程崩溃,但没有同步机制的存在,性能会有所提升
3、同步阻塞与异步阻塞:
一个线程/进程经历的5个状态, 创建,就绪,运行,阻塞,终止。各个状态的转换条件如上图,其中有个阻塞状态,就是说当线程中调用某个函数,需要IO请求,或者暂时得不到竞争资源的,操作系统会把该线程阻塞起来,避免浪费CPU资源,等到得到了资源,再变成就绪状态,等待CPU调度运行。
![](https://i-blog.csdnimg.cn/blog_migrate/37b831d4c487f3dee9d9dc22774da1bb.png)
同步是指两个线程的运行是相关的,其中一个线程要阻塞等待另外一个线程的运行。异步的意思是两个线程不相关,自己运行自己的。
线程安全问题:
定义:
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
线程安全问题概况来说有三方面:原子性、可见性和有序性。
原子性:
原子(Atomic)的字面意思是不可分割的(lndivisible)。对于涉及共享变量访问的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,相应地我们称该操作具有原子性(Atomicity)。
在生活中我们可以找到的一个原子操作的例子就是人们从ATM机提取现金:尽管从ATM软件的角度来说,一笔取款交易涉及扣减户主账户余额、吐钞器吐出钞票、新增交易记录等一系列操作,但是从用户(我们)的角度来看ATM取款就是一个操作。该操作要么成功了,即我们拿到现金(户主账户的余额会被扣减),这个操作发生过了;要么失败了,即我们没有拿到现金,这个操作就像从来没有发生过一样(当然,户主账户的余额也不会被扣减)。除非ATM软件有缺陷,否则我们不会遇到吐钞口吐出部分现金而我们的账户余额却被扣除这样的部分结果。在这个例子中,户主账户余额就相当于我们所说的共享变量,而ATM机及其用户(人)就分别相当于上述定义中原子操作的执行线程和其他线程。
可见性:
在多线程环境下,一个线程对某个共享变量进行更新之后,后续访问该变量的线程可能无法立刻读取到这个更新的结果,甚至永远也无法读取到这个更新的结果。这就是线程安全问题的另外一个表现形式:可见性(Visibility)。
如果一个线程对某个共享变量进行更新之后,后续访问该变量的线程可以读取到该更新的结果,那么我们就称这个线程对该共享变量的更新对其他线程可见,否则我们就称这个线程对该共享变量的更新对其他线程不可见。可见性就是指一个线程对共享变量的更新的结果对于读取相应共享变量的线程而言是否可见的问题。多线程程序在可见性方面存在问题意味着某些线程读取到了旧数据(Stale Data),而这可能导致程序出现我们所不期望的结果。
![](https://i-blog.csdnimg.cn/blog_migrate/ff5c52f244595044bfc0b9b506ee7f82.png)
如上图所示,线程1修改X变量,是在自己工作内存中进行修改的,并未及时刷新到主内存中,如果这时候线程2去读取主内存中的数据X读取到的还是0,但实际上X已经被修改成1了,这就是线程可见性有可能出现的问题。我们可以使用synchronized关键字来解决线程可见性问题。
有序性:
有序性(Ordering)指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器上运行的其他线程看来是乱序的(Out of order)。所谓乱序,是指内存访问操作的顺序看起来像是发生了变化。
public class Singleton {
private Singleton() { }
private volatile static Singleton instance;
public Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
上面代码中的instance=new Person(),这条语句实际上包含了三步操作
- 分配对象的内存空间;
- 初始化对象;
- 设置instance指向刚分配的内存地址
由于重排序的原因,可能会出现以下运行顺序
![](https://i-blog.csdnimg.cn/blog_migrate/056bad2e132a376cda48c2f9ddcaf0c8.png)
如果2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并没有初始化成功,显而易见对线程B来说之后的操作就会出错。我们可以使用volatile关键字来解决线程有序性问题
示例:
下面我们来看两个线程安全的例子:
(1)、不共享数据的情况
![](https://i-blog.csdnimg.cn/blog_migrate/b3863bc580f166b0e220028cc9d53a88.png)
class Mythread extends Thread{
private int count=5;
public Mythread(String name) {
this.setName(name);
}
@Override
public void run() {
while(count>0) {
count--;
System.out.println("由 "+this.currentThread().getName()+" 计算,count="+count);
}
}
}
public class Test01 {
public static void main(String[] args) throws InterruptedException {
/**
* 下面创建了三个线程A,B,C
*/
Mythread A=new Mythread("A");
Mythread B=new Mythread("B");
Mythread C=new Mythread("C");
A.start();
B.start();
C.start();
}
}
运行结果:
由 B 计算,count=4
由 A 计算,count=4
由 C 计算,count=4
由 A 计算,count=3
由 A 计算,count=2
由 B 计算,count=3
由 A 计算,count=1
由 C 计算,count