黑马程序员_多线程(线程安全和通信)

------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ------- 

多线程(线程安全和通信)

一、线程安全问题

线程安全问题,详细来说,先看一个程序。

                            If (tick>0)

                                     System.out.println(Thread.currentThread()+"..sale..."+tick- -);

这是继承了Runnable接口的子类的run方法中的一部分代码。我们知道,CPU在每个时间段,仅会执行一个程序,当我们建立多个线程来运行这一部分代码的时候,问题就发生了。假如有两个线程,线程A和线程B,当线程A执行时,这是tick的值是1,这是可以通过if语句,但是在下一条输出语句之前被暂时冻结了(注意输出语句中的tick执行完就会减1),这时线程B也进来了,但tick还是1,所以也通过if语句了,同时也在输出语句之前冻结了。这个情况之后,CPU回来执行线程A,继续执行下面的输出语句,之后tick的值就变为了0,这时其实这个if语句就该停止了,但是CPU又开始执行线程B了,所以又往下执行了一次输出语句,tick已经变了,所以最后tick编程了-1。这是我们设计程序里不合理的,这就是线程的安全问题。

简单的说:

         当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一线程就参与进来执行,导致线程共享数据错误。

解决办法:

         对多条操作共享数据段语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

 

这就引入了同步代码块:

         synchronized(对象)

{ 需要被同步的代码 }

 

同步代码块传入的是对象,注意一般来说只要是对象即可。示例程序如下:

         Object obj = new Object();

synchronizedobj

{       If (tick>0)

                      System.out.println(Thread.currentThread()+"..sale..."+tick- -);

 }

同步代码块中的对象,就相当于一个锁,当有一个线程进入到这个代码块中,就相当于把这个锁给锁上了,外面进不来,只有当这个线程执行完同步代码块中的程序,才会把这个锁给打开。另一种描述是,进入代码块的线程,持有这个锁,当线程执行完程序后,才会释放这个锁,使得后面的线程可以进入。同步代码块中同一时间只会有一个线程。

 

总结:

1、  多线程存在安全问题,多个线程操作同一条共享数据时会发生错误。

2、  错误原因是:一个线程对多条语句只执行了一部分,这时另一个线程就进来执行,导致线程共享数据错误。

3、  处理多线程安全问题用同步代码块,格式记住了。

4、  同步代码块要注意的是,一、传入的是对象,可以是任意对象。二、代码块中寸放大是需要被同步的代码,不要全部放入。三、同步代码块中仅会有一个线程再执行。

5、 同步的前提:一、必须要有两个或者两个以上的线程。二、必须是多个线程使用同一个锁。

6、 同步代码块的优点:解决了多线程安全问题。弊端:因为多加判断,较为耗费资源。

 

 

二、线程安全的各种情况

我们在写程序时,如果用到线程安全问题,该怎么去判断,怎么去具体的给需要的代码加上同步代码块?

根据我们已学的去判断的话,多线程使用的我们自己定义类中run方法中的程序,这部分程序时我们建立多线程时,线程实际执行的部分,那么就可以判断,多线程运行的代码在run方法中。然后我们不能把整个run方法都写入同步代码块中,那样岂不是成了单线程,所以要判断哪些是共享数据,一般来说,成员变量都是共享数据,只要是run方法中用到的成员变量,那么这些变量就是共享数据,但是共享数据如果只有一句的话,比如成员变量x,打印语句System.out.println(x++),这样一句语句,里面有共享数据,当多个线程执行到这部分时,不管哪个线程或停或执行,一句话过后,x都会改变,不会有x在两个线程的值是一样的。所哟要看多语句,比如:

                     void add(int x)   {

                                                        sum= sum+x

                       System.out.println(sum)

                    }

当有这样同样的sum共享数据的时候,线程A在打印语句之前休眠,之后线程B进来再执行求和语句,那么sum此时就变了,线程A在执行是就会打印后面的结果,而不是线程A本身执行完的结果。所以对于多线的判断总结一下:

 

对于多线程安全问题的判断:

1、 明确哪些代码是多线程运行代码(一般是run方法中的代码)

2、 明确共享数据(一般是用到的成员变量)

3、 明确多线程运行代码中哪些语句是操作共享数据的(注意同一数据,大于两条语句的)。

 

通过以上的判断,我们就可以再相应的语句上加上同步代码块,这样坐到精准和细致。

Synchronized也可以直接加上某个函数上,形式为:

              public synchronized void add(intx){ }

这样同步代码就有两种形式:

一、       同步代码块

二、       同步函数

 

同步代码块有锁,同步函数也有锁。因为函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this。如果同步函数被静态修饰的话,就为静态同步函数,而静态同步函数的锁是Class对象,比如:

            class  Demo implements Runnable{

                                     public static synchronized void add(int x){ }

}

看上述程序,当静态进内存的时候,内存中没有本类的对象,但是一定有该类对应的字节码文件对象,就是类名.class  对象的类型就是Class 。使用的锁就是该方法所在的字节码文件对象。则,此时的锁即是Demo.class

 

同步可以再单例设计模式中的懒汉式中可以用到,详情再查看设计模式之单例设计模式。

 

三、同步小点:死锁

死锁,就是A有一个锁,B有一个锁,A那这自己的锁想进B里面,B也拿着自己的锁,想进A里面,那么谁都不放锁,那么就会死锁。

一般程序里情况是:同步里嵌套同步。

我们要避免这中情况发生。

 

 

 

四、线程间通讯

线程间通讯就是多个线程在操作同一个资源,但是操作的动作不同。

看一个示例代码:

class Res {

数据变量;

}

class Input implements Runnable{

         public void run(){

         操作Res中的内容:给Res中输入数据。

         }

}

class Output implements Runnable{

         public void run(){

         操作Res中的内容:读取Res中的数据

         }

}

classTestForYou{

         public static void main(String[] args){

                   主函数中建立输入和输出线程。

         }

}

看上面的程序,应该明白什么事线程间通讯,就是操作同一数据,确实不同动作。当然,数据的输入和数据的输出,也得达到我们想要的效果。当然,会出现线程安全问题,按照多线程安全问题判断和同步前提两个规则来解决即可。

 

等待唤醒机制

wait():等待   .wait()

notify():唤醒  .notify()

notifyAll():全部唤醒

它们都是使用在同步中,因为要对持有监视器(锁)的线程操作。所以,都要使用在同步中,因为同步才具有监视器(锁)。

为什么这些方法定义在Object类中?因为这些方法在操作同步线程时,都必须要表示它们锁操作线程持有的锁,只有统一锁上的被等待线程,可以被同一个锁上的notify唤醒。也就是说等待和唤醒必须是同一个锁。而锁可以是任意对象,所以只能定义在Obiect类中。

 

情况一:之前,我们操作共享的数据的是两个线程,一个线程对共享数据的操作动作是输入,一个线程对共享数据的操作动作是输出,当只有两个线程的时候,我们是可以进行普通的方法进行同步,使得不会出现线程安全问题。

我们想输入一组数据后,在去除一组数据,这时就需要某个线程执行完,等待一段时间,这里就用到三个个,一是设定一个标记,二是用到wait(),三是notify()。比如下面这段程序中的一部分:

                            while(true){

                            synchronized(r){

                            if(r.flag)

                                     try{r.wait();}catch(Exceptione){}

                            if(x==0){

                            r.name ="lili";

                            r.sex ="woman";

                            }

                            else{

                            r.name ="wang  wu ";

                            r.sex ="  xxxx MAN";

                            }

                                     x=(x+1)%2;

                                     r.flag = true;

                                     r.notify();

                            }

                      }

r为共享数据的对象,namesex为共享数据中的成员变量。

flag标记初始化为false,在同步代码块中进行判断。走一遍顺序,if(r.flag)为假,那么就执行下面的输入数据的语句(x初始化为0),到后面改变flag的值为true,然后唤醒另一输入语句中的等待的线程,这时在回去判断flag的值为真,那么此线程就会等待,另一输出线程就会执行。看输出部分代码:

                   while(true){

                            synchronized(r){

                            if(!(r.flag))

                                     try{r.wait();}catch(Exceptione){}

                   System.out.println(r.name+".."+r.sex);

                   r.flag = false;

                   r.notify();

                            }

                   }

需要理解的是,线程等待是在一个线程池中,当notify要唤醒线程时,是要唤醒最早的那个等待的线程,这是默认方法。我们可以通过唤醒和锁有关的线程,来唤醒我们想要唤醒的行程,那么看上面程序,同步代码块的锁是共享数据对象r,所以,wait()notify()前面加上锁,就相当于打上了标记,这样当程序运行时候,就会唤醒同一个锁的等待线程。

 

情况二:情况一是两个线程共享一个数据,如果多个线程共享一个数据呢?还是上面示例程序,我们建立两个输入数据线程和两个输出数据线程,按照程序执行顺序来说,某个时刻上,输入数据的连个线程,后一个线程可能会覆盖前一个线程的数据,意思就是说,当我们生产了两双鞋,但商家只卖出一双,或者是生产了一双鞋,商家卖出两双,这样显然是不合理的。我们可以再判断上做改动,使得每个线程被唤醒时,可以再次判断标记flag,把if判断改为while循环:whle(flag)。这样线程被唤醒时,就可以多次判断,不会出现上述情况。但另一个问题就产生了,因为执行权是随机的,那么这个修改的程序,所有线程有可能就会全部等待,这时,我们就用到另一关键字,notifyAll,就是唤醒当前所有线线程,这就避免了线程全部等待。

 

 

JDK1.5新特性:多线程新解决方案:LockCondition

JDK1.5中,有一个接口Lock,这个替代了synchronized,同时Lock使用到了一个Condition接口,这个接口替代了Object中的wait()notify()notifyAll()

Lock中有lockunlock方法,就是一个拿锁,一个放锁。

Condition中提供了await(),就相当于wait()。还提供了signalsignalAll方法,就相当于notifynotifyAll。具体实现方式看如下程序:

                   private  Lock  lock= new ReentrantLock();

                   private  Condition  condition = lock.newCondition();

………………………….省略部分代码

         publicvoid run()  throwsInterruptedExceprion  {

                   while(true){

                   lock.lock();

                   try{           

                            while(!(r.flag))

                                     condition.await();

                            System.out.println(r.name+".."+r.sex);

                            r.flag= false;

                            condition.signal();                   

                   }

                   finally{

                            lock.unlock();

                   }

有上述程序可知,我们创建一个Lock接口的子类对象,使用lock方法中的newCondition()来创建一个Condition的引用对象,这样其实就是持有lock锁的一个对象,而这个对象里面有await() , singal() ,signalAll(),方法。在执行程序部分,lock.lock()方法来持有一个锁,在判断标记后面,用condition.await()是当前线程等待(这里会有InterruptedException抛出,所以要用try加测,但不处理,我们也抛出异常),然后用condition.signal();   等待的线程,把lock.unlock()放到finally块中,是使解锁动作一定要执行。这就是程序中Lock类体系替代原有同步代码块的方式。其实,在这一部上也就是把同步代码块替换成了我们JDK1.5的新技术,运行起来还是会有问题。因为,condition.signal()唤醒的也是最先等待的线程,即是是换成condition.signalAll()唤醒全部等待线程,但是以前的那个线程全部等待的情况依旧会发生。这里,condition就发挥作用了。看下面程序:

             private Lock  lock= new ReentrantLock();

                   private  Condition  condition_in = lock.newCondition();

private  Condition  condition_out = lock.newCondition();

………………………….省略部分代码

         publicvoid run()  throwsInterruptedExceprion  {

                   while(true){

                   lock.lock();

                   try{           

                            while(!(r.flag))

                                     condition_in.await();

                            System.out.println(r.name+".."+r.sex);

                            r.flag= false;

                            condition_out.signal();                   

                   }

                   finally{

                            lock.unlock();

                   }

我们建立了两个Condition的对象,一个是为输入数据的,一个是为输出数据的,因为同样从Lock中建立的,所以持有同样的锁。当我们执行程序时,condition_in就会是当前线程等待,就是是此时输入线程等待,而输出方(程序格式和输入方相反),有一个condition_out会使输出方线程等待。与之前的一个Condition对象不同的是,当执行到唤醒操作的时候,输入方程序为:condition_out.signal(),这时只会唤醒condition_out对象相关的等待线程,就是说,两个Condition对象,持有同一个锁,但相当于分别给输入和输出方做了标记,值等待或只唤醒固定对象所对应的线程。

 

 

 

停止线程

我们如果需要停止线程,如何动作。

以前的代码是使用stop方法,但是这个方法已经过时了。

 

只有一种,run方法结束。因为我们建立线程的时候,就是调用的run方法,而run方法中运行的代码通常是循环结构,只要控制住循环,就可以让run方法结束,就是结束了线程。

所以1、定义循环结束标记,2、使用interrupt(中断)方法。注意点是,interrupt方法是使线程结束冻结状态,回到运行状态中来。程序如下:

         privateboolean flag = true;

                  public void run() {

                   while(flag){

                            System.out.println("luanqi ba zao");

                            }

         }

         publicvoid setFlag() {

                   flag = false;

         }

在一个方法中,或是其他方法,把循环标记改变即可,是循环结束,那么线程就结束了。

但是要注意个特殊的情况:

         privateboolean flag = true;

         publicvoid run(){

                   while(flag){

                            try

                            {

                                     wait();

                            }

                            catch(InterruptedException e)

                            {

                                     System.out.println(".....Wait..A.....");

                            }

                            System.out.println("................");

                            }

         }

         publicvoid setFlag(){

                   flag = false;

         }

 

注意这个情况,当两个线程执行时,可能两个线程都在wait出等待了,此时,即便是我们改变了标记flag的值,线程也不会结束。就是说:当线程处于冻结状态,就不会读取标记,那么线程就不会结束。

这里我们就需要用到Thread中的interrupt方法,这个方法就是强制使线程接触冻结状态,但不是使线程停止,而是使线程回复运行状态。

         在主函数中调用interrupt方法,使冻结的线程回到运行状态,既然回到运行状态,那么就好解决线程停止问题了。因为interrupt会抛出一个interruptException异常,所以有两个方法可以解决。一是在主函数中,在使用interrupt方法之前改变标记。二是在异常处理中,改变标记,因为既然抛出异常了,就说明我们强制中断冻结状态想要使线程停止,那么就在catch语句中改变标记即可

 

 

 

多线程中一些其他方法

1、  setDaemon守护线程

守护线程就相当于后台线程,我们写的线程都是前台线程,这个方法就是当前台线程结束的时候,守护线程就会自动结束。拿毕老师的话来说,就是雅典娜over了,那圣斗士就都失业解散了。

注意的是使用方法:在建立线程之前设定。使用方法如下。

t1.setDaemon(true)

t1.start();

2、  Join方法

等待该线程终止(会抛出一个异常)。

那个例子来理解:

t1.start();

t1.join();

t2.start()

上例,当执行到join方法时,主线程就把执行权给到t1线程了,直到t1线程执行完毕,主线程才会获得执行权,继续下面开启线程的方法。如果这这种情况:

t1.start();

t2.start()

t1.join();

先建立了两个线程,当执行到join方法时,主线程就会交出执行权,此时,有两个线程t1t2都可以执行,所以两个线程会交替执行。直到当t1线程执行完毕后,主线程才会获得到执行权。

简单来说:当A线程执行到了B线程的join()方法时,A就会等待,等待B线程都执行完,A才会执行。

Join可以用来临时加入线程执行。

 

 

3、  优先级和yield方法

1)优先级

线程组概念,就是A个线程开启了多个线程,那么这多个线程就属于A这个线程组。

通过Thread类中的toString方法,我们可以查看每个线程的优先级,即线程组。对于优先级,一共10级,默认是5级,就是说,你级数越高,CPU执行你的概率越高,当然仅是概率。我们可以用setPriorityint 级数)方法继续设置。

         2yield方法

暂停当前正在执行的线程对象,并执行其他线程。方法时静态的。

就是暂停一下,这样使得其他线程有机会执行。

 

 

 

总结:

1)  对于线程安全问题,第一个解决方案是synchronized同步代码块,对线程运行代码中操作共同数据的语句进行同步,提高线程安全性。要注意的是,同步代码块的监视器即锁,是任意对象。

2)  同步代码块注意事项:一是有两个前提,1、多个线程,2、同一个锁。这样一定要注意,当线程出错时,一定是这两点。

3)  对什么地方加同步代码块的判断:1、明确哪些代码是多线程运行代码,2、明确哪些是共享数据,3、明确多线程运行代码中哪些语句是操作共享数据的。

4)  同步函数的锁是this,静态同步函数的锁是所属类的字节码,即Class

5)  同步中嵌套同步会形成死锁,要避免这样的编写。

6)  线程通信中(同步代码块方法),注意只有两个线程共享数据和多个线程共享数据时的区别。只有两个线程时,if判断标记即可,notify唤醒之前冻结的线程;多个线程,while循环判断标记,notify全部唤醒。

7)  线程通信中(Lock方法),用Lock方法注意的是,多个线程要建立多个Condition对象,以用于唤醒除本方线程的其他线程。其中awaitsignal都是Condition对象里的方法,用Condition兑现调用。开头的lock.lock(),在最后一定要解锁unlock,因为等待会有异常,所以要try-finally,在finally中解锁。

8)  停止线程中注意特殊情况,多个线程等待,循环不会结束,就是读不到结束标记。我们可以用interrupt方法中断冻结状态的线程,在其在判断标记钱或是异常处理中(interrupt会报出异常)改变标记,结束循环即可。

9)  守护线程用于建立线程钱,会在前台线程终止后立即终止。Join方法当A线程执行到了B线程的join()方法时,A就会等待,等待B线程都执行完,A才会执行。优先级有10级,用setPriorityint 级数)来设定,但只是CPU执行可能性提高。Yield方法时暂停一下线程,会释放执行权,减缓线程执行频率,临时释放,主要作用其实就是可以让其他线程有机会执行,有平均运行的效果。

10)             开发时怎么写线程,注意下面代码:

classDemo

{

public static void main(String[] args)

{

           循环代码1

 

           循环代码2

}

}

两个循环代码,可能是有很大数的循环,但是当单线程运行时候,尤其是循环代码1里面数字特别大,那么循环代码2就可能执行不到,那么我们这时间就可以用多线程。简单来说,就是某些代码要同时运行时,就用多线程代码进行封装。看程序:

classDemo {

public static void main(String[] args)      {

           new Thread(){

                    public void run(){

                             循环代码1

                    }

           }.start();

           Runnable r = new Runnable(){

                   pubilicvoid run(){

                             循环代码2                   

                    }

           }

           Thread t = new Thread(r);

           r.start();

}

}

黑马程序员多线程练习题主要包括两个问题。第一个问题是如何控制四个线程在打印log之前能够同时开始等待1秒钟。一种解决思路是在线程的run方法中调用parseLog方法,并使用Thread.sleep方法让线程等待1秒钟。另一种解决思路是使用线程池,将线程数量固定为4个,并将每个调用parseLog方法的语句封装为一个Runnable对象,然后提交到线程池中。这样可以实现一秒钟打印4行日志,4秒钟打印16条日志的需求。 第二个问题是如何修改代码,使得几个线程调用TestDo.doSome(key, value)方法时,如果传递进去的key相等(equals比较为true),则这几个线程应互斥排队输出结果。一种解决方法是使用synchronized关键字来实现线程的互斥排队输出。通过给TestDo.doSome方法添加synchronized关键字,可以确保同一时间只有一个线程能够执行该方法,从而实现线程的互斥输出。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [黑马程序员——多线程10:多线程相关练习](https://blog.csdn.net/axr1985lazy/article/details/48186039)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值