在Java中,每一个对象都拥有一个锁标记(monitor),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
1、先看一个简单的例子:
package BasicConcept;
import java.util.ArrayList;
public class SynchrnoizedTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
InsertData insertData = new InsertData() ;
Thread t1 = new Thread(insertData);
Thread t2 = new Thread(insertData);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class InsertData implements Runnable{
public ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert() {
// TODO Auto-generated method stub
for (int i = 0; i < 500; i++) {
if(!arrayList.contains(new Integer(i)) ) {
System.out.println(Thread.currentThread().getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
@Override
public void run() {
// TODO Auto-generated method stub
insert();
}
}
我们可以看看运行结果:
我们明明希望得到的是新建两个线程往一个arrayList里装0-499个不同的数字,但是新建线程调用方法后发现,数字存入的很明显有重复的数字。为什么呢?正式因为线程安全问题,可能线程0执行到
if(!arrayList.contains(new Integer(i)) ) {
System.out.println(Thread.currentThread().getName()+"在插入数据"+i);
的时候,发生了阻塞,这时候进入了线程1,此时i数据并未写入到arrayList中去,所以线程1依然可以通过if语句,想里插入i,进而导致重复。而在insert函数前加入synchronized关键字之后:
public synchronized void insert() {
// TODO Auto-generated method stub
for (int i = 0; i < 500; i++) {
if(!arrayList.contains(new Integer(i)) ) {
System.out.println(Thread.currentThread().getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
我们看到结果是:
一直在执行线程0而并未进入线程1,且arryList最后存入的数据就为0-499;我们可以发现
synchronized关键字相当于对方法进行上锁了,只要在执行了insert方法后,别的线程只有在不再用insert
方法后才可能调用此方法。
而在insert函数里加入synchronized代码块之后:
public void insert() {
// TODO Auto-generated method stub
for (int i = 0; i < 500; i++) {
synchronized(arrayList) {
if(!arrayList.contains(new Integer(i)) ) {
System.out.println(Thread.currentThread().getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}
我们得到的结果是:
通过synchronized代码块对arraylist对象进行了上锁,只要此对象被使用,则知道完成插入一直都是使用当前线程才能使用这个arrayList对象!
总结下,关于一个对象的synchronized方法:
1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的
3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
关于synchronized代码块:
此外,例子代码中为
synchronized(arrayList) {
还可以为:
synchronized(this) {
甚至还可以在类中new一个额外对象来上锁实现 synchronized代码块。
synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步 。
同时有一点要注意:对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2)线程执行发生异常,此时JVM会让线程自动释放锁。