当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条
记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如
果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
5.1 同一个资源问题和线程安全问题
案例:
火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位
共 100 个(即,只能出售 100 张火车票)。我们来模拟车站的售票窗口,实现
多个窗口同时售票的过程。注意:不能出现错票、重票。
5.1.1 局部变量不能共享
示例代码:
package com.atguigu.unsafe;
class Window extends Thread {
public void run() {
int ticket = 100;
while (ticket > 0) {
System.out.println(getName() + "卖出一张票,票号:" + ticke
t);
ticket--;
}
}
}
public class SaleTicketDemo1 {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口 1");
w2.setName("窗口 2");
w3.setName("窗口 3");
w1.start();
w2.start();
w3.start();
}
}
结果:发现卖出 300 张票。 问题:局部变量是每次调用方法都是独立的,那么每个线程的 run()的 ticket 是独立的,不是共享数据。
5.1.2 不同对象的实例变量不共享
package com.atguigu.unsafe;
class TicketWindow extends Thread {
private int ticket = 100;
public void run() {
while (ticket > 0) {
System.out.println(getName() + "卖出一张票,票号:" + ticke
t);
ticket--;
}
}
}
public class SaleTicketDemo2 {
public static void main(String[] args) {
TicketWindow w1 = new TicketWindow();
TicketWindow w2 = new TicketWindow();
TicketWindow w3 = new TicketWindow();
w1.setName("窗口 1");
w2.setName("窗口 2");
w3.setName("窗口 3");
w1.start();
w2.start();
w3.start();
}
}
结果:发现卖出 300 张票。 问题:不同的实例对象的实例变量是独立的。
5.1.3 静态变量是共享的
示例代码:
package com.atguigu.unsafe;
class TicketSaleThread extends Thread {
private static int ticket = 100;
public void run() {
while (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "卖出一张票,票号:" + ticke
t);
ticket--;
}
}
}
public class SaleTicketDemo3 {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
窗口 1 卖出一张票,票号:100
窗口 2 卖出一张票,票号:100
窗口 3 卖出一张票,票号:100
窗口 3 卖出一张票,票号:97
窗口 1 卖出一张票,票号:97
窗口 2 卖出一张票,票号:97
窗口 1 卖出一张票,票号:94
窗口 3 卖出一张票,票号:94
窗口 2 卖出一张票,票号:94
窗口 2 卖出一张票,票号:91
窗口 1 卖出一张票,票号:91
窗口 3 卖出一张票,票号:91
窗口 3 卖出一张票,票号:88
窗口 1 卖出一张票,票号:88
窗口 2 卖出一张票,票号:88
窗口 3 卖出一张票,票号:85
窗口 1 卖出一张票,票号:85
窗口 2 卖出一张票,票号:85
窗口 3 卖出一张票,票号:82
窗口 1 卖出一张票,票号:82
窗口 2 卖出一张票,票号:82
窗口 2 卖出一张票,票号:79
窗口 3 卖出一张票,票号:79
窗口 1 卖出一张票,票号:79
窗口 3 卖出一张票,票号:76
窗口 1 卖出一张票,票号:76
窗口 2 卖出一张票,票号:76
窗口 1 卖出一张票,票号:73
窗口 2 卖出一张票,票号:73
窗口 3 卖出一张票,票号:73
窗口 2 卖出一张票,票号:70
窗口 1 卖出一张票,票号:70
窗口 3 卖出一张票,票号:70
窗口 2 卖出一张票,票号:67
窗口 3 卖出一张票,票号:67
窗口 1 卖出一张票,票号:67
窗口 1 卖出一张票,票号:64
窗口 3 卖出一张票,票号:64
窗口 2 卖出一张票,票号:64
窗口 2 卖出一张票,票号:61
窗口 3 卖出一张票,票号:61
窗口 1 卖出一张票,票号:61
窗口 1 卖出一张票,票号:58
窗口 2 卖出一张票,票号:58
窗口 3 卖出一张票,票号:58
窗口 2 卖出一张票,票号:55
窗口 1 卖出一张票,票号:55
窗口 3 卖出一张票,票号:55
窗口 3 卖出一张票,票号:52
窗口 1 卖出一张票,票号:52
窗口 2 卖出一张票,票号:52
窗口 2 卖出一张票,票号:49
窗口 1 卖出一张票,票号:49
窗口 3 卖出一张票,票号:49
窗口 2 卖出一张票,票号:46
窗口 3 卖出一张票,票号:46
窗口 1 卖出一张票,票号:46
窗口 2 卖出一张票,票号:43
窗口 3 卖出一张票,票号:43
窗口 1 卖出一张票,票号:43
窗口 3 卖出一张票,票号:40
窗口 1 卖出一张票,票号:40
窗口 2 卖出一张票,票号:40
窗口 2 卖出一张票,票号:37
窗口 3 卖出一张票,票号:37
窗口 1 卖出一张票,票号:37
窗口 2 卖出一张票,票号:34
窗口 1 卖出一张票,票号:34
窗口 3 卖出一张票,票号:34
窗口 3 卖出一张票,票号:31
窗口 2 卖出一张票,票号:31
窗口 1 卖出一张票,票号:31
窗口 1 卖出一张票,票号:28
窗口 2 卖出一张票,票号:28
窗口 3 卖出一张票,票号:28
窗口 2 卖出一张票,票号:25
窗口 1 卖出一张票,票号:25
窗口 3 卖出一张票,票号:25
窗口 2 卖出一张票,票号:22
窗口 3 卖出一张票,票号:22
窗口 1 卖出一张票,票号:22
窗口 3 卖出一张票,票号:19
窗口 1 卖出一张票,票号:19
窗口 2 卖出一张票,票号:19
窗口 2 卖出一张票,票号:16
窗口 3 卖出一张票,票号:16
窗口 1 卖出一张票,票号:16
窗口 2 卖出一张票,票号:13
窗口 1 卖出一张票,票号:13
窗口 3 卖出一张票,票号:13
窗口 2 卖出一张票,票号:10
窗口 1 卖出一张票,票号:10
窗口 3 卖出一张票,票号:10
窗口 3 卖出一张票,票号:7
窗口 1 卖出一张票,票号:7
窗口 2 卖出一张票,票号:7
窗口 3 卖出一张票,票号:4
窗口 1 卖出一张票,票号:4
窗口 2 卖出一张票,票号:4
窗口 3 卖出一张票,票号:1
窗口 2 卖出一张票,票号:1
窗口 1 卖出一张票,票号:1
结果:
发现卖出近 100 张票。
问题 1:但是有重复票或负数票问题。
原因:线程安全问题
问题 2:如果要考虑有两场电影,各卖 100 张票等
原因:TicketThread 类的静态变量,是所有 TicketThread
类的对象共享
5.1.4 同一个对象的实例变量共享
示例代码:多个 Thread 线程使用同一个 Runnable 对象
package com.atguigu.safe;
class TicketSaleRunnable implements Runnable {
private int ticket = 100;
public void run() {
while (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖
出一张票,票号:" + ticket);
ticket--;
}
}
}
public class SaleTicketDemo4 {
public static void main(String[] args) {
TicketSaleRunnable tr = new TicketSaleRunnable();
Thread t1 = new Thread(tr, "窗口一");
Thread t2 = new Thread(tr, "窗口二");
Thread t3 = new Thread(tr, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
结果:发现卖出近 100 张票。
问题:但是有重复票或负数票问题。 原因:线程安全问题
5.1.5 抽取资源类,共享同一个资源对象
示例代码:
package com.atguigu.unsafe;
//1、编写资源类
class Ticket {
private int ticket = 100;
public void sale() {
if (ticket > 0) {
try {
Thread.sleep(10);//加入这个,使得问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖
出一张票,票号:" + ticket);
ticket--;
} else {
throw new RuntimeException("没有票了");
}
}
public int getTicket() {
return ticket;
}
}
public class SaleTicketDemo5 {
public static void main(String[] args) {
//2、创建资源对象
Ticket ticket = new Ticket();
//3、启动多个线程操作资源类的对象
Thread t1 = new Thread("窗口一") {
public void run() {
while (true) {
ticket.sale();
}
}
};
Thread t2 = new Thread("窗口二") {
public void run() {
while (true) {
ticket.sale();
}
}
};
Thread t3 = new Thread(new Runnable() {
public void run() {
ticket.sale();
}
}, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
结果:发现卖出近 100 张票。
问题:但是有重复票或负数票问题。 原因:线程安全问题
5.2 同步机制解决线程安全问题
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在
票问题,Java 中提供了同步机制 (synchronized)来解决。
根据案例简述:
窗口 1 线程进入操作的时候,窗口 2 和窗口 3 线程只能在外等着,窗口 1 操作
结束,窗口 1 和窗口 2 和窗口 3 才有机会进入代码去执行。也就是说在某个线
程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,
才能去抢夺 CPU 资源,完成对应的操作,保证了数据的同步性,解决了线程不
安全的现象。
为了保证每个线程都能正常执行原子操作,Java 引入了线程同步机制。注意:在
任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程
只能在外等着(BLOCKED)。
5.2.1 同步机制解决线程安全问题的原理
同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,我们称它为同步锁。因为 Java 对象在堆中的数据分为分为对象头、实例变量、空白的填充。而对象头中包含:
• Mark Word:记录了和当前对象有关的 GC、锁标记等信息。
• 指向类的指针:每一个对象需要记录它是由哪个类创建出来的。
• 数组长度(只有数组对象才有)
哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的 ID,这样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用”同步锁“对象。
5.2.2 同步代码块和同步方法
同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。 格式:
synchronized(同步锁){
需要同步操作的代码
}
同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。
public synchronized void method(){
可能会产生线程安全问题的代码
}
5.2.3 同步锁机制
在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
5.2.4 synchronized 的锁是什么
同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程必须使用同一个“同步锁对象”。
对于同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定为 this 或类名.class),但是对于同步方法来说,同步锁对象只能是默认的:
• 静态方法:当前类的 Class 对象(类名.class)
• 非静态方法:this
5.2.5 同步操作的思考顺序
1、如何找问题,即代码是否存在线程安全?(非常重要) (1)明确哪些代码
是多线程运行的代码 (2)明确多个线程是否有共享数据 (3)明确多线程运
行代码中是否有多条语句操作共享数据
2、如何解决呢?(非常重要) 对多条操作共享数据的语句,只能让一个线程
都执行完,在执行过程中,其他线程不可以参与执行。 即所有操作共享数据的
这些语句都要放在同步范围中
3、切记:
范围太小:不能解决安全问题
范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大,效率会降低,不能合理利用 CPU 资源。
5.2.6 代码演示
示例一:静态方法加锁
package com.atguigu.safe;
class TicketSaleThread extends Thread{
private static int ticket = 100;
public void run(){//直接锁这里,肯定不行,会导致,只有一个窗口卖票
while (ticket > 0) {
saleOneTicket();
}
}
public synchronized static void saleOneTicket(){//锁对象是 TicketS
aleThread 类的 Class 对象,而一个类的 Class 对象在内存中肯定只有一个
if(ticket > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全
问题就没有解决
System.out.println(Thread.currentThread().getName() + "卖
出一张票,票号:" + ticket);
ticket--;
}
}
}
public class SaleTicketDemo3 {
public static void main(String[] args) {
TicketSaleThread t1 = new TicketSaleThread();
TicketSaleThread t2 = new TicketSaleThread();
TicketSaleThread t3 = new TicketSaleThread();
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();
}
}
示例二:非静态方法加锁
package com.atguigu.safe;
public class SaleTicketDemo4 {
public static void main(String[] args) {
TicketSaleRunnable tr = new TicketSaleRunnable();
Thread t1 = new Thread(tr, "窗口一");
Thread t2 = new Thread(tr, "窗口二");
Thread t3 = new Thread(tr, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
class TicketSaleRunnable implements Runnable {
private int ticket = 100;
public void run() {//直接锁这里,肯定不行,会导致,只有一个窗口卖票
while (ticket > 0) {
saleOneTicket();
}
}
public synchronized void saleOneTicket() {//锁对象是 this,这里就是
TicketSaleRunnable 对象,因为上面 3 个线程使用同一个 TicketSaleRunnable 对
象,所以可以
if (ticket > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安
全问题就没有解决
System.out.println(Thread.currentThread().getName() + "卖
出一张票,票号:" + ticket);
ticket--;
}
}
}
示例三:同步代码块
package com.atguigu.safe;
public class SaleTicketDemo5 {
public static void main(String[] args) {
//2、创建资源对象
Ticket ticket = new Ticket();
//3、启动多个线程操作资源类的对象
Thread t1 = new Thread("窗口一") {
public void run() {//不能给 run()直接加锁,因为 t1,t2,t3 的三
个 run 方法分别属于三个 Thread 类对象,
// run 方法是非静态方法,那么锁对象默认选 this,那么锁对象
根本不是同一个
while (true) {
synchronized (ticket) {
ticket.sale();
}
}
}
};
Thread t2 = new Thread("窗口二") {
public void run() {
while (true) {
synchronized (ticket) {
ticket.sale();
}
}
}
};
Thread t3 = new Thread(new Runnable() {
public void run() {
while (true) {
synchronized (ticket) {
ticket.sale();
}
}
}
}, "窗口三");
t1.start();
t2.start();
t3.start();
}
}
//1、编写资源类
class Ticket {
private int ticket = 1000;
public void sale() {//也可以直接给这个方法加锁,锁对象是 this,这里就
是 Ticket 对象
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖
出一张票,票号:" + ticket);
ticket--;
} else {
throw new RuntimeException("没有票了");
}
}
public int getTicket() {
return ticket;
}
}
附: 死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
【小故事】
面试官:你能解释清楚什么是死锁,我就录取你! 面试者:你录取
我,我就告诉你什么是死锁! …. 恭喜你,面试通过了
一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线
程处于阻塞状态,无法继续。
举例 1:
public class DeadLockTest {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread() {
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
}
}
举例 2:
class A {
public synchronized void foo(B b) {
System.out.println("当前线程名: " + Thread.currentThread().get
Name()
+ " 进入了 A 实例的 foo 方法"); // ①
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().get
Name()
+ " 企图调用 B 实例的 last 方法"); // ③
b.last();
}
public synchronized void last() {
System.out.println("进入了 A 类的 last 方法内部");
}
}
class B {
public synchronized void bar(A a) {
System.out.println("当前线程名: " + Thread.currentThread().get
Name()
+ " 进入了 B 实例的 bar 方法"); // ②
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().get
Name()
+ " 企图调用 A 实例的 last 方法"); // ④
a.last();
}
public synchronized void last() {
System.out.println("进入了 B 类的 last 方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用 a 对象的 foo 方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用 b 对象的 bar 方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
举例 3:
public class TestDeadLock {
public static void main(String[] args) {
Object g = new Object();
Object m = new Object();
Owner s = new Owner(g,m);
Customer c = new Customer(g,m);
new Thread(s).start();
new Thread(c).start();
}
}
class Owner implements Runnable{
private Object goods;
private Object money;
public Owner(Object goods, Object money) {
super();
this.goods = goods;
this.money = money;
}
@Override
public void run() {
synchronized (goods) {
System.out.println("先给钱");
synchronized (money) {
System.out.println("发货");
}
}
}
}
class Customer implements Runnable{
private Object goods;
private Object money;
public Customer(Object goods, Object money) {
super();
this.goods = goods;
this.money = money;
}
@Override
public void run() {
synchronized (money) {
System.out.println("先发货");
synchronized (goods) {
System.out.println("再给钱");
}
}
}
}
诱发死锁的原因:
• 互斥条件
• 占用且等待
• 不可抢夺(或不可抢占)
• 循环等待
以上 4 个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件 1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件 2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件 3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件 4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
JDK5.0 新特性:Lock(锁)
• JDK5.0 的新增功能,保证线程的安全。与采用 synchronized 相比,Lock 可提供多种
锁方案,更灵活、更强大。Lock 通过显式定义同步锁对象来实现同步。同步锁使用Lock 对象充当。
• java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。
• 在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。
– ReentrantLock 类实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
• Lock 锁也称同步锁,加锁与释放锁方法,如下:
– public void lock() :加同步锁。
– public void unlock() :释放同步锁。
代码结构:
class A{
//1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
//2. 调动 lock(),实现需共享的代码的锁定
lock.lock();
try{
//保证线程安全的代码;
}
finally{
//3. 调用 unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
注意:如果同步代码有异常,要将 unlock()写入 finally 语句块。
举例:
import java.util.concurrent.locks.ReentrantLock;
class Window implements Runnable{
int ticket = 100;
//1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例
private final ReentrantLock lock = new ReentrantLock();
public void run(){
while(true){
try{
//2. 调动 lock(),实现需共享的代码的锁定
lock.lock();
if(ticket > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticket--);
}else{
break;
}
}finally{
//3. 调用 unlock(),释放共享代码的锁定
lock.unlock();
}
}
}
}
public class ThreadLock {
public static void main(String[] args) {
Window t = new Window();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
synchronized 与 Lock 的对比
- Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用域、遇到异常等自动解锁
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更体现面向对象。
- (了解)Lock 锁可以对读不加锁,对写加锁,synchronized 不可以
- (了解)Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,synchronized 不可以
说明:开发建议中处理线程安全问题优先使用顺序为:
Lock ----> 同步代码块 ----> 同步方法