线程安全——Synchronized
文章目录
前面我们介绍了在Java中可以用 加锁关键字 synchronized
保证原子性。
在线程安全中通过synchronized
给线程加锁,是线程由并行变为串行,这时可能会有疑问说:使用多线程的目的就是提高代码效率,加锁后就变成了单线程了,岂不是多此一举了?
因此在使用多线程时要注意一下几点
- 使用多线程的前提是必须保证结果的正确
- 在多线程修改共享变量时,才会出现线程安全问题;通过缩小锁的范围,从而提高程序的并发处理能力
1.使用方法
1.1修饰方法
这里我们以 :使用多线程实现count自增 为例
class Counter{ public int count =0; public synchronized void increase(){ count++; } }
1.2修饰代码块
class Counter{ public int count =0; public void increase(){ System.out.println("加锁之前的操作..."); //锁对象 需提供一个对象 synchronized (this){ count++; } System.out.println("加锁之后的操作..."); } }
使用synchronized
修饰的代码块涉及的指令,并不是在CPU上无脑的执行完,而是有可能执行了一半就被调度走了,但是即便被调度出CPU也不会释放锁,别的线程尝试获取锁时依然会阻塞等待
1.3锁静态方法
class Counter { public static int count = 0; public synchronized static void increase() { count++; } }
1.3 给一个线程加锁,也会出现线程安全问题
举栗:
class Counter{ public int count =0; /** * 执行自增 */ public synchronized void increase(){ count++; } /** * 不加锁 */ public void increase1(){ count++; } }
1.4锁对象
在1.2中**()里面的this指的是:是对那个对象进行加锁**
加锁操作,是针对 一个对象进行的。
而我们需要的理解的是:synchronized
锁的是什么,当两个线程竞争的是同一个锁时,才会产生线程安全(两个线程竞争不同的锁时,根本不会产生任何的线程安全,两把锁之间没有任何关系,不会产生阻塞等待)。
因此在判断是否会产生锁竞争时,只需判断一下锁对象是否是同一个对象。
1.5 锁信息的记录
在Java虚拟机中,对象在内存结中的结构可以划分为4个区域:
- markword:对象头,锁信息,GC,程序计数器
- 类型指针(_class) :类型
- 实例数据(instance_data) :成员变量信息
- 对齐填充(padding)
总结:
synchronized的几种用法
- 修饰普通方法,相当于锁实例对象
- 对代码块进行加锁,相当于锁当前调用方法的对象(也是实例对象)
- 对静态方法进行加锁,相当于锁类对象
2.特性
互斥
一个线程获取了锁之后,其他线程就必须阻塞等待,等到当前获取锁的线程释放了锁之后再去竞争
内存可见(刷新内存)
synchronized
实现内存可见的机制是通过原子性完成的,一个线程完成了读、改、写回内存这一操作之后,再释放锁,下一个线程才能获取锁,再开始读取
所以保证了下一个线程读到的值是上一个线程写回主内存的最新值
可重入
在方法调用的链路中,可能存在多个被synchronized
修饰的方法或者代码块
对于同一对象,针对同一个线程(当前获得锁的线程)互斥那么这个锁就不可以重入,会产生一个互斥等待的现象,也叫死锁
对于同一锁对象,针对同一个线程(当前获得锁的线程)不互斥那么这个锁就可以重入
可能存在多个被synchronized
修饰的方法或者代码块
对于同一对象,针对同一个线程(当前获得锁的线程)互斥那么这个锁就不可以重入,会产生一个互斥等待的现象,也叫死锁
对于同一锁对象,针对同一个线程(当前获得锁的线程)不互斥那么这个锁就可以重入