任务描述
本关任务:使右侧代码中的insert
方法在同一时刻只有一个线程能访问。
相关知识
为了完成本关任务,你需要掌握:
1.并发编程什么时候会出现安全问题;
2.怎么解决线程安全问题;
3.synchronized
关键字。
并发编程什么时候会出现安全问题
在单线程的时候是不会出现安全问题的,不过在多线程的情况下就很有可能出现,比如说:多个线程同时访问同一个共享资源,多个线程同时向数据库插入数据,这些时候如果我们不做任何处理,就很有可能出现数据实际结果与我们预期的结果不符合的情况。
现在有两个线程同时获取用户输入的数据,然后将数据插入到同一张表中,要求不能出现重复的数据。
我们必然要在插入数据的时候进行如下操作:
- 检查数据库中是否存在该数据;
- 如果存在则不插入,否则插入。
现在有两个线程ThreadA
和ThreadB
来对数据库进行操作,当某个时刻,线程A和B同时读取到了数据X,这个时候他们都去数据库验证X是否存在,得到的结果都是不存在,然后A、B线程都向数据库插入了X数据,这个时候数据库中出现了两条X数据,还是出现了数据重复。
这个就是线程安全问题,多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。
这里面,这个资源被称为:临界资源(也可以叫共享资源)。
当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等等)时,就有可能产生线程安全问题。
当多个线程执行一个方法时,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。
如何解决线程安全问题
怎么解决线程的安全问题呢?
基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称作同步互斥访问。
在Java中一般采用synchronized
和Lock
来实现同步互斥访问。
synchronized关键字
首先我们先来了解一下互斥锁,互斥锁:就是能达到互斥访问目的的锁。
如果对一个变量加上互斥锁,那么在同一时刻,该变量只能有一个线程能访问,即当一个线程访问临界资源时,其他线程只能等待。
在Java中,每一个对象都有一个锁标记(monitor),也被称为监视器,当多个线程访问对象时,只有获取了对象的锁才能访问。
在我们编写代码的时候,可以使用synchronized
修饰对象的方法或者代码块,当某个线程访问这个对象synchronized
方法或者代码块时,就获取到了这个对象的锁,这个时候其他对象是不能访问的,只能等待获取到锁的这个线程执行完该方法或者代码块之后,才能执行该对象的方法。
我们来看个示例进一步理解synchronized
关键字:
public class Example {
public static void main(String[] args) {
final InsertData insertData = new InsertData();
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
这段代码的执行是随机的(每次结果都不一样):
Thread-0在插入数据0
Thread-1在插入数据0
Thread-1在插入数据1
Thread-1在插入数据2
Thread-1在插入数据3
Thread-1在插入数据4
Thread-0在插入数据1
Thread-0在插入数据2
Thread-0在插入数据3
Thread-0在插入数据4
现在我们加上synchronized
关键字来看看执行结果:
public synchronized void insert(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
输出:
Thread-0在插入数据0
Thread-0在插入数据1
Thread-0在插入数据2
Thread-0在插入数据3
Thread-0在插入数据4
Thread-1在插入数据0
Thread-1在插入数据1
Thread-1在插入数据2
Thread-1在插入数据3
Thread-1在插入数据4
可以发现,线程1
会等待线程0
插入完数据之后再执行,说明线程0
和线程1
是顺序执行的。
从这两个示例中,我们可以知道synchronized
关键字可以实现方法同步互斥访问。
在使用synchronized
关键字的时候有几个问题需要我们注意:
- 在线程调用
synchronized
的方法时,其他synchronized
的方法是不能被访问的,道理很简单,一个对象只有一把锁; - 当一个线程在访问对象的
synchronized
方法时,其他线程可以访问该对象的非synchronized
方法,因为访问非synchronized
不需要获取锁,是可以随意访问的; - 如果一个线程A需要访问对象
object1
的synchronized
方法fun1
,另外一个线程B需要访问对象object2
的synchronized
方法fun1
,即使object1
和object2
是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
synchronized代码块
synchronized
代码块对于我们优化多线程的代码很有帮助,首先我们来看看它长啥样:
synchronized(synObject) {
}
当在某个线程中执行该段代码时,该线程会获取到该对象的synObject
锁,此时其他线程无法访问这段代码块,synchronized
的值可以是this
代表当前对象,也可以是对象的属性,用对象的属性时,表示的是对象属性的锁。
有了synchronized
代码块,我们可以将上述添加数据的例子修改成如下两种形式:
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Thread thread){
synchronized (this) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Object object = new Object();
public void insert(Thread thread){
synchronized (object) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}
上述代码就是synchronized
代码块添加锁的两种方式,可以发现添加synchronized
代码块,要比直接在方法上添加synchronized
关键字更加灵活。
当我们用sychronized
关键字修饰方法时,这个方法只能同时让一个线程访问,但是有时候很可能只有一部分代码需要同步,而这个时候使用sychronized
关键字修饰的方法是做不到的,但是使用sychronized
代码块就可以实现这个功能。
并且如果一个线程执行一个对象的非static synchronized
方法,另外一个线程需要执行这个对象所属类的static synchronized
方法,此时不会发生互斥现象,因为访问static synchronized
方法占用的是类锁,而访问非static synchronized
方法占用的是对象锁,所以不存在互斥现象。
来看一段代码:
public class Test {
public static void main(String[] args) {
final InsertData insertData = new InsertData();
new Thread(){
public void run() {
insertData.insert();
}
}.start();
new Thread(){
public void run() {
insertData.insert1();
}
}.start();
}
}
class InsertData {
public synchronized void insert(){
System.out.println("执行insert");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackT\frace();
}
System.out.println("执行insert完毕");
}
public synchronized static void insert1() {
System.out.println("执行insert1");
System.out.println("执行insert1完毕");
}
}
执行结果:
执行insert
执行insert1
执行insert1完毕
执行insert完毕
编程要求
请仔细阅读右侧代码,根据方法内的提示,在Begin - End
区域内进行代码补充,具体任务如下:
- 使
num
变量在同一时刻只能有一个线程可以访问。
测试说明
使程序的输出结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
开始你的任务吧,祝你成功!
代码示例
package step2;
public class Task {
public static void main(String[] args) {
final insertData insert = new insertData();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
public void run() {
insert.insert(Thread.currentThread());
}
}).start();
}
}
}
class insertData {
public static int num = 0;
/********* Begin *********/
public synchronized void insert(Thread thread) {
for (int i = 0; i <= 5; i++) {
num++;
System.out.println(num);
}
}
/********* End *********/
}