java多线程的简单应用

java多线程(后来也叫做并发)是java学习中一种新的思想,当然,并发在java中的学习,只是很基础很浅显的部分,并发更多的涉及操作系统等很多范畴的知识,TIJ中说可以将并发拿出来当做一门单独的学科来学习。所以本文也没有很深入的研究并发的内容,只是来讲解常用的多线程的实现使用方式。想要深入学习的同学可以自己查阅并发相关的知识。
先来讲一下,什么是多线程呢?
多线程可以理解为,计算机在执行一个或多个任务的时候,启动了多个线程共同工作。多线程解决的问题,大致可以分为“速度”和“设计可管理性”两种。特别指出一点,这里的速度,不是说多个cpu执行同一个任务的速度,而是指单个处理器上执行程序的速度。我们说的多线程,指的是单个处理器处理多个线程,而不是指多个处理器的情况。要理清并发和并行的区别。

java中的多线程实现,常用的有两种:继承thread类,实现Runnable接口(还有两种是实现Callable接口和使用Executor来创建线程。Callable接口使用较少,不做讨论。Executor会与线程池放一起讲解)。

继承Thread类

java程序中,可以通过继承java.lang.Thread类,而后重写其run方法接口。值得一提的是,java.lang.Thread也是实现了java.lang.Runnable接口。具体实现,请看代码:
main方法中有用到join()方法,关于本方法的介绍见下文。

public class ThreadDemo extends Thread{
    private String name;
    public ThreadDemo(){}

    public ThreadDemo(String name){
        this.name=name;
    }

    public void run(){
        int i=0;
        while(i<=5){
            System.out.println("thread: "+name+" is running! "+i);
            i++;
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("main线程开始执行:");
        ThreadDemo t1=new ThreadDemo("why");
        ThreadDemo t2=new ThreadDemo("sb");
        t1.start();     //启动线程  使线程进入就绪状态,等待cpu调用
        try {
            t1.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        t2.start();
        System.out.println("main线程结束执行");

    }

}

实现Runnable接口

实现Runnable接口也是实现多线程的常用方法,请直接看代码:

public class RunnableDemo implements Runnable{
    private String name;



    public RunnableDemo(){}
    public RunnableDemo(String name){
        this.name=name;
    }

    @Override
    public void run() {
        int i=0;
        do{
            System.out.println("Thread :"+name +" is running! "+i);
            try {
                Thread.sleep((int) ((Math.random()*1000)));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }while(i++<6);
    }

    public static void main(String[] args) {
        Thread t1=new Thread(new RunnableDemo("why")); //使用Runnable接口创建多线程时,需要创建Thread的实例来启动线程
        Thread t2=new Thread(new RunnableDemo("sb"));
        t1.start();
        t2.start();

        new Thread(new RunnableDemo("why2")).start(); //等价于上面的方式
        new Thread(new RunnableDemo("sb2")).start();
    }

}

关于使用Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。通常来讲,比较建议使用Runnable接口的方式来实现多线程。

总结:

实现Runnable接口比继承Thread类所具有的优势:

1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免java中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

重点:线程的状态转换

如果问对于一个初学者来说,学习多线程最重要的部分是什么?我个人认为是线程的状态转换。因为这直接关系到你对线程的理解,和对线程问题的实际解决。下图为盗的一张图:
这里写图片描述

下面为线程的几种状态:
1、新建状态(New):新创建了一个线程对象,如上面代码中new Thread()后t1的状态即为新建状态。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权(join,sleep),暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁(这也是sleep和wait的区别之一))
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期(线程池中调用shutdown或shutdownNow方法也可以进入本状态)。

线程常用函数说明

①sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),线程进入阻塞状态。
②join():指等待t线程终止。如果某个线程j在另一个线程t上调用t.jion(),那么此线程(j)将会被挂起,直至目标线程t结束才恢复(即t.isAlive()返回为假)。例如Thread的代码案例中,如果main函数中,不添加,t1.join()。那么执行结果应该是:

main线程开始执行:
main线程结束执行
thread: why is running! 0
thread: sb is running! 0
thread: sb is running! 1
thread: why is running! 1
thread: sb is running! 2
thread: why is running! 2
thread: sb is running! 3
thread: why is running! 3
thread: sb is running! 4
thread: why is running! 4
thread: why is running! 5
thread: sb is running! 5

那如果添加了t1.join() (t2不变),输出结果就变成了:

main线程开始执行:
thread: why is running! 0
thread: why is running! 1
thread: why is running! 2
thread: why is running! 3
thread: why is running! 4
thread: why is running! 5
main线程结束执行
thread: sb is running! 0
thread: sb is running! 1
thread: sb is running! 2
thread: sb is running! 3
thread: sb is running! 4
thread: sb is running! 5

需要指出的一点是,main()也是一条线程。所以可以看到,在main线程上添加t1.join()后,main线程会等待t1执行完毕才会继续执行。t2没有添加join方法,所以会正常执行。添加join之后,主线程一定会等子线程都结束了才结束。
③yield():暂停当前正在执行的线程对象,并执行其他线程(退让机制)。
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。yield会让同优先级或者高优先级的线程优先执行,若当前处理器并没有同优先级或高优先级的线程,那处理器可能继续选择当前线程执行,也就是说导致yield()并没有作用。
sleep()和yield()的区别
sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
④setPriority(): 更改线程的优先级。

//  MIN_PRIORITY = 1
//  NORM_PRIORITY = 5
//  MAX_PRIORITY = 10
Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

    
⑤interrupt():本方法不是终端线程,它只是向线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出异常,从而结束线程。但是如果你忽略了这个异常,那么这个线程还是不会中断的。

⑥wait()

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同时,释放了对象锁的控制。

结合一经典面试题来理解wait()和notify()的用法及作用:

/**
* @author fushb
* @version 创建时间:2018年5月9日 上午11:40:06
* 类说明
*   java多线程中锁的使用,wait(),notify()方法的使用
*   经典面试题目: 建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。
*/
public class SynchronizedDemo implements Runnable{
    private String name;
    private Object prev;
    private Object self;
    public SynchronizedDemo(){}
    public SynchronizedDemo(String name,Object prev,Object self){
        this.name=name;
        this.prev=prev;
        this.self=self;
    }
//  MIN_PRIORITY = 1
//  NORM_PRIORITY = 5
//  MAX_PRIORITY = 10
    @Override
    public void run() {
        int i=0;
        while(i<10){
            synchronized (prev) {
                synchronized (self) {
                    self.notify();          //唤醒当前进程  
                    System.out.print(name);
                    i++;
                }

                try {
                    prev.wait();            //让前置进程wait
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Object a=new Object();
        Object b=new Object();
        Object c=new Object();
        SynchronizedDemo pa=new SynchronizedDemo("A", c, a);
        SynchronizedDemo pb=new SynchronizedDemo("B", a, b);
        SynchronizedDemo pc=new SynchronizedDemo("C", b, c);

        new Thread(pa).start();
        Thread.sleep(100);
        new Thread(pb).start();
        Thread.sleep(100);
        new Thread(pc).start();
        Thread.sleep(100);
    }

}

这里的主要思路为:三个线程的切换执行,需要保证三个线程轮流执行且顺序不能错误。那就需要使用Synchronized方法来对对象进行加锁。且只有前置对象和本对象的锁同时获取到时,才能执行打印操作。所以执行思路是:获取前置对象锁(保证前置对象线程没有执行);获取本对象锁(保证本对象没有被其他线程占用);唤醒本对象,打印本对象;让前置对象wait()等待轮换线程时唤醒前置对象(为了防止执行完本线程后,处理器获取前置对象的线程进行打印,这样就不能保证打印的顺序了。所以需要执行完本次打印后,让前置对象wait()等待下次唤醒)。程序逻辑有一个bug,就是初始化状态的问题。按照题目要求,你需要按照A–B–C的顺序来启动,不然会导致其他的打印顺序出现。
⑦sleep()方法
sleep()使当前线程进入停滞状态(阻塞当前线程),让出Cpu的使用。目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
   sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
  在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
   wait和sleep区别
共同点:
1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。
不同点:
1. Thread类的方法:sleep(),yield()等
Object的方法:wait()和notify()等
2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用 。
4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
所以sleep()和wait()方法的最大区别是:
    sleep()睡眠时,保持对象锁,仍然占有该锁
    而wait()睡眠时,释放对象锁
  但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
  

java线程池

java中也可以使用线程池的方式来创建多线程。本文做简单介绍,详细请看其他博客。
线程池的常用类:ExecutorService(创建线程池的接口),Executors(工具类)。
使用线程池创建线程:


        Object a=new Object();
        Object b=new Object();
        Object c=new Object();
        SynchronizedDemo2 pa=new SynchronizedDemo2("A", c, a);
        SynchronizedDemo2 pb=new SynchronizedDemo2("B", a, b);
        SynchronizedDemo2 pc=new SynchronizedDemo2("C", b, c);

        ExecutorService e=Executors.newCachedThreadPool(); //创建可变的线程池
        e.execute(pa);//执行线程
        Thread.sleep(100);
        e.execute(pb);
        Thread.sleep(100);
        e.execute(pc);
        Thread.sleep(100);
        e.shutdownNow();        //关闭ExecutorService
  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值