Java线程安全:synchronized锁机制详解
目录
一. 线程安全问题的原因
二. 锁
2.1 synchronized 的引入
2.2 synchronized 的特性
2.3 synchronized 的三种使用方法
三. 死锁
3.1 死锁的概念
3.2 死锁的四个必要条件
3.3 如何避免死锁
一. 线程安全问题的原因
- 操作系统对线程的调度是随机的(无法应对)
- 两个线程针对同一变量进行修改
- 修改操作不是原子的
- 内存的可见性
- 指令重排序
二. 锁
Java提供了很多种锁的实现,整体的思路类似于 “互斥” “独占”资源。锁机制本质上是操作系统提供的功能
Java提供了关键字 —— synchronized
2.1 synchronized 的引入
在具体介绍 synchronized 的三种使用方法之前,我们先来看一个出现线程安全问题的例子--设置两个线程t1,t2,两个线程分别对同一个变量 count,进行自增操作5000次
public class Test {public static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i = 0; i < 5000; i++) {count++;}});Thread t2=new Thread(()->{for (int i = 0; i < 5000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("count的值为:"+count);}
}
我们会发现,在多次启用这个代码程序时count的值不尽相同
这段代码出现线程安全问题的核心原因是:多个线程( t1 和 t2 )同时对共享变量 count 执行非原子操作 count++(load add save) ,导致“读 - 改 - 写”步骤交叉,最终数据不一致。
那么我们该怎么样,才能将count++操作变为原子的呢,这时候我们就要用到 synchronized 关键字,进行加锁操作
public class Test {public static int count=0;public static Object locker=new Object();public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{synchronized (locker) {for (int i = 0; i < 5000; i++) {count++;}}});Thread t2=new Thread(()->{synchronized (locker) {for (int i = 0; i < 5000; i++) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count的值为:"+count);}
}
那么是什么原理呢
注意:
- synchronized实际起到的是防止插队的作用(避免上述的三个指令过程中被插队),而不是禁止线程调度(锁竞争失败后,线程并非是被“禁止线程调度”,而是处于特定的等待状态,在该状态下暂时不参与CPU资源的竞争)
- 当t2加锁失败放弃cpu进入阻塞状态时,线程随机调度(除t2线程外的线程都有可能被调度)
- 只有加锁的对象是同一个对象时才会产生锁竞争,不同对象则不会有
- 在加锁状态中,不会影响线程的调度,在中间过程还是会调度其它线程,只不过由于锁被占用,其它线程若也是加锁操作,则会加锁失败产生阻塞等待在(没有锁释放的情况下, 线程调度器不会调度它去执行)
2.2 synchronized 的特性
1. 互斥:
synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执到到同一个对象synchronized就会阻塞等待.
• 进⼊synchronized修饰的代码块,相当于加锁
• 退出synchronized修饰的代码块,相当于解锁
2. 可重入
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的情况。
public class Test {public static int count=0;public static void main(String[] args) throws InterruptedException {Test locker=new Test();Thread t1=new Thread(()->{synchronized (locker){synchronized (locker){for (int i = 0; i < 1000; i++) {count++;}}}});t1.start();t1.join();System.out.println("count的值为:"+count);}
}
总之:如果第一次加锁成功后,再进行二次加锁时,synchronized内部会判定第二次加锁的线程是否和第一次加锁是同一个线程,若是则相当于“直接跳过”,反之则产生阻塞(可重入锁的核心特点就是:同一线程可以多次获取同一把锁,而不会因为已经持有这把锁而被阻塞。)
补:一个线程可以同时拥有不同对象的锁
2.3 synchronized 的三种使用方法
- 修饰代码块
Thread t1=new Thread(()->{synchronized (locker) {for (int i = 0; i < 5000; i++) {count++;}}});
synchronized(Object obj){ } 中加锁的对象(obj)是谁,对加锁中的内容是没有影响的,只要是对同一个对象加锁即可
一个类中的代码块还可以针对this加锁:(表示对当前实例对象加锁)
public class test {public int count=0;public void func(){synchronized(this){for (int i = 0; i < 10000; i++) {count++;}}}
}
注意:this只能在实例方法,构造方法或实例初始化在使用,不能在静态方法中使用
- 修饰一个实例方法
public class test {public int count=0;public synchronized void func(){for (int i = 0; i < 10000; i++) {count++;}}public static void main(String[] args) throws InterruptedException {test obj=new test();Thread t1=new Thread(()->{obj.func();});Thread t2=new Thread(()->{obj.func();});t1.start();;t2.start();t1.join();t2.join();System.out.println("count的值为:"+obj.count);}
}
- 修饰一个静态方法
public class test {public static int count=0;public synchronized static void func(){for (int i = 0; i < 10000; i++) {count++;}}public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{test.func();});Thread t2=new Thread(()->{test.func();});t1.start();;t2.start();t1.join();t2.join();System.out.println("count的值为:"+count);}}
synchronized除了直接修饰静态方法,还可以在静态方法中修饰代码块,并用类名.class的方式获得类对象(不管一个类实例化多少个对象,类对象只有一个的)
public static void func(){synchronized(test.class){for (int i = 0; i < 10000; i++) {count++;}}}
总之:无论那种写法,synchronized()针对什么对象加锁不重要,最重要的是,两个线程是否针对一个对象加锁
三. 死锁
3.1 死锁的概念
死锁指多个进程(或线程)在执行过程中,因争夺资源 ,导致各进程(或线程)都在等待其他进程(或线程)释放已占用的资源,从而相互等待,无法继续推进的一种僵持局面。比如,线程A持有资源1,等待资源2;线程B持有资源2,等待资源1,双方都不释放已持有的资源,就形成了死锁。
3.2 死锁的四个必要条件
- 锁是互斥的(锁的基本特点)
- 锁不可被抢占
- 请求和保持
- 循环等待/环路等待
任意打破一点即可避免死锁
3.3 如何避免死锁
打破请求和保持----> 代码中避免出现锁的”嵌套“
打破循环等待----> 约定加锁的顺序