目录
内容介绍
- 线程安全和不安全介绍
- 线程不安全问题演示和分析
- 线程不安全问题解决
- Synchronized关键字原理分析
线程安全
什么是线程安全
单线程售票示意图:
多线程售票示意图:
结论:
如果多个线程操作一个资源得到的结果,和单个线程操作一个资源得到的结果是相同的,则线程安全;反之,则线程不安全。
线程安全问题演示
为了演示线程安全问题,我们采用多线程模拟多个窗口同时售卖《两只老虎》电影票。
第一步:创建售票线程类
package com.multithread.thread;
public class Ticket implements Runnable {
private int ticktNum = 100;
public void run() {
while(ticktNum > 0){
//1.模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
}
}
}
第二步:创建测试类
package com.multithread.demos;
import com.multithread.thread.Ticket;
public class TicketDemo {
public static void main(String[] args){
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket, "窗口1");
Thread thread2 = new Thread(ticket, "窗口2");
Thread thread3 = new Thread(ticket, "窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果如下:
程序出现了两个问题:
1. 相同的票数,比如5这张票被卖了两回。
2. 不存在的票,比如0票与-1票,是不存在的。
线程安全问题分析
线程安全问题都是由全局变量及静态变量引起的。
若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
综上所述,线程安全问题根本原因:
- 多个线程在操作共享的数据;
- 操作共享数据的线程代码有多条;
- 多个线程对共享数据有写操作;
线程安全问题解决
要解决以上线程问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制。本文重点介绍前两种,后续会出其他同步机制及原理。
- 同步代码块(synchronized)
- 同步方法(synchronized)
- 同步锁(ReenreantLock)
- 特殊域变量(volatile)
- 局部变量(ThreadLocal)
- 阻塞队列(LinkedBlockingQueue)
- 原子变量(Atomic*)
同步代码块
synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
语法
synchronized(同步锁){
需要同步操作的代码
}
同步锁
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象可以是任意类型。
- 多个线程要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
示例代码
package com.multithread.thread;
public class Ticket implements Runnable {
private int ticktNum = 100;
//定义锁对象
Object obj = new Object();
public void run() {
while(true){
synchronized (obj){
if(ticktNum > 0){
//1.模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
}
}
}
}
}
执行结果如下:线程的安全问题,解决了。
同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
语法
public synchronized void method(){
可能会产生线程安全问题的代码
}
同步锁
- 对于非static方法,同步锁就是this。
- 对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)。
示例代码
package com.multithread.thread;
public class Ticket implements Runnable {
private int ticktNum = 100;
//定义锁对象
Object obj = new Object();
public void run() {
while(true){
sellTicket();
}
}
private synchronized void sellTicket(){
if(ticktNum > 0){
//1.模拟出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.打印进程号和票号,票数减1
String name = Thread.currentThread().getName();
System.out.println("线程"+name+"售票:"+ticktNum--);
}
}
}
执行结果如下:线程的安全问题,解决了。
Synchronized实现原理
字节码分析
Synchronized是基于jvm指令实现的,具体可参考字节码文件。
查看字节码
使用javap -v xx.class命令查看字节码文件
比如本文:
javap -v Ticket.class
静态代码块
非静态同步方法
静态同步方法
结论:
通过字节码文件可以看到,synchronized关键字底层使用的是monitor指令
Monitor监视锁
那monitor底层又是怎么实现的呢?这就要从对象的内存结构开始了解了。下面我们一起来学习下。
对象头内存结构
以32位虚拟机为例
普通对象头
数组对象头
MarkWord结构
Monitor原理
Monitor被翻译为监视器或管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针
加锁原理
- obj是java提供的对象,monitor是操作系统提供的
- 刚开始Monitor中的Owner为null
- 当Thread-2被加上重量级锁的时候,他Mark Word的内容发生改变,他就会找到一个monitor与之关联,并记录了指向monitor的地址,不再记录之前的信息。
- monitor中的owner则记录了谁是这把锁的主人,此时记录了Thread-2
- 此时线程2成功获取到了monitor。
阻塞原理
- 1.新线程Thread-1,他会先检查obj是否关联到了一个monitor锁,此时发现已经关联
- 2.然后检查这个monitor是否有一个主人Owner,发现他有Owner为Thread-2
- 3.所以Thread-1无法获取锁,此时Thread-1会通过EntryList(等待队列或阻塞队列)与monitor发生关联,然后进行BLOCKED(阻塞)状态。
- 4.如果此时Thread-3来了,再同上面过程检查一遍后进入EntryList与Thread-1一同等待
等待唤醒
Thread-2释放锁之后,由Thread-1和Thread-3进行竞争决定谁成为新Owner
注意
synchronized必须是进入同一个对象的monitor才有上述的效果
- 不加synchronized的对象不会关联监视器,不遵从上述规则
总结
- 描述什么是线程安全和不安全
多线程操作一个共享资源得到的结果,和单线程操作一个共享资源得到的结果是相同的,则线程安全;反之,则线程不安全。
- 说出导致线程不安全的原因
- 多线程间有共享变量
- 操作共享变量的代码有多条
- 多线程对共享变量有写操作
- 说出保证线程安全的解决方案和原理
Synchronized同步代码块、同步方法可以保证线程安全
Synchronized底层原理是基于monitor监视锁实现
Monitorenter:获取锁命令
Moniterexit:释放锁指令