望各位大佬多多点赞,这里给各位大佬跳个舞祝个兴!!!
目录
synchronized的引出
我们在执行多线程任务时偶尔会需要让两个线程都对同一个变量进行写操作,那么就会出现以下问题
public class demo1 {//多线程
static int count;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
我们预期这里的代码执行完之后count会是100000,但是结果却不尽人意。
我们可以看到这里的结果并没有到达一万,那到底是为什么呢?请看大屏幕!
我们可以看到执行count++不只有一步,而是大体上可以分为3步(当然实际过程更加复杂),加载内存中的count到寄存器,在寄存器中执行加一的操作,然后再将寄存器中的count保存到内存中。有的长得帅的读者可能会有疑问,那这个操作不是没什么问题吗?确实对于单个线程来讲,这步操作没问题,但是对于多线程来说就会出现问题了,这就是线程不安全问题我们来看图片。
(他们之间排列组合的可能性没画完)但是我们可以看到,就只有count++完整执行完之后再进行下一个count++才不会逻辑上少一次增加。所以我们能想到以下代码,改变代码的结构。
public class demo1 {//多线程
static int count;
public static void main(String[] args) throws InterruptedException {
Counter cc=new Counter();
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t1.join();//等待t1执行完再执行t2
t2.start();
t2.join();
System.out.println(count);
}
}
我们可以看到这样count输出结果无论运行多少次都为100000。但是我们发现这样就是t2一整个线程的内容都在t1之后执行。这样就失去了多线程的意义,因为我单线程就能完成这个任务。这时候以我们已有的知识无法解决这个问题了,所以Java引入了synchronized关键字为代码加锁。如果多个代码块使用了synchronized,其用的是同一个锁,那么他们就会构成锁冲突(锁竞争),必须要等待先拿到锁的代码执行完之后另一个带相同锁的代码才会执行。PS:synchronized后面的锁是什么无关紧要,要构成锁冲突只要所相同就行。我们甚至可以用integer类型当锁。接下来我们示范使用一下synchronized。
public class demo1 {//多线程
static int count;
public static void main(String[] args) throws InterruptedException {
Object locker=new Object();
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
synchronized(locker) {//加入锁
count++;
}
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized(locker) {//加入相同的锁产生锁竞争
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
可以看到无论执行多少次我们的结果都是100000。
synchronized大致原理
synchronized原理粗略上来讲就是让代码块中的内容执行的时候另一个碰到这个锁的代码块暂时无法执行产生阻塞(BLOCKED),等到这个代码块执行完闭,另一个代码块的内容才能执行。
我们用图用图来表示:
钥匙只有一把,我必须要把锁开了才能把钥匙给到相同需要这把钥匙的锁。这样就解决了线程不安全的问题。
当然我们synchronized不仅仅可以形容代码块,还可以形容方法,我们熟知的StringBuffer就是使用了synchronized形容append方法,如图:
这一篇代码也就是相当于加锁了append 这个方法的内容,也就相当于以下代码:
public StringBuffer append(String str) {
synchronized(this){
toStringCache = null;
super.append(str);
return this;
}
}
死锁问题的引出:
我们使用synchronized的时候也会出现一些问题。
当我们在锁内嵌套一个相同的锁的时候,我们会发现第二把锁的钥匙还在第一把锁上,但是必须要把第一把锁打开才能得带钥匙,没有打开第二把锁第一把锁就无法打开。这样就进入了无线套娃状态,也就是死锁状态。
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Thread t1=new Thread(()-> {
synchronized (locker1) {
synchronized (locker1) {
System.out.println("线程1执行完成");
}
}
});
}
但是我们发现在Java中运行上述逻辑的代码,上述代码没有出现死锁状态。这是因为Java中的synchronized已经想到了这种情况,在同一个synchronized中嵌套同一个锁并不会出现死锁状态,在内部他会直接把它当做它已拿到锁来执行。
那当我们有多层嵌套synchronized时我们依据什么来得知我们的锁已开呢?
在底层实现方面简单来讲就是使用了计数器,加锁时碰到“{”,锁的count+1,当遇到锁的“}”时count就-1,当count为0时就开锁了。
虽然这个死锁问题Java帮我们解决了,但是不代表其他编译器会帮我们解决,就比如隔壁c++就没有解决,那我们把Java这种就叫做可重入锁,把c++那种叫做不可重入锁。
当然我们不止有这么一种死锁情况,并且Java也不可能帮我们解决所有的情况。接下来让我来讲讲Java没解决的几种死锁问题。其中一种就是锁嵌套获取另一个线程中正在执行的锁,并且这个锁要内嵌套获取前面这把锁,这样就会无限套娃循环,如图。
线程1执行locker2需要key2但是key2在线程2中,locker2要执行完才能给出key2,但是locker2中有locker1,又需要key1,key1又要执行完线程1才能执行,所以产生了死锁。代码如下:
public class demo2 {//加多把锁
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()-> {
synchronized (locker1) {
try {
Thread.sleep(1000);//防止线程1直接把代码先执行完
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("线程1执行完成");
}
}
});
Thread t2=new Thread(()-> {
synchronized(locker2) {
try {
Thread.sleep(1000);//和上述同理
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1) {
System.out.println("线程2执行完成");
}
}
});
t1.setDaemon(true);
t1.start();
t2.start();
}
}
另一种死锁情况就是上述死锁的升级版,n个线程,每个线程嵌套n把锁。
这时候就出现了“哲学家吃饭问题”
分析和解决死锁问题
分析死锁出现的条件,总结了以下四个必要条件
1.互斥使用:一个线程中有一把锁时另一个线程中也想获取这个锁(锁的性质)
2.不可抢占:当线程1拿到这个锁之后线程2只能等待线程1的锁释放之后才能拿过来不能强行抢占(锁的性质)
3.请求保持:线程1在请求其他锁时不释放另一个锁(代码结构)
4.循环等待/环路等待:等待的依赖关系形成环路了(代码结构)
其中互斥使用和不可抢占两个条件是锁的性质,我们不好更改,所以我们可以尝试更改请求保持和循环等待这两个条件。
修改请求保持,我们可以把嵌套代码改成非嵌套的,如以下代码:
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()-> {
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (locker2) {
System.out.println("线程1执行完成");
}
});
Thread t2=new Thread(()-> {
synchronized(locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (locker1) {
System.out.println("线程2执行完成");
}
});
t1.setDaemon(true);
t1.start();
t2.start();
}
可以看到线程没有再死锁了。但是我们很多时候代码中是需要嵌套锁的,所以我们可以更改循环等待这个条件,让锁从小到大嵌套。
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()-> {
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("线程1执行完成");
}
}
});
Thread t2=new Thread(()-> {
synchronized(locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("线程2执行完成");
}
}
});
t1.setDaemon(true);
t1.start();
t2.start();
}
我们可以看到线程也没有 进入死锁,那么我们死锁就完美解决了。
对于n个线程m把锁我们也可以解决了,如图:
制作不易,希望各位大佬能多多点赞收藏关注,球球各位大佬啦!!!!!