概念
线程不安全指的是程序在多线程的执行结果不符合预测。
线程不安全元素
1.抢占式执行
2.多个线程修改同一个变量
//举例 如果是单个线程执行此操作,结果正确,为0
/**
* 线程不安全问题
*/
public class ThreadDemo15 {
private static int num;
static class Counter {
//++操作
public void increment(int count) {
for (int i = 0; i < count; i++) {
num++;
}
}
//--操作
public void decrment(int count) {
for (int i = 0; i < count; i++) {
num--;
}
}
public int getNum() {
return num;
}
}
public static void main(String[] args) {
int count = 10000;
Counter counter = new Counter();
counter.increment(count);
counter.decrment(count);
System.out.println("++操作和--操作最后的结果:" + counter.getNum());
}
}
//如果是多个线程,多个线程会出现抢占式执行(此程序中也是多个线程修改同一个变量),从而使结果错误。
public class ThreadDemoVolatile {
private static int num = 0;
static class Counter {
public void increment(int count) {
for (int i = 0; i < count; i++) {
num++;
}
}
public void decrment(int count) {
for (int i = 0; i < count; i++) {
num--;
}
}
public int getNum() {
return num;
}
}
public static void main(String[] args) throws InterruptedException {
int count = 100000;
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
counter.increment(count);
});
thread1.start();
Thread thread2 = new Thread(() -> {
counter.decrment(count);
});
thread2.start();
//等待线程执行完
thread1.join();
thread2.join();
System.out.println("最终结果:" + counter.getNum());
}
}
3.非原子性操作
原子性:指不可分割性,操作要么一次性不间断的执行完,要么一个也不执行。比如,一段代码就是一个房间,每个线程就是进入房间的人,如果A进入这个房间之后没有出来,B不可以进入。否则就是不具备原子性。
如果将上述第二个程序改为串行执行,最终结果就是正确的:
必须保证第一个线程执行完成后,再执行第二个线程,此时计算的结果才是正确的。这一点与抢占式执行有关,如果线程不是“抢占式执行”,就算是原子性关系也不大。
4.内存可见性
可见性指一个线程对共享变量值的修改,能够及时被其他线程看见。
因为线程之间的共享变量存在主内存中,每个线程都有自己的工作内存。当线程要修改一个共享变量的时候,会先修改工作内存中的副本,再同步回主内存。工作时线程要先读取共享变量,先把变量从主内存中拷贝到各自的工作内存,再从工作内存读取数据。当修改线程1工作内存中的值,线程2的工作内存不一定会及时变化。
5.指令重排序
编译器优化的本质是调整代码的执行顺序,在单线程下不会出错,但在多线程下会容易出现混乱,从而造成线程安全问题。
解决方法
1.volatile 解决内存可见性和指令重排序
如果上述程序变量前没有加volatile,结果就是线程1开始执行,线程2也只执行了将flag修改为false,但是线程1不能结束。因为内存不可见性,就算线程2修改了变量,但是线程1不知道,所以线程1的变量值没有改变,将会一直执行。如果将volatile加到变量前将会解决这个问题。volatile会强制读写内存,将改变后线程2的副本值从工作内存刷新到主内存中,代码就可以从主内存中读取最新值到线程1的工作内存中,然后再从工作内存中读取副本值。
结果就会从
变为:
缺点:volatile不能解决原子性问题。
2.使用锁解决线程安全问题(最主要的手段)
一般分为 synchronized 和 lock 锁
2.1 synchronized的基本使用
2.1.1 修饰静态方法
/**
* Synchronized
* 1.修饰静态方法
*/
public class ThreadSynchronized {
private static int num = 0;
static class Counter {
//循环次数
private static int count = 100000;
public synchronized static void increment() {
for (int i = 0; i < count; i++) {
num++;
}
}
public synchronized static void decrment() {
for (int i = 0; i < count; i++) {
num--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
Counter.increment();
});
thread1.start();
Thread thread2 = new Thread(() -> {
Counter.decrment();
});
thread2.start();
//等待线程执行完
thread1.join();
thread2.join();
System.out.println("最终结果:" + num);
}
}
2.1.2 修饰普通方法
/**
* 2.修饰普通方法
*/
public class ThreadSynchronized2 {
private static int num = 0;
static class Counter {
//循环次数
private static int count = 100000;
public synchronized void increment() {
for (int i = 0; i < count; i++) {
num++;
}
}
public synchronized void decrment() {
for (int i = 0; i < count; i++) {
num--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
counter.increment();
});
thread1.start();
Thread thread2 = new Thread(() -> {
counter.decrment();
});
thread2.start();
//等待线程执行完
thread1.join();
thread2.join();
System.out.println("最终结果:" + num);
}
}
2.1.3 修饰代码块
修饰代码块是给对象加锁,有三种方法:
1.在实例类中使用this
2.在静态类中使用xxx.class
3.自定义锁对象(最常使用)
2.2 synchronized 特性
2.2.1 互斥
某个线程执行到某个对象的synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。
2.2.2 刷新内存
2.2.3 可重入
synchronized 对同一条线程来说是可重入的。
public class ThreadSynchronized4 {
public static void main(String[] args) {
synchronized (ThreadSynchronized4.class){
System.out.println("得到第一把锁");
synchronized (ThreadSynchronized4.class){
System.out.println("得到第二把锁");
}
}
}
}
2.3 Lock
lock.lock();
try{
}
finally{
lock.unlock()
}