目录
一.存在的意义
我们都知道并发编程需要具有:
- 原子性 :原子操作是不可分割的操作,一个原子操作是不会被其他线程打断的。
- 可见性 :当一个线程对共享变量进行了修改,那么其他线程可以立即看到修改后的最新值。
- 有序性 :程序代码在执行过程中的先后顺序 Java编译器会在运行期优化代码的执行顺序,导致了代码的执行顺序未必就是开发者编写代码时的顺序,但是并发编程就需要保证执行的顺序。
synchronized关键字存在的意义就是让我们写出的并发程序具有以上三个特性,总得来说就是防止线程干扰和内存一致性错误,使线程同步。
二.如何使用
有两种使用途径:
1.同步方法
class文件中静态常量池ACC_SYNCHRONIZED,表示当前的方法是同步方法
public synchornized void sync(){
//表示要访问这个成员方法必须获取当前方法所在类的this引用的锁
}
使用实例:
//实现两个线程,线程 A 输出5,4,3,2,1之后线程 B 再次输出5,4,3,2,1
class Mythread implements Runnable {
//方式1:同步方法
public synchronized void test1() throws InterruptedException {
int i = 5;
while (i >= 1) {
System.out.println(Thread.currentThread().getName() + "::" + i--);
TimeUnit.SECONDS.sleep(1);
}
}
@Override
public void run() {
try {
test1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class threadSynTest {
public static void main(String[] args) {
//测试通过synchronized关键字控制线程执行顺序
Runnable runnable = new Mythread();
Thread A = new Thread(runnable, "A");
Thread B = new Thread(runnable, "B");
A.start();
B.start();
}
}
2.同步代码块
public final Object obj = new Object();
public void sync(){
synchornized(obj){
//需要保证独占性的资源
}
}
使用实例:
//实现两个线程,线程A输出5,4,3,2,1之后线程B再次输出5,4,3,2,1
class Mythread implements Runnable {
//方式2:同步代码块
public void test2() throws InterruptedException {
//synchronized(Object object)中要传一个对象
synchronized (Mythread.class) {
int i = 5;
while (i >= 1) {
System.out.println(Thread.currentThread().getName() + "::" + i--);
TimeUnit.SECONDS.sleep(1);
}
}
}
@Override
public void run() {
try {
test2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class threadSynTest {
public static void main(String[] args) {
//测试通过synchronized关键字控制线程执行顺序
Runnable runnable = new Mythread();
Thread A = new Thread(runnable, "A");
Thread B = new Thread(runnable, "B");
A.start();
B.start();
}
}
三.底层实现原理以及锁升级机制
先给大家介绍一下Java对象头中的mark word和monitor对象。
1.markword
mark word中主要记录对象和锁相关的信息,它的组成:
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
上文中说的同步方法实现并发的底层,就是借助markword的结构来实现锁升级机制。
1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作(关于CAS的详情可以看我的另一篇blog:https://blog.csdn.net/weixin_43729854/article/details/107582833),如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。(这个阻塞将会引起系统从用户态切换为内核态<要再唤醒处于阻塞态的线程,就需要从用户态切换到内核态去在操作系统层级获取可以将该线程从阻塞态变为就绪态的权限>,这也是悲观锁的缺点之一)
2.monitor
大家都知道java中一切皆对象,所有java对象都可以称之为天生的monitor,每一个对象自创建来就带有一把锁。
一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
Monitor是线程私有的数据结构,它的组成部分如下:
Owner | 线程的唯一标识 |
EntryQ | 关联一把系统互斥锁 |
Rcthis | 保存阻塞线程的个数 |
Nest | 重入锁的计数 |
HashCode | 对象的hashcode |
Candidate | 0表示没有需要被唤醒的线程 1表示只需要唤醒一个线程来竞争锁 |
上文中说的同步代码块实现并发的底层,就是借助monitor对象来实现:
1) monitorenter:
每一个对象都跟一个monitor相关联,一个monitor的lock在某一个时刻只能够被一个线程锁获得,在一个线程中想要获取monitor的所有权,接下里有如下几件事情发生:
- 如果montior的计数器为0,则意味者当前monitor的lock还没有被获得,某个线程 获得之后对该计数器加一,该线程就是这个monitor的所有者了
- 如果当前这个线程已经获取这把monitor lock,再次获取导致monitor计数器再次+1(也就是说可重入)
- 如果monitor已经被其他线程所拥有,则当前线程会被陷入阻塞状态直到monitor的计数器变为0,再次去尝试获取对monitor的所有权
2) monitorexit:
释放对monitor所有权即对monitor的计数器-1,当计数器结果为0,那就意味着当前线程不再拥有对monitor的使用锁