Java线程进阶-一篇文章解决synchronized的具体用法
文章目录
synchronized 关键字的了解
-
synchronized是同步的意思,而异步对应的是Asynchronous - 在AJAX中这两个概念相当重要
-
同步就是整个处理过程顺序执行,当各个过程都执行完毕,并返回结果。是一种线性执行的方式,执行的流程不能跨越。一般用于流程性比较强的程序,比如用户登录,需要对用户验证完成后才能登录系统。
-
异步则是只是发送了调用的指令,调用者无需等待被调用的方法完全执行完毕;而是继续执行下面的流程。是一种并行处理的方式,不必等待一个程序执行完,可以执行其它的任务,比如页面数据加载过程,不需要等所有数据获取后再显示页面。
- 他们的区别就在于一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式,比如日志记录就可以使用异步方式进行保存
-
注意奥,这个同步异步不要与多线程的概念混淆
-
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行 -->同步线程执行的特性
-
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。学过操作系统相关知识的同学,我们知道,如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了,JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销
使用 synchronized 关键字
synchronized关键字最主要的使用方式:
-
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
-
修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例。
-
修饰代码块: 指定加锁对象,对指定对象加锁,进入同步代码库前要获得给定对象的锁。
读完一遍根没读一样是不是,下面我们具体来讲讲这到底啥玩意一个个的
修饰代码块
具体一点:一个线程访问一个对象中的synchronized(对象)同步代码块时,其他试图访问该对象的线程将被阻塞,这里的o也被称为锁对象,任何线程要执行先要拿到锁对象
class T {
private int count =10;
private Object o=new Object();
public void m(){
synchronized (o){ //任何线程要执行下面的代码,必须先拿到o的锁
count--;
System.out.println(Thread.currentThread().getName()+" count = "+count);
}
}
}
修饰代码块还有一个简化写法,可以不用每次都去新建对象作为锁对象
class T {
private int count=10;
public void m(){
synchronized (this){
count--;
System.out.println(Thread.currentThread().getName()+" count = "+count);
}
}
}
我们知道,谁调用谁是this,那么通过推导,这里的this等效类型为T的对象
修饰实例方法
class T {
private int count=10;
public synchronized void m(){
count--;
System.out.println(Thread.currentThread().getName()+" count = "+count);
}
}
同理,执行该代码的线程,首先要获得m方法的、类型为T的实例(锁对象)
修饰静态方法
class T {
private static int count=10;
public synchronized static void m(){ //这里等同于synchronized(yxxy.c_001.T.class)
count--;
System.out.println(Thread.currentThread().getName()+" count = "+count);
}
public static void mm(){
synchronized (T.class){ //这里写synchronized(this)是否可以?
count--;
}
}
}
解释:因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份),所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
学过反射的朋友,知道类对象与实例对象的区别吧?嘻嘻
什么样的代码放入synchronized中?
1.代码被多个线程访问
2.代码中有共享的数据
3.共享数据被多条语句操作
应用:双重校验实现线程安全的单例模式
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
uniqueInstance 采用 volatile 关键字修饰是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行(保证线程安全)