线程基础-synchronized概念和应用
Java的锁synchronized
在《java编程思想》一书中,有这样一个例子:在浴室里多个人都希望能单独使用浴室(即共享资源)。这时使用浴室,一个人先敲门,看看是否能使用。如果没人的话,他就进入浴室并锁上门。这时其他人要使用浴室的话,就会被“阻挡”,所以他们要在浴室的门口等待,直到浴室可以使用。
当浴室使用完毕,就该把浴室给其他人使用了,这个比喻就有点不太确定了,事实上,人们并没有排队,我们也不能确定谁将是下一个使用浴室的人,因为线程的调度机制是不确定性的。实际情况是:等待浴室的人们簇拥在浴室的门口,当锁住浴室门的那个人打开锁准备离开的时候,离门最近的那个人可能进入浴室。
如前所述,可以通过yield()和setPriority()来给线程调度器提供建议,但这些建议未必会有多大的效果,这取决你的具体平台和jvm的实现。
Java提供了关键字synchronized,为防止资源冲突提供了内置的支持。synchronized是一种同步锁。他修饰的对象有以下几种:
(1)修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
(2)修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
(3)修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
(4)修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
修饰一个代码块
1/一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
如图:
注意:上面的例子,thread1和thread2同时执行,是因为synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联。在使用并发时,将域设置为private是非常重要的,否则,synchronized关键字就不能防止其它任务直接访问域,这样会产生冲突。
多个线程多个锁
关键字synchronized取得的锁都是对象锁,而不是把一段代码(方法)当做锁,所以代码中哪个线程先执行synchronized关键字的方法,哪个线程就持有该方法所属对象的锁(lock)。两个对象,线程获得的就是两个不同的锁,他们互不影响。有一种情况则是相同的锁,即在静态方法上加synchronized关键字,表示锁定。class类,类一级别的锁(独占。class类)。
修饰一个方法
synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个方法。
如图:
注意:
1/虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
2/在定义接口方法时不能使用synchronized关键字。
3/构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
修饰一个静态方法
我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。
代码如下:
public class MyThread implements Runnable{
private static int count;
public synchronized static void method() {
}
public MyThread() {
count=0;
}
public synchronized static void method1() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
MyThread syncThread = new MyThread();
Thread thread1 = new Thread(syncThread, "syncThread1");
Thread thread2 = new Thread(syncThread, "syncThread2");
thread1.start();
thread2.start();
}
@Override
public synchronized void run() {
method1();
}
}
syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。
注意:一个任务可以多次获取对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一对象上的另一个方法,就会发生这种情况。Jvm负责跟踪对象加锁的次数。如果一个对象被解锁,(即完全释放锁)其计数变为0。在任务第一次给对象加锁的时候,计数变为1.每当这个相同的任务在这个对象上获取锁时,计数都会递增。显然,只有首先获得了锁的任务才能允许继续获取多个锁。每当任务离开一个synchronized方法,计数递减,当计数为零的时候,锁内完全释放,此时别的任务就可以使用此资源了。
修饰一个类
代码如下:
public class MyThread implements Runnable{
private static int count;
public synchronized static void method() {
}
public MyThread() {
count=0;
}
public static void method1() {
synchronized(MyThread.class) {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
MyThread syncThread = new MyThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();
}
@Override
public synchronized void run() {
method1();
}
}
Synchronized作用于一个类时,是给这个类加锁,他的所有对象用的是同一把锁。
注意在什么时候同步呢?可以运用Brian的同步规则:
如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。
总结:
1/每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
2/无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 (注意:(1)不要使用String常量加锁,会出现死循环问题.(2)锁对象的改变问题,当使用一个对象进行加锁的时候,要注意对象本身发生改变的时候,那么持有的所就不同,如果对象本身不发生改变,那么依然就是同步的,即使对象的属性发生了改变。)