线程同步
场景:目前电影院有100张票,分3个窗口同时买票
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
@Override
public void run() {
while (true) {
// t1,t2,t3三个线程
// 这一次的tickets = 1;
if (tickets > 0) {
// 为了模拟更真实的场景,我们稍作休息
try {
Thread.sleep(100); //t1进来了并休息,t2进来了并休息,t3进来了并休息,
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
//窗口1正在出售第1张票,tickets=0
//窗口2正在出售第0张票,tickets=-1
//窗口3正在出售第-1张票,tickets=-2
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();
// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
输出:
窗口1正在出售第100张票
窗口3正在出售第100张票
窗口2正在出售第99张票
窗口1正在出售第98张票
窗口3正在出售第97张票
...
...
窗口1正在出售第3张票
窗口2正在出售第2张票
窗口3正在出售第1张票
窗口2正在出售第0张票
窗口1正在出售第-1张票
结果可以看出,即有同时卖一张票的,还有卖负数票的情况。
首先,我们需要知道 CPU的每一次执行必须是一个原子性(最简单基本的)的操作。tickets--
其实分为三个原子操作:
- 取并记录 tickets 的值
- tickets 的值减去1
- 返回以前的值。
同时卖一张票
情况分析:
- 线程 t1、t3 同时进入
run()
方法并都稍作休息,然后线程 t1 抢占到 CPU 时间片,执行tickets--
的第一个原子操作记录 tickets 值为 100; - 而这时线程 t3 抢占到 CPU 时间片,也执行 tickets-- 的第一个原子操作记录 tickets 值也为 100。所以会输出 “窗口1正在出售第100张票、窗口3正在出售第100张票”。
卖负数票
情况分析:
- 线程t1、t2、t3同时进入
run()
方法并都稍作休息,然后线程 t3 抢占到 CPU 时间片,执行tickets--
的所有原子操作,输出“窗口3正在出售第1张票”,此时 tickets 的值为 0。 - 这时线程 t2 抢占到 CPU 时间片,执行
tickets--
的所有原子操作,输出“窗口2正在出售第0张票”,此时tickets的值为 -1。 - 这时线程 t1 抢占到 CPU 时间片,执行
tickets--
的所有原子操作,输出“窗口1正在出售第-1张票”。
所以卖票的类线程不安全,判断一个程序是否有线程安全问题的依据:
- 是否有多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
解决方案:
需要将操作共享数据的代码进行封装,保证单个线程执行过程中,其他线程不能执行。Java提供了:同步机制synchronized
。
同步代码块
。
这里的锁对象可以是任意对象,作用对象是括号括起来的对象实例。
synchronized (对象) {
需要被同步的代码;
}
同步方法
把同步加在方法上。这里的锁对象是this,作用对象是调用这个方法的对象。
synchronized 返回类型 method() {
需要被同步的代码;
}
静态同步方法
把同步加在方法上。这里的锁对象是当前类的字节码文件对象,作用对象是这个类的所有对象,相当于该类的一个全局锁。
private static synchronized 返回类型 method() {
需要被同步的代码;
}
同步代码块
package 多线程;
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
}
输出:
线程安全
线程 t1、t2、t3 都走到同步代码块。假设 t1 抢到 CPU 的执行权,t1 就进去执行;而线程 t2 抢到 CPU 的执行权,发现代码块是锁定状态,只能等待。线程 t1 执行完成后,释放锁,然后大家再抢占锁。这样,保证同步代码块内执行只会有一个线程。
注意:锁对象一定要是同一个对象,如果synchronized (new Object()) { ... }
,还是线程不安全。所以,一般锁对象都是类成员变量。
同步方法
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
@Override
public void run() {
while (true) {
sellTicket();
}
}
private synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
}
输出:
线程安全
定义同步方法时,我们并没有声明锁对象,而锁对象是保证线程安全的必要条件。那么同步方法的锁对象是什么呢?
我们可以做个实验,定义一个递增变量 i,用于分发调用不同代码。代码1使用锁对象是成员对象obj,代码2使用的锁对象未知。
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象
private Object obj = new Object();
int i = 0;
@Override
public void run() {
while (true) {
if (i%2 == 0) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
} else {
sellTicket();
}
i++;
}
}
private synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
}
输出:
窗口1正在出售第100张票
...
窗口1正在出售第95张票
窗口3正在出售第95张票
...
窗口2正在出售第89张票
窗口1正在出售第88张票
窗口2正在出售第88张票
...
窗口2正在出售第76张票
窗口3正在出售第76张票
...
窗口2正在出售第1张票
窗口3正在出售第1张票
输出结果看出线程不安全。说明同步方法锁对象不是 obj,而我们知道,每个类都有一个默认对象 this,所以同步方法的锁对象是 this。验证如下,将synchronized (obj)
修改为synchronized (this)
:
public class SellTicket implements Runnable {
...
@Override
public void run() {
while (true) {
if (i%2 == 0) {
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
} else {
sellTicket();
}
i++;
}
}
private synchronized void sellTicket() {
...
}
}
输出:
线程安全
故,同步方法的锁对象是 this。
静态同步方法
public class SellTicket implements Runnable {
// 定义100张票
private static int tickets = 100;
@Override
public void run() {
while (true) {
sellTicket();
}
}
private static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
}
输出:
线程安全
因为静态方法随类的加载而加载,所以静态同步方法的锁对象是当前类的字节码对象。验证如下,将synchronized (obj)
修改为 synchronized (SellTicket.class)
:
public class SellTicket implements Runnable {
...
@Override
public void run() {
while (true) {
if (i%2 == 0) {
synchronized (SellTicket.class) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
} else {
sellTicket();
}
i++;
}
}
private static synchronized void sellTicket() {
...
}
}
输出:
线程安全
线程同步的优缺点:
- 好处
使用同一个锁对象解决了多线程的安全问题。 - 缺点
当线程相当多时,因为每个线程都会去判断同步锁,这是很耗费资源的,无形中会降低程序的运行效率。
synchronized 存在的问题:申请资源的时候,如果申请不到,线程直接进入阻塞状态
,而线程进入阻塞状态,啥都干不了,也不释放线程已经占有的资源! Java提供了Lock解决该问题。
线程同步在集合应用
Java提供了许多线程安全的类:StringBuffer
、Vector
、Hashtable
等,通过查看源码发现,线程安全的类方法均通过synchronized修饰。
但如果我们需要使用线程不安全的集合怎么办呢?
Java提供Collections.synchronizedList()
可以将线程不安全的集合转换成线程安全。
- public static <T> List<T> synchronizedList(List<T> list)
public class ThreadDemo {
public static void main(String[] args) {
// 线程不安全
List<String> list1 = new ArrayList<String>();
// 线程安全
List<String> list2 = Collections.synchronizedList(new ArrayList<String>());
list2.add("1111");
list2.add("2222");
}
}
使用 Collections.synchronizedList(传入需要转换的集合对象),返回 List 对象。
看下 Collections.synchronizedList 源码:
SynchronizedList 就是在 List 的所有操作外加了一层 synchronized 同步代码块控制。
注:遍历线程安全集合时,还需要用户自己控制同步
源码:
查看源码发现,遍历迭代器并没有加 synchronize 修饰。
测试:
public class SynchronizedList {
public static void main(String[] args){
List<String> list = Collections.synchronizedList(new ArrayList<String>());
list.add("1111");
list.add("2222");
list.add("3333");
Thread t1 = new Thread(new IteratorRunnable(list), "线程1");
Thread t2 = new Thread(new ModifySynchronizeRunnable(list), "线程2");
t1.start();
t2.start();
}
}
// 读取线程
class IteratorRunnable implements Runnable{
private List<String> list;
public IteratorRunnable(List<String> list) {
this.list = list;
}
@Override
public void run() {
for (String s : list) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(s + ",");
}
}
}
// 写入线程
class ModifySynchronizeRunnable implements Runnable{
private List<String> list;
public ModifySynchronizeRunnable(List<String> list) {
this.list = list;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add("modify");
}
}
}
1111,
Exception in thread "线程1" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at 多线程.IteratorRunnable.run(SynchronizedList.java:35)
at java.lang.Thread.run(Thread.java:748)
异步修改 List 的结构,发现抛出了ConcurrentModificationException异常。
读取线程 IteratorRunnable 加入同步:
// 读取线程
class IteratorRunnable implements Runnable{
private List<String> list;
public IteratorRunnable(List<String> list) {
this.list = list;
}
@Override
public void run() {
synchronized (list) {
for (String s : list) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(s + ",");
}
}
}
}
输出:
1111,
2222,
3333,
线程安全。
Lock锁
Lock
是 Java 提供的另一种实现互斥锁
的方式,实现为ReentrantLock
、ReadWriteLock
,基于AQS
实现,并且解决了 synchronized 关键字的资源不可抢占问题。具体提供三种方式:
- 超时机制:指定时间内获取不到锁,不进入阻塞状态。
- 响应中断:给阻塞的线程发送中断信号的时候,能够唤醒它。
- 非阻塞的获取锁:当尝试获取锁失败,并不进入阻塞状态,而是直接返回。
对应Lock中的方法:
- boolean
tryLock
(long time, TimeUnit unit):超时 - void
lockInterruptibly()
:响应中断 - boolean
tryLock()
:非阻塞获取锁 - void
lock()
:获取锁 - void
unlock()
:释放锁
public class SellTicket implements Runnable {
// 定义100张票
private static int tickets = 100;
private Lock lock = new ReentrantLock();
int i = 0;
@Override
public void run() {
while (true) {
if (i%2 == 0) {
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
lock.unlock();
} else {
lock.lock();
sellTicket();
lock.unlock();
}
i++;
}
}
private void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
}
输出:
线程安全
- 队列同步器(AbstractQueueSynchronizer,AQS)
死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,这些永远在互相等待的进程称为死锁进程。
定义两个线程,通过标志flag判断进入不同的逻辑。
线程1:获取锁对象objA,失眠100ms,尝试获取objB。
线程2:获取锁对象objB,失眠100ms,尝试获取objA。
锁对象类MyLock:
public class MyLock {
// 创建两把锁对象
public static final Object objA = new Object();
public static final Object objB = new Object();
}
死锁处理类DieLock:
public class DieLock extends Thread {
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if objA");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
} else {
synchronized (MyLock.objB) {
System.out.println("else objB");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
}
主控:
public class DieLockDemo {
public static void main(String[] args) {
DieLock dl1 = new DieLock(true);
DieLock dl2 = new DieLock(false);
dl1.start();
dl2.start();
}
}
输出:
if objA
else objB
输出一直等待,因为线程dl1、dl2相互等待形成死锁。
注:代码应避免死锁的出现。
死锁解决方法
要避免死锁就需要分析死锁发生的条件,只有以下这四个条件都发生时才会出现死锁:
-
互斥,共享资源 X 和 Y 只能被一个线程占用;
-
占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
解决办法:同时申请所有资源 -
不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
解决办法:使用Lock API -
循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
解决办法:设置加锁顺序 例如:全都按照从小到大的顺序
锁分类
先引出问题:
多线程竞争锁时,阻塞等待锁释放是如何执行的?内部是用while死循环一直去判断锁是否被释放吗?
首先,锁分为两种,一种是互斥锁
;一种是自旋锁
。
互斥锁
一般应用于内核,当你的线程获取锁失败后,操作系统会将你的线程休眠(阻塞等待),同时把你的线程 id 记录到和这个锁相关的队列。当这个锁释放后,收到中断通知的线程会解除阻塞状态,加入到运行队列,等待新的 CPU 时间片的分配。
这里会有线程切换 Context Switch 的代价,即你的线程需要进入睡眠,然后再唤醒,这个是有消耗的,一般是几个us级别代价。
自旋锁
:你的线程一直在尝试获得锁,如果之前获得锁的另外线程没有释放,你的线程就会一个死循环不停尝试获取。这时,循环尝试会浪费大量的 CPU 资源,你可以观测到这个cpu core的利用率是几乎100%。
如果其他线程很快就释放了锁,获取锁线程只尝试几次就获得了锁,那这个代价就会比上下文切换 Context Switch 小。可能只有是几十个ns。
一般临界区很短时,就优先使用自旋锁。