线程的安全问题与线程的同步机制
以火车站买票的问题来举例。假设火车站有100张票,分三个窗口售卖这一百张票。
分别用继承Thread类和实现Runnable接口的方式:
实现Runnable接口:
public class WindowTest
{
public static void main(String[] args)
{
//线程的安全问题
SaleTicket ss = new SaleTicket();
Thread t1 = new Thread(ss,"窗口一");
Thread t2 = new Thread(ss,"窗口二");
Thread t3 = new Thread(ss,"窗口三");
t1.start();
t2.start();
t3.start();
//这种情况下,会出现重票错票的问题
}
}
class SaleTicket implements Runnable
{
private int ticket = 100;
public int getTicket()
{
return ticket;
}
@Override
public void run()
{
while(true)
{
if(ticket > 0)
{
try
{
Thread.sleep(10);
} catch (InterruptedException e)
{
e.getStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为" + ticket);
ticket--;
}
else
{
break;
}
}
}
}
继承Thread类:
public class WindowTest2
{
public static void main(String[] args)
{
//线程的安全问题
Window w1 = new Window("窗口一");
Window w2 = new Window("窗口二");
Window w3 = new Window("窗口三");
w1.start();
w2.start();
w3.start();
//同样会出现重票错票的问题
}
}
class Window extends Thread
{
public Window()
{
}
public Window(String name)
{
super(name);
}
private static int ticket = 100;
@Override
public void run()
{
while(true)
{
if(ticket > 0)
{
try
{
Thread.sleep(10);
} catch (InterruptedException e)
{
e.getStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为" + ticket);
ticket--;
}
else
{
break;
}
}
}
}
分别运行可以发现:两中方式卖票时都会出现重票错票的问题。
出现这种现象的原因:
例如,线程一(窗口一)操作ticket的过程中,尚未结束的情况下,其他的线程也参与进来对ticket进行操作。
要解决这种问题,必须保证一个线程(假设是线程一)在操作ticket的过程中,其他线程必须等待,直到线程一操作ticket结束以后,其他线程才可以进来继续操作ticket。
要解决线程的安全问题,就要使用线程的同步机制。
方式一:同步代码块
格式:
synchronized(同步监视器){
//需要被同步的代码
//……
}
说明:
·需要被同步的代码,即为操作共享数据的代码。
·共享数据:即多个线程都需要操作的数据。
·需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
·同步监视器,俗称锁,哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
·同步监视器,可以使用任何一个类的对象充当。多个线程(这些线程要执行同一段需要被同步的代码)必须共用同一个同步监视器。
(同步监视器类似于交通信号灯,多个线程共用同一个同步监视器即多个线程看同一个交通信号灯,多个线程都是只有获取到这个锁之后才能执行相应需要被执行的代码)
·实现Runnable的方式创建线程的方式同步监视器使用this最方便,继承Thread类的方式创建线程的方式使用 当前类名.class 最方便。
使用同步代码块后的卖票的代码如下:
实现Runnable接口:
public class WindowTest
{
public static void main(String[] args)
{
//用同步代码块线程的安全问题
SaleTicket ss = new SaleTicket();
Thread t1 = new Thread(ss,"窗口一");
Thread t2 = new Thread(ss,"窗口二");
Thread t3 = new Thread(ss,"窗口三");
t1.start();
t2.start();
t3.start();
//这种情况下,会出现重票错票的问题
}
}
class SaleTicket implements Runnable
{
private int ticket = 100;
//Object o = new Object();
public int getTicket()
{
return ticket;
}
@Override
public void run()
{
while(true)
{
try
{
Thread.sleep(10);
} catch (InterruptedException e)
{
e.getStackTrace();
}
//synchronized(o){
//如果多个线程要使用同一段同步的代码,同步监视器中的对象要保证这些线程使用同一个锁。
synchronized(this)//同步监视器中可以放任何一个唯一对象,只要保证唯一。一般情况下放this最方便。
{
if(ticket > 0)
{
try
{
Thread.sleep(10);//调用sleep代码只是为了更好的显示同步代码块时轮流买票的情况。
} catch (InterruptedException e)
{
e.getStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为" + ticket);
ticket--;
}
else
{
break;
}
}
}
}
}
继承Thread类:
public class WindowTest2
{
public static void main(String[] args)
{
//线程的安全问题
Window w1 = new Window("窗口一");
Window w2 = new Window("窗口二");
Window w3 = new Window("窗口三");
w1.start();
w2.start();
w3.start();
//同样会出现重票错票的问题
}
}
class Window extends Thread
{
public Window()
{
}
public Window(String name)
{
super(name);
}
private static int ticket = 100;
//static Object oo = new Object();
@Override
public void run()
{
while(true)
{
//synchronized (this)//w1,w2,w3使用的并非同一个同步监视器,相当于三个线程看的“交通信号灯”不是同一个。
//synchronized (oo)
synchronized (Window.class)//继承Thread类的方式,同步监视器建议使用 当前类名.class
{
if(ticket > 0)
{
try
{
Thread.sleep(100);
} catch (InterruptedException e)
{
e.getStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为" + ticket);
ticket--;
}
else
{
break;
}
}
}
}
}
方式二:同步方法
如果操作共享数据的代码完整地声明在了一个方法中,那么我们就可以将此方法声明为同步方法。(在方法声明中加上synchronized)
·非静态的同步方法,默认同步监视器为this
·静态的同步方法,默认同步监视器是 当前类名.class 。
具体代码如下:
实现Runnable接口:
public class WindowTest
{
public static void main(String[] args)
{
//线程的安全问题
//使用同步方法解决实现Runnable接口的方式的线程安全问题
SaleTicket ss = new SaleTicket();
Thread t1 = new Thread(ss,"窗口一");
Thread t2 = new Thread(ss,"窗口二");
Thread t3 = new Thread(ss,"窗口三");
t1.start();
t2.start();
t3.start();
}
}
class SaleTicket implements Runnable
{
private int ticket = 100;
public int getTicket()
{
return ticket;
}
@Override
public void run()
{
boolean b = true;//判断ticket是否大于0
while(b)
{
show(b);
}
}
public synchronized void show(boolean b)//此时的同步监视器是this
{
if(ticket > 0)
{
try
{
Thread.sleep(10);
} catch (InterruptedException e)
{
e.getStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为" + ticket);
ticket--;
}
else
{
b = false;
}
}
}
继承Thread类:
public class WindowTest2
{
public static void main(String[] args)
{
//线程的安全问题
Window w1 = new Window("窗口一");
Window w2 = new Window("窗口二");
Window w3 = new Window("窗口三");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread
{
public Window()
{
}
public Window(String name)
{
super(name);
}
private static int ticket = 100;
@Override
public void run()
{
boolean b = true;
while(b)
{
show(b);
}
}
//public synchronized void show(boolean b)//同步监视器还是默认为this,而this在这里三个不同的线程过来时分别指三个对象,不能解决问题
public static synchronized void show(boolean b)//此时同步监视器:当前类名.class//注意:不是使用静态的情况不要使用此方式,应该继续使用同步代码块的方式
{
if(ticket > 0)
{
try
{
Thread.sleep(10);
} catch (InterruptedException e)
{
e.getStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,票号为" + ticket);
ticket--;
}
else
{
b = false;
}
}
}
synchronized关键字
好处:解决了线程安全问题。
弊端:在操作共享数据指多线程其实是串行执行的,意味着性能低。