[Java高并发系列]Java 中 synchronized 关键字详解 + 死锁实例
1 概述
synchronized用于给某个对象加锁 , 其修饰的对象可以是代码块和方法(分为实例方法和静态方法).
不是说synchronized是对代码块或方法上锁, 它锁的是对象(当然也可能是类, 不过类也算对象吧hhh — 某个Object , 或者this, 或者 xxx.class)
下面来一一分析.
2 给某个对象加锁
2.1 对一个Object加锁
当某个线程中对这个object加上锁后 , 其他线程想再拿, 就得等到代码块结束后释放出这个对象, 然后再锁上该对象, 执行自己的块
e.g.1
/**
* 有一个类T , 里面一个m方法对其私有变量count 进行减减操作
*
*synchronized 关键字
* 对某个对象加锁
* @author lowfree
*/
public class T {
private int count = 10 ;
Object o = new Object();
public void m(){
synchronized (o){ // 锁定的是整个对象
count --;
System.out.println(Thread.currentThread().getName() + "count " + count);
}
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() );
T t =new T(); //new 一个 T 对象, 对其count进行 -- 操作
for(int i = 0 ; i < 10 ; i ++) { //一个线程来减太慢了 ,创建10 个线程大家一起做这个事情
new Thread(new Runnable() {
@Override
public void run() {
t.m(); //显然 , 如果m不用synchronized包起来,则会出现多线程访问临界资源问题
}
}, "T_thread : ").start();
}
}
}
上述例子中, 一个线程中将对象实例o 拿到时, 其他线程想进入锁了o的代码块时, 得等到这个线程将这个代码块执行完成,释放了o对象才行.
上述第一个线程锁上o, 进行count-- 操作, 打印消息到屏幕这两个操作之间不会有其他线程进行争夺.
下面我们抽理出来在看看:
Object o = new Object();
public void m(){
synchronized (o){
// 操作1
}
//操作2
}
实际上synchronized关键字使得能够对o上锁, m方法中的操作1变得不可分割.
2.2 对this 对象上锁
e.g.2
/* 相类似, */
public void m1(){
synchronized (this) { //任何线程要执行下面的代码, 必须先拿到this的锁
count -- ;
System.out.println(Thread.currentThread().getName() + "count =" + count);
}
}
说白了, 把自己当成一个锁的对象, 只有这个线程拿到调用m方法的对象时, 才能进入代码块.
进一步, 实际上如果写成上面那样, 是可以简写的
e.g.3
public synchronized void m2(){ //相当于在方法的代码执行时要synchronized(this)
count -- ;
System.out.println(Thread.currentThread().getName() + "count =" + count);
}
所以, 对一个方法使用synchronized 关键字, 就是执行该方法时要拿到this对象的锁.
2.3 对class对象上锁
那么要是这个方法是静态方法呢? 看下面这个例子:
e.g.4
public class T {
private static int count = 10 ;
public synchronized static void m(){ //这里相当于synchronized(包名.T.class)
count --;
System.out.println(Thread.currentThread().getName() + "count =" + count);
}
public static void mm(){
synchronized (T.class){ //考虑一下这里写synchronize(this) 是否可以?
// 不行,静态属性方法是不需要new 出对象来访问的, 因此没有 this 引用得存在
//所以锁定静态方法的时候, 相当于锁定当前类的class对象
count -- ;
}
}
}
2.4 小结
有些文章喜欢把2.2 中 e.g.3 叫为方法锁 , 把2.3 中称为类锁, 但实际上都是对对象上锁.
3 sychronized的特点
-
保证原子性 , 可见性和有序性.
可见性 : 释放锁时, 所有写入都会写回内存
获得锁后, 都会从内存读取最新数据
-
可重入性
对同个线程在获得锁后, 在调用其他需同样锁的代码可直接调用 考虑一个
原理
a) 记录锁的持有线程 & 持有数量
b) 调用synchronized 代码 时检查对象是否已经被锁
是 ==> 检查是否被当前线程锁定 & 若是 则计数加 1
不是 ==> 加入等待队列
c) 释放时计数减 1 直到0 => 则释放锁
下面是两个可重入的例子
/** * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁. * 也就是说synchronized获得的锁是可重入的 * 重入锁 */ import java.util.concurrent.TimeUnit; public class T { synchronized void m1() { System.out.println("m1 start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } m2(); } synchronized void m2() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m2"); } }
/** * 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁. * 也就是说synchronized获得的锁是可重入的 * 这里是继承中有可能发生的情形,子类调用父类的同步方法 */ package base10; import java.util.concurrent.TimeUnit; public class T { synchronized void m(){ System.out.println("m start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m end"); } public static void main(String[] args) { new TT().m(); } } class TT extends T{ @Override synchronized void m() { System.out.println("child m start"); super.m(); System.out.println("child m start"); } }
-
重量级
底层通过一个监视器对象(monitor)完成 wait() , notify() 等方法也依赖于monitor 对象
监视器锁(monitor) 的本质依赖于底层操作系统的互斥锁(Mutex lock) 实现
上述切换过程较长 所有synchronized效率低 & 重量级
ps : 相关概念
另外: 程序在执行过程中,如果出现异常,默认情况锁 会被释放. 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据. 因此要非常小心的处理同步业务逻辑中的异常.
import java.util.concurrent.TimeUnit;
/**
* @author lowfree
*/
public class T {
int count = 0;
synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start");
while(true) {
count ++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count % 5 == 0) {
int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
System.out.println(i);
}
}
}
public static void main(String[] args) {
T t = new T();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
4 一个用sychronized 实现死锁的例子
我们知道死锁指的是两个即以上进程/线程相互占有对方需要的资源, 且等待对方释放的一种阻塞状态
(具体来说由于系统资源竞争, 程序推进顺序非法, 死锁产生的四个必要条件: 互斥, 不可剥夺, 请求保持, 循环等待 产生的)
基于如此, 我们可以写一个包含两个线程t1,t2 ; 还有两个锁对象o1 , o2 的死锁例子
/**
* 死锁
* @author lowfree
*/
public class DeadLock {
Object oa = new Object(); //lock 1
Object ob = new Object(); //lock 2
public void m1(){ //m1 占据oa对象锁
synchronized (oa) {
System.out.println("m1 start ...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ob) { //中途又想锁住 ob
System.out.println("gotten ob ...");
}
System.out.println("m1 end ...");
}
}
public void m2(){ //m2 先占据ob对象锁
synchronized (ob) {
System.out.println("m2 start ...");
synchronized (oa) { //中途又想 ma
System.out.println("gotten oa ...");
}
System.out.println("m2 end ...");
}
}
public static void main(String[] args) {
DeadLock deadLock =new DeadLock();
new Thread(new Runnable() { //创建线程1并启动
@Override
public void run() {
deadLock.m1();
}
},"thread 1").start();
new Thread(new Runnable() { //创建线程2并启动
@Override
public void run() {
deadLock.m2();
}
},"thread 2").start();
}
}
下面来解释下上述程序:
- 实例化了一个DeadLock对象, 其中创建了oa和ob对象,用作锁对象
- 创建线程 thread 1, 启动.
- thread 1 中deadLock调用m1方法, 占据oa锁资源, 然后线程休息2s.
- 这时主线程继续执行, 创建thread2 启动.
- thread 2 中deadLock调用m2方法. 占据ob锁资源.
- 紧接着thread2 中deadLock继续想占据 oa , 但是oa被thread 1 占据, 等待其释放吧.
- 这时候可能thread1 的2s sleep完成, 回到thread1中 ,开始想占据 ob.
这时候, thread1 占据oa 想要占据ob 继续执行, thread2 占据ob 想要占据oa继续执行 , 即成了死锁.
参考博客: https://blog.csdn.net/carson_ho/article/details/82992269