synchronized
线程安全的主要原因
1.存在共享数据(也称临界资源)
2.存在多条线程共同操作这些共享数据
解决问题的根本方法
同一时刻且只有一个线程在操作共享数据,其他线程必须等到线程处理完数据后再对共享数据进行操作
互斥锁的特性
互斥性:即再同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样再同以时间只有一个线程对需要同步的代码块(符合操作)进行访问。互斥性也称为操作的原子性。
可见性:必须确保再锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。
synchronized 锁的不是代码,锁的都是对象
根据获取的锁的分类:获取对象锁和获取类锁
获取对象锁的两种用法
1.同步代码块(synchronized (thihs) synchronized (类的实例对象),锁是小括号(()中的实例对象 )
2.同步非静态方法( synchronized method ),锁是当前对象的实例对象。
获取类锁的两种用法
1.同步代码块( synchronized (类.class)), 锁是小括号中的类对象(Class对象)
2.同步静态方法( synchronized static method ),锁是当前对象的类对象( Class 对象)
public class SyncThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.startsWith("A")) {
async(); //异步
} else if (threadName.startsWith("B")) {
syncObjecyBlock1(); //同步代码
} else if (threadName.startsWith("C")) {
syncObjectMethod1(); //同步方法
}else if (threadName.startsWith("D")) {
syncClassBlock1();
}else if (threadName.startsWith("E")) {
syncClassMethod1();
}
}
// synchronized 修饰非静态对象
private synchronized void syncObjectMethod1() {
System.out.println(Thread.currentThread().getName() + " 同步方法 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + " 同步方法 开始 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 同步方法 结束 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//方法中有 synchronized(this|object) {} 同步代码块
private void syncObjecyBlock1() {
System.out.println(Thread.currentThread().getName() + " 同步代码块 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (this) { //this 就算当前调用此方法的 Thread对象
try {
System.out.println(Thread.currentThread().getName() + " 同步代码块 开始 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 同步代码块 结束 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//异步方法
private void async() {
try {
System.out.println(Thread.currentThread().getName() + " 异步开始 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 异步结束 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//类锁 同步代码块
private void syncClassBlock1(){
System.out.println(Thread.currentThread().getName() + " 同步类代码块 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (SyncThread.class) { //this 就算当前调用此方法的 Thread对象
try {
System.out.println(Thread.currentThread().getName() + " 同步类代码块 开始 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 同步类代码块 结束 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//类锁 修饰静态方法
private synchronized static void syncClassMethod1() {
System.out.println(Thread.currentThread().getName() + " 同步类 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + " 同步类 开始 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 同步类 结束 " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SyncDemo {
public static void main(String[] args) {
//下面的线程都使用同一个锁
// 如果使用的是 不同对象 所有线程会编程异步 ( Thread A1 = new Thread(new SyncThread(), "A1"); 就算创建不同对象
//同一个类不同对象锁 是互补干扰的
SyncThread syncThread = new SyncThread();
//异步执行
/*从结果中可以发现 A1 A2线程是异步执行的 A1启动之后,A2并未等待A1结束就开始执行start
并且A2线程 还比 A1线程先结束了 并且可以看到 A开头的线程并未受到其他线程同步代码块的影响
* */
Thread A1 = new Thread(syncThread, "A1");
Thread A2 = new Thread(syncThread, "A2");
//同步代码块执行
/*
* B类线程是同步的 其中一个线程在访问对象代码块时 另一个访问对象同步代码块的线程会被阻塞
* 看到结果就可以看得出 B2执行完之后 B1才能开始执行 是顺序执行的
* 原因是 synchronized()方法是要等待当前对象的 SyncThread 的同步锁
* 输出 同步代码块 这条语句还是异步的 没有相互等待
* */
Thread B1 = new Thread(syncThread, "B1");
Thread B2 = new Thread(syncThread, "B2");
//同步方法执行
/*
synchronized 的修饰方法 C 类的线程都是同步的 C的 同步方法语句输出 和 开始都是同时发生的
一个线程在访问对象的同步方法的时候 另一个访问同步方法的线程 会被阻塞
* */
Thread C1 = new Thread(syncThread, "C1");
Thread C2 = new Thread(syncThread, "C2");
//在统一对象锁的时候 类锁表现出来的行为和 对象锁表现的行为是一致的
Thread D1 = new Thread(syncThread, "D1");
Thread D2 = new Thread(syncThread, "D2");
Thread E1 = new Thread(syncThread, "E1");
Thread E2 = new Thread(syncThread, "E2");
/*B类线程 和C类线程做对比
B类线程是 同步代码块 C类线程是 同步方法
C类线线程 其中一个执行完了之后 B类线程才能开始执行
所以可以看出 synchronized 同步代码块 和 同步方法 synchronized 锁的是同一个对象
都是共用this 对象
* */
A1.start();
A2.start();
B1.start();
B2.start();
C1.start();
C2.start();
D1.start();
D2.start();
E1.start();
E2.start();
}
}
结果
B1 同步代码块 18:11:36
B2 同步代码块 18:11:36
C1 同步方法 18:11:36
D1 同步类代码块 18:11:36
E2 同步类 18:11:36
C1 同步方法 开始 18:11:36
E2 同步类 开始 18:11:36
D2 同步类代码块 18:11:36
A2 异步开始 18:11:36
A1 异步开始 18:11:36
E2 同步类 结束 18:11:37
C1 同步方法 结束 18:11:37
B2 同步代码块 开始 18:11:37
D2 同步类代码块 开始 18:11:37
A1 异步结束 18:11:37
A2 异步结束 18:11:37
B2 同步代码块 结束 18:11:38
D2 同步类代码块 结束 18:11:38
D1 同步类代码块 开始 18:11:38
B1 同步代码块 开始 18:11:38
B1 同步代码块 结束 18:11:39
D1 同步类代码块 结束 18:11:39
E1 同步类 18:11:39
C2 同步方法 18:11:39
E1 同步类 开始 18:11:39
C2 同步方法 开始 18:11:39
C2 同步方法 结束 18:11:40
E1 同步类 结束 18:11:40
对象锁和l类锁的总结
1.有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
2.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞
3.若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞
4.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然
5.同一个类的不同对象的对象锁互不干扰
6.类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的
7.类锁和对象锁何不干扰
synchronized底层实现原理
实现synchronized的基础
1.java对象头
2.Monitor
对象在内存中的布局
1.对象头
2.实例数据
3.对齐填充
对象头结构
Mark Word
考虑JVM的空间效率,Mark Word被设计成为非固定的数据结构,以便存储更多有效的数据,会根据对象本身的状态,复用自己的存储空间。
重量级锁,通常使用的synchronized锁,指针指向的是Monitor的起始地址,每个对象都存在着Monitor与之关联。对象与Monitor存在着多种实现方式,例如Monitor可以与对象一起创建销毁,或者单线程试图获取对象锁池,自动生成。但当Monitor被线程持有之后,便处于锁定状态
Monitor:每个java对象天生自带了一把看不见的锁
Monitor是由ObjectMonitor实现的 查看ObjectMonitor源码
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/3ece33697a35/src/share/vm/runtime/objectMonitor.hpp
ObjectMonitor里面会有两个队列,一个是WaitSet 等待池 另一个是 EntryList 锁池用来保存ObjectWait的对象列表,每个对象锁的线程都会被封装称ObjectWait来保存到里面。
_owner 是指向持有ObjectMonitor 对象的线程,当多个线程同时访问,同一个代码的时候,首先会进入到 EntryList 集合里面,
当线程获取到线程的Monitor后,就进入到_object区,并把Monitor中的_owner变量设置为当前线程,同时Monitor的 _count就会+1。若线程调用wait方法,将释放当前持有的Monitor。_owner就会被恢复称NULL,_count也会被-1。同时该线程的objectwait实例就会被记录到_WaitSet集合中,等待被唤醒。若当前线程执行完毕,_WaitSet也会释放 Monitor 锁。并复位对应变量的值,以便其他线程进入获取Monitor锁。
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //等待池
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //锁池
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
Monitor锁的竞争,获取与释放
重入
从互斥锁的设计上来说,当一个线程视图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入
早期的synchronized缺点
1.早期版本,synchronized属于重量级锁,依赖于Mutex Lock实现
2.线程之间的切换需要从用户转换到核心态,开销比较大
java6以后,synchronized性能得到了很大的提升 因为有了下面的技术
Adaptive Spinning(自适应自旋锁) Lock Eliminate(锁消除) Lock Coarasening(锁初始化) Lightweight Locking(轻量级锁)
Biased Locking(偏向锁)
这些技术都是为了在线程之间更高效的共享数据,以及解决竞争问题,从而提高程序的执行效率
自旋锁
1.许多情况下,共享数据的锁定状态持续时间比较短,切换线程不值得。
2.通过让线程执行忙循环等待锁的释放,不让出CPU
缺点:若锁被其他线程长时间占用,会带来许多性能上的开销
自适应自旋锁
1.自旋的次数不再固定
2.由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
锁消除
更彻底的优化
JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁
public class StringBufferWithoutSync {
public void add(String str1,String str2){
//StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
//因此sb属于不可能共享的资源,JVM会自动消除内部的锁
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 100; i ++){
withoutSync.add("aaa","bbb");
}
}
}
synchronized的四种状态
无锁,偏向锁,轻量级锁,重量级锁
锁膨胀方向:无锁—>偏向锁—>轻量级锁—>重量级锁
偏向锁:减少同一线程获取锁的代价
1.大多数情况下,锁不存在竞争,总是由同一线程多次获得
核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作
不适用于竞争锁比较激烈的多线程场合
轻量级锁
轻量级锁是由偏向锁来升级的,偏向锁运行在一个线程进入同步代码块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁
使用的场景:线程交替执行同步块
若存在同一时间访问同一锁的情况,就会导致轻量级锁级别膨胀为重量级锁
锁的内存语义
当线程释放锁时, Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中;
而当线程获取锁时, Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
偏向锁,轻量级锁,重量级锁的汇总