1.线程同步
写一个模拟现实售票的例子。
首先,定义一个售票类Tickets。
//售票类
class Tickets extends Thread {
//定义总票数为100,因为多个窗口售票,所以定义为静态的,即类成员共享
private static int tickets = 100;
public Tickets(String name) {
super(name);
}
//重写run方法,封装运行代码
public void run() {
//循环售票
while(true) {
//如果票数大于0,则可以继续出售。每售出一张,票数减一
if(tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出第" + (tickets--) + "号票");
}
}
}
}
接下来,开启四个线程模拟四个售票窗口
public class TicketDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
//创建四个售票窗口
Tickets t1 = new Tickets("1号窗口");
Tickets t2 = new Tickets("2号窗口");
Tickets t3 = new Tickets("3号窗口");
Tickets t4 = new Tickets("4号窗口");
//开始售票
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行程序,可以看到四个窗口开始售票,直到票售完为止。
但是,这个程序有没有安全隐患呢?
我们把售票类的循环体内代码稍微改动一下。
//售票类
class Tickets extends Thread {
//定义总票数为100,因为多个窗口售票,所以定义为静态的,即类成员共享
private static int tickets = 100;
public Tickets(String name) {
super(name);
}
//重写run方法,封装运行代码
public void run() {
//循环售票
while(true) {
//如果票数大于0,则可以继续出售。每售出一张,票数减一
if(tickets > 0) {
//让线程执行到此,休眠10毫秒
//多线程操作时,会出现安全问题,可能出现0,-1,-2号票
try{
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出第" + (tickets--) + "号票");
}
}
}
}
再次运行程序,会出现如下运行结果:
我们看到,售票售出了0、-1、-2号票,当然,每次运行的结果可能不同,但是可能会出现与现实情况不符的现象。
这是为什么呢?
这就是多线程同步的安全问题。出现问题的原因是:多个线程(4个窗口)操作同一共享数据(票)时,一条线程只执行了一部分,还没执行完,而另一条线程参与进来执行,导致共享数据错误。就如上例所示,假使票卖到只剩1张时,一个窗口将要出售最后一张票。当其判断票数大于0成立后,还没执行下一句,另一条线程又抢夺到cpu的执行权,同样判断票数大于0成立。于是,下面无论这两条线程哪一条抢夺到cpu的执行权,这两条线程都会先后执行,就会出现票为-1的现象。
如何解决多线程安全问题呢?
对多条线程操作共享数据时,只能让一个线程全都执行完。在执行过程中,其他线程不可以参与执行。
Java对于多线程安全问题,提供了专业的解决方式:使用同步代码块。
synchronized(对象)
{
要被同步的代码;
}
对象如同锁,持有锁的线程可以在同步中执行。没有锁的线程,即便获取了cpu的执行权,也无法执行。
我们把售票类的代码加上同步代码块
//售票类
class Tickets extends Thread {
//定义总票数为100,因为多个窗口售票,所以定义为静态的,即类成员共享
private static int tickets = 100;
public Tickets(String name) {
super(name);
}
//重写run方法,封装运行代码
public void run() {
//循环售票
while(true) {
//同步代码块,使用本类字节码对象作为锁
synchronized(Tickets.class) {
//如果票数大于0,则可以继续出售。每售出一张,票数减一
if(tickets > 0) {
//让线程执行到此,休眠10毫秒
//多线程操作时,会出现安全问题,可能出现0,-1,-2号票
try{
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出第" + (tickets--) + "号票");
}
}
}
}
}
此时再次运行程序,将会看到不会在出现0、-1、-2号票。
最终,我们去掉使线程休眠的代码,完成售票类的最终代码。
//售票类
class Tickets extends Thread {
//定义总票数为100,因为多个窗口售票,所以定义为静态的,即类成员共享
private static int tickets = 100;
public Tickets(String name) {
super(name);
}
//重写run方法,封装运行代码
public void run() {
//循环售票
while(true) {
//同步代码块,使用本类字节码对象作为锁
synchronized(Tickets.class) {
//如果票数大于0,则可以继续出售。每售出一张,票数减一
if(tickets > 0) {
System.out.println(Thread.currentThread().getName() + "售出第" + (tickets--) + "号票");
}
}
}
}
}
注:能不能将while循环放在同步代码块中呢?
不能!否则会出现死循环!因为拿到锁的线程会一直将同步代码块都执行完才会释放锁,而while(true)会一直循环下去。
同步的前提:
1)必须要有两个或两个以上的线程
2)必须是多个线程使用同一个锁
如何判断哪些代码要加同步呢?
1)明确哪些代码是多线程运行代码
2)明确共享数据
3)明确多线程运行代码中哪些语句操作共享数据
2.死锁
使用线程同步,有时会出现死锁的情况。看下面程序:
package com.itheima;
/**
* 死锁程序
* @author YP
* */
public class DeadLockDemo {
public static void main(String[] args) {
// TODO Auto-generated method stub
//开启两个线程
new Thread(new DeadLock(true)).start();
new Thread(new DeadLock(false)).start();
}
}
//提供锁的类
class MyLock
{
//为了方便运用锁,将锁定义为静态的
static Object locka = new Object();
static Object lockb = new Object();
}
//实现implements接口创建线程类
class DeadLock implements Runnable {
//判断执行何种语句的标志
private boolean flag;
DeadLock(boolean flag){
this.flag = flag;
}
public void run() {
if(flag){
//先要获得a锁才可执行下面语句
synchronized(MyLock.locka){
System.out.println("if locka");
//要继续执行下面语句,必须再获取b锁
synchronized(MyLock.lockb) {
System.out.println("if lockb");
}
}
}else{
//先要获得b锁才可执行下面语句
synchronized(MyLock.lockb){
System.out.println("else lockb");
//要继续执行下面语句,必须再获取a锁
synchronized(MyLock.locka) {
System.out.println("else locka");
}
}
}
}
}
运行程序:
我们看到,出现了死锁。
死锁是如何出现的呢?
当一个线程拥有A锁,还要去拿B锁才可执行;而另一个线程拥有B锁,还要去拿A锁才可执行。两个线程互相僵持,致使死锁情况出现。
所以,我们写代码时要避免死锁情况的出现。给出如下建议:
1、在程序中尽量使用开放调用。依赖于开放调用的程序,相比于那些在持有锁的时候还调用外部方法的程序,更容易进行死锁自由度的分析。重新构建synchronized使开放调用更加安全。所谓开放调用是指调用的方法本身没有加锁,但是要以对方法操作的内容进行加锁。
2、如果你必须获得多个锁,那么锁的顺序必须是你设计工作的一部分:尽量减少潜在锁之间的交互数量,遵守并文档化该锁顺序协议。监测代码中死锁自由度的策略有:
1)识别什么地方会获取多个锁,并使锁数量尽可能少,保证它们的顺序在程序中一致。
2)在没有非开放调用的程序中,发现那些获得多重锁的实例是非常简单的。
3、尝试定时的锁,使用每个显式Lock类中定时tryLock特性,来替代使用内部锁机制。
2、如果你必须获得多个锁,那么锁的顺序必须是你设计工作的一部分:尽量减少潜在锁之间的交互数量,遵守并文档化该锁顺序协议。监测代码中死锁自由度的策略有:
1)识别什么地方会获取多个锁,并使锁数量尽可能少,保证它们的顺序在程序中一致。
2)在没有非开放调用的程序中,发现那些获得多重锁的实例是非常简单的。
3、尝试定时的锁,使用每个显式Lock类中定时tryLock特性,来替代使用内部锁机制。