进程与线程(一)(基本定义和demo)

线程和进程
 
进程定义
进程指正在运行的程序 确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。(比如电脑中运行的酷狗,qq等)
线程定义
线程是进程中的一个执行单元,负责当前进程中程序的执行,线程共享进程的资源。
线程与进程的关系
一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
主线程
jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。
多线程定义
即就是一个程序中有多个线程在同时执行。多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。提到多线程就要提到两个概念:串行/并行
串行
单个线程执行多个任务。比如下载文件,第一个没下完不能开始下载第二个,属于不同时刻。缺点很明显,效率很低。
并行
多个线程执行多个任务。比如开启迅雷同时下载好几个文件,属于同一时刻。效率很高,但是要注意不要每个任务都建一个线程去处理,任务数量越多,内存压力越大,严重会导致宕机。
程序运行原理
分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
抢占式调度
优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个线程(线程随机性),java使用的为抢占式调度。
 
使用线程和不适用线程的区别(一个简单的例子)
 
不使用线程
代码如下:
public static void main(String[] args) {
    ThreadTest();
    System.out.println("abc");
}

public static void ThreadTest(){
    for(int i=0;i<10000;i++){
        System.out.println(i);
    }
}

执行main方法可以看到输出结果是按照顺序先执行ThreadTest方法,输出1--9999的数字,之后输出的abc(从上往下输出)

效果如下:

使用线程

创建线程有三种方法

注意:多次启动一个线程使非法的,特别是当线程已经结束执行后,不能再重新启动。

1. 继承Thread类,重写Thread类的run方法

创建步骤:

(1)定义一个类继承Thread

(2)重写run方法

(3)创建子类对象,就是创建线程对象。

(4)调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。

public static void main(String[] args) {
    SubThread st = new SubThread();//创建一个继承了Thread类的对象
    //st.run();//如果直接调用run,还是单线程程序
    st.start();//调用start方法,是多线程程序
    for(int i=0;i<50;i++){
        System.out.println("main"+i);
    }
}

//继承Thread类,重写run方法
public class SubThread extends Thread{
    public void run(){
        for(int i=0;i<50;i++){
            System.out.println("run:"+i);
        }
    }
}

执行main方法,可以看到并不是如单线程一样从上到下执行的。

start方法的作用:使该线程开始执行,Java虚拟机调用该线程的run方法。

效果:

总结一下:

1. 上面的程序,其实是两个线程,一个是运行main方法的主线程,一个是运行run方法的线程,这两个线程都会被CPU选择(随机选择)。

2. 为什么要继承Thread类呢?因为Thread类是用来描述线程,具备线程该有的功能。那么为什么不直接创建Thread类的对象呢?比如,Thread t = new Thread(); t.start()这种写法。首先要说这么写没有问题,调用start()方法的时候也会去执行run方法,但是这个run方法是Thread类里的run方法,里面没有我们想要运行的代码,我们也不能将我们要运行的代码写到这里。

3. 创建线程的目的是什么?是为了建立程序独立的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定线程要执行的任务。对于之前主线程,它的任务定义在main函数中。自定义的线程需要执行的任务都定义在run方法中。

4. 线程对象调用run方法和start方法的区别?

(1)调用run方法不开启线程,仅仅是对象调用方法。

(2)调用start方法开启线程,并让jvm调用run方法在开启的线程中执行。

5. 多线程执行得时候,在内存中是如何运行的呢?

多线程执行的时候,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。也可以说栈内存都是线程私有的。当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

 

2. 实现Runnable接口,实现run方法。

实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法,将线程任务代码定义到run方法中。

创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。

实现Runnable的好处。既避免了单继承的局限性,Runnable接口对线程对象和线程任务进行解耦。

public static void ThreadTest(){
    SubRunnable sr = new SubRunnable();
    Thread t = new Thread(sr);
    t.start();
    for(int i=0;i<10000;i++){
        System.out.println("main"+i);
    }
}

public class SubRunnable implements Runnable{
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println("run"+i);
        }
    }
}

3. 实现Callable接口。

具体的使用方法参照下篇文章线程池中的案例。

 

线程的名字

 

1. 线程名字的获取(getName和currentThread方法)

(1)主线程名称就是main。因为getName()方法是非静态的,并且静态方法(mian)是不允许调用非静态的方法的,所以main中不能直接写getName方法,需要写currentThread方法

public static void main(String[] args) {
        System.out.println(Thread.currentThread());//哪个线程运行的这句代码,获取到的就是线程的对象
        System.out.println(Thread.currentThread().getName());//哪个线程运行的这句代码,获取到的就是线程的对象
    }

效果:

(2)其他线程名字都为Thread-0、Thread-1...等等。方法是直接在run方法中写super.getName()即可,其中super可以省略。也可以写currentThread方法

public class SubThread extends Thread{
    public void run(){
        System.out.println(super.getName());
        System.out.println(getName());
        System.out.println(Thread.currentThread());
        System.out.println(Thread.currentThread().getName());
    }
}

效果:

2. 线程名字的设置(setName方法)

(1)最好是在start方法上面先定义,逻辑上比较好理解。

public static void main(String[] args) {
        SubThread st = new SubThread();//创建一个继承了Thread类的对象
        st.setName("Thread-111");
        st.start();//调用start方法,是多线程程序
}

public class SubThread extends Thread{
    public void run(){
        System.out.println(super.getName());
        System.out.println(getName());
        System.out.println(Thread.currentThread());
        System.out.println(Thread.currentThread().getName());
    }
}

(2)子类中设置

public static void main(String[] args) {
        SubThread st = new SubThread();//创建一个继承了Thread类的对象
        st.start();//调用start方法,是多线程程序
}

public class SubThread extends Thread{

    public SubThread(){
        super("222");
    }

    public void run(){
        System.out.println(super.getName());
        System.out.println(getName());
        System.out.println(Thread.currentThread());
        System.out.println(Thread.currentThread().getName());
    }
}

效果:

 

 

Thread类的常用方法

 

 

Thread类的sleep方法

sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂时停止执行),此操作受到系统计时器和调度程序精度和准确性的影响。该线程不丢失任何监视器的所属权。

下面代码的效果:每隔1s输出一次。

public static void main(String[] args) throws InterruptedException {
    for(int i=0;i<10;i++){
        Thread.sleep(1000);//暂停1s
        System.out.println(i);
    }
}

如果是写在run方法里,由于不能够抛出异常,所以需要写try catch

public class SubThread extends Thread{
    public void run(){
        for(int i=0;i<10;i++){
            try {
                Thread.sleep(1000);//暂停1s
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    }
}

 

匿名内部类实现线程程序

 

注意:

1. 匿名内部类的前提,必须有继承或者接口的实现。

2. 写法为new 父类或者接口(){重写抽象方法}

//匿名内部类的前提,必须有继承或者接口的实现
//new 父类或者接口(){
//  重写抽象方法
//}
public static void main(String[] args) throws InterruptedException {
    //写法一:继承方式 xxx extends Thread{public void run(){}}
    new Thread(){
        public void run(){
            System.out.println("1");
        }
    }.start();

    //写法二:实现接口方式  xxx implement Runnable{public void run(){}}
    Runnable r = new Runnable() {
        public void run() {
            System.out.println("2");
        }
    };
    new Thread(r).start();

    //写法三:
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("3");
        }
    }).start();
}

 

线程的状态(生命周期)

1. 新建状态(new)

当程序使用new关键字创建了一个线程后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。

2. 就绪状态(runnable)

当线程对象调用了start方法之后,该线程就处于就绪状态。java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

3. 运行状态(running)

如果处于就绪状态的线程获得了cpu,开始执行run方法的线程执行体,则该线程处于运行状态。

4. 阻塞状态(blocked)

线程因为某种原因放弃了cpu的使用权,暂时停止运行,直到线程进入了就绪状态,才有机会重新获得cpu的使用权,转到运行状态。阻塞的情况分三种:

(1)等待阻塞(o.wait-->等待队列):运行的线程执行wait方法,JVM会把该线程放入等待队列中。

(2)同步阻塞(lock-->锁池):运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

(3)其他阻塞(sleep/join):运行的线程执行sleep/join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5. 线程死亡(dead)

线程通常会以下面三种方式结束,结束后就是死亡状态。

(1)正常结束:run/call方法执行完成。

(2)异常结束:线程抛出一个未捕获的Exception或error。

(3)调用stop:直接调用该线程的stop方法来结束该线程--这种方法会导致死锁,不推荐使用。

 

 终止线程的方式

1. 正常运行结束

2. 使用退出标志退出线程

比如定义一个boolean类型的变量flag,为true时执行xxx,为false时执行xxx。同一时刻只能允许一个线程修改flag的值。

3. Interrupt

使用Interrupt方法来中断线程有两种情况:

(1)线程处于阻塞状态:如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当调用线程的Interrupt方法时,会抛出InterruptException异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们可以结束这个线程。

(2)线程未处于阻塞状态:使用isInterrupted判断线程的中断标志来退出循环。当使用interrupt方法时,中断标志就会置为true,和使用自定义的标志来控制循环是一样的道理。

4. stop方法终止线程(线程不安全)

类似于电脑的强制关机,可能会造成不可预料的错误,不安全主要是stop方法被调用后,创建子线程的线程就会抛出ThreadDeatherror错误,并且会释放子线程所持有的所有锁。那么数据的安全性就可能被破坏。

 

线程基本方法

 

方法有:wait、notify、notifyAll、sleep、join、yield。

1. 线程等待(wait)

调用该方法的线程进入waiting状态,只有等待另外的线程通知或被中断才会返回,需要注意的是调用wait方法后,会释放对象的锁。因此,wait方法一般用在同步方法和同步代码块中。

2. 线程睡眠(sleep)

sleep导致当前线程休眠,与wait方法不同的是sleep不会释放当前占有的锁,sleep会导致线程进入timed-waiting状态。

3. 线程让步(yield)

yield会使当前线程让出cpu执行时间片,与其他线程一起重新竞争cpu时间片。一般情况下,优先级高的线程有更大的成功性成功竞争到cpu时间片,但这不是绝对的,有的操作系统对于线程优先级不太敏感。

4. 线程中断(interrupt)

(1)中断一个线程,其本意就是给这个线程一个通知信号,会影响这个线程内部的一个中断标志位。但是运行中的线程并不会被终止

(2)若调用sleep而使线程处于timed-waiting状态,这时调用interrupt方法,会抛出InterruptedException,从而使下沉提前结束timed-waiting状态。

(3)许多声明抛出InterruptedException的方法,如sleep,抛出异常前,都会清楚中断标识位,所以抛出异常后,调用isInterrupted方法将会返回false。

(4)中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程thread的时候,可以调用thread.interrupt方法,在线程的run方法内部可以根据thread.isInterrupted的值来优雅的终止线程。

5. join

当前线程调用join方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再从阻塞状态变为就绪状态,等待cpu使用权。很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是主线程需要子线程结束之后才能结束,这个时候就要用到join方法了。

6. 线程唤醒(notify)

Object类中的方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个wait方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有notifyAll,唤醒在此监视器上等待的所有线程。

7. 其他方法

(1)sleep:强迫一个线程睡眠N毫秒。

(2)isAlive:判断一个线程是否存活。

(3)join:等待线程终止。

(4)activeCount:程序中活跃的线程数。

(5)enumerate:枚举程序中的线程。

(6)currentThread:得到当前线程。

(7)isDaemon:一个线程是否为守护线程。

(8)setDaemon:设置一个线程为守护线程。(用户线程和守护线程的区别为,是否等待主线程依赖于主线程结束而结束)。

(9)setName:为线程设置一个名称。

(10)wait:强迫一个线程等待。

(11)notify:通知一个线程继续运行。

(12)setPriority:设置一个线程的优先级。

(13)getPriority:获得一个线程的优先级。

 

线程上下文切换

定义

所谓的上下文切换,其实就是CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载,这段过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗cpu上执行。变成了可能。

上下文

是指某一时间点cpu寄存器和程序计数器的内容。

寄存器

是CPU内部的数量较少但是速度很快的内存(与之对应的是CPU外部相对较慢的RAM主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

程序计数器

是一个专用的寄存器,用于表明指令序列中CPU正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。

PCB-“切换桢”

上下文切换可以认为是内核(操作系统的核心)在cpu上对于进程(包括线程)进行切换,上下文切换过程中的信息是保存在进程控制块(pcb)中的。PCB还经常被称作“切换桢”。信息会一直保存到cpu的内存中,直到他们被再次使用。

上下文切换的活动

1. 挂起一个进程,将这个从进程在cpu中的状态(上下文)存储与内存中的某处。

2. 在内存中检索下一个进程的上下文,并将其在cpu的寄存器中恢复。

3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。

引起线程上下文切换的原因

1. 当前执行任务的时间片用完之后,系统cpu正常调度下一个任务。

2. 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务。

3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务。

4. 用户代码挂起当前任务,让出cpu时间。

5. 硬件中断。

 

如何在两个线程之间共享数据

java里面进行多线程通信主要方式就是共享内存的方式,共享内存的主要关注点有两个:可见性和有序原子性。java内存模型JMM解决了可见性和有序性的问题,而锁解决了原子性的问题,理想情况下我们希望做到同步和互斥。

 

 

参考:

1. 黑马程序员视频

持续更新!!!

  
 
 

posted @ 2020-03-19 06:25  夏夜凉凉  阅读(478)  评论(0编辑  收藏  举报