前言
本篇文章是多线程系列的第二篇(第一篇可参考多线程(一)),主要讲解:线程安全问题、同步、锁。文章讲解的思路是:先通过一个例子引出一系列问题,然后再通过多种方式尝试解决,最终引出解决方案。大家可以根据我的目录进行选择性地查看。文章的重点部分我都会用红色字体展示。
正文
如何实现售票功能?
考虑如下情景:动物园门口有4个人工售票窗口同时出售100张票,那么我们如何使用多线程技术来描述这一场景呢?
class Ticket extends Thread
{
private int num = 100; // 一共售卖100张票
public void run()
{
while(true)
{
if(num > 0)
{
System.out.println(Thread.currentThread().getName() + "......sale..." + num--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
// 用4个线程模拟4个人工售票窗口
Thread t1 = new Thread();
Thread t2 = new Thread();
Thread t3 = new Thread();
Thread t4 = new Thread();
t1.start();
t2.start();
t3.start();
t4.start();
}
}
上面代码的问题在于:由于Ticket类的num属性不是共享的,这就导致4个窗口最后一共卖出了400张票,这显然不是我们需要的。利用我们之前学的知识点,我们首先可以想到:是否可以将num属性设置为static的呢?就像下面这样:
class Ticket extends Thread
{
private static int num = 100; // 将num使用static修饰,这样num就是共享的了。
public void run()
{
while(true)
{
if(num > 0)
{
System.out.println(Thread.currentThread().getName()+"......sale..." + num--);
}
}
}
}
经过测试这样做确实解决了一共卖400张票的问题,但是这样做不太符合实际生活:因为这100张票当中可能有一部分是学生票,有一部分是退休军人票,使用static就无法进行区分了。于是我们只能转换思路,是否可以像下面这样呢:
class Ticket extends Thread
{
private int num = 100;
public void run()
{
while(true)
{
if(num > 0)
{
System.out.println(Thread.currentThread().getName()+"......sale..." + num--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t1 = new Ticket();
t1.start();
t1.start();
t1.start();
t1.start();
}
}
虽然这样做不符合题目规定,但我们可以通过执行结果看出:多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
我们只好继续转换思路:既然继承Thread类有那么多问题,那我们实现Runnable接口会怎样呢:
class Ticket implements Runnable
{
private int num = 100;
public void run()
{
while(true)
{
if(num > 0)
{
System.out.println(Thread.currentThread().getName()+"......sale..." + num--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket(); // 创建一个线程任务对象
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
通过实现Runnable接口并创建一个任务(此处即是卖票),4个线程就可以共享此任务。这样就能成功实现售票功能。
多线程安全问题以及产生的原因?
我们发现当Ticket类变成下面这样时,"售票功能"就会再一次出现问题:
class Ticket implements Runnable
{
private int num = 100;
public void run()
{
while(true)
{
if(num > 0)
{
try{Thread.sleep(10);}catch (InterruptedException e){} // 让线程在此处"停顿"一会儿
System.out.println(Thread.currentThread().getName()+"......sale..." + num--);
}
}
}
}
通过多次执行上面的代码,我们会发现问题:有些"窗口"卖出了0号甚至-1号票。这其实就是一种多线程安全问题。那么我们在更好地解决这个问题之前需要先分析该问题为什么会出现。原因分析参考下图:
于是我们就可以知道:当多个线程在执行操作共享数据的多条代码时,就有可能会出现线程安全问题隐患。在这里我们还需要注意一点:如果多个线程只是去访问共享数据,那么就不会产生线程安全问题,而如果有线程对共享数据进行了修改操作,那么就会存在线程安全问题隐患。分析完原因之后,我们的解决思路大致就清晰了:我们可以将多条操作共享数据的代码封装起来,在线程执行这些代码期间其他线程不可以参与运算。只有当当前线程把代码都执行完毕后,其他线程才可以参与运算。
同步?
同步代码块解决方案?
在Java中,我们就可以使用同步代码块来封装这些代码。
同步代码块的格式:
synchronized(对象) // 关于括号中对象的作用,此处可以理解为一个"锁",关于它的作用下面还会提到。
{
// 需要被同步的代码;
}
于是,上面的"售票功能"就可以使用如下代码进行解决:
class Ticket implements Runnable
{
private int num = 100;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj) // 同步代码块
{
if(num > 0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName() + "......sale..." + num--);
}
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
同步的好处、弊端以及前提?
如上所述,同步代码块成功地解决了"售票功能"的线程安全问题。并且根据对线程安全问题的分析,我们可以总结出同步的好处和弊端:
-
好处:它可以解决线程的安全问题。
-
弊端:由于同步外的线程都会判断"同步锁"(即同步代码块中的对象),所以它效率就相对降低了。
-
上面说到:当多条操作共享数据的代码被同步代码块封装起来之后,在某一个线程执行这些代码期间其他线程不可以参与运算。这句话其实是有前提的,也就是实现同步是有前提的。前提即是:这多个线程必须使用同一把"锁"。我们可以这样理解:这里的"锁"其实起着监视这多条线程的作用。我们其实可以通过一个简单的方法进行验证:
class Ticket implements Runnable
{
private int num = 100;
public void run()
{
Object obj = new Object(); // 将"锁"变成了局部变量
while(true)
{
synchronized(obj)
{
if(num > 0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName() + "......sale..." + num--);
}
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
由于上面代码中的多条线程使用的不是同一把"锁",于是"售票功能"的线程安全问题就又出现了。
同步函数
通过上面的代码我们其实可以发现:方法和同步代码块都是一种封装体,那我们也可以让方法具备同步功能,这其实就是同步函数。例如:
class Ticket implements Runnable
{
private int num = 100;
public void run()
{
while(true)
{
show();
}
}
public synchronized void show() // 同步函数
{
if(num > 0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName() + "......function..." + num--);
}
}
}
那么问题就来了:同步代码块有"锁",那同步函数的"锁"又是什么呢?我们可以通过如下思路来进行验证:还是上面的"卖票功能",只不过我们只创建两个线程,让其中一个线程在同步代码块中运行,让另外一个线程在同步函数中运行,如果它们使用的是同一个"锁",那么"卖票功能"就不会出现线程安全问题,反之,就会出现线程安全问题。(这样做的目的是同步代码块可以指定"锁"),代码如下:
class Ticket implements Runnable
{
private int num = 100;
boolean flag = true;
public void run()
{
if(flag)
while(true)
{
synchronized(this) // 同步代码块卖票;这个this就是线程任务t
{
if(num > 0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....obj...." + num--);
}
}
}
else
while(true)
this.show(); // 同步函数卖票
}
public synchronized void show()
{
if(num > 0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName() + ".....function...." + num--);
}
}
}
class SynFunctionLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(InterruptedException e){}
t.flag = false;
t2.start();
}
}
通过上面的运行结果我们可以验证:上面代码中的两个线程使用的是同一把"锁",即同步函数使用的"锁"是this。由于同步代码块的"锁"可以是任意对象(但注意最好不要使用字符串对象,因为字符串常量池具有缓存功能),所以在开发中建议使用同步代码块。
验证完了同步函数的锁之后,我们知道static与this不能共存,所以我们不免疑惑:静态同步函数的"锁"又是什么呢?我们可以按照上面相同的思路再次来进行验证:
class Ticket implements Runnable
{
private static int num = 100;
boolean flag = true;
public void run()
{
if(flag)
while(true)
{
synchronized(Ticket.class) // 同步代码块卖票
{
if(num > 0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName() + ".....obj...." + num--);
}
}
}
else
while(true)
show(); // 同步函数卖票
}
public static synchronized void show() // 静态同步函数
{
if(num > 0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName() + ".....function...." + num--);
}
}
}
class SynFunctionLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(InterruptedException e){}
t.flag = false;
t2.start();
}
}
通过上面的运行结果我们可以验证:静态同步函数使用的"锁"是该函数所属的字节码文件对象,该对象可以用"getClass()"获取,也可以用"当前类名.class"获取。