并发问题的产生
java-memory-model-3.png
为什么会产生并发问题呢?这个得从Java内存模型(JMM)说起。
JMM将java虚拟机内部划分为堆栈,栈也称线程栈是每个线程独有的。而堆是线程共享的。
每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈,即方法的本地变量是不存在线程安全问题的。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。
20160921182337904.png
所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。
堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。
两程序员小A与小Q的讨论:
Q:为甚么说方法中的变量不存在线程安全问题?
A:因为线程的内部变量属于线程私有,每个线程都有自己的线程栈。
Q:为什么两个线程调用同一个方法时会产生线程安全问题呢?不是说线程栈是线程私有的吗?
A:线程栈确实是线程私有,但是如果线程访问的方法中访问实例变量就会产生线程安全问题。
Q:等等,你前面不是说线程的内部变量是属于线程私有的吗?
A:是的这个说法很正确,对于线程来说调用方法就会创建一个栈帧,然后入线程栈,而方法访问的本地变量是存放在栈里面的,但是成员变量会随着对象存储在堆空间,而对象的引用同样是存储在栈空间的,方法通过引用去访问堆中的对象,又因为堆中的对象是共享的,那么如果两个线程同时对这个共享对象操作你觉得会产生什么结果.
synchronized应用
Java 同步块(synchronized block)用来标记方法或者代码块是同步的。Java同步块用来避免竞争。
背景故事:MT小镇开了一家电影院,电影院老板觉得每次都那么多人过来排队,只有一个窗口效率低下,很多顾客反映本来想看个电影,但是排队等待太久了就放弃了,希望影院能提供更好高效的售票服务。老板一想觉得现在都互联网时代了,这么落后的手工售票方式是时候淘汰了,于是打算招聘技术人员开发这套系统。参加应试的小Q、小A、小M 都想获得这个职位,于是老板就让他们各自设计一个系统
-
共享资源争抢带来的问题
小Q刚步入社会,想着一定要大展身手拿下这个offer,走向人生巅峰,迎娶白富美。于是设计了如下的系统:
售票系统:
public class TicketService { //总票数 private int totalNum = 10; public void saleTicket() { System.out.println(Thread.currentThread().getName() + "请稍等"); if (totalNum > 0) { System.out.println(Thread.currentThread().getName() + "正在为您出票 票号为 " + this.totalNum); this.totalNum--; } }
模拟用户
class UserThread implements Runnable { private TicketService ticketService; public UserThread(TicketService ticketService) { this.ticketService = ticketService; } @Override public void run() { System.out.println("我要买票我是" + Thread.currentThread().getName()); this.ticketService.saleTicket(); } }
上线测试:
public static void main(String args[]) { TicketService ticketService = new TicketService(); UserThread userThread = new UserThread(ticketService); for (int i = 0; i < 10; i++) { Thread thread01 = new Thread(userThread); thread01.setName("User0" + i); thread01.start(); } }
测试结果非常的糟糕:
我要买票我是User00 User00请稍等 User00正在为您出票 票号为 10 我要买票我是User01 User01请稍等 User01正在为您出票 票号为 9 我要买票我是User04 User04请稍等 User04正在为您出票 票号为 8 我要买票我是User05 User05请稍等 我要买票我是User08 User08请稍等 User08正在为您出票 票号为 7 User05正在为您出票 票号为 7 我要买票我是User02 User02请稍等 我要买票我是User06 我要买票我是User03 User03请稍等 User03正在为您出票 票号为 5 User06请稍等 我要买票我是User09 User02正在为您出票 票号为 5 User09请稍等 User06正在为您出票 票号为 4 我要买票我是User07 User07请稍等 User09正在为您出票 票号为 3 User07正在为您出票 票号为 2
-
synchronized 实例方法同步
小A一看机会来了,轮到自己一展身手了,在想上面的方案其实就是没有考虑到共享资源竞争的问题,我只需要控制好资源的竞争就行
方案如下:public synchronized void saleTicket01() { System.out.println(Thread.currentThread().getName()+"请稍等"); if (totalNum > 0) { System.out.println(Thread.currentThread().getName()+"正在为您出票 票号为 " + this.totalNum); this.totalNum--; } }
输出:
我要买票我是User00 我要买票我是User01 User00请稍等 User00正在为您出票 票号为 10 User01请稍等 User01正在为您出票 票号为 9 我要买票我是User04 User04请稍等 我要买票我是User02 我要买票我是User03 我要买票我是User05 User04正在为您出票 票号为 8 我要买票我是User06 User06请稍等 User06正在为您出票 票号为 7 User05请稍等 User05正在为您出票 票号为 6 我要买票我是User09 User03请稍等 User03正在为您出票 票号为 5 User02请稍等 我要买票我是User07 我要买票我是User08 User02正在为您出票 票号为 4 User08请稍等 User08正在为您出票 票号为 3 User07请稍等 User07正在为您出票 票号为 2 User09请稍等 User09正在为您出票 票号为 1
老板觉得可行,这样客户就可以自己在家买票,随时随地买票。而且不会出现一张票卖个多个人的现象。
-
synchronized 实例方法中同步代码块
小M看了之后觉得自己想要拿到offer必须得有比小A更好的方案才行,他想到了性能提升采用同步代码块的方式,在操作共享资源的时候加锁,这样就不会使得不需要加锁的地方也锁上了,导致执行不了,执行效率低下。
改进方案:public void saleTicket02() { System.out.println(Thread.currentThread().getName() + "请稍等"); synchronized (this) { if (totalNum > 0) { System.out.println(Thread.currentThread().getName() + "正在为您出票 票号为 " + this.totalNum); this.totalNum--; } } }
最后老板选择了小M的方案。
- 证明synchronized 同步实例方法时锁的为当前对象
现在假设有两个用户服务窗口分别调用saleTicket01,saleTicket02最终我们发现两个窗口能够做到很好的同步。从而进一步证明加锁的为同一个对象。
public synchronized void saleTicket01() {
System.out.println(Thread.currentThread().getName() + "请稍等");
if (totalNum > 0) {
System.out.println(Thread.currentThread().getName() + "saleTicket01窗口 正在为您出票 票号为 " + this.totalNum);
this.totalNum--;
}
}
public void saleTicket02() {
System.out.println(Thread.currentThread().getName() + "请稍等");
synchronized (this) {
if (totalNum > 0) {
System.out.println(Thread.currentThread().getName() + "saleTicket02窗口 正在为您出票 票号为 " + this.totalNum);
this.totalNum--;
}
}
}
- synchronized 静态方法同步
private static int totalNum = 10;
public static synchronized void saleTicket03() {
System.out.println(Thread.currentThread().getName() + "请稍等");
if (totalNum > 0) {
System.out.println(Thread.currentThread().getName() + "正在为您出票 票号为 " + totalNum);
totalNum--;
}
}
将次方法与saleTicket02对比就能发现他们锁的不是同一个对象,会导致一张票卖多人的问题,那么如何证明这个锁的对象是TicketService.class呢?只需要将saleTicket02改成如下方式,即可保证售票系统正确运行
public void saleTicket02() {
System.out.println(Thread.currentThread().getName() + "请稍等");
synchronized (TicketService.class) {
if (totalNum > 0) {
System.out.println(Thread.currentThread().getName() + "saleTicket02窗口 正在为您出票 票号为 " + this.totalNum);
this.totalNum--;
}
}
}
- 数据类型String的常量池特性
在JVM中存在String常量池缓存的功能。由于传递的参数都是LOCK,两个线程获取的是同一把锁,谁先抢到cpu资源,谁先执行,那么另外一个线程将永远处于等待中,这就是String常量池带来的问题,所以synchronized都不用String做锁,而是采用new Object()。
class PrintService{
public void print(String param){
synchronized (param){
while(true){
System.out.println(Thread.currentThread().getName());
}
}
}
public static void main(String[] args){
PrintService printService = new PrintService();
ThreadA threadA = new ThreadA(printService);
ThreadB threadB = new ThreadB(printService);
threadA.start();
threadB.start();
}
}
class ThreadA extends Thread{
private PrintService printService;
ThreadA(PrintService printService){
this.printService = printService;
}
@Override
public void run() {
super.run();
this.setName("AA");
printService.print("LOCK");
}
}
class ThreadB extends Thread{
private PrintService printService;
ThreadB(PrintService printService){
this.printService = printService;
}
@Override
public void run() {
super.run();
this.setName("BB");
printService.print("LOCK");
}
}
- 任意对象可作为监视器
任意对象都可以作为锁,这个后面的深入理解synchronized的原理章节中会说到。
Object object = new Object();
public void print(){
synchronized (object){
while(true){
System.out.println(Thread.currentThread().getName());
}
}
}
- synchronized锁的可重入性
当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。但是在synchronized中,如果一个线程获得一个锁后再次获取同一把锁是可以获取的,这就是锁的可重入性。
优点:避免了重复获取同一把锁造成的死锁
- 出现异常自动释放锁
网友评论