目录
ps:线程安全问题是在多线程并发的时候会产生的问题 主要原因是因为线程调度存在随机性 在不确定的调度顺序下 很可能出现线程安全问题 会导致程序异常、数据异常等等
线程不安全的主要原因
- 线程是抢占式执行的 所以线程的调度充满了随机性(线程安全的万恶之源)
- 多线程对于同一个变量进行修改(如果是单纯的读取的话不会产生不安全)
- 针对变量的操作不是原子性的(原子性表示 不可分割的最小单位 也就是要么全部执行 要么都不执行)
- 内存可见性(JVM对于程序执行的一种优化机制 但是这种机制可能会产生线程安全问题)
- 指令重排序 (指令重排序 也是编译器对于程序执行的一种优化机制 也是有可能产生线程安全问题的)
线程不安全案例
案例1
class Sum{
public int count = 0;
public void add(){
count++;
}
}
public class ThreadDemo7 {
public static void main (String[] args) throws InterruptedException {
Sum sum = new Sum();
Thread t1 = new Thread(()->{
for (int i = 0 ; i < 50000 ; i++) {
sum.add();
}
});
Thread t2 = new Thread(()->{
for(int i = 0; i < 50000; i++){
sum.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sum.count);
}
}
我们来观察一下上述代码 上述代码中创建了一个Sum类 然后提供了一个可以使count++ 的方法 为何我们不直接定义一个变量呢?例如这样
public class ThreadDemo7 {
public static void main (String[] args) throws InterruptedException {
//Sum sum = new Sum();
int count = 0;
Thread t1 = new Thread(()->{
for (int i = 0 ; i < 50000 ; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for(int i = 0; i < 50000; i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
此时我们会发现 在线程中访问count的时候出现了波浪线 这是因为Lambda表达式会有一个变量捕获 此时是获取不到count的 要么是常量,要么未在线程中发生改变 这是Lambda的硬性要求 那么我们如果不使用Leambda是否可以呢? 答案是不可以 使用其他方式创建出的线程都会出现类似的变量捕获的问题 这个问题我们需要额外的注意一下
好了 回到我们之前的代码 此外我们还定义出了两个线程 t1线程负责循环5w次 将count++ t2线程负责循环5w次 将count++ 我们来执行多次看一下最后结果
咦 这是什么情况 为何我们两个线程对于一个变量执行一共10w次自增 会出现错误的数字呢?
而且 每次的数字都不相同 里面究竟发生了什么?
我们来实现一下这个情况的模拟 如此来看我们的两个线程执行count++是没问题的 但是因为我们的++操作并不是原子的操作 所以其中的排序 可能会变成另一种情况
我们其中顺序如果发生改变会怎么样?
如此一来 便产生了数据错误的问题 也可以说是出现了线程安全问题
案例2
public class ThreadDemo8 {
static boolean flg = true;
public static void main (String[] args) {
Thread t1 = new Thread(()->{
while(flg){
System.out.println("线程1结束");
}
});
t1.start();
Thread t2 = new Thread(()->{
try {
Thread.sleep(3000);
flg = false;
System.out.println("线程1循环停止");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t2.start();
}
}
我们的代码是这样的 t1线程会不断的读取flg 如果为true执行循环 为false执行结束并且打印线程1结束 t2线程会在休眠3s后将flg修改为false 并且打印线程1循环停止
我们在执行后发现 t2线程是可以正常执行的 但是t1线程没有按照我们预期的停止循环 此处的问题便是内存可见性的问题
为何会出现内存可见性的问题呢?
因为我们编译器对于这种频繁读取一个内存中的同一个值有相应的优化 当读取频率到达一定水平 并且值都未发生改变 那么我们便把这个值存入缓存区中 因为读取缓存区的速度是比较内存快很多的 (有的程序经过如此的优化(读取优化 指令重排序) 可能本身需要30分钟的程序 经过一系列的优化可以达到10分钟的世界 这也证明了开发编译器的都是佬中佬啊)但是正是因为这种优化策略 所以会出现 线程安全问题
解决方案
synchronized
synchronized不光可以保证原子性 还可以禁止指令重排序 所以此处使用synchronized可以解决大部分问题 但是仍旧有一个争议(synchronized是否可以保证内存可见性问题 众说纷纭 俺也不知道)
1.直接修饰普通的方法
使用synchronized的时候,本质上是在针对某个“对象”进行加锁
2.修饰一个代码块
需要显式指定针对哪个对象加锁(Java中的任意对象都可以作为锁对象)
3.修饰一个静态方法
相当于针对当前类的类对象加锁
我们尝试给之前的代码中添入synchronized试试 我们只需要修改add方法修饰synchronized即可
class Sum{
public int count = 0;
synchronized public void add(){
count++;
}
}
public class ThreadDemo7 {
public static void main (String[] args) throws InterruptedException {
Sum sum = new Sum();
Thread t1 = new Thread(()->{
for (int i = 0 ; i < 50000 ; i++) {
sum.add();
}
});
Thread t2 = new Thread(()->{
for(int i = 0; i < 50000; i++){
sum.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(sum.count);
}
}
使用synchronized的时候也有许多要注意到的问题 可以参考一下此篇文章
《JavaEE》锁的多种形态http://t.csdn.cn/hgMoT
volatile
volatile可以保证内存可见性 但是不能保证原子性
也就是说 想要保证内存可见性 (案例2) 就可以加入volatile来保证不会对代码进行内存可见性的优化
如此一来 我们只需要将flg修饰volatile 即可保证内存可见性
public class ThreadDemo8 {
static volatile boolean flg = true;
public static void main (String[] args) {
Thread t1 = new Thread(()->{
while(flg){
}
System.out.println("线程1结束");
});
t1.start();
Thread t2 = new Thread(()->{
try {
Thread.sleep(3000);
flg = false;
System.out.println("线程1循环停止");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t2.start();
}
}
我们的程序恢复正常了~
总结
- 使用synchronized关键字 被synchronized包裹起来的代码 即可保证操作的原子性
- 使用volatile关键字 volatile和原子无关 但是能够保证内存可见性 禁止了编译器进行优化
在适当的时机 添加synchronized 配合volatile可以在一定程序上保证我们的线程安全