线程安全问题
1.线程不安全:是指使用多线程执行任务时,得到的结果与预期不相符。
private static int count=0;
public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <100000 ; i++) {
count++;
}
}
});
t1.start();
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
count--;
}
}
});
t2.start();
System.out.println(count);
}
预期结果为0,这就发生了线程不安全问题
2.造成线程不安全的因素
(1)CPU是抢占式执行的(万恶之源)
(2)多个线程同时修改同一个变量
(3)可见性问题,即内存不可见
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,这就会导致共享变量在多线程之间不能及时看到改变,这就是内存不可见。(共享的主内存的值修改了,而先操作的线程的工作内存的值没有修改)
(4)原子性问题
- 原子性操作:一个或某几个操作只能在一个线程执行完之后,另一个线程才能开始执行该操作,也就是说这些操作是不可分割的,线程不能在这些操作上交替执行。
- 比如:n++就不是原子性操作
它是由三步操作组成的。从内存把数据读到CPU,进行数据操作,把数据写回CPU。 - 不保证原子性会给多线程带来什么问题?
在一个线程对一个变量操作时,会有别的线程插入进来,造成线程混乱
(5)编译器优化即指令重排序
在复杂的多线程中会出现混乱,从而导致线程不安全
如何解决线程不安全问题
线程不安全问题解决方案分析:
1.CPU抢占式执行的问题(不可控的)
2.多个线程同时修改同一个变量(让每个线程操作自己的私有变量,可能可以)
3.内存不可见问题(volatile关键字)
4.指令重排序问题(volatile关键字)
5.原子性问题(加锁)
详细说明两个关键字
1.volatile关键字(修饰操作的变量)
- 可以解决内存不可见和指令重排序问题,但是不能解决原子性问题
- 解决内存不可见:强制将线程自己的工作内存中的值清除,然后从共享的主内存中取值。
2.加锁(解决原子性问题)
java语言的加锁操作有两种:synchronized关键字和手动锁Lock
操作锁流程:尝试获取锁—>使用锁(具体业务)---->释放锁
synchronized关键字(监视器锁monitor lock)
使用方法:
// 全局变量
private static int number = 0;
// 循环的最大次数
private static final int maxSize = 100000;
public static void main(String[] args) throws InterruptedException {
// 声明锁对象
Object lock = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
// 实现加锁
synchronized (lock) {
// 代码1
number++;
}
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
synchronized (lock) {
number--;
}
}
}
});
t2.start();
// 等待两个线程执行完成
t1.join();
t2.join();
System.out.println("最终执行结果:" + number);
}
1.synchronized关键字三个维度
-
是JVM层面锁的解决方案,它帮咱们实现了加锁和释放锁的过程,加锁monitorenter ,释放锁monitorexit
-
在操作系统层面使用的是mutex lock互斥锁来实现的
-
针对Java语言来说,是将锁信息存放在对象头里(对象头里有两个重要的标识:标识锁状态,标识锁的拥有者
锁存放的地方:对象头
锁信息monitor
2.使用锁的注意事项:如果是同一业务的多线程执行,一定要使用同一把锁。
3.synchronized锁升级的过程
- JDK1.6之前:重量级锁(用户态----->内核态),有特别大的性能消耗
- JDK1.6之后
手动锁Lock
使用方法:
// 全局变量
private static int number = 0;
// 循环的最大次数
private static final int maxSize = 100000;
public static void main(String[] args) throws InterruptedException {
Lock lock=new ReentrantLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < maxSize; i++) {
// 加锁
lock.lock();
try {
//业务操作
number++;
}finally {
//释放锁
lock.unlock();
}
}
}
});
t1.start();
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();
// 等待两个线程执行完成
t1.join();
t2.join();
System.out.println("最终执行结果:" + number);
}
1.Lock(接口)手动锁的主要方法有
lock():加锁
trylock():在没有锁的时候可以尝试获取锁,不用死等
unlock():释放锁
2.注意事项:
(1)lock()操作一定要放在try外面
如果放在try里面可能会造成两个问题:
- 如果try里面抛出异常了,还没加锁成功就执行finally里面的释放锁操作了(没得到锁就释放锁)
- 在没有的到锁的情况下试图释放锁,这个时候产生的异常就会将业务代码产生的异常(try里面的异常)覆盖,增加了代码调试的难度。
如果一定要放在try里面,一定要放在第一行
(2)unlock()必须放在finally里面
公平锁和非公平锁
- 公平锁调度
1.一个线程释放锁
2.(主动)唤醒“需要得到锁”的队列来得到锁(按序执行) - 非公平锁调度
当一个线程释放锁之后,另一个线程刚好执行到获取锁的代码就可以直接获取锁。(抢占式执行)
1.非公平锁的性能更高
2.在Java语言中所有的锁的默认实现方式都是非公平锁
3.synchronized是非公平锁,ReentrantLock是默认是非公平锁,但也可以显式的声明为公平锁。
显式的声明公平锁:
Lock lock=new ReentrantLock(true);
synchronized和Lock的区别
1.关键字不同
2.synchronized自动进行加锁和释放锁,而Lock需要手动加锁和释放锁。
3.Lock是Java层面的锁的实现,而synchronized是JVM层面的实现
4.synchronized和Lock适用范围不同,Lock只能用来修饰代码块,而synchronized既可以修饰代码块,又可以用来修饰静态方法和普通方法
5.synchronized锁的模式只有非公平锁模式,而Lock既可以使用公平锁的模式又可以使用非公平锁的模式
6.Lock的灵活性更高(trylock() )
7.Lock的粒度比较小,修饰的东西比较细致