线程安全——Synchronized
[toc]
前面我们介绍了在Java中可以用加锁关键字synchronized
保证原子性。
在线程安全中通过synchronized
给线程加锁,是线程由并行变为串行,这时可能会有疑问说:使用多线程的目的就是提高代码效率,加锁后就变成了单线程了,岂不是多此一举了?
因此在使用多线程时要注意一下几点
- 使用多线程的前提是必须保证结果的正确
- 在多线程修改共享变量时,才会出现线程安全问题;通过缩小锁的范围,从而提高程序的并发处理能力
1.使用方法
1.1修饰方法
这里我们以 :使用多线程实现count自增 为例
java
代码解读
复制代码
class Counter{
public int count =0;
public synchronized void increase(){
count++;
}
}
1.2修饰代码块
java
代码解读
复制代码
class Counter{
public int count =0;
public void increase(){
System.out.println("加锁之前的操作...");
//锁对象 需提供一个对象
synchronized (this){
count++;
}
System.out.println("加锁之后的操作...");
}
}
使用
synchronized
修饰的代码块涉及的指令,并不是在CPU上无脑的执行完,而是有可能执行了一半就被调度走了,但是即便被调度出CPU也不会释放锁,别的线程尝试获取锁时依然会阻塞等待
1.3锁静态方法
java
代码解读
复制代码
class Counter {
public static int count = 0;
public synchronized static void increase() {
count++;
}
}
1.3 给一个线程加锁,也会出现线程安全问题
举栗:
java
代码解读
复制代码
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
修饰的方法或者代码块对于同一对象,针对同一个线程(当前获得锁的线程)互斥那么这个锁就不可以重入,会产生一个互斥等待的现象,也叫死锁
对于同一锁对象,针对同一个线程(当前获得锁的线程)不互斥那么这个锁就可以重入