看到一篇讲线程的故事性文章,觉得很有意思,很佩服作者能这么生动地讲述出来,点击可跳转阅读此文章:《我是一个线程》
继续我的笔记中总结 - -
理解线程安全问题:
下面是书上看到的卖票例子:模拟3个窗口同时在售10张票。
上篇博文笔记总结了多线程创建的两种方式,那我们就分别以这两种实现多线程的方式来解决这个场景。
-
使用继承于Thread类的方式
上Demo:
class SaleTicket extends Thread { int num = 10; // 票数 public SaleTicket(String name) { super(name); } @Override public void run() { while (true) { if (num > 0) { System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票" ); try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } num--; } else { System.out.println("售罄了..."); break; } } } } public class Demo1 { public static void main(String[] args) { //创建三个线程对象 SaleTicket th1 = new SaleTicket("A"); SaleTicket th2 = new SaleTicket("B"); SaleTicket th3 = new SaleTicket("C"); //开启线程 th1.start(); th2.start(); th3.start(); } }
Dmeo1执行结果图为:
从实验结果得到,本来10张的门票却卖出了30张,Why?
原来,这是因为:票数 num 声明为了一个非静态成员变量,而非静态成员变量是在每个对象中都会维护一份数据的。那么创建了三个线程对象就有三个num变量了~即我们这样要使得票数num为三个线程共享一份,因而需要将票数num变量设置为静态成员变量。即:
static int num = 10;//票数 非静态的成员变量,非静态的成员变量数据是在每个对象中都会维护一份数据的。
改完后再运行之,结果如下:
这时候发现确实输出的是10条记录(除去头三条可能出错的三条),说明static起作用,达到了共享的目的。
但细细一看,新的问题随之而来,A、B、C窗口居然会出现售出同一张票的情况!Why?
这便搭上了今天的主题了,线程安全问题。
分析如下:
假设首先A线程抢夺了cpu的执行权,开始执行,当执行到
System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票" );
后(还未执行num--),num为10,此时B线程抢占了cpu执行权开始执行,同样执行到
System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票" );
后,num为50,C线程抢夺了cpu执行权,C同样执行到AB执行的相同的代码处,此时ABC三者num值都为10;因而才会出现以上现象。
那么就得考虑,如何保证当一个线程在执行整块的代码时,不受其他线程的干扰?
于是乎,Java提供了一种同步机制来解决线程安全问题。
同步机制分为同步代码块和同步方法
先来看同步代码块的格式:
synchronized(锁对象){ //需要被同步的代码 ... }
当一个线程要使用火车票这个资源时,我们就交给它一把锁,等它把事情做完后再把锁给另一个需要用这个资源的线程.
此时将上面分析的有可能引发线程安全问题的代码放在synchronized代码块中:
synchronized (SaleTicket.class) { if (num > 0) { System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票" ); try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } num--; } else { System.out.println("售罄了..."); break; } }
再次运行之:
由此得到了我们想要的正确结果。
同步代码块要注意的事项:
1. 锁对象可以是任意的一个对象。
2. 一个线程在同步代码块中sleep了,并不会释放锁对象。
3. 锁对象必须是多线程共享的一个资源,否则锁不住。
4. 如果不存在着线程安全问题,千万不要使用同步代码块,因为会降低效率。
同步代码块也可以使用同步方法的方式来实现:
同步方法即将可能会发生线程安全问题的代码置于一个函数中,上面的实现方式为:
public void run() { while (true) { if (num > 0) { try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.sale(); } else { break; } } }// 同步方法 public synchronized void sale() { if(num > 0) { System.out.println(Thread.currentThread().getName() + "窗口售出了第" + (num--) + "号票" ); } }
同步函数要注意的事项 :
1. 如果是一个非静态的同步函数的锁 对象是this对象,如果是静态的同步函数的锁 对象是当前函数所属的类的字节码文件(class对象)。
2. 同步函数的锁对象是固定的,不能随便指定的。
那么,对于这两种实现同步机制的方式,推荐使用的是:同步代码块
原因也很简单:
1. 同步代码块的锁对象可以由我们随意指定,方便控制。而同步方法的锁对象是固定的。
2. 同步代码块可以很方便控制需要被同步代码的范围,同步函数必须是整个函数 的所有代码都被同步了。
-
使用实现Runnable接口的方式:
Demo2:
class SaleTicket2 implements Runnable { int num = 10; @Override public void run() { while (true) { synchronized (SaleTicket2.class) { if (num > 0) { System.out.println(Thread.currentThread().getName() + "窗口售出了第" + num + "号票"); num--; } else { break; } } } } } public class Demo2 { public static void main(String[] args) { //创建了一个Runnable实现类的对象 SaleTicket2 saleTicket = new SaleTicket2(); //创建三个线程对象模拟三个窗口 Thread th1 = new Thread(saleTicket, "A"); Thread th2 = new Thread(saleTicket, "B"); Thread th3 = new Thread(saleTicket, "C"); //开启线程售票 th1.start(); th2.start(); th3.start(); } }
运行之,结果如下:
同样也得到了正确的结果。
总结:
引发线程安全问题的根本原因是什么?
1. 存在两个或两个以上的线程对象,并且线程之间共享同一个资源。
2. 有多条语句操作了共享资源。
解决方案:
使用同步机制来解决,即通过同步代码块(推荐)或同步方法将可能引发线程安全问题的代码段“锁”住。