线程安全问题--引出同步代码块
通过下面的例子,了解传统多线程存在的线程安全隐患。
/*
需求:买票;
四个窗口同时买票。
*/
class Ticket implements Runnable//extends Thread
{
private/*static*/ int num = 3;//使用静态可以实现静态数据的共享。
public void run()
{
while(num>0)
{
/*try{
//sleep(long millis):该方法可使调用该方法的线程休眠指定的时间段。但是,该线程不丢失任何监视器的所属权。
Thread.sleep(10);//使用该方法,使当前线程休眠指定的时间段。
}catch (Exception e) {}
*/
System.out.println(Thread.currentThread().getName()+"...sale..."+num--);
}
}
}
public class ThreadProblems {
/**
* @param args
*/
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();*/
Ticket1 tt = new Ticket1();
Thread tt1 = new Thread(tt);
Thread tt2 = new Thread(tt);
Thread tt3 = new Thread(tt);
Thread tt4 = new Thread(tt);
tt1.start();
tt2.start();
tt3.start();
tt4.start();
/*
使用这种方式,会导致每一个线程中都使用num,会卖出总共400张票,因为,每创建一个Ticket对象,都会初始化一个num属性。
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();*/
/*t1.start();
//t1.start();//多次启动一个线程是非法的。对t1开启两次线程,会产生线程启动异常,
t2.start();
t3.start();
t4.start();
*/
}
}
一开始,使用被注释的代码执行,代码执行会出现如下的结果(这只是结果的一种,因为,多线程的结果不固定):
Thread-3...sale...3Thread-0...sale...2
Thread-1...sale...1
Thread-2...sale...0
Thread-3...sale...-1
Thread-0...sale...-2
会看到出现了0、-1、-2的结果,票数出现了小于等于零的现象,这就是线程不够安全的表现。
原因:由于CPU切换线程是随机的,有可能会导致某一个线程进入共享数据判断和处理模块并剥夺其控制权,使其进入等待队列;而切换到
另一个线程中。从而导致对共享数据处理的问题,产生线程安全问题。
线程安全产生的原因:
1、多个线程在操作共享的数据。
2、操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。就会导致线程安全问题的产生。
解决思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。 必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
这时就引入了同步代码块的概念:
封装的方法:
使用synchronized同步关键字,封装代码。
synchronized(对象)
{
需要被同步的代码;
}
对象:相当于一个标记、标志锁。
将上面的例子改为:
class Ticket implements Runnable//extends Thread
{
private <span style="color:#ff6666;">/*static*/</span> int num = 3;//使用静态可以实现静态数据的共享。
public void run()
{
synchronized(this)//通过添加同步代码块,从而解决了上述的线程安全问题。
{
while(num>0)
{
System.out.println(Thread.currentThread().getName()+"...sale..."+num--);
}
}
}
}
为什么要在synchronize的后面引入一个对象参数?
这个对象具有锁的特征。确保在同步代码块中这有一个线程在操作同步数据。只有当同步代码块中没有线程,才会允许其他的线程进入。
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程都会判断同步代码锁。
同步的前提:
1、同步中必须有多个线程,并且处理一个共享数据。
2、操作同步代码块时使用同一个锁。
同步函数:
不使用同步代码块,将要操作的共享代码封装成一个方法,并对该方法定义,使其实现同步代码块的功能。
package ThreadDemo;
/*
* 需求:
* 储户,两个。每个都到银行存钱每次存100,共享三次。
*
*同步函数使用的锁是:this;
*
*同步函数和同步代码块的区别:
* 同步函数的锁是固定的this;而同步代码块的锁是任意的对象。
* 建议使用同步代码块。
*静态的同步函数使用的锁是:该函数所属字节码文件对象。
*可以用 getClass方法获取,也可以用当前类名.class表示。
*/
class Bank
{
private int sum;
//private Object obj = new Object();//同步代码块使用的锁在使用同步函数的时候,就失去了意义。那么同步函数的锁是什么?
public synchronized void add(int num)//同步函数。同步函数中的锁是:this
{
//synchronized(obj){
sum = sum +num;
System.out.println("sum="+sum);
//}
}·
}
class Cus implements Runnable
{
private Bank b = new Bank();
public void run()
{
for(int x=0;x<3;x++)
{
b.add(100);
}
}
}
public class ThreadDemo2 {
/**
* @param args
*/
public static void main(String[] args) {
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
通过使用上面介绍的同步代码块或同步函数,很好的解决了多线程的安全问题。但是,仍然有一些问题,我们需要对其进行进一步的探索。