变量的线程安全
- 方法内声明的变量是线程安全的,因为每个线程各自有这个变量的一个副本,数据不共享;
- 成员变量(对象级变量)是非线程安全的,因为可能存在多个线程争相修改的情况,多线程争抢即不安全;
synchronized使用的锁对象
锁定对象
- 可以锁定Object对象
- 可以锁定Class对象
锁定Object对象的几种方法
- 通过给对象的非静态方法增加synchronized声明,即可将this对象作为监视器,锁对象,同一个对象中的多个synchronized方法是同步执行的,因为多个方法使用了synchronized,那么他们都是基于this实例对象作为锁对象,所以他们是同步执行的;
- 通过synchronized(this)的做法,可以声明同步代码块,不需要将整个方法都设置成同步执行,以便提高效率,这种做法也是基于this实例对象,即同一个对象中所有的同步代码块,非静态同步方法,都是同步执行的,如果是多个对象,那么他们的this不同,所以多个对象之间的多线程不受影响;
锁定Class的方法
- 通过给类静态方法增加synchronized声明,这时候由于是静态方法,所以这时候的锁对象是类,即
xx.class
,与上面的锁Object的this对象区分开,不相互影响; - 通过
synchronized(xx.class)
的方式,也可以对类对象进行加锁,那么这个时候所有进入这个同步代码块的线程都是需要排队执行过这一段(多个对象的并发线程,也是同步执行,因为是将类对象Class作为锁对象)
synchronized的特性
不保证synchronized和非synchronized方法的同步执行
synchronized关键字只是标识进入该方法或者该代码片段的线程会同步执行,不能保证在同步代码块内的所有变量不会被其他线程所修改;
一个典型的例子就是有变量A和变量B,有一个同步方法和一个非同步方法,都会修改这两个变量,那同步方法可以保证方法内执行的逻辑是多个线程同步执行,但是不能保证变量A和变量B在同步方法修改前或者修改后不会被其他线程修改,因为同步方法只是限定了一段代码块不会被多个线程异步执行,但是不会限制变量不能被多个线程修改;
锁重入
当一个线程已经获得某个锁的时候,他再次申请获得该锁,那他是一定可以获得的;举例就是一个线程调用了一个对象同步方法,然后再调用这个对象的另外一个同步方法,这个时候也是可以获得锁的,这也是合理的做法,一个线程不可能被自己所阻塞了;
同步方法的一些其他特性
- 当在同步代码块中抛出异常,锁对象会自动释放;
- 同步不具备继承性,父类的方法是同步方法,子类重载这个方法,子类的方法执行段不是同步的,通过super调用父类的方法是同步的;
同步代码块
与同步方法比较
同步代码块可以执行锁定部分代码片段,不一定要同步整个方法的代码,这样如果控制得好可以提升性能;这个操作就类似于try-catch尽量包括少的代码片段一个道理;
同步代码块可以使用synchronized(this), synchronized(其他任意对象)作为锁对象,需求同一个锁对象的多个线程会在同步代码块同步执行;
常量池给同步方法带来的特性
由于java中将字符串、int部分字符缓存成常量池,可以在代码中看到:
String a = "AA";
String b = "AA";
System.out.println(a == b); // true
因为a、b都指向了常量池的同一个字符串常量,应用在同步对象锁的原理相同,此时 synchronized(a|b)
的线程会同步执行,由此可以得知synchronized
方法判断对象锁的方式是比较对象是否相等,即对象的内存地址;
多线程的死锁
之前讨论到了同步锁可以是任意的对象,假设在同步代码块中(取得到了锁对象A),之后再去要求锁对象B,这时候就会可能引发多线程的死锁条件(资源永远不可能得到满足),参考:
class ThreadA {
synchronized(lockA) {
doSomeThing();
synchronized(lockB) {
// 此处可能得不到满足
}
}
}
class ThreadB {
synchronized(lockB) {
doSomeThing();
synchronized(lockA) {
// 此处可能得不到满足
}
}
}
上面的伪代码中,当线程A获得了lockA,线程B获得了lockB,之后他们再继续申请lockB和lockA是不可能得到锁对象,因为各自线程都持有对方想要的锁,而去申请对方持有的锁,这就导致了永远无法满足这种情况,出现了多线程的死锁;(多线程的死锁可以通过jstack -l <pid>
去发现 )
锁对象的改变
上面提到过,可以将synchronized
使用的锁对象理解为对象的内存地址,那么当引用指向的对象发生改变,同样的在synchronized
锁的对象也就相应发生改变,运行到synchronized
语句后,解析引用为具体对象地址,然后后续不会随引用改变而改变;可以看一个例子
package test;
/**
* 锁对象发生改变
*/
public class LockObjectChangeService {
private String lock = "123";
public void testMethod() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " begin " + System.currentTimeMillis());
lock = "456";
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end " + System.currentTimeMillis());
}
}
public static void main(String[] args) throws InterruptedException {
LockObjectChangeService service = new LockObjectChangeService();
ThreadA threadA = new ThreadA(service);
threadA.setName("a");
ThreadB threadB = new ThreadB(service);
threadB.setName("b");
threadA.start();
Thread.sleep(50); // 这里为了保证A线程能够执行到更改lock的那一句
threadB.start();
}
}
class ThreadA extends Thread {
private final LockObjectChangeService lockObjectChangeService;
ThreadA(LockObjectChangeService lockObjectChangeService) {
this.lockObjectChangeService = lockObjectChangeService;
}
@Override
public void run() {
lockObjectChangeService.testMethod();
}
}
class ThreadB extends Thread {
private final LockObjectChangeService lockObjectChangeService;
ThreadB(LockObjectChangeService lockObjectChangeService) {
this.lockObjectChangeService = lockObjectChangeService;
}
@Override
public void run() {
lockObjectChangeService.testMethod();
}
}
通过这个例子可以发现,执行synchronized
方法的时候,先解析引用地址,然后将地址作为锁对象(其实如果通过jvm的实现来看,他是在对象的对象头markword部分写入当前线程信息,即获得该对象的对象锁的线程信息,那可以理解,其实就是在那个对象上面打了一个自己线程的记号)
volatile关键字的使用
作用
volatile的作用就是使变量在多个线程之间可见
保证内存可见性
一般jvm为了提升多线程的执行效率,会有一个公用数据栈,之后各个线程之间会从公用数据栈复制一份作为自己的私有数据栈,每次修改完成私有数据栈之后,再同步回公用数据栈,再通知其他线程从公用数据栈同步回私有数据栈;
举例说明:如果有AB两个线程同时拿到变量i,进行递增操作。A线程将变量i放到自己的工作内存中,然后做+1操作,然而此时,线程A还没有将修改后的值刷回到主内存中,而此时线程B也从主内存中拿到修改前的变量i,也进行了一遍+1的操作。最后A和B线程将各自的结果分别刷回到主内存中,看到的结果就是变量i只进行了一遍+1的操作,而实际上A和B进行了两次累加的操作,于是就出现了错误。究其原因,是因为线程B读取到了变量i的脏数据的缘故;
此时如果对变量i加上volatile关键字修饰的话,它可以保证当A线程对变量i值做了变动之后,会立即刷回到主内存中,而其它线程读取到该变量的值也作废,强迫重新从主内存中读取该变量的值,这样在任何时刻,AB线程总是会看到变量i的同一个值。
不保证原子性
关键字虽然保证了实例变量在多个线程之间的可见性,但是他不具备同步性,那么他也就不具备原子性。
举例说明,我们常用的i++
,他是分成两个部分,第一步计算 i+1
的结果,第二步将结果赋值回i
,因为不是原子操作,可能导致线程A,线程B同时进行了第一步,然后同时执行第二步,这就导致了i
的结果等于2(初始化i=1
)
假设要保证i++
的原子性,需要配合synchronized
关键字来加同步锁。
synchronzied代码块也具有volatile的作用
synchronized关键词可以保证代码块的同步执行,也会将当前线程的工作内存与主内存进行同步,刷新回主内容,让其他线程感知。
比较synchronized和volatile关键字
- 关键字
volatile
是线程同步的轻量级实现,volatile
的性能优于synchronized
,其中volatile
只能修饰变量,synchronized
可以修饰方法、代码块;随着JDK新版本的发布,synchronized
的执行效率大幅度提升(主要体现在新版synchronized
使用了偏向锁、自旋锁、重量锁三个等级的锁升级),synchronized
关键字的使用比例大大增加; - 多线程访问
volatile
不会发生阻塞,多线程访问synchronized
会发生阻塞; volatile
可以保证数据可见性,但是不能保证数据原子性;synchronized
可以保证数据的原子性,也能间接地保证数据的可见性(synchronized
会将工作内存与主内存进行同步)synchronized
和volatile
解决的问题不一样,synchronized
解决的问题是多个线程并发访问资源的同步性,volatile
解决的是变量在多个线程的可见性;