zoukankan      html  css  js  c++  java
  • java 多线程剖析

    问题的缘由源自于一道简单的面试题:题目要求如下:

    建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。

    解决问题前我们前补充一些基本知识:

    Runnable和Thread

    线程的启动

    线程的起动并不是简单的调用了你的RUN方法,而是由一个线程调度器来分别调用你的所有线程的RUN方法,
    我们普通的RUN方法如果没有执行完是不会返回的,也就是会一直执行下去,这样RUN方法下面的方法就不可能会执行了,可是线程里的RUN方法却不一样,它只有一定的CPU时间,执行过后就给别的线程了,这样反复的把CPU的时间切来切去,因为切换的速度很快,所以我们就感觉是很多线程在同时运行一样.

    你简单的调用run方法是没有这样效果的,所以你必须调用Thread类的start方法来启动你的线程.所以你启动线程有两种方法
    一是写一个类继承自Thread类,然后重写里面的run方法,用start方法启动线程
    二是写一个类实现Runnable接口,实现里面的run方法,用new Thread(Runnable target).start()方法来启动

    这两种方法都必须实现RUN方法,这样线程起动的时候,线程管理器好去调用你的RUN方法.

    start()和run()的关系

    通过调用Thread类的start()方法来启动一个线程, 
    这时此线程是处于就绪状态,
    并没有运行。
    然后通过此Thread类调用方法run()来完成其运行操作的,
    这里方法run()称为线程体,
    它包含了要执行的这个线程的内容,
    Run方法运行结束,
    此线程终止,
    而CPU再运行其它线程,

    而如果直接用Run方法,
    这只是调用一个方法而已,
    程序中依然只有主线程--这一个线程,
    其程序执行路径还是只有一条,
    这样就没有达到写线程的目的。

    区别

    Thread类是在java.lang包中定义的。一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了,但是一个类只能继承一个父类,这是此方法的局限。

    实现Runnable接口相对于继承Thread类来说,有如下显著的好处: 

    (1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。 

    (2)可以避免由于Java的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么,这个类就只能采用实现Runnable接口的方式了。 

    (3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当共享访问相同的对象是,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。 

    我在测试中发现,继承Thread的时候线程是顺序的,实现Runnable的时候确实不确定顺序的

    wait和notify以及sleep

    Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

    对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

    sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

    在调用sleep()方法的过程中,线程不会释放对象锁。

    而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

    有了这下知识之后,下面我们来解决问题吧

    题目剖析

    从题目我们可以得到几个要点:

    1:线程同时运行:但是对线程的启动没有做要求

    2:每个线程打印十次对应的字母

    3:交替打印:意味着三个线程的的打印操作有严格的顺序性

    思路分析

    1:每个线程打印不同的字母,操作类似,可以用统一的类来实现,并且用不用的自己初始化

    2:线程之间之间有先后性,打印A的线程打印了,打印B的线程才能打印,然后到C,循环

    如何保证线程之间的执行顺序的有序性是本题目的难点,

    难点突破

    打印的线程应该在打印一次A之后就应该进入等待状态,直到打印C的线程打印一次C之后在重新执行

    打印B、C的线程也类似

    简单思路

    定义三个Object遍历a,b,c,每个线程同时获得这三个对象锁才打印对应的字母一次,打印一次后释放另外两个对象的锁

    问题

    每个线程都要同时获得三个对象锁才能打印,那个第一个线程打印完,如果只是释放另外两个对象的锁,那个就没有线程可以继续打印了;

    如果三个都释放,那意味着当前线程可以继续打印了。

    所以,用三个对象锁的思路似乎是行不通了

    正确的思路之一(可能还有其他思路)

    每个线程只有同时获得自己和自己的前缀对象的锁(例如A的前缀是C,B的是A),才能打印一次自己的字母,然后释放自己前缀对象的锁,进入自己前端对象的等待线程池里面,等待自己前缀对象被唤醒之后才能继续打印

    那么自己前缀对象什么时候被唤醒呢?

    很简单的思路,打印A的线程在打印一次A之后进入等待C对象的线程池里面,那么只要打印C的线程在打印自己的时候唤醒一下自己就好了,这样打印A的线程就可以在打印C的线程打印一次C之后马上可以打印一次A;

    打印完一次自己后,马上唤醒自己,然后释放自己前缀对象的锁,进入自己前端对象的等待线程池里面,让自己的后缀线程可以马上打印,依次循环,问题好像已经解决了。

    潜在问题

    由于每个线程只拥有两个锁,并且只等待一个锁,那么对线程的启动顺序还是有要求的。假如启动的顺序是ACB,我们来分析一下:

    线程A:一开始获得AC两个锁,打印A,唤醒A,进入等待C被唤醒的线程池,释放AC

    线程B:一开始获得AB两个锁,打印B,唤醒B,进入等待A被唤醒的线程池,释放AB

    线程C:一开始获得BC两个锁,打印C,唤醒C,进入等待B被唤醒的线程池,释放BC

    这完全是没有问题的,所以我们必须保证打印ABC对应的线程顺序启动

    做法很简单:启动A后短暂sleep一下就好,如果只是这样

            new Thread(aThread).start();
    //        Thread.sleep(10);
            new Thread(bThread).start();
    //        Thread.sleep(10);
            new Thread(cThread).start();
    //        Thread.sleep(10);

    如果你的类是implements Runnable的话,那个着三个线程启动的顺序是没有保证的,

    当然,你可以改成extends Thread,这样的话这三个线程就是顺序启动的。

    下面是完整的实现代码:

    /** 
     * 
     * @author 戚伟杰 
     * @version 2015年11月20日 上午10:15:07  
     */
    public class MyThreadPrint implements Runnable{
        private String name;
        private Object prev;
        private Object self;
        public MyThreadPrint(String name,Object prev,Object self) {
            // TODO Auto-generated constructor stub
            this.name = name;
            this.prev = prev;
            this.self = self;
        }
        @Override
        public void run() {
            // TODO Auto-generated method stub
            int count = 10;
            while(count>0){
                synchronized (prev) {
                    synchronized (self) {
                        System.out.print(name);
                        count--;
    //                    self.notify();
                    }
                    try {
                        prev.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }
        }
        public static void main(String[] args) throws InterruptedException{
            Object a = new Object(); 
            Object b = new Object();
            Object c = new Object();
            MyThreadPrint aThread = new MyThreadPrint("a",c,a);
            MyThreadPrint bThread = new MyThreadPrint("b",a,b);
            MyThreadPrint cThread = new MyThreadPrint("c",b,c);
            new Thread(aThread).start();
    //        Thread.sleep(10);
            new Thread(bThread).start();
    //        Thread.sleep(10);
            new Thread(cThread).start();
    //        Thread.sleep(10);
    //        aThread.run();
    //        bThread.run();
    //        cThread.run();
        }
    }

    p

  • 相关阅读:
    openldap
    Java实现 洛谷 P1200 [USACO1.1]你的飞碟在这儿Your Ride Is He…
    Java实现 洛谷 P1200 [USACO1.1]你的飞碟在这儿Your Ride Is He…
    Java实现 洛谷 P2141 珠心算测验
    Java实现 洛谷 P2141 珠心算测验
    Java实现 洛谷 P2141 珠心算测验
    Java实现 洛谷 P2141 珠心算测验
    Java实现 洛谷 P2141 珠心算测验
    Java实现 洛谷 P1567 统计天数
    Java实现 洛谷 P1567 统计天数
  • 原文地址:https://www.cnblogs.com/qwj-sysu/p/4980716.html
Copyright © 2011-2022 走看看