资讯专栏INFORMATION COLUMN

Java基础学习——多线程之线程间通信(安全问题、等待唤醒机制)

CocoaChina / 2694人阅读

摘要:线程间通信其实就是多个线程操作同一个资源,但动作不同。同步前提是多线程。将该线程载入线程池,等待唤醒。该方法抛出异常,故需要配合使用随机唤醒线程池中一线程。线程为了检测死锁,它需要递进地检测所有被请求的锁。

线程间通信

其实就是多个线程操作同一个资源,但动作不同。
示例:在某个数据库中,Input输入人的姓名,性别,Output输出,两个线程同时作用。
思考:1.明确哪些代码是多线程操作的?2.明确共享数据。3.明确多线程代码中哪些是共享数据的。
思考后发现,Input和Output类中的run方法对Res类的Field数据同时操作。故需要考虑使用同步。
同步前提:1.是多线程。2.必须是多个线程使用同一个锁
唯一的锁有:类字节码文件(非静态同步函数不推荐),资源对象r

class Res    //共同处理的资源库,包含两个属性
{
    String name;
    String 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 (x==0)
            {
                r.name="mike";
                r.sex="male";
                x=1;
            }
            else    
            {
                r.name="莉莉";
                r.sex="女女女";
                x=0;
            }
            }
        }
    }
}

class Output implements Runnable
{
    private Res r;
    Output (Res r)
    {
        this.r = r;
    }
    public void run()
    {
        while (true)
        {
            synchronized (r)
            {
            System.out.println(r.name+"————"+r.sex);
            }
        }
    }
}


class InputoutputDemo
{
    public static void main(String[] args) 
    {
        Res r = new Res();
        Input in = new Input(r);
        Output out = new Output(r);
        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);
        t1.start();
        t2.start();
    }
}

观察结果:

由于输入线程一直抢夺资源,导致输出线程长时间属于阻塞状态。为了使其达到输入-输出的行为,考虑等待唤醒机制。

注意:以下三种方法使用时要求必须有监视器(锁),因此必须使用在同步里。需要标示他们所操作线程持有的锁。等待和唤醒必须是同一个锁。
-wait();将该线程载入线程池,等待唤醒。(该方法抛出异常,故需要配合try catch使用)
-notify();随机唤醒线程池中一线程。
-notifyAll();唤醒线程池中所有线程。
代码如下:

  class Res    //共同处理的资源库
    {
        String name;
        String sex;
        boolean flag = false;    //标识位来表示和判断已输入or已输出
    }
    
    
    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();//wait配合try catch使用,且要标识锁。
                        }
                        catch (Exception e)
                        {
                        }
                    }
                    else        //否则输入数据,置标识位为真并唤醒输出。
                    {
                        if (x==0)
                        {
                        r.name="mike";
                        r.sex="male";
                        x=1;
                        }
                        else    
                        {
                        r.name="莉莉";
                        r.sex="女女女";
                        x=0;
                        }
                        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)    //如果标识位为真,则有数据等待输出,此时取出数据后置标识位为假,唤醒输入
                    {    
                        System.out.println(r.name+"————"+r.sex);
                        r.flag = false;
                        r.notify();
                    }
                    else        //否则关闭输出。等待输入
                        try
                        {
                            r.wait();
                        }
                        catch (Exception e)
                        {
                        }
                }
            }
        }
    }


    class InputoutputDemo
    {
        public static void main(String[] args) 
        {
            Res r = new Res();
            Input in = new Input(r);
            Output out = new Output(r);
            Thread t1 = new Thread(in);
            Thread t2 = new Thread(out);
            t1.start();
            t2.start();
        }
    }
    
    

最后考虑到设计惯例,封装数据和操作方法,优化后代码如下(参考设计思路和设计惯例)

    class Res    //共同处理的资源库
    {
        private String name;
        private String sex;
        private boolean flag = false;    //标识位来表示和判断已输入or已输出        
        public synchronized void set(String name,String sex)
        {    
            if (flag)
            
                try
                {
                    this.wait();    //非静态同步函数的锁为this
                }
                catch (Exception e)
                {
                }
                this.name = name;
                this.sex = sex;
                flag = true;
                this.notify();
        }
        

        public synchronized void out()
        {
            if (!flag)
                
                try
                {
                    this.wait();
    
                }
                catch (Exception e)
                {
                }
            System.out.println(name+"......."+sex);
            flag = false;
            this.notify();
            
        }
      }
    
  
    class Input implements Runnable
    {
        private Res r;        
        Input (Res r)        
        {
            this.r = r;
        }
        public void run()
        {
            int x = 0;
            while (true)
            {    
                if (x==0)
                    r.set("mike","male");             
                 else   
                     r.set("莉莉","女女女女");             
                    x = (x+1)%2;
            }
        }
    }


    class Output implements Runnable
    {
        private Res r;
        Output (Res r)
        {
            this.r = r;
        }
        public void run()
        {
            while (true)
            {
                r.out();
            }
        }
    }
    


    class InputoutputDemo
    {
        public static void main(String[] args) 
        {
            Res r = new Res();
            new Thread(new Input(r)).start();        //匿名对象,简化代码
            new Thread(new Output(r)).start();
/*            Input in = new Input(r);
            Output out = new Output(r);
            Thread t1 = new Thread(in);
            Thread t2 = new Thread(out);
            t1.start();
            t2.start();
*/
        }
    }
    
避免死锁的方法:

1.加锁顺序
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:

Thread 1:
  lock A 
  lock B

Thread 2:
   wait for A
   lock C (when A locked)

Thread 3:
   wait for A
   wait for B
   wait for C
   

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁,并对这些锁做适当的排序,但总有些时候是无法预知的。

2.加锁时限
另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁,因为这些线程等待相等的重试时间的概率就高的多。
这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。

3.死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。

那么当检测出死锁时,这些线程该做些什么呢?

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁,原因同超时类似,不能从根本上减轻竞争。

一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69785.html

相关文章

  • Java 线程核心技术梳理(附源码)

    摘要:本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,的使用,定时器,单例模式,以及线程状态与线程组。源码采用构建,多线程这部分源码位于模块中。通知可能等待该对象的对象锁的其他线程。 本文对多线程基础知识进行梳理,主要包括多线程的基本使用,对象及变量的并发访问,线程间通信,lock的使用,定时器,单例模式,以及线程状态与线程组。 写在前面 花了一周时...

    Winer 评论0 收藏0
  • 第10章:并发和分布式编程 10.1并发性和线程安全

    摘要:并发模块本身有两种不同的类型进程和线程,两个基本的执行单元。调用以启动新线程。在大多数系统中,时间片发生不可预知的和非确定性的,这意味着线程可能随时暂停或恢复。 大纲 什么是并发编程?进程,线程和时间片交织和竞争条件线程安全 策略1:监禁 策略2:不可变性 策略3:使用线程安全数据类型 策略4:锁定和同步 如何做安全论证总结 什么是并发编程? 并发并发性:多个计算同时发生。 在现代...

    instein 评论0 收藏0
  • JAVA线程通信简介

    摘要:线程通信的目标是使线程间能够互相发送信号。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。如果方法调用,而非,所有等待线程都会被唤醒并依次检查信号值。 线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。 showImg(http://segmentfault.com/img/bVbPLD); 例...

    CHENGKANG 评论0 收藏0
  • 『并发包入坑指北』阻塞队列

    摘要:自己实现在自己实现之前先搞清楚阻塞队列的几个特点基本队列特性先进先出。消费队列空时会阻塞直到写入线程写入了队列数据后唤醒消费线程。最终的队列大小为,可见线程也是安全的。 showImg(https://segmentfault.com/img/remote/1460000018811340); 前言 较长一段时间以来我都发现不少开发者对 jdk 中的 J.U.C(java.util.c...

    nicercode 评论0 收藏0
  • Java线程汇总

    摘要:线程需要避免竟态,死锁以及很多其他共享状态的并发性问题。用户线程在前台,守护线程在后台运行,为其他前台线程提供服务。当所有前台线程都退出时,守护线程就会退出。线程阻塞等待获取某个对象锁的访问权限。 1、多线程介绍 多线程优点 资源利用率好 程序设计简单 服务器响应更快 多线程缺点 设计更复杂 上下文切换的开销 增加资源消耗线程需要内存维护本地的堆栈,同时需要操作系统资源管理线程。...

    Lsnsh 评论0 收藏0

发表评论

0条评论

CocoaChina

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<