Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免 || 单例模式”懒汉式“的线程同步安全问题
每博一文案
常言道:“不经一事,不懂一人”。
一个人值不值得交往,在关键时候才能看得清。看过这样的一个故事:晚清历史上,红顶商人胡雪岩家喻户晓。
有一名商人在生意中惨败,需要大笔资金周转。为了救急,他主动上门,开出低价想让胡雪岩收购自己的产业。
胡雪岩给出正常的市场价,来收购对方的产业。手下们不解地问胡雪岩,为啥送上门的肥肉都不吃。
胡雪岩说:“你肯为别人打伞,别人才原意为你打伞。”
那个商人的产业可能是几辈子人积攒下来的,我要是以他开出来的价格来买,当然很占便宜,但人家可能就
一辈子也翻不了身。这不是单纯的投资,而是救了一家人,既交了朋友,又对得起良心。
谁都有雨天没伞的时候,能帮人遮点雨就遮点吧。落叶才知秋,落难才知友。
做人真正的成功,不是看你认识哪些人,而是看你落魄时,还有哪些人原意认识你。
身处低谷之时,才知道谁假,经历重重的苦难,才真正看透人心。
相信时间,相信它最终告诉你,谁是虚伪的脸,谁是真心的伴。
余生,把心情留给懂你的人,把感情留给爱你的人,别交,交不透的人,别府不值得付的心。
—————— 一禅心灵庙语
文章目录
- Java多线程:多线程同步安全问题的 “三“ 种处理方式 ||多线程 ”死锁“ 的避免 || 单例模式”懒汉式“的线程同步安全问题
- 每博一文案
- 1. 多线程同步安全的”三“ 种处理方式
- 1.1 多线程同步的安全问题
- 1.2 synchronized 关键字的介绍
- 1.3 解决多线程同步安全问题方式一: synchronized () { } 代码块
- 1.3.1 java 三大变量在作为 同步监视器 ”锁“ 的线程安全问题的讨论
- 1.4 解决多线程同步安全问题方式二:synchronized( ) 方法
- 1.4.1 synchronized ( ) 非静态方法的 ”锁“
- 1.4.2 synchronized () 静态方法的 ”锁“
- 1.4.3 synchronized() 代码块的方式 与 synchronized()方法的解决线程安全问题的异同
- 1.5 解决多线程同步安全问题的方式三:lock. 的使用
- 1.5.1 synchronized 与 Lock 的对比
- 1.6 如何避免多线程安全问题:
- 1.7 开发中如何处理线程安全问题及其注意事项
- 2. 多线程的 ”死锁“ 现象
- 2.1 "死锁" 介绍
- 2.2 释放锁的操作
- 2.3 不会释放锁的操作
- 2.4 如何避免 ”死锁“ 的出现
- 3. 单例模式 ”懒汉式“ 的线程安全问题
- 4. 关于 ”锁“ 的面试题:
- 4.1 题目一
- 4.2 题目二
- 4.3 题目三
- 4. 总结:
- 5. 最后:
1. 多线程同步安全的”三“ 种处理方式
1.1 多线程同步的安全问题
所谓的多线程安全问题:
- 多个线程执行的不确定性引起执行结果的不稳定。
- 多个线程对进程中的共享数据,操作对数据的修改不同步,造成数据的损坏。
举例如下:
模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票。
package blogs.blog4;
/**
* 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票
*/
public class ThreadTest6 {
public static void main(String[] args) {
// 创建窗口对象
Window window = new Window();
Thread t1 = new Thread(window); // 售票窗口一
Thread t2 = new Thread(window); // 售票窗口二
Thread t3 = new Thread(window); // 售票窗口三
t1.setName("售票窗口一:");
t2.setName("售票窗口二:");
t3.setName("售票窗口三:");
t1.start();
t2.start();
t3.start();
}
}
/**
* 火车窗口
*/
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
// 有票,便出售
if (this.ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);
this.ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
}
}
}
我们可以附加上一个 sleep() 线程睡眠(进入阻塞状态) 的情况,提高出现线程安全问题的概率。如下:
package blogs.blog4;
/**
* 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票
*/
public class ThreadTest6 {
public static void main(String[] args) {
// 创建窗口对象
Window window = new Window();
Thread t1 = new Thread(window); // 售票窗口一
Thread t2 = new Thread(window); // 售票窗口二
Thread t3 = new Thread(window); // 售票窗口三
t1.setName("售票窗口一:");
t2.setName("售票窗口二:");
t3.setName("售票窗口三:");
t1.start();
t2.start();
t3.start();
}
}
/**
* 火车窗口
*/
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
// 有票,便出售
if (this.ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
this.ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
}
}
}
上述代码出现线程安全问题的原因分析:
- 三个售票窗口,三个线程(售票窗口1,2,3线程)。售票窗口1线程进入到打印票号 (ticket = 100) 时,并没有将
ticket票号--
语句执行给执行完,(该线程睡眠了 sleep(0.001s)方法)出于网络原因停止了一小下,售票窗口2线程就进入到了打印 ticket 票号,这时侯的票号,因为上一个售票窗口1线程并没有将ticket票号--
语句执行就睡眠了 sleep(),所以这时候的 ticket 票号还是和售票窗口1线程的票号是一样的 100 的,这时候售票窗口2线程也睡眠了,也没有执行到(ticket票号--
语句),售票窗口3线程进来了,这时候的 ticket 票号可能还是 100,因为可能这时候的售票窗口1线程并没有醒来(也就还没有执行,ticket票号--
语句)。这样的结果就是:售票窗口1,售票窗口2,售票窗口3 都出售了 同一张 100 的票号的火车票,导致的结果就是 有三个人买到了 同一张一模一样的火车票,如果始发站都一样的话:那可怕的就是:三个人座同一张座位,发生争执。 - 代码图示解析:
- 实例图解
1.2 synchronized 关键字的介绍
多线程出现安全问题的原因:
当多个线程在操作同一个进程共享的数据的时候,一个线程对共享数据的执行仅仅只执行了一部分,还没有执行完,另一个线程参与进来执行。去操作所有线程共享的数据,导致共享数据的错误。
就相当于生活当中:你上厕所,你上到一半还没有上完,另外一个人,就来占用你这个茅坑上厕所。
解决办法
对于多线程操作共享数据时,只能有一个线程操作,其他线程不可以操作共享数据的内容,只有当一个线程对共享数据操作完了,其他线程才可以操作共享数据。就相等于对于共享数据附加上一把锁,只有拿到了这把锁的钥匙的线程才可以操作共享数据的内容,而锁只有一把,只有当某个线程操作完了,将手中的锁钥匙释放了,其他线程才可以拿到该锁钥匙,操作共享数据。就是拿到锁钥匙的线程睡眠了,阻塞了,其他线程也必须等到该线程将手中的锁钥匙释放了,其他线程才可以拿到锁钥匙,操作共享数据。
就相当于生活当中:你上厕所,就把厕所门给锁了,其他想上厕所的人进不来,就算你在厕所中睡着了,没有打开厕所门的锁,其他人也是进不去厕所的,只有当你将厕所门的锁打开了,其他人才能进去上厕所。
同理我们Java当中使用 synchronized
关键字附加上锁 。
synchronized几种写法
- 修饰代码块
- 修饰普通方法
- 修饰静态方法
1.3 解决多线程同步安全问题方式一: synchronized () { } 代码块
synchronized 修饰代码块的使用方式
synchronized (同步监视器也称"锁") {
// 这里放可以加锁的逻辑:其实就是操作共享数据的内容:修改共享数据方面的内容
}
同步监视器 : 所谓的同步监视器,也称为 “锁”。任何一个对象都可以充当一个锁,成为锁对象,也称为同步锁 。比如 Object ,String ,自定义对象都可以充当锁,但是要实现达到解决对应的线程安全问题,就需要根据实际情况,设置锁的对象了。但是 同步监视器“锁”不可以为 null 不然报 NullPointerException
null 指针异常的。
synchronized()
后面的小括号中的这个 “锁”, 设置锁对象是 关键 ,这个锁 必须是多线程共享的 对象,才能到达多线程排队拿锁钥匙,解决多线程安全问题的效果。这样的效果的锁,被称为 同步锁 。
比如:synchronized()
放什么对象,那要看你想让哪些线程同步,假设 t1,t2,t3,t4,t5 有5个线程,你只想让 t1,t2,t3线程访问共享数据时的线程安全问题,排队获取同步锁进行。t4,t5 不解决,不需要排队,怎么办设置 ”锁“: 你设置的同步锁对象,就需要是 t1,t2,t3线程共享的对象了,而这个对象对于 t4,t5 来说是不共享的。这样就达到了,t1,t2,t3线程排队获取同步锁,执行操作共享数据,而t4,t5 不用排队获取同步锁,可以多线程并发操作共享数据。
需要注意一点就是:这个同步锁的对象一定要选好了,这个”锁“一定是你需要排队获取”锁“后执行操作共享数据的线程对象所共享的,多加注意定义的”锁“对象的作用域 。
- 在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。
- 当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。
例如,synchronized(obj)就获取了“obj这个对象”的同步锁。 - 不同线程对同步锁的访问是互斥的。
也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。
例如,现在有两个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。
举例:解决上述买火车票的多线程安全问题: 设置不同的 同步监视器 ”锁“,达到的效果也是不一样的。有的可以解决多线程安全问题,有的不能,一起来看看吧。
设置 同步监视器 ”锁“的对象为 Object object = new Object();
的成员变量。
package blogs.blog4;
/**
* 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票
*/
public class ThreadTest6 {
public static void main(String[] args) {
// 创建窗口对象
Window window = new Window();
Thread t1 = new Thread(window); // 售票窗口一
Thread t2 = new Thread(window); // 售票窗口二
Thread t3 = new Thread(window); // 售票窗口三
t1.setName("售票窗口一:");
t2.setName("售票窗口二:");
t3.setName("售票窗口三:");
t1.start();
t2.start();
t3.start();
}
}
/**
* 火车窗口
*/
class Window implements Runnable {
private int ticket = 100;
Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (object) { // object 是三个线程共享的
// 有票,便出售
if (this.ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
this.ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
}
}
}
}
从结果上看是可以解决多线程安全问题的:因为我们这里使用的是 implements Runnable 接口的方式创建的线程对象,其中所传的对象都是 window 地址,其中的 object 的对象是 三个 售票线程共享的对象的一把 ”锁“,售票1,2,3线程需要排队获取到 锁,才能操作共享数据的内容,如下图示:
将同步监视器 ”锁“ 设置为: Object object = new Object();
中的 run()方法当中,作为局部变量,这样导致的结果就是:售票1,2,3线程共用的不是同一把 ”锁“了,因为局部变量,是存在于栈当中的(出了run()方法的作用域就销毁了,再次进入run()方法就会重写建立新的一个局部变量),栈每个线程各自都一份,线程之间不共享。售票1,2,3线程各个都有一把自己独有的锁,不共享,不需要等待别人手中的锁了,自己就有,不需要排队执行了,多线程安全问题就出现了。
package blogs.blog4;
/**
* 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票
*/
public class ThreadTest6 {
public static void main(String[] args) {
// 创建窗口对象
Window window = new Window();
Thread t1 = new Thread(window); // 售票窗口一
Thread t2 = new Thread(window); // 售票窗口二
Thread t3 = new Thread(window); // 售票窗口三
t1.setName("售票窗口一:");
t2.setName("售票窗口二:");
t3.setName("售票窗口三:");
t1.start();
t2.start();
t3.start();
}
}
/**
* 火车窗口
*/
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
Object object = new Object(); // 局部变量,售票1,2,3线程不共享,各个都有,不需要等待别人手中的锁了
while (true) {
synchronized (object) { // object局部变量,所有线程都可以进入了。不需要等待对方的锁了。线程安全问题。
// 有票,便出售
if (this.ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
this.ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
}
}
}
}
将同步监视器 ”锁“ 设置为 ”this“ 当前对象的引用。 因为我们这里使用的是 implements Runnable 接口的方式创建的线程对象,其中所传的对象都是 window 地址,其中的 object 的对象是 三个 售票线程共享的对象的一把 ”锁“,售票1,2,3线程需要排队获取到 锁,才能操作共享数据的内容,如下图示:和 我们将 锁设置为 Object object = new Object(); 成员变量是一样的。
package blogs.blog4;
/**
* 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票
*/
public class ThreadTest6 {
public static void main(String[] args) {
// 创建窗口对象
Window window = new Window();
Thread t1 = new Thread(window); // 售票窗口一
Thread t2 = new Thread(window); // 售票窗口二
Thread t3 = new Thread(window); // 售票窗口三
t1.setName("售票窗口一:");
t2.setName("售票窗口二:");
t3.setName("售票窗口三:");
t1.start();
t2.start();
t3.start();
}
}
/**
* 火车窗口
*/
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) { // this 当前对象:是三个线程共享了。一
// 有票,便出售
if (this.ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
this.ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
}
}
}
}
将同步监视器”锁“ 设置为 ”类对象“, 类名.class(这里的类对象为:Window.class
) / 字符串(这里我们设置为”abc“
)。都是可以解决多线程安全问题的,因为:类对象 是存放在方法区当中的,而且类仅仅只会加载一次到内存当中,所有对象,线程共用,而字符串在 JVM 中的字符串池中存在的,同样也是仅仅只会生成一个唯一的字符串对象,所有对象共用,线程共用。
package blogs.blog4;
/**
* 模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票
*/
public class ThreadTest6 {
public static void main(String[] args) {
// 创建窗口对象
Window window = new Window();
Thread t1 = new Thread(window); // 售票窗口一
Thread t2 = new Thread(window); // 售票窗口二
Thread t3 = new Thread(window); // 售票窗口三
t1.setName("售票窗口一:");
t2.setName("售票窗口二:");
t3.setName("售票窗口三:");
t1.start();
t2.start();
t3.start();
}
}
/**
* 火车窗口
*/
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized ("abc") { // 字符串池的存在:所有对象/线程共享
// 有票,便出售
if (this.ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
this.ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
}
}
}
}
同步监视器"锁" 不可以为 null ,编译器会报错,就算骗过了编译器,在运行的时候也是会报错的:NullPointerException
null 异常。
补充:
1.在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器"锁",因为我们使用的都是同一个Runnable对象创建的 Thread对象,
2.如果是 extends Thread 的方式创建多线程,我们可以考虑使用 “类.class " 的方式充当同步监视器"锁”,因为类仅仅只会加载一次,但是这种继承方式慎用 this充当"锁"同名监视器
1.3.1 java 三大变量在作为 同步监视器 ”锁“ 的线程安全问题的讨论
Java当中有 三大变量
- 局部变量: 存放在栈中
- 成员变量: 存放在堆中
- 静态变量: 存放在方法区中
对于着三种变量充当同步监视器 ”锁“ 存在的线程安全问题。
一个进程一个堆和一个方法区,一个进程包含多个线程,一个线程一个栈。
所以对于同一个进程中的堆和方法区中的数据,对于所有的线程来说都是共享的。
以上三大变量中:局部变量 是永远不存在线程安全问题。因为局部变量存在栈中(一个线程一个栈),是每个线程各自独立拥有的,不使用特殊方式的话,是无法共享的。
实例变量在堆中,堆只有一个,静态变量在方法区中,方法区只有一个,一个进程一个堆一个方法区,所有多线程共享的,所有有可能存在线程的安全问题。
1.4 解决多线程同步安全问题方式二:synchronized( ) 方法
同样的 synchronized
可以修饰代码块,也是可以修饰方法的。
修饰方法用两种用法:1. 修饰非静态方法,2. 修饰静态方法。这两者之间是又差异的。
1.4.1 synchronized ( ) 非静态方法的 ”锁“
synchronized
还可以放在方法声明中,表示整个方法 同步方法 ,这里修饰非静态方法。
private synchronized void sell() {
// 这里放可以加锁的逻辑:其实就是操作共享数据的内容:修改共享数据方面的内容
}
使用 extends Thread 的方式创建多线程,同样是:模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票。
这里我们使用 synchronized 修饰非静态方法的方式处理就不行了。
synchronized 修饰非静态方法时,默认的同步监视器 ”锁“是this
当前对象的引用,不可以修改的。
package blogs.blog4;
/**
* synchronized 修饰方法
*/
public class ThreadTest7 {
public static void main(String[] args) {
Thread t1 = new MyThread7(); // 售票窗口1
Thread t2 = new MyThread7(); // 售票窗口2
Thread t3 = new MyThread7(); // 售票窗口3
// 设置线程名
t1.setName("售票窗口: ");
t2.setName("售票窗口2: ");
t3.setName("售票窗口3: ");
// 创建线程
t1.start();
t2.start();
t3.start();
}
}
/**
* 售票
*/
class MyThread7 extends Thread {
// 设置为 static 的成员变量,不然会出现每个售票窗口都有 100 张火车票的情况了
private static int ticket = 100; // static 静态的和类一起加载,仅仅只会加载一次,所有对象共享。
@Override
public void run() {
this.sell();
}
private synchronized void sell() { // synchronized 修饰方法: 同步方法。
while (true) {
// 有票,便出售
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
}
}
}
为什么这里使用 synchronized 修饰非静态方法无法解决 线程安全问题 ???
是因为 synchronized 修饰非静态方法 的同步监视器 ”锁“ 是 this
这是默认写死了的,是无法修改的。这里我们使用的是 extends Thread 的方式创建的多线程,
其中的 run() 方法是在(栈区中)不是共享的对象,所以 this 也就不是共享的对象了,也就不是三个 售票1,2,3线程共享的”锁“了,自然无法实现排队获取 ”锁“,也就无法处理线程安全问题了。
解决 : 将 synchronized 修饰的方法改为 static 静态方法。
1.4.2 synchronized () 静态方法的 ”锁“
synchronized
还可以放在方法声明中,表示整个方法 同步方法 ,这里修饰 静态方法。
private synchronized static void sell() {
// 这里放可以加锁的逻辑:其实就是操作共享数据的内容:修改共享数据方面的内容
}
synchronized 修饰静态方法时,默认的同步监视器 ”锁“是类名.class
也就是类对象,类是存储在 方法区当中的,仅仅只能加载一次到内存当中,所有对象,线程共用,无法修改。
这里使用 synchronized 修饰静态方法 就可以简单的解决 上述 extends Thread 创建多线程的火车售票问题了。
如下:
package blogs.blog4;
/**
* synchronized 修饰方法
*/
public class ThreadTest7 {
public static void main(String[] args) {
Thread t1 = new MyThread7(); // 售票窗口1
Thread t2 = new MyThread7(); // 售票窗口2
Thread t3 = new MyThread7(); // 售票窗口3
// 设置线程名
t1.setName("售票窗口1: ");
t2.setName("售票窗口2: ");
t3.setName("售票窗口3: ");
// 创建线程
t1.start();
t2.start();
t3.start();
}
}
/**
* 售票
*/
class MyThread7 extends Thread {
// 设置为 static 的成员变量,不然会出现每个售票窗口都有 100 张火车票的情况了
private static int ticket = 100; // static 静态的和类一起加载,仅仅只会加载一次,所有对象共享。
@Override
public void run() {
this.sell();
}
private synchronized static void sell() { // synchronized 修饰方法: 同步方法。
while (true) {
// 有票,便出售
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
}
}
}
从结果上我们可以看到,这里执行所有的票,都是被 售票3线程给出售了,其他售票线程根本就没有机会,是因为:
这里我们是 run() 方法调用被 synchronized 修饰的静态方法,使用的是 类.class 这个类对象锁,所有对象共用,导致了,只要是
一个售票线程拿到 类锁(所有线程共享共用),进入了 sell()方法,其他线程必须等待其释放类锁才有机会进入到 sell() 方法中,但是其中的 sell()方法中有一个while(true) 循环,当该线程执行完 sell()方法,其票也已经出售完了。所以就出现了一个线程将所有票都出售完了。
@Override
public void run() {
this.sell();
}
private synchronized static void sell() { // synchronized 修饰方法: 同步方法。
while (true) {
// 有票,便出售
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
}
}
我们可以修改一下,将 while()循环方法 run() 方法中,不要放到 sell()方法就可以了,如下
package blogs.blog4;
/**
* synchronized 修饰方法
*/
public class ThreadTest7 {
public static void main(String[] args) {
Thread t1 = new MyThread7(); // 售票窗口1
Thread t2 = new MyThread7(); // 售票窗口2
Thread t3 = new MyThread7(); // 售票窗口3
// 设置线程名
t1.setName("售票窗口1: ");
t2.setName("售票窗口2: ");
t3.setName("售票窗口3: ");
// 创建线程
t1.start();
t2.start();
t3.start();
}
}
/**
* 售票
*/
class MyThread7 extends Thread {
// 设置为 static 的成员变量,不然会出现每个售票窗口都有 100 张火车票的情况了
private static int ticket = 100; // static 静态的和类一起加载,仅仅只会加载一次,所有对象共享。
@Override
public void run() {
while (true) {
this.sell();
}
}
private synchronized static void sell() { // synchronized 修饰方法: 同步方法。
// 有票,便出售
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--; // 售票成功,减减
} else {
return ;
}
}
}
1.4.3 synchronized() 代码块的方式 与 synchronized()方法的解决线程安全问题的异同
- synchronized 无论是修饰 代码块,还是方法都有 同步监视器 ”锁“的机制存在。
- 不同的是 synchronized 修饰代码块,可以灵活的设定同步监视器 ”锁“ 的对象,而 synchronized 修饰方法却不可以了,synchronized 修饰非静态方法,默认同步监视器”锁“ 是
this
,修饰静态方法 static 默认的同步监视器”锁“ 是类.class
类对象,类锁这些都是固定的无法修改,比较死板。 - synchronized 修饰方法处理多线程比较方便,简单,直接在方法中加 synchronized 就可以了。
- synchronized 修饰代码块的效率 比 synchronized 修饰方法的效率更快,因为:synchronized 出现在方法上,表示整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率降低(多线程转为单线程处理同步安全问题的逻辑事务更多了)。
- 一般建议优先使用 synchronized 修饰代码块的方式,处理多线程安全问题。
1.5 解决多线程同步安全问题的方式三:lock. 的使用
从 JDK 5.0开始,Java提供了更强大的线程同步机制——> 通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock
接口是控制多个线程对共享资源进行访问的 工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。Lock 是一个接口,我们是无法 new 的我们需要找到其实现类就是 ReentrantLock
其中 Lock 接口对应的抽象方法如下
ReentrantLock
类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock
可以显式加锁、释放锁。同样的 ReentrantLock重写了 Lock 中的重写方法。
ReentrantLock 的重写的 lock()
显式的启动/获取锁,unlock()
显式释放手中的 ”锁“的方法
使用 Lock 接口中 lock() 获取锁 / unlock () 释放锁解决多线程安全问题的步骤
- 首先创建 ReentrantLock 的实例对象,用于调用其中重写 Lock 接口中的抽象方法 lock() 获取锁,unlock() 释放锁,这里定义为成员变量
private ReentrantLock reentrantLock = new ReentrantLock();
- 在适合的位置,通过 lock() 显式的获取/启动 ”锁“
reentrantLock.lock(); // 2.调用lock()显式启动锁
- 最后在合适的位置调用 unlock() 显示释放当前线程的 ”锁“。一般是定义在 finally{} 中防止该线程因为一些异常原因,没有释放手中的锁,让其他线程拿到锁,无法访问。
- 注意点:一般是将 lock() 调用在 try{} 中 ,unlock() 调用在finally{} 中,确保线程手中的锁一定会被释放 ,让其他线程可以获取到 ”锁“,进行共享数据的操作。
完整实现如下: 同样:模拟火车站卖票,一共有 100 张火车票,让三个人工售票窗口(三个线程)一起售票。
package blogs.blog4;
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决多线程同步机制的方式三: Lock
*/
public class ThreadTest8 {
public static void main(String[] args) {
// 创建窗口对象
Window8 window = new Window8();
Thread t1 = new Thread(window); // 售票窗口一
Thread t2 = new Thread(window); // 售票窗口二
Thread t3 = new Thread(window); // 售票窗口三
t1.setName("售票窗口一:");
t2.setName("售票窗口二:");
t3.setName("售票窗口三:");
t1.start();
t2.start();
t3.start();
}
}
/**
* 火车窗口
*/
class Window8 implements Runnable {
private int ticket = 100;
// 1.创建ReentrantLock 实例对象调用其中的 lock()启动锁,unlock() 手动解锁
private ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
reentrantLock.lock(); // 2.调用lock()显式启动锁
// 有票,便出售
if (this.ticket > 0) {
System.out.println(Thread.currentThread().getName() + "所售票号: " + this.ticket);
try {
Thread.sleep(10); // 当前线程睡眠: 0.001s ,提高出现线程安全问题的概率。
} catch (InterruptedException e) {
e.printStackTrace();
}
this.ticket--; // 售票成功,减减
} else {
break; // 没票了,停止出售。
}
} finally {
reentrantLock.unlock(); // 3.释放锁,注意使用 finally 无论是否出现异常都一定会被执行,一定会释放锁
}
}
}
}
1.5.1 synchronized 与 Lock 的对比
相同: 这两者都可以解决线程安全问题。
不同:
- synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器(锁),以及是隐式设置锁的。
- Lock 是手动通过调用 lock() 方法显式获取锁的,以及调用 unlock() 手动释放 锁的
- lock 比 synchronized(无论是修饰代码块,还是方法)都更加的灵活。
- Lock 只有代码锁,synchronized 有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
1.6 如何避免多线程安全问题:
- 局部变量 是永远不存在线程安全问题。因为局部变量存在栈中(一个线程一个栈),是每个线程各自独立拥有的,不使用特殊方式的话,是无法共享的。所以可以尽可能使用局部变量。
- 对于成员变量,静态变量,尽可能不要被多线程操作了。
- 如果必须是成员变量,那么可以考虑创建多个对象,这样成员变量的内存就不是共享的(锁就不是唯一的一把了)(一个线程对应一个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)
- 集合上的线程安全需要明确:
- 对于 String 字符串:如果使用局部变量的话:建议使用 StringBuilder 因为局部变量不存在线程安全问题,选择StringBuilder 更合适,StringBuffer 效率比较低,因为进行了 synchronized 的处理.
- ArrayList 是非线程安全的
- Vector 是线程安全的
- HashMap, HashSet 是非线程安全的
- Hashtable 是线程安全的
1.7 开发中如何处理线程安全问题及其注意事项
-
是一上来就选择线程同步吗? synchronized,不是,synchronized 会让程序的执行效率降低,用户体验不好,系统的用户的吞吐量降低,用户体验差,在不得以的情况下,再选择线程同步机制。
-
明确哪些代码是多线程运行的代码
-
明确多个线程是否有共享数据
-
明确多线程运行代码中是否有多条语句操作共享数据
-
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其 他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中
-
同步锁的使用注意:范围:范围太小:没锁住所有有安全问题的代码,范围太大:没发挥多线程的功能,过多的没有线程安全问题的代码,从多线程处理变成了单线程处理,效率降低了。
-
注意同步监视器 ”锁“的对象,是否需要所有线程同一把锁,以及对象锁,类锁 的使用。
-
三种处理线程安全问题的,合理顺序:Lock ——> 同步代码块(已经进入了方法体,分配了相应资源)——> 同步方法(在方法体之外)
-
同步线程 :解决了多线程的安全问题,但是同样也会降低一些效率。合理运用
2. 多线程的 ”死锁“ 现象
2.1 “死锁” 介绍
四锁: 不同的线程分别占用对方需要的同步资源 “锁” 不放弃 ,都在等待对方放弃自己需要的同步资源 ”锁“,就形成了线程的死锁。
出现了死锁之后,不会出现提示,只是所有线程都处于阻塞状态,无法继续,这种最难调试了。 不过可以通过 JDK 自带的 jconsole 工具检测 死锁 。
举例: 编写一个 死锁 程序:如下,两个线程(线程一,线程二),两个锁(o1,o2)
package blogs.blog4;
/**
* 死锁现象
*/
public class ThreadTest9 {
public static void main(String[] args) {
Object o1 = new Object(); // 锁一
Object o2 = new Object(); // 锁二
Thread t1 = new MyLock1(o1,o2); // 线程一
Thread t2 = new MyLock2(o1,o2); // 线程二
// 设置线程名
t1.setName("线程一:");
t2.setName("线程二:");
// 创建新线程,启动run()
t1.start();
t2.start();
}
}
class MyLock1 extends Thread {
private Object o1 = null;
private Object o2 = null;
public MyLock1() {
super();
}
public MyLock1(Object o1, Object o2) {
super(); // 调用父类的构造器
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
// 锁一
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "begin");
try {
Thread.sleep(1000); // 当前线程睡眠 1s,模拟网络延迟了 1s
} catch (InterruptedException e) {
e.printStackTrace();
}
// 锁二
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "end");
}
}
}
}
class MyLock2 extends Thread {
private Object o1 = null;
private Object o2 = null;
public MyLock2() {
super();
}
public MyLock2(Object o1, Object o2) {
super(); // 调用父类的构造器
this.o1 = o1;
this.o2 = o2;
}
@Override
public void run() {
// 锁一
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "begin");
try {
Thread.sleep(1000); // 当前线程睡眠 1s,模拟网络延迟了 1s
} catch (InterruptedException e) {
e.printStackTrace();
}
// 锁二
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "end");
}
}
}
}
使用JDK 中的 Jconsole 检测死锁的存在 具体使用大家可以移步至 : 🔜🔜🔜 Java多线程:创建多线程的“四种“ 方式
分析上述代码形成 ”死锁“的原因:
线程一拿到 ”o1“ 锁,进入到语句块中,当线程一还想要再拿到 “02"锁的时候,这时候出现了网络延迟了1s(sleep(1000)模拟的),这时候的线程二 趁机拿到了 ”o2” 锁,当 线程二 还想要再拿到 “o1” 锁时,已经不行了,因为“o1”锁被 线程一 拿到了,线程二 只能等到 线程一 释放 “o1” 锁,才有可能拿到了,可是这个时候的线程一 从网络延迟中恢复过来,想要去拿 “o2” 锁,拿不到了,因为已经被 线程二 给拿到了,现在就出现了这样一个死循环问题:
- 线程一 想要的 ”o2“ 锁,在 线程二 手上抓住不放。而线程二 想要的 ”o1“ 锁 ,在 线程一 手上抓住不放。
- 线程一 只有拿到了 ”o1" 和 “o2" 两把锁才会释放锁,而线程二 也是只有拿到了 ”o1“ 和 ”o2“ 这两把锁才会释放锁,现在两个线程各个占用各个需要的同步资源:”锁“,互补相让。形成了 ”死锁“。
如下图示:
这里我们注意到一点没有就是形成 ”死锁“ 的关键资源: 同步资源”锁“ 被他方抢到了。
所以我们这里需要认识到:什么时候会释放锁,什么时候不会释放锁。知道了这些我们才可以避免写出 死锁 。
2.2 释放锁的操作
- 当前线程的同步方法,同步代码块执行结束。
- 当强线程的同步方法,同步代码块中遇到 break,return 终止了该代码块,该方法的执行。
- 当强线程的同步方法,同步代码块中出现了 未处理的 Error 或 Exception 异常,导致异常的结束。
- 当强线程的同步方法,同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放当前线程所占用的锁 。
2.3 不会释放锁的操作
- 线程执行同步代码块 或 同步方法时,程序调用了 Thread.sleep( ),Thread.yield( ) 方法暂停了当前线程的执行。
- 线程执行同步代码块,其他线程调用了该线程的 **suspend( ) ** 方法,将该线程挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用 suspend( ) 和 resume( ) 来控制线程。
2.4 如何避免 ”死锁“ 的出现
- 尽量避免编写出 嵌套的 synchronized同步锁 。
- 尽量减少同步资源 ”锁“对象的定义。
- 使用专门的算法,原则,规避。
3. 单例模式 ”懒汉式“ 的线程安全问题
存在多线程安全的单例模式的 ”懒汉式“的编写 : 导致其中的 instance
经历了多次赋值,第一次是无效的,被第二次的线程给覆盖了,第三次线程覆盖了,只有最后一次线程的赋值才是有效的。
如下:
package blogs.blog4;
public class BankTest {
}
class Bank {
private static Bank instance = null;
// 构造器私有化
private Bank() {
}
public static Bank getInstance() {
if(instance == null) {
instance = new Bank();
}
return instance;
}
}
解决方式一: 使用 synchronized 修饰 方法 附加上同步锁,synchronized 修饰静态方法默认的是 类.class 类锁。
package blogs.blog4;
public class BankTest {
}
class Bank {
private static Bank instance = null;
// 构造器私有化
private Bank() {
}
public synchronized static Bank getInstance() { // 静态方法:默认是类.class 类锁
if(instance == null) {
instance = new Bank();
}
return instance;
}
}
解决方式二: 使用 synchronized 修饰代码块,设置为 类.class 的锁。
package blogs.blog4;
public class BankTest {
}
class Bank {
private static Bank instance = null;
// 构造器私有化
private Bank() {
}
public static Bank getInstance() {
synchronized (Bank.class) { // 设置类锁
if (instance == null) {
instance = new Bank();
}
}
return instance;
}
}
方式二的优化 : 方式二的第一种方式,效率低,因为存在这样一种情况:多线程等到锁。
如下所示:当多个线程需要等待获取 ”锁“ 进入if (instance == null) 时,其中只有第一个获取”锁“的线程,才执行了 instance = new Bank();
赋值的操作,因为等第一个线程赋值以后,instance != null ,无法赋值了,那前面的多个线程进入 synchronized {} 锁块以后,什么也没有干,那等了半天的 ”锁“进入。
就相当于是:排队核酸作检测,明明前面已经没有检测试剂了,却不早说,而是等,排到你的时候,才告诉你没有检测试剂了(让你白白浪费了时间),而不是已经没有试剂了,就告诉大家不要排队了,没有检测试剂。
如下优化:就是提前告诉其他线程,已经赋值好了,不要在排队了。就是当 if (instance == null) 的时候才进行 synchronized 同步锁机制
package blogs.blog4;
public class BankTest {
}
class Bank {
private static Bank instance = null;
// 构造器私有化
private Bank() {
}
public static Bank getInstance() {
if (instance == null) {
synchronized (Bank.class) {
if (instance == null) { // 防止多线程安全问题,进一步再判断。
instance = new Bank();
}
}
}
return instance;
}
}
4. 关于 ”锁“ 的面试题:
观察如下代码,思考其执行结果
4.1 题目一
该代码中: doSome()方法执行的时候需要等待doOther() 方法的锁释放结束吗???
package blogs.blog4;
public class ThreadTest10 {
public static void main(String[] args) {
MyClass m1 = new MyClass();
Thread t1 = new MyThread(m1);
Thread t2 = new MyThread(m1); // 多态性
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000); // 这个睡眠的作用: 为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread{
private MyClass mc = null;
public MyThread(MyClass mc) {
super();
this.mc = mc;
}
@Override
public void run() {
if("t1".equals(super.getName())) {
mc.doSome();
}
if("t2".equals(super.getName())) {
mc.doOther();
}
}
}
class MyClass {
public static void doSome() { // 这里的同步监视器是: this 锁
System.out.println("doSome begin");
try {
Thread.currentThread().sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public static synchronized void doOther() {
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
答: 不用,因为虽然 doOther()是静态方法又被 synchronized 修饰了,默认是 类.class 类锁,所有对象,线程共用,doSome 也是静态方法所有对象,线程共用,但是 doSome 并没有附加 synchronized 同步锁,是不需要等待 doOther()方法的类锁释放的。
4.2 题目二
如果 doSome 加上 synchronized 后, doOther 方法执行的时候需要等待doSome方法的锁释放结束吗???
package blogs.blog4;
public class ThreadTest10 {
public static void main(String[] args) {
MyClass m1 = new MyClass();
Thread t1 = new MyThread(m1);
Thread t2 = new MyThread(m1); // 多态性
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000); // 这个睡眠的作用: 为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread {
private MyClass mc = null;
public MyThread(MyClass mc) {
super();
this.mc = mc;
}
@Override
public void run() {
if ("t1".equals(super.getName())) {
mc.doSome();
}
if ("t2".equals(super.getName())) {
mc.doOther();
}
}
}
class MyClass {
public static synchronized void doSome() { // 这里的同步监视器是: this 锁
System.out.println("doSome begin");
try {
Thread.currentThread().sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public static synchronized void doOther() {
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
答: 需要因为 无论是 doSome() 还是 doOther() 方法都是静态方法,并且都被 synchronized 修饰了,默认是 类.class 类锁,类锁是仅仅只有一个所有对象,线程共用,所以 t2线程中的 doOther()方法需要等待 t1线程执行完 doSome()方法释放类锁,才可以执行。
4.3 题目三
该代码中: doSome()方法执行的时候需要等待doOther() 方法的锁释放结束吗???
package blogs.blog4;
public class ThreadTest10 {
public static void main(String[] args) {
MyClass m1 = new MyClass();
MyClass m2 = new MyClass();
Thread t1 = new MyThread(m1);
Thread t2 = new MyThread(m2); // 多态性
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000); // 这个睡眠的作用: 为了保证t1线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread {
private MyClass mc = null;
public MyThread(MyClass mc) {
super();
this.mc = mc;
}
@Override
public void run() {
if ("t1".equals(super.getName())) {
mc.doSome();
}
if ("t2".equals(super.getName())) {
mc.doOther();
}
}
}
class MyClass {
public static synchronized void doSome() { // 这里的同步监视器是: this 锁
System.out.println("doSome begin");
try {
Thread.currentThread().sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doSome over");
}
public static synchronized void doOther() {
System.out.println("doOther begin");
System.out.println("doOther over");
}
}
答: 需要,因为这两个对象虽然是不同的,但是 synchronized static 修饰静态方法,默认是 类.class 类锁,类锁是仅仅只有一个所有对象,线程共用,所以 t2线程中的 doOther()方法需要等待 t1线程执行完 doSome()方法释放类锁,才可以执行。
4. 总结:
- synchronized() 修饰代码块任何对象都可以设置为同步监视器”锁“,Object,“abc”,类.class,this 但是 同步监视器“锁”不可以为 null 不然报
NullPointerException
null 指针异常的。 - synchronized () 修饰非静态方法,默认是 this 对象锁,修饰静态方法,默认是 类.class 类锁,仅仅只会加载一次,所有 对象,线程共用。无法修改
- java 三大变量在作为 同步监视器 ”锁“ 的线程安全问题的讨论
- 避免写出 ”死锁“。
- 会释放锁 ,不会释放锁的操作有哪些。
- 单例模式中的 ”懒汉式“ 的优化 线程安全问题,提前告知法。
5. 最后:
限于自身水平,其中存在的错误,希望大家给予指教,韩信点兵——多多益善,谢谢大家,后会有期,江湖再见 !!!