1,多个线程在操作共享的数据。
2,操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。
就会导致线程安全问题的产生。
还是以上一篇博客中讲的出售火车票问题来讲
class Ticket implements Runnable
{
private int num = 100;
Object obj = new Object();
public void run()
{
while(true)
{
if(num>0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
}
}
}
}
下面是运行结果:
从上面可以看出程序出现了负数,这就是当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算而导致的线程安全问题。
那么怎么解决这种线程安全问题呢?
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
在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--);
}
}
}
}
}
让后再运行就没有了上面出现负数的情况。
java中同步还有另外一种实现方式,那就是同步函数。
同步代码块和同步函数的区别:
同步函数的锁是固定的this
同步代码块的锁是任意的对象。
在实际开发中建议使用同步代码块。
在前面的一篇博客单例模式说到懒汉式在多线程中可能无法保证对象的唯一性,解决代码如下:
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
s = new Single();
}
}
return s;
}
}
可以看到上面的代码中有两处出现了if(s==null),其实这么实现非常有必要,假如某一瞬间线程A和线程B都在调用getInstance()方法,此时s的对象为null,都能通过s==null的判断,由于实现了synchronized加锁机制,线程A进入了锁定的代码中执行实例创建代码,线程B进入排队等待状态,必须等线程A执行完毕后才可以进入锁定的代码,但当A执行完毕时,线程B不再进行s==null的判断,并不知道实例依已经创建,将继续创建实例,导致产生多个单例对象。因此需要再进行一次判断,这种方式被称为双重检查锁定。
总结:
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。
同步的前提:同步中必须的多个线程只能使用同一个锁。也就是说方法或同步的代码块只能被一个线程执行。
这种同步锁的方式称为隐式锁,在JDK 1.5之后将同步和锁封装成了对象并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显式动作。
那就是Lock,lock的作用就是用来替代synchronized,修改后的代码如下:
class Ticket implements Runnable
{
private int num = 100;
Lock l=new ReentrantLock();
public void run()
{
while(true)
{
l.lock();
if(num>0)
{
try{Thread.sleep(10);}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
}
l.unlock();
}
}
}