多线程(五):解决线程不安全方案
线程不安全原因 | 解决方案 |
---|---|
①CPU抢占 执行(万恶之源) | 无法解决 |
②代码非原子性 | 在关键代码处,让使用的CPU排队执行(加锁) |
③(内存)不可见 | 可使用 volatile 关键字 |
④编译器/代码优化(指令重排序) | 可使用 volatile 关键字 |
⑤多个线程同时修改了同一个变量 | 不通用,修改难度大 |
volatile关键字
volatile 关键字 轻量级解决线程不安全的方案
代码示例如下:
public class ThreadDemo29 {
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
}
System.out.println("终止执行");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("设置flag = true");
flag = true;
}
});
t2.start();
}
}
该代码执行结果为:
我们发现,此代码与上篇博客中的ThreadDemo27的代码基本相同,就是在就是在定义全局变量 flag 时,添加了 volatile 关键字,通过解决内存不可见的方法,解决了线程不安全的问题。
volatile 作用:
①禁止指令重排序
②解决线程可见性的问题,实现原理:当操作完变量之后,强制删除掉线程工作内存中的此变量。
注意:
volatile 关键字,无法解决多线程非原子性问题。
public class ThreadDemo30 {
static class Counter {
//定义私有变量
private volatile int num = 0;
//定义任务执行次数
private final int maxSize = 100000;
//num++
public void incrment() {
for (int i = 0; i < maxSize; i++) {
num++;
}
}
//num--
public void decrment() {
for (int i = 0; i < maxSize; i++) {
num--;
}
}
public int getNum() {
return num;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
counter.incrment();
});
t1.start();
Thread t2 = new Thread(() -> {
counter.decrment();
});
t2.start();
t1.join();
t2.join();
System.out.println("最终执行结果:" + counter.getNum());
}
}
代码执行结果:
可见,volatile 关键字,无法解决多线程非原子性问题,进而无法解决线程非安全。
锁操作
Java中解决线程安全操作(锁的操作)
1.使用 synchronized 关键字来加锁和释放锁【JVM层面的解决方案,自动帮我们进行加锁和释放锁的操作】
2.Lock 手动锁【Java层面的解决方案,需要程序员自己去加锁和释放锁】
公平锁与非公平锁
公平锁可以按顺序进行执行,而非公平锁执行的效率更高。在Java中所有锁默认的策略都是非公平锁。
synchronized 的锁机制是非公平锁。
Lock 默认的锁策略也是非公平锁,但是 Lock 也可以声明为公平锁。
锁操作的关键步骤
1.尝试获取(如果成功拿到锁,加锁,进行排队等待)
2.释放锁
synchronized 的使用
synchronized的底层是使用操作系统的mutex lock实现的。
1.当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
2.当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
synchronized用的锁是存在Java对象头里的。
synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
程序的关键操作加锁,示例代码如下:
public class ThreadDemo31 {
//全局变量
private static int number = 0;
//定义循环次数
private static final int maxSize = 100000;
public static void main(String[] args) throws InterruptedException {
//创建锁
Object lock = new Object();
//线程1:自增10W次
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
//实现加锁操作
synchronized (lock) {
number++;
}
}
}
});
t1.start();
//线程2:自减10W次
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
synchronized (lock) {
number--;
}
}
}
});
t2.start();
//等待线程1和线程2执行完毕
t1.join();
t2.join();
System.out.println("最终执行结果:" + number);
}
}
注意事项:在进行加锁操作的时候,同一组业务必须为共同的锁对象。
该代码的执行结果如下:
我们发现,此程序就是线程安全的。
synchronized 实现原理:
1.操作:互斥锁 mutex
2.JVM:帮我们实现的监视器锁的加锁和释放锁的操作
3.Java:
a) 锁对象 mutex
b) 锁存放的地方:变量的对象头
synchronized 在 JDK 6 之前,使用重量级锁实现的,性能非常低,所以用到的并不多。
JDK 6 对 synchronized 做了优化(锁升级 )
synchronized 的使用场景:
1.使用 synchronized 来修饰代码块 (加锁对象可以自定义)
上述 ThreadDemo31 就是 synchronized 来修饰代码块的使用场景
2.使用 synchronized 来修饰静态方法,示例如下:
public class ThreadDemo35 {
//全局变量
private static int number = 0;
//定义循环次数
private static final int maxSize = 100000;
public static synchronized void incrment() {
for (int i = 0; i < maxSize