什么是线程
线程也被称为轻量级进程,并且大多数现代操作系统把线程作为时序调度的基本单元,而不是进程。
多线程的安全风险
public class UnsafeCount {
private int value;
public int next() {
return value++;
}
}
上面这段代码,由于value++包含了三个操作:
- 读取value值
- 计算value+1
- 将value+1的结果赋值给value
当有多个线程同时访问同一个UnsafeCount对象的next()方法时,就可能会发生以下情况
- 线程B在线程A没有完成将结果10写入内存之前读取了value的值为9。
- 当线程A将10写入内存之后,线程B又将10再次写入内存。
- 虽然value++执行了两次,但是实际上只加了1,这不是我们预想的结果,所以我们称这个类为线程不安全的类。
线程安全
当多个线程访问同一个类时,如果不考虑这些线程在运行环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不需要做额外的协调,这个类的行为仍然是正确的,那么称这个类为线程安全的类。
有些拗口,简单地说,就是一个类可以在多个线程共享时随便调用并且类的行为仍然时正确的,就是线程安全的类
Java对线程安全的支持
- 每个java对象都可以隐式地扮演一个用于同步的锁的角色,这些内置的锁被称为内部锁或监视器锁
在java的世界里,内部锁(监视器锁)扮演了互斥锁的角色,即至多只有一个线程可以持有互斥锁。 - 用于同步的关键字synchronized.
synchronized可以修饰方法,也可以在代码块中使用,看下面这段代码。- 修饰普通方法时,持有的是当前对象(this)的锁
- 修饰静态方法时,持有的是Class类的对象(SafeCount.class)的锁
- 在代码块中使用时,持有的是指定对象的锁。
public class SafeCount {
private int value;
private Object obj = new Object();
// 修饰方法,持有的是当前对象的锁
public synchronized int next() {
return value++;
}
// 代码块,持有指定对象的锁
public int getNext() {
synchronized(obj) {
return value++;
}
}
// 修饰静态方法,持有当前Class的class对象的锁,比如这里持有的是SafeCount.class的锁
private static synchronized void doSomething() {
//todu
}
}
- 可重入锁(Reentrant Lock)
当线程A已经持有了一个锁,而线程B试图持有同一个锁时,线程B就会阻塞,直到线程A释放该锁。但是如果线程A试图获得它已占有的锁时,是可以成功的,因为java的内部锁是可重入锁。比如下面这个递归调用
public static synchronized int factorial(int n) {
if (n == 0 || n == 1) return 1;
return n * factorial(n-1);
}
第一次进入方法时,会持有一个Class对象的锁,发生递归的时候会继续尝试获得这个锁,如果这个锁是不可重入的,就会一直等待,而代码没法执行完,锁永远得不到释放,这就是死锁。
因为java的内部锁是可重入的,所以synchronized也是可重入的。
- volatile 关键字修饰变量来保证有序性(一定程度上阻止指令重排)和可见性。
- 显式锁