您的位置:首页 > 职场人生

黑马程序员——java基础拾遗之多线程(二) 线程同步、线程通信

2014-08-25 19:16 561 查看
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

线程安全的概念:当多个线程同时运行一段代码时,如果结果和单线程运行时一致,而且其他变量也和预期的一致,说明是这段代码是线程安全的。

但是,多线程运行的过程中会出现单线程时候不会出现的问题,大多出现在多个线程同时操作全局变量或者静态变量的时候。当出现这种场景的时候,往往会出现和预期不一致的程序运行结果。比如下面的例子:

但是,多线程运行的过程中会出现单线程时候不会出现的问题,大多出现在多个线程同时操作全局变量或者静态变量的时候。当出现这种场景的时候,往往会出现和预期不一致的程序运行结果。比如下面的例子:

还是经典的卖票程序:

<span style="font-size:14px;"><span>		</span>class Ticket implements Runnable
{
private int ticket = 100;
Object obj = new Object();
public void run(){
while(true){
if(ticket > 0){
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"sell the ticket " + ticket--);
}
}
}
}

public class ThreadRunableTest
{
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();
}
}</span>


这里在卖票数ticket--的前面加上这样一条语句try{Thread.sleep(10);}catch(Exception e){},目的是让线程在票数--前停10毫秒,这时线程安全的问题就暴露出来了,控制台打印出如下的内容:

……

Thread-3sell the ticket 1

Thread-0sell the ticket 0

Thread-1sell the ticket -1

Thread-2sell the ticket -2


if(ticket > 0)的条件没有生效,为什么会这样?其实很好分析:

1.Thread.sleep(10)这句话让线程在执行--操作之前停止了10毫秒,当ticket为1时,后续的其他就绪态的线程获得CPU的时间片,会继续进入if(ticket > 0);

2.ticket为1的时候,进入if(ticket > 0)的线程可能不止一个,这些线程都因为Thread.sleep(10)停止而没有去执行--操作。

3.被停止的线程10毫秒后继续执行--操作,这时多个已经进入循环的线程对ticket多次--,就造成了最后三条数据——

Thread-0sell the ticket 0

Thread-1sell the ticket -1

Thread-2sell the ticket -2



这就是最常见的由于多个线程同时操作共享数据而造成的线程安全问题。

如何解决这个问题,java提供了很多方式去解决这个问题,下面依次对这些方式进行介绍:

1.同步代码块

格式:

synchronized(同步对象){

需要被同步的代码

}

这里需要注意几个点:

1.两个以上的线程才需要同步代码块;

2.多个线程必须使用同一个锁;

3.同步对象可以是任何类的对象,自定义的类对象,或者干脆是Object类的对象;

4.需要被同步的代码是指线程运行代码中,操作共享数据的代码。同步代码块会保证代码块中的代码被在同一个锁对象持有的时候,会持续拥有CPU资源直到代码执行完毕,这个过程中不会有其他线程插入;通俗点理解就是一个线程一旦进入同步代码块会不被打断的执行结束,保证了结果的正确。




优点:解决了线程安全问题;

弊端:每个线程都要判断锁,效率有所下降;


修改后的Ticket类

<span style="font-size:14px;"><span>	</span>class Ticket implements Runnable
{
private int ticket = 100;
Object obj = new Object();
public void run(){
while(true){
synchronized (obj) {
if(ticket > 0){
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"sell the ticket " + ticket--);
}
}
}
}
}</span>


运行结果:

Thread-0sell the ticket 4

Thread-0sell the ticket 3

Thread-0sell the ticket 2

Thread-0sell the ticket 1


结果正确,这里多个线程用的都是同一个锁obj。

注意:不要将run()方法中的所有代码都加入到同步代码块中,这样本质上就变成了单线程,失去了多线程的意义。

2.同步函数

先看下面一个实例程序:

<span style="font-size:14px;"><span>		</span>class Bank
{
private int sum;
public void add(int n){
sum = sum + n;
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);
}
}
}

class BankDemo
{
public static void main(String[] args) {
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}</span>


这个小程序模拟两个储户往银行里存钱,分别存300,每个储户分三次存,每次100;

执行结果:

sum=100

sum=200

sum=300

sum=400

sum=500

sum=600


这个程序也是有线程安全隐患的,可以看出Bank类的add()方法,sum = sum + n;和 System.out.println("sum="+sum);两句都是操作共享数据的,但是这两句话没有同步,意味着这两句执行的空隙可能被其他线程插入。用sleep函数来模拟这个过程,可以看到问题所在;

修改后的Bank类

<span style="font-size:14px;"><span>	</span>class Bank
{
private int sum;
public void add(int n){
sum = sum + n;
try{Thread.sleep(10);}catch(Exception e){}
System.out.println("sum="+sum);
}
}</span>


执行结果:

sum=200

sum=200

sum=400

sum=500

sum=600

sum=600


可以看出,一旦sum = sum + n;执行完,发生中断,没有立即输出,而是CPU时间片分配给其他的线程,则可能出现两个线程在都执行完sum = sum + n;后,一起输出sum的情况,此时sum的值是已经变动过不止一次的,所以会出现输出两个200,两个600的情况。

这个时候就需要同步函数了,在这个小例子中,需要同步的数据都在Bank的add()方法中,因此可以将add()函数定义为同步函数。定义方法很简单,就是在方法上加synchronized关键字修饰。

修改过的add函数:

<span style="font-size:14px;"><span style="white-space:pre">	</span>public synchronized void add(int n){
<span style="white-space:pre">		</span>sum = sum + n;
<span style="white-space:pre">		</span>try{Thread.sleep(10);}catch(Exception e){}
<span style="white-space:pre">		</span>System.out.println("sum="+sum);
<span style="white-space:pre">	</span>}</span>


之前买票的例子,也可以改成用同步函数的方式,只要将while(true)中的代码单独定义为一个同步函数即可,修改后的Ticket类代码:

<span style="font-size:14px;"><span style="white-space:pre">	</span>class Ticket implements Runnable
{
private int ticket = 100;
Object obj = new Object();
public void run(){
while(true){
show();
}
}
public synchronized void show(){
if(ticket > 0){
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"sell the ticket " + ticket--);
}
}
}</span>


3.线程间通信

线程间通信常用在多个线程操作同一个资源,但是操作不同的情况下;不同的操作之间有先后的次序,java中用线程间通信的方式,来解决这种情况下的线程安全问题。

先看下面这段代码:

<span style="font-size:14px;"><span style="white-space:pre">	</span>class Res
{
String name;
String sex;

@Override
public String toString(){
return name+"....."+sex;
}
}

class input implements Runnable
{
private Res r;
input(Res r){
this.r = r;
}

public void run(){

int x = 0;
while(true){
if(0 == x){
r.name = "小明";
r.sex = "男";
}else{
r.name = "小红";
r.sex = "女";
}
x = (x+1)%2;
}
}
}

class output implements Runnable
{
private Res r;
output(Res r){
this.r = r;
}
public void run(){
while(true){
System.err.println(r.toString());
}
}
}

public class NotifyDemo
{

public static void main(String[] args) {
Res  r = new Res();
Thread t1 = new Thread(new input(r));
Thread t2 = new Thread(new output(r));
t1.start();
t2.start();
}
}</span>


这段代码模拟了两个对象同时操作一个共享资源Res的过程,input对象一个负责交替给Res赋值小明男小红女,还有一个对象output负责输出Res的值。程序执行一段时间以后,输出了以下的值:

……

小红.....女

小红.....男

小明.....女

小明.....男

这里可以看出,程序的目的是为了输出成对的小红.....女和小明.....男,而出现错误的原因很明显,是操作Res的共享数据name和sex时,没有将成对的赋值操作一起结束,就被output输出了,导致了小红小明时男时女。

这里用之前说过的同步代码块的形式,可以解决这里数据不一致的问题,修改过的代码:

<span style="font-size:14px;"><span style="white-space:pre">	</span>class input implements Runnable
{
private Res r;
input(Res r){
this.r = r;
}

public void run(){

int x = 0;
while(true){
synchronized(r){
if(0 == x){
r.name = "小明";
r.sex = "男";
}else{
r.name = "小红";
r.sex = "女";
}
x = (x+1)%2;
r.flag = true;//放入数据
}
}
}
}

class output implements Runnable
{
private Res r;
output(Res r){
this.r = r;
}
public void run(){
while(true){
synchronized (r) {
System.err.println(r.toString());
}
}
}
}</span>


输出的结果是:

小明.....男

小明.....男

小明.....男

小明.....男

小红.....女

小红.....女

小红.....女

小红.....女


数据正确性的问题解决了,但是和程序想要的结果还是有所区别的。因为这样一个存,一个取的过程,一般都希望Res内容变化后,就被output知晓并且打印,而不是连续的打印一片没有变化过的小红.....女和小明.....男。这个时候,就需要引入线程间通信的机制。

java的线程通信机制,主要是通过两个方法来实现的,wait()和notify(),这两个方法可以查API,都是由监视器来调用的,这里说的监视器就是上面说的锁,因为锁可以是任意的对象,所以这里wait()和notify()是定义在Object对象中的方法。wait()是让当前线程等待,notify()方法是唤醒线程池中第一个等待的线程。

用这两个方法改造这个代码的原理就是,给资源一个标志位,比如起名flag,一旦赋值,修改标志位的状态为已赋值,一旦输出,则修改状态为未赋值。在同步代码块中先对falg进行判断,如果input判断flag为已赋值,则调用wait方法让input线程等待、直到output输出后调用notify方法唤醒input线程,让input继续赋值,否则说明没有赋值,这时执行赋值操作,修改状态为已赋值,最后执行notify方法唤醒等待中的output线程。output同理,执行时先判断是否已经赋值,是则执行输出后调用notify方法唤醒等待的input线程,最后修改状态为未赋值,否则调用wait()方法,等待赋值后方能被唤醒执行输出。


修改后的代码如下:

<span style="font-size:14px;"><span style="white-space:pre">	</span>class Res
{
String name;
String sex;
boolean flag = false;

@Override
public String toString(){
return name+"....."+sex;
}
}

class input implements Runnable
{
private Res r;
input(Res r){
this.r = r;
}

public void run(){

int x = 0;
while(true){
synchronized(r){
if(r.flag){
try{r.wait();}catch(Exception e){}
}
if(0 == x){
r.name = "小明";
r.sex = "男";
}else{
r.name = "小红";
r.sex = "女";
}
x = (x+1)%2;
r.flag = true;//放入数据
r.notify();
}
}
}
}

class output implements Runnable
{
private Res r;
output(Res r){
this.r = r;
}
public void run(){
while(true){
synchronized (r) {
if(!r.flag){
try{r.wait();}catch(Exception e){}
}
System.err.println(r.toString());
r.flag = false;//取出数据
r.notify();
}
}
}
}

public class NotifyDemo
{

public static void main(String[] args) {
Res  r = new Res();
Thread t1 = new Thread(new input(r));
Thread t2 = new Thread(new output(r));
t1.start();
t2.start();
}

}</span>


修改后执行的结果变为:

小红.....女

小明.....男

小红.....女

小明.....男

小红.....女


说明输入输出线程交替执行,相互唤醒。直到这里,这个说明线程通信的例子就说完了。

最后,线程通信还有注意几个点:

1)等待和唤醒必须是同一个锁,只有同一个锁上的notify方法能唤醒这个锁上的等待进程;

2)除了notify()方法外,还有 notifyAll()方法,用法一样,区别的是,它会唤醒这个锁上面所有的等待进程;notifyAll方法一般用于多个生产者和多个消费者的情况,这时执行代码时候的判断就需要用循环while(r.flag)的形式,防止重复生产或者重复消费;


4.JDK 5.0以后对锁的升级,Lock类

用Lock类来实现经典的生产者消费者例子:

<span style="font-size:14px;"><span style="white-space:pre">	</span>class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
private Lock lock = new ReentrantLock();

private Condition condition = lock.newCondition();

public void set(String name) {
lock.lock();
try {
while (flag) {
condition.await();
}
this.name = name + "--" + count++;
System.out.println(Thread.currentThread().getName() + "...生产者.."
+ this.name);
flag = true;
condition.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void out(){
lock.lock();
try {
while(!flag){
condition.await();
}
System.out.println(Thread.currentThread().getName() + "...消费者......."+ this.name);
flag = false;
condition.signal();
} catch (Exception e) {
}finally{
lock.unlock();
}
}

}

class Producer implements Runnable
{
private Resource res;
Producer(Resource res){
this.res = res;
}
public void run(){
while(true){
try {
res.set("+商品+");
} catch (Exception e) {
}
}
}
}
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res){
this.res = res;
}
public void run(){
while(true){
try {
res.out();
} catch (Exception e) {
}
}
}
}

public class ProductorConsumer
{
public static void main(String[] args) {
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();

}
}</span>


在这个例子中,有两个生产者,两个消费者,同时对资源进行操作,生产者负责count++,消费者负责打印++后的结果。

执行后,发现结果打印结果没有继续,打印一段之后就停住,

……

Thread-0...生产者..+商品+--19

Thread-2...消费者.......+商品+--19

Thread-0...生产者..+商品+--20

Thread-3...消费者.......+商品+--20


分析原因,在于Condition的signal()方法,这个方法不会指定唤醒的是生产者还是消费者,因此如果消费者进程唤醒的还是消费者进程,则flag始终为true,程序中所有进程都会处于等待状态。

这时,有两种解决问题的办法,一个是使用signalAll()方法,但是这种方式和JDK 5.0之前的notifyAll()方法本质是相同的,体现不出JDK 5.0方法在锁上的优势,因此这里用Lock类特有的方式去解决这个问题:

Lock类可以在一个Lock对象上支持多个Condition对象,这样就可以办到生产者只唤醒消费者,消费者只唤醒生产者,代码如下:

<span style="font-size:14px;"><span style="white-space:pre">	</span>class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
private Lock lock = new ReentrantLock();

private Condition condition_pro = lock.newCondition();//生产者
private Condition condition_con = lock.newCondition();//消费者

public void set(String name) {
lock.lock();
try {
while (flag) {
condition_pro.await();
}
this.name = name + "--" + count++;
System.out.println(Thread.currentThread().getName() + "...生产者.."
+ this.name);
flag = true;
condition_con.signal();
} catch (Exception e) {
} finally {
lock.unlock();
}
}
public void out(){
lock.lock();
try {
while(!flag){
condition_con.await();
}
System.out.println(Thread.currentThread().getName() + "...消费者......."+ this.name);
flag = false;
condition_pro.signal();
} catch (Exception e) {
}finally{
lock.unlock();
}
}

}</span>


在上面的代码中condition_pro.await()生产者的等待,只能由condition_pro.signal()生产者的condition_pro唤醒,condition_con.await()消费者的等待只能由消费者的condition_con唤醒。可以由不同的condition指定唤醒的线程。

注意:在使用Lock类最后释放锁的时候,需要将unlock方法放在finally中执行。使得每个锁都必须被释放。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: