1. synchronized有什么用?
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法和 synchronized 块。
synchronized关键字可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。
观察synchronized锁多对象
package Thread;
/**
* Author:weiwei
* description:synchronized锁多对象
* Creat:2019/2/20
**/
class Sync{
public synchronized void test(){
System.out.println("test方法开始,当前线程为: "+Thread.currentThread().getName());
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法结束,当前线程为: "+Thread.currentThread().getName());
}
}
class MyThread extends Thread{
@Override
public void run(){
Sync sync = new Sync();
sync.test();
}
}
public class Test0220 {
public static void main(String[] args) {
for(int i=0;i<3;i++){
Thread thread = new MyThread();
thread.start();
}
}
}
运行结果:
test方法开始,当前线程为: Thread-1
test方法开始,当前线程为: Thread-2
test方法开始,当前线程为: Thread-0
test方法结束,当前线程为: Thread-1
test方法结束,当前线程为: Thread-2
test方法结束,当前线程为: Thread-0
Process finished with exit code 0
观察运行结果我们可以发现,没有看到synchronized起到作用,三个线程同时运行test()方法。
实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。即synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。
当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才
能再次给对象加锁,这样才达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。
那么,如果真要锁住这段代码,要怎么做?
有两种思路:
- 第一种也是最容易想到的,只要锁住同一对象不就OK了?
修改代码,锁住同一对象
package Thread;
/**
* Author:weiwei
* description:synchronized锁同一对象
* Creat:2019/2/20
**/
class Sync{
public void test(){
synchronized(this){
System.out.println("test方法开始,当前线程为: "+Thread.currentThread().getName());
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法结束,当前线程为: "+Thread.currentThread().getName());
}
}
}
class MyThread extends Thread{
private Sync sync ;
public MyThread(Sync sync){
this.sync =sync;
}
@Override
public void run(){
this.sync.test();
}
}
public class Test022 {
public static void main(String[] args) {
Sync sync = new Sync();
for(int i=0;i<3;i++){
Thread thread = new MyThread(sync);
thread.start();
}
}
}
运行结果:
test方法开始,当前线程为: Thread-0
test方法结束,当前线程为: Thread-0
test方法开始,当前线程为: Thread-1
test方法结束,当前线程为: Thread-1
test方法开始,当前线程为: Thread-2
test方法结束,当前线程为: Thread-2
Process finished with exit code 0
与锁多对象运行结果相比,锁同一对象的时候synchronized就起到了锁住对象的作用,使三个线程依次执行
- 第二种思路也是我们常用的思路,让synchronized锁住这个类对应的Class对象(全局锁)
观察全局锁
package Thread;
/**
* Author:weiwei
* description:synchronized锁Class对象
* Creat:2019/2/20
**/
class Sync{
public void test(){
synchronized(Sync.class){
System.out.println("test方法开始,当前线程为: "+Thread.currentThread().getName());
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法结束,当前线程为: "+Thread.currentThread().getName());
}
}
}
class MyThread extends Thread{
@Override
public void run(){
Sync sync = new Sync();
sync.test();
}
}
public class Test022 {
public static void main(String[] args) {
for(int i=0;i<3;i++){
Thread thread = new MyThread();
thread.start();
}
}
}
上面代码用synchronized(Sync.class)实现了全局锁的效果。因此,如果要想锁的是代码段,锁住多个对象的同一
方法,使用这种全局锁,锁的是类而不是this。
static synchronized方法,static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是this,而是
类的Class对象,所以,static synchronized方法也相当于全局锁,相当于锁住了代码段。
2. synchronized实现原理
在java代码中使用synchronized可使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景:
3.synchronized底层实现
对象锁(monitor)机制
执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor
任意Object及其子类读写内部在JVM中都附加Monitor,获取一个对象的锁
实际上就是获取该对象的Monitor(计数器)
当一个线程尝试获取对象Monitor时,
1.若此时Monitor(目标锁对象的计数器)值为0,该对象未被任何线程获取,当前线程获取Monitor,将持有线程置为当前线程,Monitor 值加1
2.若此时Monitor值不为0,此时该Monitor已被线程持有
a.若当前线程恰好是持有线程,Monitor值再次加1,当前线程继续进入同步块(叫做锁的可重入)
b.若持有线程不是当前线程,当前线程进入同步同列,等待Monitor值减为0
加锁:monitorenter + 1
减锁:monitor - 1
任意时刻只有当前Monitor值为0 表示无锁状态
之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁
4. synchronized优化
现在我们对Synchronized应该有所印象了,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。这种方式肯定效率低下,每次只能通过一个线程,既然每次只能通过一个,这种形式不能改变的话,那么我们能不能让每次通过的速度变快一点呢。
打个比方,去收银台付款,之前的方式是,大家都去排队,然后取纸币付款收银员找零,有的时候付款的时候在包里拿出钱包再去拿出钱,这个过程是比较耗时的,然后,支付宝解放了大家去钱包找钱的过程,现在只需要扫描下就可以完成付款了,也省去了收银员跟你找零的时间的了。同样是需要排队,但整个付款的时间大大缩短,是不是整体的效率变高速率变快了?这种优化方式同样可以引申到锁优化上,缩短获取锁的时间。
JDK1.6后的synchronized优化
自旋CAS:(Compare And Swap) 无锁保证线程安全
使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的
时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共
享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞
停顿的状态
CAS的操作过程
CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为
- V:主内存存放的实际变量值
- O:当前线程认为的变量值(预期的值)
- N:希望经变量替换的值(更新的值)
当O == V时,认为此时没有线程修改主内存的值,成功将N值替换回主内存,0-->主内存
当O != V时,认为此时已经有别的线程修改了主内存的值,修改失败,返回主内存的最新值,再次重试
ABA问题:一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化(也就是改了三次,把值又改回去了)
解决办法:
添加修改次数,就不会误认为没有改值或者 添加版本号解决
与线程阻塞相比,自旋会浪费大量的处理器资源,这是因为当前线程仍处于运行状况,只不过跑的是无用指令。它
期望在运行无用指令的过程中,锁能够被释放出来。
所以1.6之后有了自适应自旋:重量级锁的优化
就是获取锁失败的线程不会立即阻塞,而是空跑一段无用代码,若在此时间段成功获取锁,则下次再获取锁失败时,空跑时间适当延长,否则下次空跑时间缩短
自旋状态还带来另外一个副作用,不公平的锁机制,而lock体系可以实现公平锁
锁分类:
偏向锁--->轻量级锁--->重量级锁(JDK1.6之前,synchronized就是重量级锁)
重量级锁(悲观锁):获取Monitor的失败的线程进入同步队列,状态置为阻塞态(只要我获取锁,就一定有人跟我竞争这个锁)
偏向锁(乐观锁):认为只有一个线程在来回进入同步块,直接将加锁和解锁的过程都省略,每次进入代码块之前只是判断一下(只要我获取锁,就一定没有人会跟我竞争这个锁)
轻量级锁:不同时刻有不同的线程进入同步块,每次线程在进入同步块时,都需要加锁与解锁
重量级锁:同一时刻有不同线程进入同步块,
随着竞争的不断升级,竞争会不断膨胀,锁也会不断升级,但不会降级
锁粗化:
public class Main{
static StringBuffer sb = new StringBuffer();
public static void main(String [] args){
sb.append("hello");
sb.append("world");
sb.append("bit");
}
}
当前sb变量为共享变量,存在竞争,加锁
将多次连续加减锁过程粗化为一次大的加锁与解锁过程,减少无用的加减锁过程,提高效率
锁消除:
public static void main(String [] args){
StringBuffer sb = new StringBuffer();
sb.append("hello");
sb.append("world");
sb.append("bit");
}
当前sb变量为主线程私有,不存在竞争,所以不需要加锁,解除锁
当变量为线程私有变量时,将原先方法上的synchronized消除掉
5. JDK 1.5 提供的Lock体系
ReentrantLock是Lock体系下常用的一把锁
在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起
线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java
提供的Lock对象,性能更高一些。
首先来了解一下死锁
死锁就是多个线程彼此之间相互等待,造成资源浪费
检测死锁的工具: jstack
产生死锁的四个条件:
- 互斥:资源x在任意一个时刻只能被一个线程拥有
- 占有且等待:线程1占有资源x 的同时等待资源y,并不释放x
- 不可抢占:资源x一旦被线程占有后,其他线程不能抢x
- 循环等待:线程1持有x,等待y,线程2持有y,等待x
死锁产生的原因:以上四个条件同时满足(注意是同时满足)
那么如何解决死锁?
破坏四个中共任意一个就不满足了,(synchronized四个中一个都搞不定,所以产生了Lock)
[synchronized是用C语言写的,不可控,Lock是java写的 ,可控]
Lock使用格式
a.使用格式
try{
//同步代码块
//显式加锁
lock.lock();
}catch (Exception e){
}finally{
lock.unlock();//保证Lock不管出没出现异常,都能释放
}
b.常用方法:
lock() :加锁 ,语义与synchronized完全一致
unlock():解锁,
Lock源码解析
第一个方法: 中断异常,响应中断加锁
void lockInterruptibly() throws InterruptedException;
此异常破坏了上述四个条件中的第三个不可抢占
synchronized不响应中断,[死锁后程序不停止]
第二个方法:非阻塞式获取锁
boolean tryLock();
获取锁成功返回true,进入同步块,获取锁失败返回false,线程继续执行其他代码
此方法破坏了上述四个条件中的占有且等待
第三个方法:支持超时
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
获取锁失败之后再等待一段时间
此异常破坏了上述四个条件中的第三个不可抢占和第二个占有且等待
面试题:synchronized与ReentrantLock有什么关系与区别?
联系
1.都属于独占锁(任意一个时刻只有一个线程能获取到资源)的实现
2.都支持可重入锁
区别:
1.synchronized是关键字,JVM层面实现
ReentrantLock是java语言层面实现的"管程"
2.ReentrantLock具备一些synchronized不具备的功能,比如响应中断,非阻塞式获取锁,支持超时获取锁,支持公平锁
(公平锁:等待时间最长的线程最优先获取到锁)