说到线程同步就会想到在单线程中每次只能做一件事情,后面的程序代码必须等待前面的代码执行完才能接着执行。如果使用的是多线程的话,就要先考虑两个线程之间抢占资源的问题。如两个用一双筷子,两个人过要做独木桥,两个人看一本书等等。在java中提供了线程同步的机制来防止资源访问的冲突。
首先来说说线程安全。
一、线程安全:
在实际开发中我们会遇到很多问题,因为多线程程序的情况有很多。以火车站售票系统为例,在代码中判断当前票数是否大于0,如果大于0则执行售票给乘客的功能,但是当两个线程同时访问这段代码时(假如此时只剩一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断是否有票的判断,并得出票数大于零的情况,于是也会执行售票操作,这样就会产生负数;所以在编写多线程代码时应该注意线程安全问题,即两个线程同时存取单一对象的数据;
下面是一个实例一,主要模拟火车站售票系统的功能;
package ThreadSafe;
class ThreadSafeTest implements Runnable{
private int tickets=10;//设置当前票数
@Override
public void run() {
System.out.println("----1111");
// TODO Auto-generated method stub
while(true){
if(tickets>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//输出当前线程的剩余票数
System.out.println("tickets"+Thread.currentThread().getName()+":"+tickets--);
}
}
}
public static void main(String []args){
ThreadSafeTest thread = new ThreadSafeTest();//实例化一个对象
Thread A = new Thread(thread);//以该类对象实例化四个线程
Thread B = new Thread(thread);
Thread C = new Thread(thread);
Thread D = new Thread(thread);
A.start();
B.start();
C.start();
D.start();
}
}
上述代码输出结果:
从上图可以看出,最后出售剩余的票数为负数,这在实际的售票系统中是需要避免的;
那么为什么会出现上述的情况呢?由于创建了4个线程,这4个线程都会执行run()方法,在ticke变量变为1的时候,线程0、线程1、线程2、线程3都会tickets变量有存储功能,当线程0执行run方法时,还没来得及做递减操作,就指定它调用了sleep()方法进入了就绪状态,这时线程1、线程2和线程3都进入了run()方法,发现变量依然大于0,但此时线程0休眠时间一到,将ticke变量递减,同时线程1,、线程2、线程3页都对tickets变量进行递减操作,从而产生了负值。
二、线程同步机制
线程同步机制是用来干嘛用的呢?是用来解决资源共享问题的,基本上所有解决多线程资源中冲突问题的方法都是采用给定的定时时间只允许一个线程访问共享资源,这时就需要给共享资源加上一道锁。就好比你们班上有一本书在你手上,等你看完后,其他同学才能拿来阅读。
线程同步方式有两种:
1.同步代码块
2.同步方法
先来说一说同步代码块;
*同步代码块:
在java中提供了同步机制,可以有效的防止资源冲突。同步机制使用synchronized关键字。
下面一个实例二,只需在实例一种修改run()方法,把tickets操作设置在同步代码块中。
package ThreadSafe;
class ThreadSynchronized implements Runnable{
private int tickets=10;
//static Object obj;
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
synchronized(""){
if(tickets>0){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//输出当前线程的票数
System.out.println("tickets"+Thread.currentThread().getName()+":"+tickets--);
}
}
}
}
public static void main(String []args){
ThreadSynchronized thread = new ThreadSynchronized();//实例化一个对象
Thread A = new Thread(thread);//以该类对象实例化四个线程
Thread B = new Thread(thread);
Thread C = new Thread(thread);
Thread D = new Thread(thread);
A.start();
B.start();
C.start();
D.start();
//obj = new Object();
}
}
输出结果:
从上图可以看出输出结果打印票数没有出现负数,这时因为将资源放到了同步代码块中。这个代码块通常被称为临界区,它使用synchronized关键字建立,但是上述代码每次的输出结果会不一样,它的语法如下所示:
synchronized(Object){
//实例代码
}
通常将共享资源的操作放置在synchronized定义的区域中,这样当其他线程也获取到这个锁时,必须等待锁被释放才能进入该区域。Object为任意一个对象,每个对象都存在一个标志位,并具有两个值,分别为0和1。一个线程运行到同步代码块时首先检查该对象的状态,如果为0状态,表明此同步代码块中国存在其他线程在运行。这时该线程处于就绪状态,直到处于同步代码块中的线程执行完同步代码块中的代码为止。这时该对象的标志位被设置为1,该线程才能执行同步代码块中的代码,并将Object对象标志位设置为0,表示其他线程执行同步代码块中的代码。
*同步方法:
同步方法就是在方法前面用synchronized修饰,它的语法如下:
synchronized void f(){ //定义同步方法
//代码
}
当某个对象调用同步方法时,该对象上的其他方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为synchronized,否则会出错;
下面是一个实例三代码,将共享资源放在类的同步方法中;
package ThreadSafe;
class ThreadSynMethod implements Runnable {
private int tickets = 10;
@Override
public synchronized void run() {
// TODO Auto-generated method stub
while (true) {
if (tickets > 0) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 输出当前线程的票数
System.out.println("tickets" + Thread.currentThread().getName() + ":" + tickets--);
}
}
}
public static void main(String []args){
ThreadSynMethod thread = new ThreadSynMethod();//实例化一个对象
Thread A = new Thread(thread);//以该类对象实例化四个线程
Thread B = new Thread(thread);
Thread C = new Thread(thread);
Thread D = new Thread(thread);
A.start();
B.start();
C.start();
D.start();
}
}
从上图可以看出实例三的运行结果和实例二的运行结果一致,没有打印出负数的售票数。总而言之,无论是同步代码块还是同步方法都能实现线程的同步。
参考:java从入门到精通书籍