如何解决线程安全问题

怎么解决线程的安全问题呢?

基本上所有解决线程安全问题的方式都是采用“序列化临界资源访问”的方式,即在同一时刻只有一个线程操作临界资源,操作完了才能让其他线程进行操作,也称作同步互斥访问。

在Java中一般采用synchronizedLock来实现同步互斥访问。

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关键字的时候有几个问题需要我们注意:

  1. 在线程调用synchronized的方法时,其他synchronized的方法是不能被访问的,道理很简单,一个对象只有一把锁;
  2. 当一个线程在访问对象的synchronized方法时,其他线程可以访问该对象的非synchronized方法,因为访问非synchronized不需要获取锁,是可以随意访问的;
  3. 如果一个线程A需要访问对象object1synchronized方法fun1,另外一个线程B需要访问对象object2synchronized方法fun1,即使object1object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

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);
            }
        }
    }
}

上述代码就是synchronized代码块添加锁的两种方式,可以发现添加synchronized代码块,要比直接在方法上添加synchronized关键字更加灵活。

当我们用sychronized关键字修饰方法时,这个方法只能同时让一个线程访问,但是有时候很可能只有一部分代码需要同步,而这个时候使用sychronized关键字修饰的方法是做不到的,但是使用sychronized代码块就可以实现这个功能。

并且如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

来看一段代码:

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);
            }
        }
    }
}

执行结果:

执行insert` `执行insert1` `执行insert1完毕` `执行insert完毕