synchronized
synchronized是Java中的一个关键字,在多线程共同操作共享资源的情况下,可以保证在同一时刻只有一个线程可以对共享资源进行操作,从而实现共享资源的线程安全。
- synchronized是一种可重入、非公平、独占式的悲观锁。synchronized的锁定状态可以分为偏向锁、轻量级锁、重量级锁3种状态。所谓可重入是指当一个线程再次请求自己持有的对象锁的同步方法或代码块时,就属于重入。而非公平是指无法保证等待着的线程获取锁的顺序、悲观就是指每次去取数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁。在JDK1.6中增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁、重量级锁这些优化策略,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。
在并发程序中,线程间共享资源存在线程安全问题,产生线程安全的原因主要有以下两个:
- 多个线程间存在共享资源
- 多个线程共同操作共享资源
synchronized作用范围
- synchronized主要可以作用在方法(实例方法、静态方法)和代码块上。这里主要需要注意的是,如果锁的对象是类的class对象(synchronized修饰的是静态方法或代码块()中为类class对象),尽管通过new关键字可以创建多个实例对象,但这些实例对象仍然属于同一个类,因此这些对象依然会被锁住,即当锁的对象是类的class对象,那么多个对象之间仍然存在线程同步问题,多个线程下它们依然是互斥(一个线程访问、另一个线程等着)地访问该同步方法或代码块。
具体如下图:
- synchronized修改实例方法,以下将通过代码来测试多个线程下访问同一个对象的同步方法、普通方法、非同步方法的区别。
package com.gjy.demo.Thread.Synchronized;
/**
* @Author GJY
* @Date 2021/6/27 14:27
* @Version 1.0
* synchronized修饰实例方法,当线程拿到锁,其他线程无法拿到该对象的锁,那么其他线程就无法访问该对象的其他同步方法
* 但是可以访问该对象的其他非synchronized方法
* 锁住的是类的实例对象
*/
public class synchronizedDemo1 implements Runnable {
//模拟一个共享数据
private static int total=0;
//同步方法,每个线程获取到锁之后,执行5次累加操作
public synchronized void increase(){
for (int i = 1; i < 6; i++) {
System.out.println(Thread.currentThread().getName()+"执行累加操作..."+"第"+i+"次累加");
try {
total=total+1;
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//实例对象的另一个同步方法
public synchronized void declare(){
System.out.println(Thread.currentThread().getName()+"执行total-1");
total--;
System.out.println(Thread.currentThread().getName()+"执行total-1完成");
}
//普通实例方法
public void simpleMethod(){
System.out.println(Thread.currentThread().getName()+ " ----实例对象的普通方法---");
}
@Override
public void run() {
//线程执行体
System.out.println(Thread.currentThread().getName()+"准备执行累加,还没获取到锁");
//执行普通方法
simpleMethod();
//调用同步方法执行累加操作
increase();
//执行完increase同步方法后,会释放掉锁,然后线程1和线程2会再一次进行锁的竞争,谁先竞争得到锁,谁就先执行declare同步方法
System.out.println(Thread.currentThread().getName()+"完成累加操作");
//调用实例对象的另一个同步方法
System.out.println(Thread.currentThread().getName()+"准备执行total-1");
declare();
}
public static void main(String[] args) throws InterruptedException {
synchronizedDemo1 syn = new synchronizedDemo1();
Thread thread1 = new Thread(syn,"线程1");
Thread thread2 = new Thread(syn,"线程2");
thread1.start();
thread2.start();
}
}
- 运行结果
线程1准备执行累加,还没获取到锁
线程2准备执行累加,还没获取到锁
线程2 ----实例对象的普通方法---
线程2执行累加操作...第1次累加 //线程2通过与线程1的竞争率先拿到了锁,进入increase同步方法
线程2执行累加操作...第2次累加
线程1 ----实例对象的普通方法--- //从这里可看出,在线程2访问同步方法时,线程1是可以访问非同步方法的,但是不可以访问另外一个同步方法
线程2执行累加操作...第3次累加
线程2执行累加操作...第4次累加
线程2执行累加操作...第5次累加
线程2完成累加操作 //线程2执行累加后会释放掉锁
线程2准备执行total-1
线程1执行累加操作...第1次累加 //然后线程1拿到锁后进入increase同步方法执行累加
线程1执行累加操作...第2次累加
线程1执行累加操作...第3次累加
线程1执行累加操作...第4次累加
线程1执行累加操作...第5次累加
线程1完成累加操作 //线程1完成累加操作也会释放掉锁,然后线程1和线程2会再进行一次锁竞争
线程1准备执行total-1
线程2执行total-1 //线程2通过竞争率先拿到锁进入declear方法执行total-1操作
线程2执行total-1完成
线程1执行total-1
线程1执行total-1完成
- 通过以上例子可以知道,每次当线程进入synchronized修饰的方法或者synchronized包裹的代码块时都会要求当前线程必须持有对象锁,如果当前有其他对象正在持有该实例对象锁,那么新到的线程就必须等待,直到其他线程执行完同步方法。
synchronized的特性
-
原子性。synchronized可以确保多线程下对共享资源的互斥访问,被synchronized作用的代码可以实现原子性。
-
可见性。synchronized保证对共享资源的修改能够及时被看见。在Java内存模型中,对一个共享变量操作后进行释放锁即进行unlock操作前,必须将修改同步到主内存中。如果对一个共享资源进行加锁即lock操作之前,必须将工作内存中共享变量的值清空(因为每一个线程获取的共享变量都是主存中共享变量的一个副本,如果不进行清空,就会发生数据不一致,即当前线程中的共享变量与主存中的共享变量不一致),*在使用此共享变量时,就需要从主存中重新加载此共享变量以获得该共享变量最新的值。
-
有序性。synchronized可以有效解决重排序问题,即一个unlock解锁操作必定先行发生于后面线程对同一个锁的lock操作,这样就会保证主内存值的共享变量永远是最新的。
synchronized死锁问题
死锁是指两个或更多个线程阻塞着等待获取其他处于死锁状态的线程所持有的锁。例如,线程1持有了锁A,并在尝试获取锁B;而线程2持有锁B,并在尝试获取锁A,那么这两个线程就在相互等待获取已经被对方持有的锁,这种情况下,线程1永远获取不到锁B,线程2永远获取不到锁A,这就是死锁现象。
- 如下例子
//锁A
private static final Object lockA = new Object();
//锁B
private static final Object lockB = new Object();
public static void synchronizedDead(){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
//线程1持有锁A
synchronized (lockA) {
System.out.println("线程1持有锁A");
//睡眠一会,让线程2有机会拿到锁B
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程1尝试获取锁B,如果获取不到线程1就会一直等待,没有执行完毕不释放已获取的锁A
synchronized (lockB) {
System.out.println("线程1尝试获取锁B");
}
}
}
}, "线程1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockB) {
System.out.println("线程2持有锁B");
//睡眠一会,让线程1有机会拿到锁A
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程2尝试获取锁A
synchronized (lockA) {
System.out.println("线程2尝试获取锁A");
}
}
}
}, "线程2");
thread1.start();
thread2.start();
}
public static void main(String[] args) {
synchronizedDeadDemo.synchronizedDead();
}
synchronized底层实现原理
- Java中任何一个对象都有一个与之关联的Monitor(监视器)对象。当一个Monitor对象被持有后,对象就处于锁定状态,其他线程想要获取锁就只能阻塞等待。synchronized就是基于进入和退出Monitor对象来实现方法同步/代码块同步。代码块加锁是在前后分别加上monitorenter和monitorexit指令来实现的,方法加锁通过一个标记位来判断的。在多线程的环境下,线程之间如果需要共享数据,就需要解决互斥访问数据的问题以保证线程安全,Monitor可以确保数据在同一时刻只会有一个线程在访问。与一切皆对象一样,所有的Java对象都有成为Monitor的潜质,因为在Java的设计中,每一个Java对象自创建出来就带有一把看不见的锁,这个锁就是内部锁或者Monitor锁。