Java高级特性 - 多线程基础(3)线程同步
使用synchronized关键字同步线程
任务描述
本关任务:使右侧代码中的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){
//在方法声明中添加synchronized关键字
for (int i = 0; i <= 5; i++) {
num++;
System.out.println(num);
}
}
/*方法二:在代码块中使用synchronized (object) 的形式
public void insert(Thread thread){
synchronized (this) { //this表示当前对象,也可以用其他对象作为锁*/
/********* End *********/
}