目录
线程安全问题
线程不安全问题演示
线程不安全的原因
修改共享数据
原子性
可见性
代码顺序性
解决线程不安全问题
synchronized关键字
synchronized的作用
synchronized的使用示例
volatile关键字
volatile的作用
volatile的语法
synchronized和volatile关键字使用示例
synchronized解决线程安全问题示例
volatile解决线程安全问题示例
简单地说,如果多线程环境下程序运行的结果与该程序在单线程环境运行的结果相同,就称该段程序是线程安全的,反之则为线程不安全。
例如下列代码,两个线程同时进行n++操作,理论上,最终n的结果应该为20000,可代码运行得到的结果与预期结果不符合,这即为线程不安全。
public class ThreadPresent {
private static int n = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
n++;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
n++;
}
}
});
t1.start();
t2.start();
//main线程一直等待,直至t1和t2执行完
t1.join();
t2.join();
System.out.println(n);
}
}
在上述线程不安全的演示中,涉及到两个线程针对n这个静态成员变量进行修改,此时n是一个多个线程都能访问到的“共享变量”。当多个线程同时修改共享数据时,就可能会出现线程不安全的问题。
假如将一段代码想象成一个房间,每个线程就是要进入这个房间的人,当线程A进入房间后,其他线程就不能再进入房间,就称这段代码满足原子性,否则就不满足原子性,如果不满足原子性就存在线程安全问题。
例如在上述代码演示中,n++操作实际上可以拆分成三个步骤:①从内存中读取数据到CPU、②进行数据更新、③将数据写回主存,因此其不满足原子性,因此在运行中会出现线程不安全问题,不能得到预期结果。
一行Java代码不一定是满足原子性的,也不一定只是一条指令
可见性即一个线程对共享变量值的修改,其他线程都能够及时看到.
当多个线程之间并发并行的执行,使用各自的内存,互相之间不可见(即不具有可见性),因此在多个线程对共享变量进行操作时,假如A线程操作结束后,其他线程的内存不能及时更新共享变量的值,因此就容易出现线程不安全问题。
在执行字节码指令或是执行机器码指令时,都可能为了提高执行效率,使用指令重排序的方式来执行。
· 执行字节码指令:运行期执行,具体操作为“java 类名”,在此过程中存在解释器,翻译class字节码为机器码时就可能发生重排序
· 执行机器码指令:CPU执行机器码时,也可能发生重排序
· 提高执行效率:不能为了提高执行效率去重排序有前后依赖关系的指令
· 指令重排序:更改多行指令的执行顺序
假如有一段代码是:①去教室写作业、②去食堂吃饭、③去教室给老师交作业,执行顺序为①->②->③,为了提高执行效率,就会将上述代码重排序为①->③->②,这样就会少跑一次教室,提高了执行效率.
·设计多线程代码的原则:在满足线程安全的前提下,尽可能的提高执行效率
(1)如果某个线程对共享变量是写操作,可以以加锁的方式来保证线程安全.
· 加锁可以解决产生线程不安全的原因:原子性、可见性、有序性
· Java中加锁的方式有两种:
①synchronized关键字:申请对给定的java对象,对象头加锁;
②Lock:锁的接口,它的实现类提供了锁这样的对象,可以调用方法来加锁/释放锁.
(2)如果某个线程对共享变量是读操作,使用volatile关键字就可以保证线程安全.
· volatile关键字是修饰变量的,变量的读操作本身就具有原子性,volatile的作用是保证可见性和有序性,所以结合起来就可以满足线程安全.
· 互斥:它的互斥作用就保证了原子性,当某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象的synchronized就会阻塞等待。进入synchronized修饰的代码块相当于加锁,退出synchronized修饰的代码块相当于解锁。
· 刷新内存:它保证了可见性,synchronized结束释放锁,会把工作内存中的数据刷新到主存中,当其他线程申请锁时,获取的始终是最新的数据。
· 有序性:某个线程执行一段同步代码,不管如何重排序,过程中都不可能有其他线程执行的指令,这样保证了多个线程执行同步代码时满足一定的顺序。
· 可重入:即同一个线程可以多次申请同一个对象锁
例如下述代码,increase和increase2两个方法都加了synchronized,此处的synchronized都是针对this当前对象加锁的;在调用increase2时,先加了一次锁,执行到increase时又加了一次锁.
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
总的来说就是,多个线程对同一个对象进行加锁操作,具有同步互斥的作用.
· 同一个对象:必须是同一个对象,否则就没有同步互斥的效果.
①直接修饰普通方法
这里的synchronized锁的是SynchronizedDemo对象
public class SynchronizedDemo {
public synchronized void t() {
}
}
②修饰静态方法
这里的synchronized锁的是SynchronizedDemo对象
public class SynchronizedDemo {
//修饰静态方法
public synchronized static void t2() {
}
}
③修饰代码块
明确指定了修饰哪个对象
· 锁当前对象
这里的当前对象指的是当前线程,即谁调用的这个t3方法,当前对象就是谁.
public class SynchronizedDemo {
//锁当前对象
public void t3() {
synchronized (this) {
}
}
}
· 锁类对象
public class SynchronizedDemo {
//锁类对象
public void t4() {
synchronized (SynchronizedDemo.class) {
}
}
}
①保证可见性:多个线程对同一个共享变量的操作(这里的操作指的是满足原子性的指令),具有可见性.
②禁止指令重排序,建立内存保障.
注意:volatile不保证原子性操作
volatile的使用场景:共享变量的读操作以及常量赋值操作(因为这些操作本身保证了原子性).
volatile修饰的某个变量,实例变量或静态变量.
例如:
public volatile int flag = 0;
volatile强制内存读写,速度是变慢了,但是数据变得更准确了.
下列代码用来解决文章开头演示的线程不安全问题,分别在两个线程中n进行累加操作,利用synchronized对类对象进行加锁,保证线程安全.
public class SynchDemo {
private static int n = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (SynchDemo.class) {
for (int i = 0; i < 10000; i++) {
n++;
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (SynchDemo.class) {
for (int i = 0; i < 10000; i++) {
n++;
}
}
}
});
t1.start();
t2.start();
//main线程等待t1和t2线程运行结束后输出n的值
t1.join();
t2.join();
System.out.println(n);
}
}
未加锁时的运行结果
加锁后的运行结果
例如下列代码:
· 创建了两个线程t1和t2
· t1中包含一个循环,当flag为非0时,结束操作
· t2中从键盘读入一个整数,并将这个整数赋值给flag
· 当用户输入非0时,t1线程结束
代码分析:①如果flag变量的定义未加volatile,当输入非0数后,t2线程结束,t1线程不会结束,一直处于运行态,由于代码中加入了t1.join(),main线程也不会结束;
②如果加上volatile,保证了内存可见性,当输入非0数后,t2线程结束,之后t1线程结束,最后main线程结束.
import java.util.Scanner;
public class VolatileDemo {
public volatile static int flag = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while(flag == 0) {
//一直无限循环
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
Scanner scan = new Scanner(System.in);
System.out.println("请决定是否结束t1线程([0]继续运行 [1]结束运行)");
flag = scan.nextInt();
}
});
t1.start();
t2.start();
t1.join();
System.out.println("t1线程结束");
}
}
不加volatile的运行结果
加上volatile的运行结果