1.线程安全的概念
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
2.线程不安全的五个因素:
1、CPU抢占执行
2、内存可见性问题 volatile
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
3、指令重排序问题 volatile
刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了,什么问题呢。可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的。
4、原子性问题
比如刚才我们看到的 n++,其实是由三步操作组成的:
- 从内存把数据读到 CPU (load)
- 进行数据更新 (calc)
- 把数据写回CPU (save)
5、多个线程同时修改同一个变量
3.volatile关键字
轻量级解决“线程安全”的方案:
(1)禁止指令重排序
(2)解决内存可见性问题(当操作完变量之后,强制删除线程工作内存中的变量)
4.synchrinized
1、实现原理
底层是使用操作系统的mutex lock 实现的
(1)操作系统:互斥锁
(2) JVM: 帮我们实现的监视器的加锁和释放锁操作
(3)Java: 锁存放在变量的对象头里面
2、synchronized作用
(1)原子性:synchronized保证语句块内操作是原子的
(2)可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)
(3)有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)
3、三种使用场景:
1、使用synchronized来修饰代码块(加锁对象自定义)
2、使用synchronized来修饰静态方法(加锁对象:当前类方法)
3、使用synchronized可以修饰普通方法(加锁对象:当前类的实例)
4、synchrinized JDK 1.6优化【四种状态】
(1)无锁
(2)偏向锁
(3)轻量级锁
(4)重量级锁
5.手动锁Lock
Interface Lock:
从Lock接口中我们可以看到主要有个方法,这些方法的功能从注释中可以看出:
(1)lock():获取锁,如果锁被暂用则一直等待
(2)unlock():释放锁
(3)tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
(4)tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
(5)lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事
以下是lock的一个代码示例:
public class ThreadDemo32 {
// 全局变量
private static int number = 0;
//循环次数
public static final int maxSize = 100000;
public static void main(String[] args) throws InterruptedException {
//1.创建一个锁对象
Lock lock = new ReentrantLock();
//+10w
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <maxSize ; i++) {
//2.加锁
lock.lock(); //一定要把lock放在try外面:
//1.如果将lock()方法加在try里面,那么当try里面的代码出现异常之后
//那么就会执行finally里面的代码,但是这个时候枷锁还没成功 ,就去释放锁
//2、如果将lock放在try里面,那么当执行finally里面释放锁的代码时
try {
number++;
}finally {
//3、释放锁
lock.unlock();
}
}
}
});
t1.start();
//-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();
//等待两个线程执行完
t1.join();
t2.join();
System.out.println("最终执行结果:" + number);
}
}
注意事项:
一定要把lock()放到try外面:
(1)、如果将lock()方法放在 try 里面,那么当try
里面的代码出现异常之后,那么就会执行finally 里面的释放锁的代码,但这个时候加锁还没成功,就去释放锁。
(2)、如果将lock()方法放在 try
里面,那么当执行finally里面释放锁的代码的时候就会报错(线程状态异常),释放锁的异常会覆盖掉业务代码的异常报错,从而增加了排除错误成本。
6.volatile 和 synchronized区别?
区别:
volatile可以解决内存可见性问题和禁止指令重排序,但 volatile 不能解决原子性问题;synchronized是用来保证线程安全,也就是 synchronized可以解决任何关于线程安全的问题(关键代码排队执行,始终只有一个线程会执行加锁操作;原子性问题…)。
7.synchronized 和 Lock 区别?
1.synchronized 既可以修饰代码块,又可以修饰静态方法或者普通方法;而Lock 只能修饰代码块。
2.synchronized 只有非公平锁的锁策略,而Lock 既可以是公平锁也可以是非公平锁(ReentrantLock 默认是非公平锁,也可以通过构造函数设置 true声明它为公平锁)。
3.ReentrantLock更加的灵活(比如tryLock ) 。
4.synchronized是自动加锁和释放锁的,而 ReentrantLock需要自己手动加锁和释放锁。