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

本文深入探讨Java多线程中的线程安全问题及解决方法,包括同步代码块、死锁预防、线程间通信机制等内容,并介绍了JDK1.5中新增的Lock和Condition接口,帮助开发者有效管理和协调多线程程序。

------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();

}

}

### 关于Java多线程编程的拓展学习资料 #### 一、线程安全同步机制 在Java中,为了确保多个线程能够安全地访问共享资源而不发生数据竞争,通常会采用多种方式来实现线程间的同步控制。上述提到的`VolatileAtomicThread`类展示了通过`synchronized`关键字锁定对象实例的方式来进行同步处理[^1]。 ```java public class VolatileAtomicThread implements Runnable { private int count = 0; private final Object lockObj = new Object(); @Override public void run() { for (int i = 0; i < 100; ++i) { synchronized (lockObj){ count++; System.out.println("count =======>> " + count); } } } } ``` 除了使用`synchronized`外,还可以利用更高效的原子操作(如`AtomicInteger`),以及显式的锁机制(如`ReentrantLock`)。这些方法可以减少不必要的阻塞开销并提高程序性能。 #### 二、并发工具包中的高级特性 Java提供了丰富的并发工具库——`java.util.concurrent`,其中包含了诸如`CountDownLatch`, `CyclicBarrier`, `Semaphore`等实用组件。它们可以帮助开发者更加灵活地管理复杂的多线程场景下的协作逻辑。 例如,在某些情况下可能需要等待一组线程完成特定的任务之后再继续执行后续的操作;此时就可以考虑应用`CountDownLatch`: ```java import java.util.concurrent.CountDownLatch; class Worker extends Thread{ private CountDownLatch latch; public Worker(CountDownLatch latch){ this.latch=latch; } @Override public void run(){ try { // 执行任务... Thread.sleep((long)(Math.random()*10)); // 表示当前工作已完成 latch.countDown(); } catch (InterruptedException e){} } } // 主线程创建计数器,并启动子线程们 final int N=5; CountDownLatch cdl=new CountDownLatch(N); for(int i=0;i<N;++i) new Worker(cdl).start(); try { // 等待所有子线程结束 cdl.await(); } catch (InterruptedException ignored){} System.out.println("All threads have finished."); ``` #### 三、异步通信模式的应用实践 当涉及到不同模块之间的解耦合设计时,“发布/订阅”模型是一种非常有效的解决方案之一。这里提到了一个简单的观察者接口定义[^2],它允许主体通知其注册过的监听者有关状态变化的信息。 ```java public interface Observer { void update(String message); } ``` 基于此基础之上构建完整的事件驱动架构,则可以使系统的各个部分保持松散联系的同时又能高效互动。 #### 四、数据库连接池配置优化建议 对于频繁读写的Web应用程序而言,合理设置JDBC连接参数至关重要。下面给出了一组典型的MySQL数据库URL及其用户名密码组合[^3]: ```properties jdbc.url=jdbc:mysql://localhost:3306/day06?useSSL=false&serverTimezone=UTC jdbc.user=root jdbc.password=root ``` 需要注意的是实际项目部署过程中应当遵循最小权限原则分配账户角色,并且尽可能启用加密传输选项以保障安全性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值