Java多线程入门

来源:互联网 发布:速卖通行业数据分析 编辑:程序博客网 时间:2024/06/06 07:06

说来惭愧,距离第一次发布Java的博客已经过去整整一个多月了,可恶的是我居然刚到多线程这里徘徊,这效率可真是急煞人也,兴许是中间参杂了一些其他的技术课程给耽搁了,感觉每天都很忙,备忘录里一大堆的事情数都数不过来,想好好总结一下刚结束的网络程序设计C#+SQLSever建站过程,又想将`scrapy框架添个redis再梳理梳理,还忙着复习复习下周就结课的操作系统,又要抽空看看多媒体的PCA,特征脸,K-L变换,最近又接了个牛客网大使的活,马上双十一也来了,又想去蹲点捡个垃圾建个NAS玩玩。。等等备忘录事件,所以 :) 很忙。忙归忙,活还是要干的,今天来梳理梳理Java的多线程知识

想要理解多线程先来了解多进程,线程和进程有很大的关系

多进程

进程是程序在操作系统上运行的过程,是系统进行资源分配和调度的独立单位,一个程序可由多个进程共用,一个进程活动时可顺序执行若干个程序,进程包括程序、数据和PCB(进程控制块) ,(并发是指一段时间内同时运行多个进程,不是同步;并行是指在一个时间点同时运行多个进程,同步)
多进程实际上是CPU交替轮流执行多个进程的结果,是并发的

多线程

在一个进程内部可执行多个任务,一般将进程内部的任务称为线程
线程是在进程的概念基础上提出来的, 线程和进程都是与计算机中的并发执行相关概念
在一个进程内的多个线程是共享一个存储空间的,在多进程程序中通信切换进程时需要改变地址空间位置,而在多线程程序只需要改变执行次序,因为它们都位于同一个存储空间内。

Java多线程

在Java中只有实现类Runnable接口的类对象才能称为线程。Java提供了两种途径实现多线程
一、继承Thread类(该类已实现Runnable接口),
二、直接实现Runnable接口。

一、Thread类

Thread类属于java.lang包,故系统运行时会自动导入,Thread已经实现Runnable接口,故只需要让一个类继承Thread类,并将线程的代码写在run()方法中,即重写Thread类的run()方法即可创建线程。

Thread多线程类定义如下

[public] class 类名 extends Thread{    属性;    方法;    public void run(){           //线程程序代码    }}

一看就懂,接下来实战一下
EG:

public class ThreadDemo1 extends Thread{    private String name;    public ThreadDemo1(String name){        setName(name);    }    public void run(){        for (int i=0;i<10;i++) {            System.out.println(getName()+" "+i);        }    }    public static void main(String []args){        ThreadDemo1 t1=new ThreadDemo1("xiancheng1");        ThreadDemo1 t2=new ThreadDemo1("xiancheng2");        t1.start();            t2.start();    }}

这里写图片描述

上面的代码中值得注意的是start()方法和set Name以及getName方法,
因为线程的运行需要本机操作系统的支持,所以通过start()方法启动线程。而且一个线程对象只能调用一次start方法,否则会抛出“IllegalThreadStateException”异常,set Name和getName方法是Thread类的静态方法,用来设置和得到线程名称。

二、Runnable接口创建线程

通过实现Runnable接口的抽象方法run()方法即可

定义

[public] class 类名 implements Runnable{    属性;    方法;    public void run(){           //线程程序代码    }}

EG:

import java.util.Date;class ThreadDemo implements Runnable{    public void run(){        for(int i=1;i<=5;i++){            System.out.println("now "+Thread.currentThread().getName()+", "+i+" "+(new Date()));        }    }}public class ThreadDemo2 {    public static void main(String []args){        ThreadDemo tm=new ThreadDemo();        Thread t1=new Thread(tm,"线程1");        Thread t2=new Thread(tm,"线程2");        t1.start();        t2.start();    }}

这里写图片描述

上面因为Runnable接口对线程没有任何支持,因此在获得线程实例后,必须通过Thread类的构造方法来实现。

说到这里就不得不说一说Runnable和Thread到底用哪个比较合适了,

比如:在一个售票系统中:

使用继承Thread类来设计

class ticket extends Thread{    private int tick=5;    private String name;    public ticket(String name){        setName(name);    }    public void run(){        while(tick>0)            if(tick>0){            System.out.println(Thread.currentThread().getName()+"卖出"+(tick--)+"张票");            }    }}public class ThreadDmo4 {    public static void main(String []args){        ticket window1=new ticket("first");        ticket window2=new ticket("second");        window1.start();        window2.start();    }}

这里写图片描述

可以看出上面Thread实现的窗口售票显然是不合适的

用Runnable接口实现

class ticket1 implements Runnable{    private int tick=5;    private String name;    public void run(){        while(tick>0)            if(tick>0){                System.out.println(Thread.currentThread().getName()+"卖出"+(tick--)+"张票");            }    }}public class ThreadDemo5 {    public static void main(String []args){       ticket1 window=new ticket1();       Thread t1=new Thread(window,"first");       Thread t2=new Thread(window,"second");       t1.start();       t2.start();    }}

这里写图片描述

上面虽然也是两个线程,但是每个线程都是调用同一个run()方法,访问的都是同样的资源,所以实现了共享。

其实,上述问题也可以用static变量来实现,只是容易发生资源被抢占的风险(即另一个窗口迟迟无法售票)。

线程的状态(参考Java并发编程:线程的基本状态)

一、线程的基本状态

线程基本上有5种状态,分别是:NEW、Runnable、Running、Blocked、Dead。

1)新建状态(New)

当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

2)就绪状态(Runnable)

当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

3)运行状态(Running)

当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中

4)阻塞状态(Blocked)

处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。

根据阻塞产生的原因不同,阻塞状态又可以分为三种:

1、等待阻塞

运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

2、同步阻塞

线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

3、其他阻塞

通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5)死亡状态(Dead)

线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

这里写图片描述

注意:对于Java的状态是一个动态的概念,Java的application应用程序的主线程是main()方法的执行步骤,对于Java的applet小应用程序,其主线程是按其声明周期执行的步骤。

线程的部分基本方法

获取并设置线程的名称

Thread.currentThread()    //可获得当前线程的对象引用

一般若线程没有设置线程名称,系统会自动命名,当然也可以使用setName去设置,使用getName去获得

线程的优先级
在Java中每个线程都有优先级,一般取值范围是:1~10,默认为5,可以使用Thread类中的setPriority()方法设置一个线程的优先级,当然啦范围必须在1~10内,否则会产生异常。在Java中定义了三种优先级,MIN_PRIORITY(最低表示为1),MAX_PRIORITY(最高表示为10),NORM_PRIORITY(默认表示为5)。

示例:

class ThreadDemosix implements Runnable{    public void run(){        for(int i=1;i<=10;i++){            System.out.println("now:"+Thread.currentThread().getName()+", i="+i);        }    }}public class ThreaDemo6 {    public static void main(String []args){        ThreadDemosix tm=new ThreadDemosix();        Thread t1=new Thread(tm,"name-1");        Thread  t2=new Thread(tm,"name-2");        Thread t3=new Thread(tm,"name-3");        t1.setPriority(Thread.MIN_PRIORITY);        t2.setPriority(2);        t3.setPriority(Thread.MAX_PRIORITY);        t1.start();        t2.start();        t3.start();    }}

这里写图片描述

事实上从程序的运行结果来看,线程的优先级似乎并没有起到什么太大的作用。
故注意:优先级高的线程只是优先级获得CPU,不是绝对获得CPU。此外主线程的优先级为默认值5。


线程的休眠
说白了就是指线程暂时处于阻塞状态,使用sleep,join,yield方法可以使得线程阻塞,然而它们又有着很大的不同。
sleep

1、 sleep是静态方法,在主方法内无论通过线程对象去调用sleep还是直接通过Thread.sleep形式去调用sleep方法,其都是休眠主线程;如果想要线程实例休眠,sleep方法应该放入run()方法里。
2、 当线程处于sleep时,如果线程被中断,则会抛出Interrupted Exception异常,中断线程可以使用Thread类提供的interrupt方法。

join

假设当前运行的线程为A中调用了线程B的join()方法,则A会等待B的执行,
1、如果join中没有指定时间,则线程A会等待B运行结束后才由阻塞转变为就绪状态,然后等待获取CPU。
2、如果join中指定了时间,且线程B还没有运行完,则线程A也会在时间结束时,从阻塞状态转变为就绪状态。
3、如果join指定了时间,且线程B已经执行完了,则线程A立马从阻塞状态转变为就绪状态。

yield

yield方法指当前正在运行的线程退出运行状态,暂时让给其他线程先执行,可通过Thread.yield()方法实现,该方法只能把运行权让出来,让出后,哪个抢到就是哪个的,且抢到的优先级只能大于等于当前的。而sleep则不管。
sleep是让当前线程转到阻塞状态,而调用yield则将当前线程转到就绪状态
sleep会抛出异常,而yield不会抛出任何异常
sleep具有更好的移植性。

线程同步

线程安全指多线程访问同一代码,不会产生不确定的结果

回到那个售票系统
这里写图片描述

得到结果

这里写图片描述

只是添加一个0.1秒的休眠竟然会出现这种情况,当然是不能够容忍的,假若这个休眠模拟的是网络延迟,那这个售票系统无疑得跪,故而寻找解决办法。
在Java中解决同步问题的方法有三种,其一为同步代码块,其二为同步方法,其三是JDK1.5之后加入的同步锁(需要引入包java.util.concurrent.locks.ReentrantLock)

同步代码块

synchronized(Object obj){    //同步的代码    }
package test;class ticket1 implements Runnable{    private int tick=5;    private String name;    public void run(){        while(tick>0) {            synchronized (this) {      //添加同步代码块                try {                    Thread.sleep(100);                } catch (InterruptedException e) {                    e.printStackTrace();                }                if (tick > 0) {                    System.out.println(Thread.currentThread().getName() + "卖出" + (tick--) + "张票");                }            }        }    }}public class ThreadDemo5 {    public static void main(String []args){       ticket1 window=new ticket1();       Thread t1=new Thread(window,"first");       Thread t2=new Thread(window,"second");       t1.start();       t2.start();    }}

只需添加进同步代码块,问题即解决,十分方便

同步方法

[访问控制符] synchronized 返回类型 方法名(参数列表){            //需要同步的代码            [return  返回值]            }
package test;class ticket1 implements Runnable{    private int tick=5;    private String name;    public void run(){        while(tick>0){            this.sa();           //调用同步方法        }}    public synchronized void sa(){    //同步方法        while(tick>0) {            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }            if (tick > 0) {                System.out.println(Thread.currentThread().getName() + "卖出" + (tick--) + "张票");            }        }    }}public class ThreadDemo5 {    public static void main(String []args){       ticket1 window=new ticket1();       Thread t1=new Thread(window,"first");       Thread t2=new Thread(window,"second");       t1.start();       t2.start();    }}

资源问题也得到了解决

同步锁

在Java中,任何一个对象都有一个同步锁(设O为对象)

  • 对象O的同步锁在任何时刻最多只能被一个线程拥有
  • 若对象O的同步锁被线程T 拥有,则当其他线程访问O时,线程将被放到O的锁池中,并将它们转化为同步阻塞状态。

  • 拥有O的锁的线程T执行完后,会自动释放O的锁,若执行中,线程T发生异常退出,则也将自动释放O的锁

  • 若线程T在执行同步代码块时,调用了O的wait()方法,则线程T同样会是否O的锁,线程T也将进入阻塞状态

  • 如果线程T在执行同步代码块时,调用了Thread类的sleep方法,线程T将放弃运行权,即放弃CPU,但线程T不会放弃对象O的锁,即其他线程无法执行此同步代码块

  • 如果线程T释放了对象O的锁,并放弃了运行权,则CPU将会随机分配给对象O锁池中的线程,该线程也将获得对象O的锁。

以上述售票系统为例

import java.util.concurrent.locks.*;class ticket2 implements Runnable{    private int tick=5;    private String name;    private final ReentrantLock lock=new ReentrantLock();    public void run(){        while(tick>0) {        lock.lock();            try {                Thread.sleep(100);            } catch (InterruptedException e) {                e.printStackTrace();            }            if (tick > 0) {                System.out.println(Thread.currentThread().getName() + "卖出" + (tick--) + "张票");            }            lock.unlock();        }    }}public class ThreadDemo7 {    public static void main(String []args){        ticket2 window=new ticket2();        Thread t1=new Thread(window,"first");        Thread t2=new Thread(window,"second");        t1.start();        t2.start();    }}

这里写图片描述

完美实现了同步问题

sleep和wait

  • sleep和wait都会让出运行权,且都会使得当前线程进入阻塞状态
  • sleep属于Thread静态方法,而wait方法属于Object方法
  • 定义sleep必须设置时间,而wait则可以不用
  • 使用sleep可以使用interrupt方法唤醒,而执行wait方法则使用notify和notifyAll随机取出锁池中的线程唤醒
  • 若线程T拥有对象O的对象锁时,执行sleep,线程T将会进入对象O的锁池,但不会释放对象O的锁,而wait,进入锁池后会释放对象O的锁。

小结

终于终于突破100了,这是第100篇博文,很开心,时间:2017年10月28日星期六16点31分,字数这里写图片描述,嗯,这是一篇高质量的Java多线程博文,多线程应用在很多工程领域,其中面试也是少不了的一个环节,先mark,以后再来复习

原创粉丝点击