为了更好地对线程执行结果有正确的预期。
一、synchronized同步方法。
1> 实例变量非线程安全:
-- 公共对象的(即使private)成员变量,在异步方式调用的方法中被访问时,会产生数据不一致(两个线程访问一个公共对象)。
2> 锁个数==实例对象个数;
-- 线程获取的锁是对象锁:当线程A调用Object对象中加入synchronized关键字的方法F()时,线程A就获得了该方法F()所在对象的锁--即Object对象的锁。
3> 共享对象的类Object中有:
public class Object{
public void methodA(){}
public void methodB(){}
}
--1. methodA为syn,methodB为普通:线程A先持有object对象的锁(调用了methodA),B线程可以以异步方式调用object中的非synchronized方法methodB;
--2. methodA和methodB都为syn:线程A先持有object对象的锁(调用了methodA),B线程调用对象中另外的syn方法,也需要等待。
4> 脏读。(读与写不同步)
-- 线程A对公共实例变量进行写时(未完成),线程B对齐进行读,取到的是错误的数据。
5> synchronized锁重入
-- 线程获得实例对象的锁后,再次请求该锁时是可以获得的。对照<3>中,若methodA()和methodB()都是syn的,那么在methodA中调用methodB是可以的(否则的话也不合理)。
--* 如果不锁重入的话,会造成死锁。
--* 锁重入存在于继承中 [ 子类对于syn的A方法和syn的B(继承来的)方法,可锁重入 ]。
6> 同步不具有继承性质
-- 子类S继承父类M中的syn方法,子类必须显式对其添加synchronized关键字,才可以实现同步。
二、synchronized(xx)同步代码块。
1. synchronized方法对于非临界资源的耗时操作很不友好,增加了时耗(体现:时间花在等待上而不是在操作上)。可以用 [ synchronized(this) ] 将代码块区分开,一半同步一半异步。对临界资源的操作,放在synchronized中;隔离其它操作。
2. 当一个线程访问object的一个synchronized同步代码块时,其它线程对同一个object中所有其它synchronized同步代码块的访问将被阻塞(this对象监视器是同一个)。
3*. syn同步方法和syn同步代码块,访问前者时,后者也被阻塞;反之亦然。
4. synchronized(任意对象)同步代码块。
-- 使用synchronnized(非this对象x)同步代码块时,对象监视器必须是同一个(实例对象),否则结果就是异步调用。
-- 特别注意对可能出现脏读的数据处理,对多线程的共享变量的操作时,对其上锁。
5. synchronized public static void method(){}和synchronized(Object.class). 其实一样,对该类的Class文件持有锁,对所有该类实例生效。在线程A持有该Class的锁后,其他线程访问该类中其他static方法时会阻塞。
synchronized public static void methodA(){}
synchronized public static void methodB(){}
Object o1 = new Object();
Object o2 = new Object();
MyThread t1 = new MyThread(o1);
MyThread t2 = new MyThread(o2);
-- 在t1和t2当中对methodA或B方法的调用,都是同步的,无论传进的对象不是同一个。
6. 使用synchronized(String)时注意常量池的特性,若传入的是一个String常量,则持有的锁是同一个,无法将代码块同步;如果传入的是一个[new String()],则持有的锁不是同一个,可以将其同步化。
7. 对于两个方法methodA()和methodB(),若要他们各自对自身同步(不同线程执行自己这个方法的时候是同步的),则可以利用:
Object lock1 = new Object();
public void methodA()
{
synchronized(lock1)
{...}
}
Object lock2= new Object();
{
synchronized(lock2)
{...}
}
8. 死锁。双方互相持有对方的锁,各自都得不到释放。
避免:
-- 加锁顺序(线程按照一定的顺序加锁)
-- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
-- 死锁检测
三、volatile关键字
1. volatile保证了操作的可见性
假如线程1先执行,线程2后执行:
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
--* 在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程。
--* 解释:每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
--* 但是用volatile修饰之后就变得不一样了:
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。那么线程1读取到的就是最新的正确的值。
2. volatile不保证操作的原子性
下面看一个例子:
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
运行它会发现每次运行结果都不一致,都是一个小于10000的数字。
因为,可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
--* 假如某个时刻变量inc的值为10,
线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值;
线程2发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,inc只增加了1。
--* 前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?
注意,线程1对变量进行读取操作之后被阻塞了,并没有对inc值进行修改。所以其它线程并没有从公共内存中读取新的inc值。(总的过程:线程1从内存中读到inc为10,线程2在此时读到10,线程1和2各自对inc+1,各自都写入内存,此时inc为11。)
--** (自增操作不是原子性的)
自增操作(i++)分三步:1.从内存中取出i的值;2.计算i的新值;3.将i的新值写入内存。
posted on
浙公网安备 33010602011771号