使用多线程,避免不了要考虑线程安全的问题,常见解决线程安全的方式:是采用“序列化访问临界资源”的方案。
即在同一时刻,只能有一个线程访问临界资源,其他线程只能阻塞等待,这种方式也称作同步互斥访问。synchronized同步锁就能实现这种效果,解决线程安全的问题。
① synchronized同步锁
解决资源共享的问题:给共享的资源加锁,让线程一个个通过,以确保每次线程读取的数据是正确的。
当synchronized用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
春运已至,看看大家最关心的火车票:
private int ticker = 1000;
class SubTickerCount implements Runnable{
@Override
public void run() {
// 这个循环只是为了能够一直卖票而已
for (int index = 0; index < 1100; index++) {
if (ticker > 0) {
try {
// 为了避免一个线程执行到底
Thread.sleep(10);
}catch (Exception e){}
System.out.println(Thread.currentThread().getName() +
"号窗口卖出:" + ticker-- + "号票");
}
}
}
}
...
// 开十个线程,售1000张票
private void saleTicker(){
SubTickerCount runnable = new SubTickerCount();
threadCount1 = new Thread(runnable,"SubCount1");
threadCount2 = new Thread(runnable,"SubCount2");
...
threadCount10 = new Thread(runnable,"SubCount10");
threadCount1.start();
threadCount2.start();
...
threadCount10.start();
}
打印结果:
十个并发线程访问同一个对象时,最终出现卖出负数的错误。
修改代码,添加synchronized:
synchronized (this) {
if (ticker > 0) {
System.out.println(Thread.currentThread().getName() + "号窗口卖出:" + ticker-- + "号票");
}
打印结果:
总结:当多个并发线程访问同一个对象中的synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。其他必须等待,直到当前线程执行完同步代码块之后才能执行该代码块。
那当一个线程访问object的一个synchronized(this)同步代码块时,其他线程可以访问该object中的非同步代码块吗?
public void testSynchronized2_1() {
synchronized (this) {
for (int index = 0; index < 5; index++) {
Log.v(TAG, Thread.currentThread().getName() +
" synchronized loop " + index);
try {
Thread.sleep(10);
} catch (Exception e) {
}
}
}
}
public void testSynchronized2_2() {
for (int index = 0; index < 5; index++) {
Log.v(TAG, Thread.currentThread().getName() +
" no synchronized loop " + index);
try {
Thread.sleep(10);
} catch (Exception e) {
}
}
}
private void testSynchronizedOrNot(){
final SynchronizedOrNot synchro = new SynchronizedOrNot();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchro.testSynchronized2_1();
}
}, "Thread1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchro.testSynchronized2_2();
}
}, "Thread2");
thread1.start();
thread2.start();
}
打印结果:
总结:当一个线程访问object的一个synchronized(this)同步代码块时,其他线程仍然可以访问该object中的非同步代码块。
来一首诗歌吧
public synchronized void printBefore() {
delayPrint("我打江南走过");
delayPrint("那等在季节里的容颜如莲花的开落");
delayPrint("东风不来,三月的柳絮不飞");
delayPrint("你底心如小小的寂寞的城");
}
public synchronized void printAfter(){
delayPrint("恰若青石的街道向晚");
delayPrint("跫音不响,三月的春帷不揭");
delayPrint("我达达的马蹄是美丽的错误");
delayPrint("我不是归人,是个过客……");
}
public synchronized void printTitle(){
delayPrint("~~~《错误》 郑愁予");
}
//线程1先打印诗歌前半段,再打印后半段和标题
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
print.printBefore();
print.printAfter();
print.printTitle();
}
}, "Thread1");
//线程2先打印诗歌后半段,再打印前半段和标题
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
print.printAfter();
print.printTitle();
print.printBefore();
}
}, "Thread2");
thread1.start();
thread2.start();
}
打印结果:
总结:当一个线程访问object的同步代码块或同步方法时,其他线程对object中所有同步代码块或方法的访问将被阻塞。
因为对象锁就这么一个,一个线程获得这个锁,其他线程对该对object所有同步代码区域的访问都被暂时阻塞。
通俗的例子
假设我去餐厅吃饭,我占用了一个或多个餐桌,(我自己吃饭或我帮一起来吃饭的小伙伴占位)就好比我给这些餐桌加了锁,那么其他人想要用这个餐桌,只能等我们用餐结束后,离开这个餐桌(释放了锁),其他人才可以使用这个餐桌。但是如果存在无人占用的餐桌(未加锁的餐桌)其他人还是可以使用的。
② 锁的重入性:
public synchronized void method1(){
Log.v(TAG,"----method1----");
method2();
}
public synchronized void method2(){
Log.v(TAG,"----method2----");
method3();
}
public synchronized void method3(){
Log.v(TAG,"----method3----");
}
打印结果
----method1----
----method2----
----method3----
总结:当一个线程已经持有一个对象锁后,再次请求该锁对象是可以得到锁的。这种方式称为锁的可重入性,它是线程安全的一种,自己可以获取自己的内部锁。
③ 对象锁
- synchronized修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{ }括起来的代码,作用的对象是synchronized(object)中的object对象。
- synchronized修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- sychronized(this)锁住的只是对象本身,同一个类的不同对象调用sychronized方法并不会被锁住,不会产生互斥;
对象锁作用于:调用这个方法或代码块的对象
④ 类锁
- static synchronized 修饰一个静态的方法,其作用的范围是整个静态方法;
- synchronized修饰一个类,其作用的范围是synchronized后面括号括起来的部分。
类锁作用于:这个类的所有对象
对象锁:修饰方法或对象或代码块,相同对象,即是同一个对象锁,可以实现不同线程的互斥效果。但如果是不同对象,就会有不同的对象锁,不能实现互斥。类锁:实现了全局锁的功能,这个类的所有对象,调用被类锁修饰的方法,都受到锁的影响
So同一个时间段,只可能有一个线程获取类锁,从而执行这段代码。全局锁,单例就是很好的例子。
public static CommonDialog getInstance(){
if (null == instance) {
instance = new CommonDialog();
}
return instance;
}
假设两个线程:
线程①执行到代码 if (null == instance) 还未执行instance = new CommonDialog();
线程②执行到代码 if (null == instance) 此时线程①的instance 还new未出来,仍然为null。
而接下来,线程① instance创建一次,之后线程②instance再被创建一次,instance被重复创建了。
加上synchronized,单例方式一:
public static synchronized CommonDialog getInstance(){
if (null == instance) {
instance = new CommonDialog();
}
return instance;
}
这样写instance的确不会被重复创建,但是锁(代码块)的粒度太大,我们只关心instance创建部分,没有必要给整个方法加锁。如果多个线程频繁调用getInstance()方法,synchronized导致性能开销较大,程序执行性能也就下降了。
可以采用synchronized(className.class)的方式。
所以上文可以写成:单例方式二:
public static CommonDialog getInstance(){
if (null == instance) {
//类锁,这个类的所有对象调用此方法,都会受到锁的影响
synchronized (CommonDialog.class) {
instance = new CommonDialog();
}
}
return instance;
}
这样写貌似没有什么问题, 但是我们考虑一种情况:
当instance为null,线程①与线程②都进入if语句,步骤如下:
1.线程①获得了锁资源,线程②等待锁资源
2.线程①执行完代码块instance = new CommonDialog(); instance 被创建,释放锁资源
3.线程②获得锁资源,执行代码块instance = new CommonDialog();instance 被创建,释放锁资源。instance又被创建了两次。
基于java内存模型与synchronized实现了内存可见性,此类情况很可能出现
于是我们再次修改代码:
public static CommonDialog getInstance() {
if (null == instance) {
//这个类的所有对象调用此方法,都会受到锁的影响
synchronized (CommonDialog。class) {
//其他线程可能获取过锁,并且实例化了instance 而当前线程一直被阻塞到此处
if(null == instance) {
instance = new CommonDialog();
}
}
}
return instance;
}
这种方式叫做双重检查上锁的单例模式,此类方式是基于synchronized实现了可见性与原子性的特性。
这两种方式,作用的对象都是这个类的所有对象,作用的范围都是整个方法,区别在锁的位置。
锁的粒度: 我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。(synchronized使用起来非常简单,却是牺牲性能换取的代码的可读性。所以锁的使用原则:锁的范围越小越好)
注意,以上的示例是为了描述synchronized的用法,但这并不是最好的单例,因涉及到java内存模型,所以我们单开一篇文章来说java内存模型
目前最推崇的单例模式如下:
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton () { }
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本,完美!
⑤ 死锁
Person a = new Person() ;
Person b = new Person() ;
synchronized(a) {
...
}
synchronized(b) {
...
}
private void method1(){
synchronized(a) {
synchronized(b) {
}
}
}
private void method2(){
synchronized(b) {
synchronized(a) {
}
}
}
假设线程①进入method1,获得锁a,执行代码... 同时线程②进入method2,获得锁b,执行代码... 此时线程①未释放锁a,等待锁b,而线程②未释放锁b,等待锁a,两者都再等待对方释放锁,以便自己获得锁。这种情况就是死锁。
有一张很经典的图例:
避免死锁的关键在于,尽量保持一致的锁的获取顺序。
还是以几个面试题结尾
①当对一个方法加锁的时候,锁的是谁,描述这个过程?
当对一个方法加锁时,实际是对调用此方法的对象加锁。
使用synchronized关键字来标记一个方法/代码块,当某个线程调用该对象的synchronized方法/代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法/代码块,只有等待这个方法/代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能获取锁资源,执行这个方法/代码块。
②类锁与对象锁互斥吗?
类锁与对象锁,不是一个锁,所以不存在互斥。
如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为调用static synchronized方法占用的是类锁,而调用非static synchronized方法占用的是对象锁,不是同一个锁。
③当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?
答:不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。既然线程已进入同步方法A,说明此线程已获得对象锁并且未释放,那么试图进入B方法的线程就只能在等锁池中等待对象的锁。
④synchronized锁的可重入性
可重入锁:当一个线程已经持有一个对象锁后,再次请求该锁对象是可以得到锁的。
这种方式称为锁的可重入性,它是线程安全的一种,自己可以获取自己的内部锁。
这种方式也是必须的:否则在一个synchrnoized方法内部就无法调用该对象的另外一个synchrnoized方法了。
工作原理:锁的重入性,是设置一个计数器,关联占有它的线程,当计数器为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM会记录锁的占有者,并将计数器设置为1。 如果同一个线程再次请求该锁,计数器会递增,每次占有的线程退出同步代码块时,计数器会递减,直至减为0时,锁才会被释放。 重入性原理参考为文章:http://www.cnblogs.com/pureEve/p/6421273.html
⑤synchronized的缺点
synchronized可以轻松的解决线程同步的存在的隐患,但却不够灵活,主要是通过牺牲性能换来语法上的简洁与可读。
⑥synchronized出现异常时,会怎样?
锁自动释放:当一个线程执行的代码出现异常的时候,其所持有的锁会自动释放,So还是比较安全的。
本文先用几个例子,介绍了synchronized的概念与用法,以及对象锁与类锁的区别。然后用几个面试题,再次回顾synchronized的特性,将这些知识点串联在一起。
喜欢学习,乐于分享,不麻烦的话,给个❤鼓励下吧!