大家知道被synchronize关键字修饰会使线程进行同步。先看下没有用同步时会发生什么事情:
class Foo {
private int x = 100;
public int getX() {
return x;
}
public int fix(int y) {
x = x - y;
return x;
}
}
public class TestThread implements Runnable {
private Foo foo = new Foo();
public static void main(String[] args) {
TestThread r = new TestThread();
Thread ta = new Thread(r, "Thread-A");
Thread tb = new Thread(r, "Thread-B");
ta.start();
tb.start();
}
public void run() {
for (int i = 0; i < 3; i++) {
foo.fix(30);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " : 当前foo对象的x值= " + foo.getX());
}
}
}
结果:
Thread-A : 当前foo对象的x值= 40
Thread-B : 当前foo对象的x值= 40
Thread-B : 当前foo对象的x值= -20
Thread-A : 当前foo对象的x值= -50
Thread-B : 当前foo对象的x值= -80
Thread-A : 当前foo对象的x值= -80
为什么100,后面直接输出都是40?
这就是没有同步的时候,A,B两个线程在存活阶段只有就绪和运行两个状态,不过这里有sleep,还有阻塞状态。虽然是两个线程拿的是同一个对象,则对象里面的属性,肯定被两个线程公用。对于结果可以这样分析:A线程先运行,运行到sleep时(此时值为70)运行线程B,而线程运行到sleep时(此时值为40)这时CUP要调度:第一种情况调用A:A线程继续走到打印(打印出40),然后进入循环。第二种情况调用B:B线程继续走到打印(打印出40),然后进入循环。
这就是不同步导致的结果。
1、使用synchronized关键字进行同步
为了解决线程中同步的问题,Java的多线程引入了同步监视器来解决这个问题,使用同步监视器的代码块就是同步代码块,同步代码块的格式如下:
1>修饰对象:同步监视器是obj
synchronized(obj) {
//....
//此处的代码块就是同步代码块
}
2>修饰方法,表示整个方法为同步方法:非静态方法的同步监视器是this(类对象本身)
public synchronized void timmer() {
//......
//此处的代码块就是同步代码块
}
上面代码块中,不管synchronized修饰的是方法还是对象,它始终锁定的是对象的实例变量,或者类变量。当执行同步代码块的时候,就会先获取该对象的同步监视器的锁定,直到线程执行完同步代码块之后才会释放对同步监视器的锁定。
在如上的run()方法中加上synchronized关键字,变成了同步方法,运行后的结果如下:
Thread-B : 当前foo对象的x值= 70
Thread-B : 当前foo对象的x值= 40
Thread-B : 当前foo对象的x值= 10
Thread-A : 当前foo对象的x值= -20
Thread-A : 当前foo对象的x值= -50
Thread-A : 当前foo对象的x值= -80
可以看到值进行了同步的修改,非静态方法的同步监视器是this,也就是TestThread类对象本身。
或者使用同步代码块来实现:
public void run() {
synchronized (foo) {
for (int i = 0; i < 3; i++) {
foo.fix(30);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " : 当前foo对象的x值= " + foo.getX());
}
}
}
可以看到,如上的同步方法是对foo对象的锁定,这样其他线程无法获取锁,也就无法修改它的值。
往深了探究,其实最终被synchronized关键字同步的是:在不同的线程中表示被锁定对象的内存块。也就是synchronized代码块或方法执行完成之后,对被锁住对象的所做的任何修改全部都会在线程锁释放之前刷回到主内存中。
另外,当进入一个同步的代码块,得到线程锁后,对被锁住对象的任何修改都是从主内存中读出来的,所以在锁定区域代码开始执行之前,持有锁的线程就和锁定对象主内存中的视图同步了。(摘自:Java程序员修炼之道)
这就是为什么synchronized关键字既要在变量的set方法上加,也要在变量的get方法上加的原因了。
2、必须清楚的几个问题
1)、synchronized只能修饰对象和方法,不能用来修饰构造器、属性,类。想一下就知道为什么,这是用来同步监视的,修饰其他的得到不效果。
2)、线程睡眠时,它所持的任何锁都不会释放,但是在实际应用中还是非常有用的。例如,可以启动一个线程来定时去执行某些特定的任务,如检查内存的同步状况。
3)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。也就是锁的重入。
4)、同步锁并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
5)、一个线程得不到锁会变为锁池状态,当满足条件后转为就绪或运行状态。
6)、调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。
7)、静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。
8)、由于非静态方法的同步监视器是this,所以其类似与代码块如下的写法:
synchroizened(this){ ... }
不变类是线程安全的,而其他类是线程不安全的,可以通过synchronized关键字达到方法和部分代码的同步,也可以使静态的方法同步,需要一个用于整个类对象的锁,这个对象是就是这个类:
public static synchronized int setName(String name){// 使用同步的方法
Xxx.name = name;
}
public static int setName(String name){ // 使用代码块同步
synchronized(Xxx.class){
Xxx.name = name;
}
}
9)、同步块与同步方法的区别:
使用同步方法时,多线程的执行效率并不高,例如对两个互不干扰的变量进行操作,如下:
public class test09 {
int x = 0;
int y = 0;
public synchronized void fixx(int xx) {
x = xx;
}
public synchronized void fixy(int yy) {
y = yy;
}
}
这时候对于修改x和y变量的线程完全可以并发地执行,但是由于两个方法对同一个对象加锁,导致只能串行的执行,使用同步块来改进一下:
public class test09 {
int x = 0;
int y = 0;
Object first = new Object();
Object second = new Object();
public void fixx(int xx) {
synchronized (first) {
x = xx;
}
}
public void fixy(int yy) {
synchronized (second) {
y = yy;
}
}
}
可以看到,修改两个变量的线程可以并发地执行。