Java中锁的使用及死锁
1 Java中锁的使用
1.1 synchronized
①修饰一个代码块,被修饰的代码称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
【修饰代码块】
②修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
【修饰方法】
③修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。【修饰静态方法】
④修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。
【修饰类】
1.1.1 sync修饰代码块
一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞
package demo;
public class Sync1 {
public static void main(String[] args) {
System.out.println("使用sync修饰代码块");
SyncThread syncThread = new SyncThread();
Thread threadA = new Thread(syncThread, "threadA");
Thread threadB = new Thread(syncThread, "threadB");
threadA.start();
threadB.start();
}
}
class SyncThread implements Runnable {
private static int count;
public SyncThread(){
count = 0;
}
@Override
public void run() {
synchronized (this){
for(int i = 0; i < 5; i++){
try{
System.out.println("线程名:" + Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (Exception e){
e.printStackTrace();
}
}
}
}
public int getCount(){
return count;
}
}
效果:
在定义接口方法时不能使用synchronized关键字。
构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
1.1.2 sync修饰方法
new Thread()的时候使用同一个对象重写Thread的run
public class Sync2 {
public static void main(String[] args) {
//使用同一个对象syncThread2
SyncThread2 syncThread2 = new SyncThread2();
Thread threadA = new Thread(() -> syncThread2.run(), "threadA");
Thread threadB = new Thread(() -> syncThread2.run(), "threadB");
threadA.start();
threadB.start();
}
}
class SyncThread2 implements Runnable{
static int count = 0;
@Override
public synchronized void run() {
for(int i = 0; i < 5; i++){
System.out.println("线程名:" + Thread.currentThread().getName() + ":" + (count++));
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
synchronized修饰普通方法的时候,锁的是this对象,锁的是当前调用方法的对象
- 因此用同一个对象调用的时候是同步的
- 用两个不同的对象调用,不能保证同步,因为不是同一把锁
1.1.3 sync修饰静态方法
锁的是类的class【Sync3.class】
public class Sync3 {
static int count = 0;
//用sync修饰静态方法
public synchronized static void run() {
try {
for (int i = 0; i < 5; i++) {
System.out.println("线程名:" + Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//使用不同对象访问
Sync3 sync1 = new Sync3();
Sync3 sync2 = new Sync3();
Thread threadA = new Thread(() -> sync1.run(), "threadA");
Thread threadB = new Thread(() -> sync2.run(), "threadB");
threadA.start();
threadB.start();
}
}
1.1.4 sync修饰一个类
锁的是类的class字节码,因此虽然是两个不同的对象调用方法,但是都是同一个类,共用一把锁
public class Sync4 {
int count = 0;
public static void main(String[] args) {
Sync4 sync1 = new Sync4();
Sync4 sync2 = new Sync4();
new Thread(() -> sync1.run(), "threadA").start();
new Thread(() -> sync2.run(), "threadB").start();
}
public void run(){
synchronized (Sync4.class){
System.out.println("sync修饰类:锁住类的class字节码-----");
for(int i = 0; i < 5; i++){
System.out.println("当前线程名:" + Thread.currentThread().getName() + ":" + (count++));
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
1.2 Lock锁
JDK1.5之后新增的Lock接口及相关实现类
相比于synchronized来说,Lock锁更加的灵活,可以控制什么时候获取锁,什么时候释放锁
实现类:ReentrantLock、ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock
因为Lock锁是接口,因此使用的时候要结合它的实现类,finally语句块是保证获取到锁之后,锁能够最终被释放
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally{
lock.unlock();
}
【注意:最好不要把锁的获取过程卸载try语句块中,因为如果在获取锁时发生了异常,异常抛出的同时也会导致锁不容易释放】
1.2.1 Lock锁的基础使用
注意:由于Lock锁过于复杂,此处仅展示基础用法,详细说明不在此叙述
public class LockDemo {
private Lock lock = new ReentrantLock();
static int count = 0;
public static void main(String[] args) {
LockDemo lockDemo = new LockDemo();//因为此处lock锁是类属性,所以为需要用同一个对象
Thread threadA = new Thread(() -> lockDemo.run(),"threadA");
Thread threadB = new Thread(() -> lockDemo.run(), "threadB");
threadA.start();
threadB.start();
}
public void run() {
lock.lock();
try {
System.out.println("Lock锁演示....");
for (int i = 0; i < 5; i++) {
System.out.println("线程名:" + Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(300);
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("==========");
lock.unlock();
}
}
}
其他用法:
- tryLock
- interruptlock
- delay
详细见:https://blog.csdn.net/caideb/article/details/85289790
1.3 Lock锁与synchronized特点及区别
【1】synchronized特点:
- synchronized具有同步功能,是一种互斥锁。synchronized修饰普通方法的时候,锁的是this,是调用方法的对象。修饰静态方法的时候,锁的是字节码文件。
- synchronized可以用来修饰代码块和方法
- synchronized可以保证原子性,有序性,可见性。(volatile不能保证原子)
- synchronized底层由JVM实现,不能手动控制锁的释放,不如lock锁灵活,synchronized修饰的方法一旦出现异常,JVM保证锁会被释放(Lock锁需要在finally中释放)
- synchronized是非公平锁,不保证公平性。
【2】Lock锁特点:
- 尝试非阻塞地获取锁
- 能被中断地获取锁
- 超时获取锁
- 发生异常不会释放锁
【3】区别
- sync是关键字,内置语言实现,Lock是接口
- sync在线程发生异常时会自动释放,不会发生死锁。Lock异常时不会自动释放,需要在finally中手动释放
- Lock是可中断锁,sync是非中断锁,必须等线程执行完成才释放锁
- Lock可以使用读锁来提高多线程读效率
2 死锁问题
2.1 手写一个死锁
思路:
①创建两个字符串a和b
②创建两个线程A和B
③让每个线程都用synchronized锁住字符串(A先锁字符串a,再去锁b;线程B先锁字符串b再锁a)
④如果A锁住a,B锁住b,A就无法锁住b,B也无法再锁住a,这个时候就产生了死锁
public class MyDeadLock {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
//线程A先获取obj1
Thread threadA = new Thread(new Lock1());
//线程B先获取obj2
Thread threadB = new Thread(new Lock2());
threadA.start();
threadB.start();
}
}
class Lock1 implements Runnable {
@Override
public void run() {
try {
System.out.println("Lock1 running...");
while (true) {
//以字符串obj1为锁
synchronized (MyDeadLock.obj1) {
System.out.println("Lock1 lock obj1");
//获取到obj1资源后先让线程A等一会,让Lock2有足够的时间锁住obj2
Thread.sleep(3000);
//线程A尝试获取obj2
synchronized (MyDeadLock.obj2) {
System.out.println("Lock1 lock obj2");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Lock2 implements Runnable {
@Override
public void run() {
try {
System.out.println("Lock2 is running");
while (true) {
//获取obj2
synchronized (MyDeadLock.obj2) {
System.out.println("Lock2 lock obj2");
Thread.sleep(3000);
//尝试获取obj1
synchronized (MyDeadLock.obj1) {
System.out.println("Lock2 lock obj1");
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试死锁:
我们也可以通过Lambda方式实现死锁
:
public class DeadLockDemo2 {
//使用Lambda方式实现
//以DeadLockDemo2.class 和 Object.class分别作为两个互斥资源
public static void main(String[] args) {
new Thread(() -> {
System.out.println("threadA is running----------");
try {
synchronized (DeadLockDemo2.class) {
System.out.println("threadA is get obj1");
Thread.sleep(3000);
synchronized (Object.class) {
System.out.println("threadA is get obj2");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("threadB is running---------");
try {
synchronized (Object.class) {
System.out.println("threadB is get obj2");
Thread.sleep(3000);
synchronized (DeadLockDemo2.class) {
System.out.println("threadB is get obj1");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
最简单方式实现死锁
:
public class DeadLockDemo {
//资源1
private static Object resources1 = new Object();
//资源2
private static Object resources2 =new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (resources1){
System.out.println(Thread.currentThread().getName() + " get resources1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting get resources2");
synchronized (resources2){
System.out.println(Thread.currentThread().getName() + " get resources2");
}
}
}, "thread1").start();
new Thread(()->{
synchronized (resources2){
System.out.println(Thread.currentThread().getName() + " get resources2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting get resources1");
synchronized (resources1){
System.out.println(Thread.currentThread().getName() + " get resources1");
}
}
}, "thread2").start();
}
}
解决死锁之一— —破坏循环等待条件:
public class DeadLockDemo {
//资源1
private static Object resources1 = new Object();
//资源2
private static Object resources2 =new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (resources1){
System.out.println(Thread.currentThread().getName() + " get resources1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting get resources2");
}
//破坏循环等待条件
synchronized (resources2){
System.out.println(Thread.currentThread().getName() + " get resources2");
}
}, "thread1").start();
new Thread(()->{
synchronized (resources2){
System.out.println(Thread.currentThread().getName() + " get resources2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " waiting get resources1");
}
//破坏循环等待,不要嵌套sync
synchronized (resources1){
System.out.println(Thread.currentThread().getName() + " get resources1");
}
}, "thread2").start();
}
}
2.2 查看java进程中是否包含死锁(排查死锁)
①通过在cms窗口输入jsp命令获取到当前正在运行的java进程
这个时候我们怀疑进程号为14648的Java进程存在问题。
②通过jstack+PID(进程号)判断该进程是否存在死锁
在命令行输入:
jstack 14648
可以发现成功发现1个死锁
2.3 死锁产生原因及条件
- 死锁产生原因:
1. 系统资源不足
2. 进程运行推进的顺序不合适
3. 资源分配不当
"如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则可能会因
争夺有限的资源而陷入死锁。"
"其次,如果进程运行推进的顺序与速度不同,也可能产生死锁"
产生死锁的四个必要条件
:
1. 互斥条件:一个资源每次只能被一个进程使用
2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
"只要上述4个条件有一个不满足,就不会发生死锁"
2.4 死锁的解除与预防
既然上述4个条件必须全部满足,那么我们可以通过破坏其中之一来解除死锁
1)破坏"请求和保持"条件
让进程不要那么`贪心`,自己已经有资源了就不要去竞争那些不可抢占的资源。
如:
①让进程一次性申请所有需要用到的资源,不要一次一次申请,当申请的资源没有空时,就让线程等
待,但是这个方法比较浪费资源,线程可能经常处于饥饿状态。
②要求进程在申请资源前,释放自己拥有的所有资源
2)破坏"不可抢占"条件
允许进程进行抢占
①如果线程去抢资源,被拒绝(失败),就释放自己的资源
②操作系统允许抢,只要你优先级大,你就可以抢到
3)破坏"循环等待"条件
将系统中的所有资源进行统一编号,进程可以在任何时刻提出资源申请,但所有申请必须按照资源
的编号顺序(升序)提出
2.5 死锁的监测
-
每个进程、每个资源制定唯一编号
-
设定一张资源分配表,记录各进程与占用资源之间的关系
-
设置一张进程等待表,记录各进程与要申请资源之间的关系
3 拓展(快速幂)
3.1 概念
如果我们要求一个数的n次方,我们会怎么求呢?
有人会说直接循环,每次相乘就行了
这个我们通常的写法:
//求base^pow
public static int power1(int base, int pow) {
int result = 1;
for (int i = 0; i < pow; i++) {
result *= base;
}
return result;
}
这样写的时间复杂度为O(n),如果数字过大,会比较消耗时间,那么有没有什么方法能够降低时间复杂度的呢?这个时候就需要用到快速幂了,它能够将时间复杂度降低到O(logn)
比如:我们要求a^b
那么如何通过快速幂求取正确的值呢?
如果b是偶数:a ^ b = (a ^ 2) ^ (b / 2);
如果b是奇数:a ^ b = a * (a ^ 2) ^ (b / 2);
...一直分解下去,直到最后的幂为0或者1
"由此可以看出之前我们需要乘b次,现在我们逐个分解下去,直到最后幂为0或1"
-- 时间复杂度由原来的O(n)变为了O(logn)
3.2 实现
- 递归版本
public static int quickPower(int base, int pow) {
int result = 1;
while (pow > 0) {
if (pow % 2 == 0) {
//幂为偶数 直接减半 底数(base)平方
pow /= 2;
base *= base;
} else {
//幂为奇数 会多出来一个数 需要记录下来
pow /= 2;
result *= base;
base *= base;
}
}
return result;
}
- 位运算(常数时间优化)
public static int quickPower2(int base, int pow) {
int result = 1;
while (pow > 0) {
if ((pow & 1) == 1) {
//奇数
result *= base;
}
//偶数
pow >>= 1;
base *= base;
}
return result;
}