线程安全
单线程不会存在线程安全问题,只有多线程才会有这个问题,什么是线程安全问题呢?就是线程的执行结果和我们的预期结果不一致
比如当我们设计一个售票系统,假设只有一张票了,这个时候两个用户同时买票,A用户查询发现有一张票,B用户查询也发现有一张票,这个时候两个人同时下单,系统会将票同时分给AB两个人,而实际上此时只能有一个人获取到票。
这个就是线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。
这里面,这个资源被称为:临界资源(也有称为共享资源)。
也就是说,当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程安全问题。
不过,当多个线程执行一个方法,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。
如何避免
基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。
实战
synchronized修饰方法
public class MyTest {
private ArrayList<Integer> list = new ArrayList<Integer>();
public synchronized void addNum(Thread thread) {
for (int i = 0; i < 5; i++) {
list.add(i);
System.out.println(thread.getName() + ", i = " + i);
}
}
public static void main(String[] args) {
final MyTest test = new MyTest();
new Thread() {
@Override
public void run() {
test.addNum(Thread.currentThread());
}
}.start();
new Thread() {
@Override
public void run() {
test.addNum(Thread.currentThread());
}
}.start();
}
}
执行结果:
Thread-0, i = 0
Thread-0, i = 1
Thread-0, i = 2
Thread-0, i = 3
Thread-0, i = 4
Thread-1, i = 0
Thread-1, i = 1
Thread-1, i = 2
Thread-1, i = 3
Thread-1, i = 4
Process finished with exit code 0
如果说不加synchronized会怎么样呢?
public class Test {
private ArrayList<Integer> list = new ArrayList<Integer>();
public void addNum(Thread thread) {
for (int i = 0; i < 5; i++) {
list.add(i);
System.out.println(thread.getName() + ", i = " + i);
}
}
public static void main(String[] args) {
final Test test = new Test();
new Thread() {
@Override
public void run() {
test.addNum(Thread.currentThread());
}
}.start();
new Thread() {
@Override
public void run() {
test.addNum(Thread.currentThread());
}
}.start();
}
}
执行结果:
Thread-1, i = 0
Thread-2, i = 0
Thread-1, i = 1
Thread-2, i = 1
Thread-1, i = 2
Thread-2, i = 2
Thread-1, i = 3
Thread-1, i = 4
Thread-2, i = 3
Thread-2, i = 4
需要注意的是:
当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的。
每个类也会有一个锁,它可以用来控制对static数据成员的并发访问。
一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。
synchronized修饰代码块
有时候整个方法不需要全部被加锁同步,只需要对方法中特定的部分有同步要求,这个时候就可以通过synchronized对代码块进行加锁
代码块加锁有两种方式,一种是synchronized(this),即对当前对象加锁,一种是对对象中的某个变量加锁synchronized(0bject),表示获取该属性的锁。
public class Test {
private ArrayList<Integer> list = new ArrayList<Integer>();
public void addNum(Thread thread) {
synchronized (this) {
for (int i = 0; i < 5; i++) {
list.add(i);
System.out.println(thread.getName() + ", i = " + i);
}
}
}
public static void main(String[] args) {
final Test test = new Test();
new Thread() {
@Override
public void run() {
test.addNum(Thread.currentThread());
}
}.start();
new Thread() {
@Override
public void run() {
test.addNum(Thread.currentThread());
}
}.start();
}
}
执行结果:
Thread-1, i = 0
Thread-1, i = 1
Thread-1, i = 2
Thread-1, i = 3
Thread-1, i = 4
Thread-2, i = 0
Thread-2, i = 1
Thread-2, i = 2
Thread-2, i = 3
Thread-2, i = 4
或者对成员进行加锁
public class Test {
private ArrayList<Integer> list = new ArrayList<Integer>();
private String num = "true";
public void addNum(Thread thread) {
synchronized (num) {
for (int i = 0; i < 5; i++) {
list.add(i);
System.out.println(thread.getName() + ", i = " + i);
}
}
}
public static void main(String[] args) {
final Test test = new Test();
new Thread() {
@Override
public void run() {
test.addNum(Thread.currentThread());
}
}.start();
new Thread() {
@Override
public void run() {
test.addNum(Thread.currentThread());
}
}.start();
}
}
对于static的synchronized和非static的synchronized用法如下
public class Test {
private ArrayList<Integer> list = new ArrayList<Integer>();
private String num = "true";
public synchronized void addNum1(Thread thread) {
System.out.println("执行非static方法开始");
try {
Thread.sleep(1000);
;
} catch (Exception e) {
}
System.out.println("执行非static方法结束");
}
public synchronized static void addNum2(Thread thread) {
System.out.println("执行static方法开始");
System.out.println("执行static方法结束");
}
public static void main(String[] args) {
final Test test = new Test();
new Thread() {
@Override
public void run() {
test.addNum1(Thread.currentThread());
}
}.start();
new Thread() {
@Override
public void run() {
test.addNum2(Thread.currentThread());
}
}.start();
}
}
执行结果:
执行非static方法开始
执行static方法开始
执行static方法结束
执行非static方法结束
可见方法static的synchronized方法不会阻塞非static的synchronized方法。
总结
synchronized原理和操作系统中提到的PV操作是一样的,当访问到synchronized修饰的方法或者代码块时,会对相应的加锁对象也就是临界资源的锁计数器加1,等方法或者代码块执行完成后,会对锁计数器减一。
一旦一个线程访问临界资源并且拿到锁后,后面的线程在来访问这个资源时都需要等待第一个线程放弃锁后才能拿到锁接着执行。注意这里的重点:
- 其它线程会一直等待阻塞,直到之前的线程释放锁后拿到锁才能接着执行
所以当前线程一直在等待锁这种方式降低了系统效率,因此出现了另一种加锁方式就是Lock,下节会接着讲述Lock的用法。