在单线程程序中,每次只能做一件事情,后面的事情需要等待前面的事情完成之后才可以进行,但是如果使用多线程程序,就会发生两个线程抢占资源的问题,如两个人同时说话、两个人同时过同一个独木桥等。所以在多线程编程中需要防止这些资源访问的冲突。
5.线程安全
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不行进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
如果不提供数据访问保护,有可能出现多个线程先后更改数据所得到的数据是脏数据。
线程不安全的情况,以较为常见的售票窗口为例:
//实现Runnable接口的售票系统
public class ThreadSafeTest implements Runnable{
int num = 10;//全局变量:总票数
public void run(){//多线程具体实现的内容
while(true){
if(num > 0){
try{
Thread.sleep(100);
//使线程休眠0.1秒,增加出错的可能性
} catch (Exception e){
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
}
public static void main(Sting[] args){
ThreadSafeTest t = new ThreadSafeTest();
Thread a = new Thread(t);
Thread b = new Thread(t);
Thread c = new Thread(t);//创建三个线程
a.start();
b.start();
c.start();//线程启动
}
}
运行结果:
tickets10
tickets9
tickets8
tickets7
tickets6
tickets5
tickets4
tickets3
tickets2
tickets1
tickets0
tickets-1
tickets-2
在该例子中,最后打印售票剩下的票为负值,是因为同时创建了3个线程,这3个线程执行run()方法,在num变量为1时,线程1、线程2、线程3都对num变量有存储功能,当线程1执行run()方法时,还没有来得及做递减操作,就指定它条调用了sleep()方法进入就绪状态,这是线程2、线程3都进入了run()方法,发现num变量依然大于0,各自进入线程中的run()方法,并休眠。之后线程1休眠时间到,将num变量值递减,接着线程2、线程3也都对num变量进行递减操作,从而产生了负值。
6.线程同步
解决多线程资源冲突问题的方法大多都是采用给定时间只允许一个线程访问共享资源,这时就需要给共享资源上一道锁。
方法一:同步代码块(同步块)
1、共享数据:多个线程共同操作的同一个数据(变量);
2、同步监视器:由一个类的对象来充当。哪个线程获取此监视器,谁就执行。
方法二:同步方法
6.1 同步块
在Java中提供了同步机制,可以有效地防止资源冲突。同步机制使用synchronized关键字。
//实现Runnable接口的售票系统
public class ThreadSafeTest implements Runnable{
int num = 10;//全局变量:总票数
public void run(){
//多线程具体实现的内容
Object obj = new Object();//创建任意一个对象
synchronized(obj){
//将需要同步的代码使用synchronized关机键字包括起来。
//对象可使用this关键词,表示该对象为当前对象。
while(true){
if(num > 0){
try{
Thread.sleep(100);
//使线程休眠0.1秒,增加出错的可能性
} catch (Exception e){
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
}//synchronized结束
}
public static void main(Sting[] args){
ThreadSafeTest t = new ThreadSafeTest();
Thread a = new Thread(t);
Thread b = new Thread(t);
Thread c = new Thread(t);//创建三个线程
a.start();
b.start();
c.start();//线程启动
}
}
运行结果:
tickets10
tickets9
tickets8
tickets7
tickets6
tickets5
tickets4
tickets3
tickets2
tickets1
tickets0
这样,我们就避免了原先出现负数的情况。
通常我们将共享资源的操作放置在synchronized定义的区域内,这样当其他线程也获取到这个锁时,必须等待锁被释放时才能进入该区域。
其中Obj为任意一个对象,每个对象都存在一个标志位,并具有两个值,分别为0和1(类似Sql数据库中的bit数据类型)。一个线程运行到同步块时,首先检查该对象的标志位,如果为0状态,表明此同步块中存在其他线程在运行。这是该线程处于就绪状态,知道处于同步块中的线程执行完同步块中的代码为止。这时该对象的标志位被设置为1,该线程才能执行同步块中的代码,并将Object对象的标志位设置为0,防止其他线程执行同步块中的代码。
注意:需要同步数据的线程必须使用同一把锁。使用this关键词,调用当前对象作为同步锁,当创建该线程时,如果多次实例化了当前对象,则会创建多个同步锁,即各个线程使用各自的锁,该同步锁将会失去作用。
6.2 同步方法
同步方法就是在方法前面修饰synchronized关键词的方法。
当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为synchronized,否则就会出错。
public synchronized void doit(){//定义同步方法
if(num > 0){
try{
Thread.sleep(100);
} catch (Exception e){
e.printStackTrace();
}
System.out.println("tickets" + num--);
}
}
public void run(){
while(true){
doit();//在run()方法中调用该同步方法
}
}
将共享资源的操作放置在同步方法中,运行结果与使用同步块的结果一致。
6.3 死锁的问题
死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
解决方法:
特定的算法、原则;
尽量减少同步资源的定义。
7.线程通信
java.lang.Object中定义了三个只能在synchronized方法或synchronized代码块中才能使用的方法:
wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待。
notifyAll():唤醒正在排队等待资源的所有线程结束等待。
若没有在synchronized方法或synchronized代码块中才能使用,则会报:java.lang.IllegaMonitorStateException异常。
管道通信:使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信。
分布式系统中说的两种通信机制:共享内存机制和消息通信机制。
文章中的synchronized关键字就“属于” 共享内存机制。而管道通信,更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个。