第三章Synchronized如何避免并发中的三大问题
一、synchronized保证原子性:
package atomic;
import java.util.ArrayList;
import java.util.List;
public class AtomicDemo {
//定义一个共享变量number
//对number进行1000次++操作
//使用5个线程来进行
private static Integer number = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
Runnable runnable = () -> {
//每一个线程对number的访问是同步的;
synchronized (number) {
for (int j = 0; j < 1000; j++) {
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
number++;
}
}
};
for (int i = 0; i < 5; i++) {
Thread t = new Thread(runnable);
t.start();
list.add(t);
}
for (Thread thread : list) {
//等待线thread程死亡(执行结束)
//join方法其实就是阻塞当前调用它的线程,等待join执行完毕,当前线程继续执行
thread.join();
}
Thread.sleep(1000);
System.out.println("number is :" + number);
}
}
number is :5000
synchronized保证原子性的原理
对number++;增加同步代码块后,保证同一时间只有一个线程操作number++(这就是synchronized能保证原子性的关键所在);。就不会出现安全问题。
synchronized保证原子性的原理,synchronized保证只有一个线程拿到锁,能够进入同步代码块。
synchronized保证可见性的原理:
二、synchronized保证可见性的原理:
synchronized保证可见性Demo:
package visablity;
/**
* @author Jack
* @date 2020/4/24-20:08
*/
public class VisablityTest02 {
/**
* 目标演示可见性问题
* 1.创建一个共享变量,
* 2、创建多条线程修改和读取共享变量
*/
//volatile关键字保证我们的可见性
private static boolean flag = true;
//
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
//synchronized保证的是obj的同步,为什么能保证flag的可见性呢??
/**
*在多线程的JMM中我们有两个线程共享变量flag和obj,在某个线程对对象进行同步操作的 *时候,
* 首先根据JMM的8个原子操作对这共享变量obj进行lock操作,lock操作就会让直行同步操 * 作的线程去刷新工作内存,工作内存中的所有线程共享变量就会刷新,得到最新值;
* System.out.println();中有同步操作,会从主内存中进行共享变量的读取和lock操 * 作,导致工作内存刷新
*/
synchronized (obj) {
}
}
}, "Thread1").start();
Thread.sleep(2000);
Thread thread = new Thread();
thread.join();
new Thread(() -> {
flag = false;
System.out.println("线程修改了变量的值为false");
}, "Thread2").start();
}
}
synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值
思考一个问题:(自己的猜想。不准确)
什么样的变量才算是共享变量存放在主内存中的呢?一个线程自己内部存在同步变量是可行的吗?
如在线程Thread内部中的一段代码
System.out.println("")中的println调用了一个同步的方法,对printStream进行了同步操作,printStream是静态的,这个静态的常量虽然不是我们自己定义的但是是可以被多个线程共享,只需要调用System.out就可调用了,我猜想他也应该是我们的线程共享变量吧,存在主内存中的...
public final static PrintStream out = null;
注:PrintStream的作用是什么:
PrintStream是打印输出流,它继承于FilterOutputStream。
它可以为其他输出流添加了功能,使它们能够方便地打印各种数据值表示形式。 与其他输出流不同,PrintStream永远不会抛出IOException;它产生的IOException会被自身的函数所捕获并设置错误标记,用户可以通过checkError()返回错误标记,从而查看PrintStream内部是否产生了IOException。另外,PrintStream提供了自动冲洗和字符集设置功能。所谓自动冲洗,就是往PrintStream写入的数据会立即调用flush()函数。
Synchronized保证可见性的原理
Synchronized能够实现原子性和可见性;在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
三、 synchronized与有序性
学习使用synchronized保证有序性的原理
为什么要重排序
为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。
as-if-serial语义
as-if-serial语义的意思是:不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的。 以下数据有依赖关系,不能重排序。
写后读
int a = 1;
int b = a;
写后写
int a = 1;
int a = 2;
读后写
int a = 1;
int b = a;
int a = 2;
**编译器和处理器不会对存在数据依赖关系的操作做重排序,**因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
int a = 1;
int b = 2;
int c = a + b;
也可以这样
int b = 2;
int a = 1;
int c = a + b;
上面3个操作的数据依赖关系如图所示:
1574136281215
如上图所示a和c之间存在数据依赖关系,同时b和c之间也存在数据依赖关系**。因此在最终执行的指令序列中,c不能被重排序到a和b的前面。但a和b之间没有数据依赖关系,编译器和处理器可以重排序a和b之间的执行顺序**。
Test03Ordering.java
package com.itheima.concurrent_problem;
import org.openjdk.jcstress.annotations. ;
import org.openjdk.jcstress.infra.results.I_Result;
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Test03Ordering { int num = 0;
boolean ready = false;
// 线程一执行的代码
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2执行的代码
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
synchronized保证有序性的原理
synchronized后,虽然进行了重排序,保证只有一个线程会进入同步代码块,也能保证有序性。
小结
synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性,保证了单线程情况下结果的正确性有序性除了使用synchronized之外还可以使用voliatle关键字避免指令重排