线程不安全原因 | 解决方案 |
---|---|
①CPU抢占 执行(万恶之源) | 无法解决 |
②代码非原子性 | 在关键代码处,让使用的CPU排队执行(加锁) |
③(内存)不可见 | 可使用 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 关键字,无法解决多线程非原子性问题,进而无法解决线程非安全。
1.使用 synchronized 关键字来加锁和释放锁【JVM层面的解决方案,自动帮我们进行加锁和释放锁的操作】
2.Lock 手动锁【Java层面的解决方案,需要程序员自己去加锁和释放锁】
公平锁可以按顺序进行执行,而非公平锁执行的效率更高。在Java中所有锁默认的策略都是非公平锁。
synchronized 的锁机制是非公平锁。
Lock 默认的锁策略也是非公平锁,但是 Lock 也可以声明为公平锁。
1.尝试获取(如果成功拿到锁,加锁,进行排队等待)
2.释放锁
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
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; i++) {
number++;
}
}
public static synchronized void decrment() {
for (int i = 0; i < maxSize; i++) {
number--;
}
}
public static void main(String[] args) throws InterruptedException {
//线程1:自增10W次
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
incrment();
}
});
t1.start();
//线程2:自减10W次
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
decrment();
}
});
t2.start();
//等待线程1和线程2执行完毕
t1.join();
t2.join();
System.out.println("最终执行结果:" + number);
}
}
该代码的执行结果:
我们发现,使用synchronized 来修饰静态方法,也能使该程序线程安全。
3.使用 synchronized 可以用来修饰普通方法(加锁对象是当前类的实例),示例如下:
public class ThreadDemo35 {
//全局变量
private static int number = 0;
//定义循环次数
private static final int maxSize = 100000;
// public static synchronized void incrment() {
public synchronized void incrment() {
for (int i = 0; i < maxSize; i++) {
number++;
}
}
// public static synchronized void decrment() {
public synchronized void decrment() {
for (int i = 0; i < maxSize; i++) {
number--;
}
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo35 threadDemo35 = new ThreadDemo35();
//线程1:自增10W次
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
threadDemo35.incrment();
}
});
t1.start();
//线程2:自减10W次
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
threadDemo35.decrment();
}
});
t2.start();
//等待线程1和线程2执行完毕
t1.join();
t2.join();
System.out.println("最终执行结果:" + number);
}
}
该代码的执行结果:
我们发现,使用synchronized 来修饰普通方法,也能使该程序线程安全。
Lock 的使用,示例代码如下:
public class ThreadDemo32 {
//全局变量
private static int number = 0;
//定义循环次数
private static final int maxSize = 100000;
public static void main(String[] args) throws InterruptedException {
//1.创建手动锁
Lock lock = new ReentrantLock();
//线程1:自增10W次
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
//2.加锁
lock.lock();
try {
number++;
} finally {
//3.释放锁
lock.unlock();
}
}
}
});
t1.start();
//线程2:自减10W次
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
lock.lock();
try {
number--;
} finally {
lock.unlock();
}
}
}
});
t2.start();
//等待线程1和线程2执行完毕
t1.join();
t2.join();
System.out.println("最终执行结果:" + number);
}
}
注意事项:一定要把 lock( ) 放在 try 外面,原因如下:
1.如果将1ock()方法放在 try 里面,那么当 try 里面的代码出现异常之后,那么就会执行 finally 里面的释放锁的代码,但这个时候加锁还没成功,就去释放锁。
2.如果将 lock( ) 方法放在try里面,那么当执行 finally 里面释放锁的代码的时候就会报错(线程状态异常),释放锁的异常会覆盖掉业务代码的异常报错,从而增加了排除错误成本。
演示:将 lock( ) 方法放入 try 里面,示例代码如下:
public class ThreadDemo33 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
try {
int num = 1 / 0;//异常业务
lock.lock();
} finally {
lock.unlock();
}
}
}
该代码的执行结果如下:
我们发现,该程序执行时会报错,且异常类型为锁操作异常,并非业务异常信息。
如果将lock()方法放在try的外面,示例代码如下:
public class ThreadDemo33 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
try {
int num = 1 / 0;//异常业务
} finally {
lock.unlock();
}
}
}
该代码的执行结果如下:
我们发现:异常类型为我们预期的业务异常类型。
Lock 声明公平锁示例代码如下:
public class ThreadDemo34 {
public static void main(String[] args) throws InterruptedException {
//声明一个公平锁
Lock lock = new ReentrantLock(true);
//业务逻辑处理:打印AABBCCDD
//公共任务
Runnable runnable = new Runnable() {
@Override
public void run() {
for (char item : "ABCD".toCharArray()) {
lock.lock();
try {
System.out.print(item);
} finally {
lock.unlock();
}
}
}
};
Thread t1 = new Thread(runnable, "t1");
Thread t2 = new Thread(runnable, "t1");
Thread.sleep(10);
t1.start();
t2.start();
}
}
该代码执行结果为:
Lock 的使用场景:只能用来修饰代码块。
线程和锁的关系(一对多):一个线程可以拥有多把锁;但是一个锁只能被一个线程拥有。
定义:在多线程编程中(两个或两个以上的线程),因为资源抢占,造成线程无限等待的问题。
死锁问题,示例代码:
public class ThreadDemo36 {
public static void main(String[] args) {
//创建锁A(资源A)
Object lockA = new Object();
//创建锁B(资源B)
Object lockB = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//线程1得到锁A
synchronized (lockA) {
System.out.println("线程t1得到了锁A");
try {
//休眠1S,让线程2先得到锁B
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1等待获取锁B");
//线程1尝试获取锁B
synchronized (lockB) {
System.out.println("线程1:得到了锁B");
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//线程2得到锁B
synchronized (lockB) {
System.out.println("线程t2得到了锁B");
try {
//休眠1S,让线程1先得到锁A
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2等待获取锁A");
//线程2尝试获取锁A
synchronized (lockA) {
System.out.println("线程2:得到了锁A");
}
}
}
}, "t2");
t2.start();
}
}
该代码的执行结果如下:
我们发现,线程进入的无限等待状态从而无法使程序执行完毕。
1.使用 Java监控和管理控制台(jconsole)可以检测出,该线程出现了死锁问题。
2.使用 jvisualvm 工具可以检测出,该线程出现了死锁问题。
上面造成死锁的四个条件中,互斥条件与不可剥夺条件无法修改,只能从请求拥有条件和环路等待条件入手。
从以下条件入手,修改任意一个条件即可:
1.请求拥有条件
2.环路等待条件
其中,最容易实现的方法就是修改环路等待条件。
通过修改 控制请求锁的有序性 即让线程1和线程2都先请求锁A,再让线程1和线程2再去请求锁B。
我们修改 ThreadDemo36 的部分代码,即可解决死锁问题,代码如下:
public class ThreadDemo37 {
public static void main(String[] args) {
//创建锁A(资源A)
Object lockA = new Object();
//创建锁B(资源B)
Object lockB = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//线程1得到锁A
synchronized (lockA) {
System.out.println("线程t1得到了锁A");
try {
//休眠1S,让线程2先得到锁B
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1等待获取锁B");
//线程1尝试获取锁B
synchronized (lockB) {
System.out.println("线程1:得到了锁B");
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
//线程2得到锁B
synchronized (lockA) {
System.out.println("线程t2得到了锁A");
try {
//休眠1S,让线程1先得到锁A
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2等待获取锁B");
//线程2尝试获取锁A
synchronized (lockB) {
System.out.println("线程2:得到了锁B");
}
}
}
}, "t2");
t2.start();
}
}
之前在学习线程休眠 Thread.sleep() 的时候,这个方法有一个弊端:必须有明确的结束时间,在休眠期间无法唤醒。为了解决这个问题,Java提供了 wait(休眠)/ notify(唤醒)/ notifyall(唤醒全部) 机制
线程通讯机制:一个线程的动作可以让另一个线程感知到就叫做线程通讯。
示例代码如下:
public class ThreadDemo38 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1:进入线程方法。");
synchronized (lock) {
//wait 的使用
try {
//线程等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1:执行完成");
}
}, "t1");
t1.start();
Thread.sleep(1000);
System.out.println("唤醒线程1");
synchronized (lock) {
//唤醒线程
lock.notify();
}
}
}
wait 为什么要加锁:
wait 在使用的时候,必须要释放锁,在释放锁之前,必须要有一把锁,所以要加锁。
wait 为什么要释放锁:
wait 默认是不传任何值的,当不传递任何值的时候,表示永久等待,这样就会造成一把锁被一个线程一直持有,为了这个问题的发生,所以在使用 wait 时,一定要释放锁。
public class ThreadDemo39 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1:进入线程" + new Date());
synchronized (lock) {
//wait 的使用
try {
//线程等待
lock.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1:执行完成" + new Date());
}
}, "t1");
t1.start();
Thread.sleep(2000);
// System.out.println("唤醒线程1");
// synchronized (lock) {
//唤醒线程
// lock.notify();
// }
}
}
notifyAll的使用,示例代码如下:
public class ThreadDemo39 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1:进入线程");
synchronized (lock) {
//wait 的使用
try {
//线程等待
lock.wait(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程1:执行完成");
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2:进入线程");
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程2:执行完成");
}
}, "t2");
t2.start();
Thread.sleep(2000);
System.out.println("唤醒线程1和线程2");
synchronized (lock) {
//唤醒线程
lock.notifyAll();
}
}
}
Thread.sleep(0)
和 Object.wait(0)
的区别:
1.sleep 是 Thread 的静态方法;而 lock 是Object 的方法。
2.sleep(0) 立即触发一次CPU资源的抢占;而 lock(0) 会让线程永久等待下去。
相同点:
- 两者都可以使当前的线程休眠。
- 两者都要处理一个 Interrupt 的异常。
不同点:
- wait 来自于Object 中的一个方法;而 sleep 来自于 Thread 中的一个静态方法。
- 传递参数不同:wait 可以没有参数;而 sleep 必须有一个大于等于0的参数。
- wait 使用时,必须加锁;而 sleep 使用时,不用加锁。
- wait 使用时,会释放锁;而 sleep 使用时,不会释放锁。
- 不传参的情况下 wait 会进入WAITING 状态;而 sleep 会进入TIMED_WAITING 状态。
为什么 wait 释放锁;而 sleep 不释放锁?
答:wait 默认等待无限期。
为什么 wait 要放在 Object 中而不是 Thread 中?
答:wait 操作必须要加锁和释放锁,而锁属于对象级别,而非线程级别(线程和锁是一对多的关系,也就是一个线程可以有多把锁),为了灵活起见(一个线程会有多把锁),就把 wait 放在了 Object 中。
LockSupport.park();
同样会使线程进入 WAITING 状态,示例代码如下:
public class ThreadDemo42 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1:进入线程");
//线程休眠
LockSupport.park();
System.out.println("线程1:执行完成");
}
}, "t1");
t1.start();
}
}
代码执行时,使用 jconsole 工具可以观察线程状态,如下:
使用LockSupport.unpark(线程名);
唤醒线程,示例代码如下:
public class ThreadDemo42 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1:进入线程");
//线程休眠
LockSupport.park();
System.out.println("线程1:执行完成");
}
}, "t1");
t1.start();
Thread.sleep(1000);
System.out.println("唤醒线程");
LockSupport.unpark(t1);
}
}
使用LockSupport.unpark(线程名);
唤醒线程,可以指定唤醒线程顺序,示例代码如下:
public class ThreadDemo42 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程1:进入线程");
//线程休眠
LockSupport.park();
System.out.println("线程1:执行完成");
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2:进入线程");
//线程休眠
LockSupport.park();
System.out.println("线程2:执行完成");
}
}, "t2");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程3:进入线程");
//线程休眠
LockSupport.park();
System.out.println("线程3:执行完成");
}
}, "t3");
t1.start();
t2.start();
t3.start();
Thread.sleep(1000);
System.out.println("唤醒线程");
LockSupport.unpark(t1);
LockSupport.unpark(t2);
LockSupport.unpark(t3);
}
}
该代码的执行结果如下:
使用LockSupport.park(参数);
传参,示例代码如下:
import java.util.Date;
import java.util.concurrent.locks.LockSupport;
public class ThreadDemo44 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程进入休眠:" + new Date());
LockSupport.parkUntil(System.currentTimeMillis() + 1000);
System.out.println("线程终止休眠:" + new Date());
}
}).start();
}
}
该代码的执行结果如下:
我们发现,使用LockSupport.park(参数);
传参,线程是可以自动唤醒的。
相同点:
1.两者都可以使线程休眠。
2.两者都可以无参或者传递参数,并且两者的线程状态也是一致的。
不同点:
1.wait 必须要配合 synchronized 一起使用(必须加锁),而 wait LockSupport 不许加锁。
2.wait 只能唤醒全部或随机的一个线程,而 LockSupport 可以按顺序唤醒指定线程。