synchronized处理同步有两种模式:
同步代码块:
- 锁 类的实例对象 synchronized(this){ }
- 锁类对象synchronized(类名.class){ }
- 锁任何实例对象 String lock = " ";synchronized(lock){ }–全局锁
同步方法:
- 普通方法+synchronized,锁的是当前对象
- 静态方法+synchronized,锁的是类,相当于全局锁
下面使用"黄牛卖票"的例子来解释:
1.同步代码块:
要使用同步代码块,必须要设置一个锁的对象,一般可以锁当前对象this
package com.matajie;
/**
* Data: 2019 - 12 - 13
* Time: 14:29
* author:我的程序才不会有Bug!
*/
class MyThread implements Runnable{
private int ticekt = 100;
@Override
public void run() {
//三个线程都可以进入这个while循环
while (ticekt > 0){
//为程序上锁,只有一个线程可以进入
synchronized (this){
if(ticekt>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"还剩下"+ticekt--+"张票");
}
}
}
}
}
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread thread1 = new Thread(mt,"黄牛A");
Thread thread2 = new Thread(mt,"黄牛B");
Thread thread3 = new Thread(mt,"黄牛C");
thread1.start();
thread2.start();
thread3.start();
}
}
2.同步方法:在方法上添加synchronized关键字表示此方法只有一个线程可以进入,
隐式锁对象-this.
package com.matajie;
/**
* Data: 2019 - 12 - 13
* Time: 14:29
* author:我的程序才不会有Bug!
*/
class MyThread implements Runnable{
private int ticekt = 100;
@Override
public void run() {
sale();
}
public synchronized void sale(){
while (ticekt > 0){
System.out.println(Thread.currentThread().getName()+"还有"+ticekt-- +"张票");
}
}
}
public class Test {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread thread1 = new Thread(mt,"黄牛A");
Thread thread2 = new Thread(mt,"黄牛B");
Thread thread3 = new Thread(mt,"黄牛C");
thread1.start();
thread2.start();
thread3.start();
}
}
当然,我们必须知道锁的是谁!!!
下面看一个例子:
package com.matajie;
/**
* Data: 2019 - 12 - 13
* Time: 14:47
* author:我的程序才不会有Bug!
*/
class Sync{
//任意时刻只有一个线程(同一个对象)能进入此方法
public synchronized void fun(){
System.out.println(Thread.currentThread().getName()+"fun()开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"fun()结束");
}
}
class MyThread implements Runnable{
@Override
public void run() {
Sync sync = new Sync();
sync.fun();
}
}
public class Test2 {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread thread1 = new Thread(mt,"线程A");
Thread thread2 = new Thread(mt,"线程B");
Thread thread3 = new Thread(mt,"线程C");
thread1.start();
thread2.start();
thread3.start();
}
}
如果我们锁了的话,那么运行结果应该是AA连续开始结束,BB连续开始结束,CC连续开始结束.可是,运行结果却不是这样的:
为什么会出现这种问题呢?
因为我们有三个对象,默认锁的是当前对象,线程B启动,创建了一个对象,然后线程C也跟着启动,并且创建了一个对象,然后线程A也跟着启动并且也创建了一个对象.
它们之间各锁各的,互不影响.
如果你还是不理解,那么举个不雅观的例子:
比如三个人需要上厕所它们进入了厕所大门以后,里面有三个小坑,他们各自选择一个坑位,然后把各自坑位的门锁上.
这样一来,他们谁先从厕所出来就看他们自己了.
那么则怎么锁同一个对象呢?
- 第一种方法
添加static修饰,将其编程静态的,将类锁住(static修饰的方法是类方法,类属性和类方法和对象实例化无关,不管多少对象,类方法共享一个对象.),相当于把厕所大门锁了,这样别人就进不来了.
public static synchronized void fun(){
//代码
}
结果如下:
2. 第二种方法把sync传给MyThread类,这样一定锁的是同一个对象
package com.matajie;
/**
* Data: 2019 - 12 - 13
* Time: 14:29
* author:我的程序才不会有Bug!
*/
class Sync{
public synchronized void fun(){
System.out.println(Thread.currentThread().getName()+"fun()开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"fun()结束");
}
}
class MyThread implements Runnable{
private Sync sync;
public MyThread(Sync sync) {
this.sync = sync;
}
@Override
public void run() {
sync.fun();
}
}
public class Test{
public static void main(String[] args) {
Sync sync = new Sync();
MyThread mt = new MyThread(sync);
Thread thread1 = new Thread(mt,"线程A");
Thread thread2 = new Thread(mt,"线程B");
Thread thread3 = new Thread(mt,"线程C");
thread1.start();
thread2.start();
thread3.start();
}
}
下面来证明锁的可重入性和互斥性:
锁的可重入性证明:
class MyThread implements Runnable{
public synchronized void test1(){
if(Thread.currentThread().getName().equals("线程A")){
System.out.println("线程A进入了test1()");
test2();
}
}
private synchronized void test2() {
System.out.println(Thread.currentThread().getName()+"进入了test2()");
}
@Override
public void run() {
test1();
}
}
public class Test{
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread thread1 = new Thread(mt,"线程A");
thread1.start();
}
}
运行结果:
证明锁的互斥性:
class MyThread implements Runnable{
public synchronized void test1(){
if(Thread.currentThread().getName().equals("线程A")){
while (true){}
}
}
public synchronized void test2(){
if(Thread.currentThread().getName().equals("线程B")){
System.out.println("线程B进入了test2()");
}
}
@Override
public void run() {
test1();
test2();
}
}
public class Test{
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread thread1 = new Thread(mt,"线程A");
Thread thread2 = new Thread(mt,"线程B");
thread1.start();
thread2.start();
}
}
运行结果为死循环,所以证明锁的互斥性.
Synchronized底层原理
对象锁(monitor机制)(重量级锁)
同步代码块:执行同步代码块后要执行monitorenter指令,退出时要执行monitorexit指令,使用内建锁(synchronized)进行同步,关键在于要获取指定锁对象,当线程获取monitor后才能继续往下执行,否则就只能等待,这个过程是互斥的,即同一时刻只有一个线程可以获取到对象monitor.
通常一个monitorenter指令会包含若干条monitorexit指令,原因在于JVM需要确保所有正常执行路径和异常执行路径都可以正常解锁.
同步方法:当使用synchronized标记方法时,编译后的字节码的方法中出现一个ACC_SYNCHRONIZED,该标记表示,进入该方法时,JVM需要进行monitorenter操作,退出该方法时,无论是否正常返回,都需要执行monitorexit操作.
当执行monitorenter时,如果目标锁对象的monitor计数器为0,表示此对象没有被其他线程所占有,此时JVM会将该锁的持有线程置为当前线程,并将monitor+1,如果目标锁的计数器不为0,判断锁对象的持有者是否是当前线程,如果是的话将计数器+1(锁的可重入性),不是则需要等待,直至持有线程释放锁.
当执行monitorexit时,JVM会将锁对象-1,减到0时代表该锁被释放.
CAS
首先来了解一下什么是乐观锁,什么是悲观锁
悲观锁(JDK1,6之前的内建锁):假设每次执行临界区代码时总是会产生冲突,所以当线程成功获取到锁时会阻塞其他尝试获取该锁的线程.
乐观锁():假设线程每次访问共享资源时都不会产生冲突,所以也就不会有线程被阻塞.
CAS(Compare And Swap).
CAS是一种无锁操作,使用CAS(比较交换)来判断是否产生冲突,
CAS(V,O,N):
V - 实际值
O - 旧值
N - 期望值
当V == O时,说明上次修改后该值没有被任何线程在此修改,所以可以直接把N替换到内存中.
当V != O时,说明有线程修改了该值,则不能直接将N替换,返回最新值V
当多个线程使用CAS操作同一个变量时,只有一个线程会成功,并成功更新变量值,其他线程都会失败,失败线程会选择挂起(也就是阻塞)或者自旋重新尝试.
元老级内建锁最主要的问题就是在线程获取锁失败后会直接选择挂起,这种操作是十分消耗资源的,效率很低(阻塞同步)(从运行->阻塞 操作系统需要从用户到内核态转化),而CAS不是武断地挂起,采用的是不断尝试若干次CAS操作,并非进行耗时的挂起与唤醒操作,因此是非阻塞式同步.
当然,CAS也会带来许多问题
- ABA问题
就是线程1将值改为A后,线程2将值改为了B,然后线程3又将值改回了A,当线程1在进行判断时发现值并未改变但是实际上却是发生了改变.
解决方法:使用数据库的乐观锁策略:添加一个版本号:1A-2B-3A,这样根据版本号不同来区分 - 自旋会消耗大量的CPU资源
与线程阻塞相比,自旋会消耗大量的CPU资源,因为此时线程依然处于运行状态,只不过跑的都是无用指令,期望在不断尝试期间锁能被释放出来.
解决办法:自适应自旋,即根据以往自旋能否获取到锁,来动态调整自旋的时间.如果在上次自旋时获取到锁,那么这一次自旋的时间就长一点,如果上次自旋没有获取到锁,那么这次自旋时间就短一点. - 非公平性
处于自旋状态的线程会有更大的几率抢到锁,内建锁无法实现公平性,但是lock体系可以实现.
内建锁的优化
JDK1.6之后还对内建锁做了优化,新增了偏向锁和轻量级锁.
锁状态在对象头的mark word字段中.
无锁 : 001
偏向锁 : 101
轻量级锁 : 00
重量级锁 : 10
1. 偏向锁
偏向锁:最乐观的锁,从始至终只有一个线程请求一把锁
偏向锁的获取:当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录中记录存储偏向锁的ID.以后该线程再次进入同步块时,不需要再次CAS来加锁和解锁,只需要进行简单的测试一下对象头中的mark word字段中的线程ID是不是当前线程ID,如果是,线程直接进入同步代码块,如果不是检查当前偏向锁字段是否为0,若为0,则通过CAS操作将当前偏向锁设置为1,并更新到自己线程中的mark word字段中,如果不是,则说明该锁已经被其他线程所获取,此时线程不断尝试使用CAS获取锁或者升级为轻量级锁(升级锁的概率更高一点)
偏向锁的释放:偏向锁使用一种等待竞争出现才释放锁的机制,当有其他线程尝试获取该锁时,持有偏向锁的线程才会释放偏向锁(因为偏向锁的撤销开销较大,需要等待线程进入全局安全点safepoint(当前线程在CPU上没有执行任何有用字节码))
偏向锁从JDK1.6以后默认开启,但是它在应用程序起动几秒后才激活.
-XX:BiasedLockingStartUpDelay = 0,将延迟关闭,JVM一启动就激活偏向锁
-XX: UseBiasedLocking = false.关闭偏向锁,程序默认进入轻量级锁.
2. 轻量级锁
轻量级锁 : 多个线程在不同时间段请求同一把锁,也就是基本不存在锁竞争,针对此种情况,JVM采用轻量级锁来避免线程的阻塞和唤醒.
加锁:线程在执行同步代码块之前,JVM先在当前线程的栈帧中创建用于存储锁记录的空间.并将对象头的mark word字段直接复制到此空间中,然后线程尝试使用CAS将对象头的mark word替换为指向锁记录的指针(指向当前线程).如果成功表示获取到轻量级锁,如果失败,表示其他线程竞争轻量级锁,当前线程不断不断尝试.
解锁:解锁时,会使用CAS将复制的mark word替换回对象头,如果成功,表示没有竞争发生,正常解锁,如果失败,表示当前锁存在竞争,升级为重量级锁.
3. 重量级锁
上文已经解释,不再赘述.