多线程的主要内容

1、串行,并发和并行

2、进程与线程

3、线程的生命周期

4、线程的开辟方式

5、线程的常用方法

6、临界资源问题

7、线程池

 串行,并发和并行

串行通信(英语:Serial communication)是指在计算机总线或其他数据通道上,每次传输一个位元数据,并连续进行以上单次过程的通信方式。

并行通信,它在串行端口上通过一次同时传输若干位元数据的方式进行通信。

通俗的来讲:

串行就是按照一定的顺序,顺序执行多个任务;

并发就是同一时间,同一个人交替完成多个任务,交叉时间段只能选择一个任务来完成,现并发的方式有多种:比如多进程、多线程、IO多路复用;

并行就是多个人同一时间,每个人一个任务的方式共同完成多个任务。

(实际上CPU在执行任务的过程中,一次只能执行一个任务不能同时执行多个任务(CPU是非常专一的男人),但是CPU具有高速运算能力,例如你在用你的计算机进行Word工作的同时,还在用音乐软件听歌,两个程序能够同时进行。实际上他们是分开进行的,CPU在轮流的为两个程序进行计算,但由于CPU的高速运算能力,让我们产生了两个程序是分开进行的幻觉)

 

进程与线程

多进程:进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

多线程:线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

线程和进程各自有什么区别和优劣呢?

进程是资源分配的最小单位,线程是程序执行的最小单位。

进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间

线程的生命周期

关于线程的生命周期有很多种的说法,比如有说四种状态的(新建,运行,中断,死亡),有说六种状态的(初始化,可运行/运行,阻塞,无时间限制等待状态,有时间限制等待状态,终止态),也有说五种状态的(新生态,就绪态,运行态,阻塞态,死亡太);下面我就以为五种状态为标准解释一下:

1、新生态:New

  线程对象被创建的状态,即new这个对象的过程是的状态。此时在计算机中已经为这个线程分配了资源。但还未开始争抢CPU时间片。

2、就绪态:Ready

  一个线程对象被开启,(调用了start方法),已经开始争抢CPU时间片的状态。

3、运行态:Run

  线程对象抢到了CPU时间片,开始执行线程中的逻辑。

4、阻塞态:Interrupt

  一个线程在运行状态时,遇到了某些情况导致该线程放弃了占有的CPU资源。

  导致线程阻塞的原因主要有:

    线程执行了sleep(int)方法进入了休眠状态,该状态属于有时间限制的等到状态传入次方法的参数是一个整数单位是毫秒。

    线程执行了wait()方法进入了等待状态,该状态属于无时间限制的等待状态,等待其他线程执行了notify()或者notifyAll()方法后在等待区的线程再继续执行操作。

    等待用户输入时进入阻塞态

5、死亡态:Dead

  线程死亡有两种方式。(就跟人死的方式一样有两种,要么老死,要么发生了意外而死)线程死亡的方式,正常结束run方法后线程死亡、或者run方法执行过程中被强制结束线程死亡;非正常结束,出现程序异常启动了异常处理,退出线程,线程死亡。

                   

线程的开辟方式

首先要开辟线程就需要有一个目标对象,即开辟的这个线程要他来做什么事。

1、使用Thread的子类

  使用Thread子类来创建一个目标对象。在编写Thread子类时,需要重写父类的run方法。Thread中的run方法不是其本身的方法,他是implements Runnable接口中的抽象方法run。run方法是线程所要执行的逻辑内容。如果不在子类中重写run方法,线程也就不会执行操作。

  通过Thread子类创建线程的优点是可以在子类中增加新的成员变量,使其具有某种属性,也可以在子类中增加方法,实现某种功能;但是java不支持多继承Thread子类不能再继承其他的类。

package thread_test1;

public class TestThread {

    public static void main(String[] args) {
        
        Thread thread1;
        ThreadCar car1 = new ThreadCar("Car_one");
        Thread thread2 = new ThreadDog("Dog_one");
        thread1 = car1;
        thread1.setPriority(10);
        thread1.setPriority(1);
        thread1.start();
        thread2.start();
//        a.out.println();
    }
    
}

class ThreadCar extends Thread {
    public ThreadCar(){}
    public ThreadCar(String name) {
        this.setName(name);
    }
    public void run() {
        for(int i=0; i<10; i++) {
            System.out.print(this.getName()+i+" ");
        }
        System.out.println();
        System.out.println("Car Thread");
        System.out.println();
    }
}

class ThreadDog extends Thread {
    
    public ThreadDog(){}
    public ThreadDog(String name) {
        super(name);
    }
    
    public void run() {
        for(int i=0; i<15; i++) {
            System.out.print(this.getName()+i+" ");
        }
        System.out.println();
        System.out.println("Dog Thread");
        System.out.println();
    }
}

为了解决继承的问题,我们可以采用直接实现Runnable接口的方式,或者采用匿名内部类的方式来创建线程,这样就提高的代码的灵活性

package thread_test1;

/**
 * 创建了一个house类实现了Runnable接口,Runnable接口中有
 * 一个抽象方法run()需要在实现类中实现此方法
 * 该方法是线程抢到了cup资源时所执行的逻辑内容
 * @author Dell
 *
 */
public class House implements Runnable{

    private int water = 10;
//    Thread dog, cat;
    
//    public House() {
//
//        dog = new Thread(this);
//        cat = new Thread(this);
//    }
    
    public void run() {
        
        while(water>1) {
            if(Thread.currentThread().getName().equals("dog")) {
                dogDrink();
                
            }
            if(Thread.currentThread().getName().equals("cat")) {
                catDrink();
                
            }
            
        }
    }
    
    /*
     * synchronized 是一个修饰符,与final static 等修饰符为同一类型
     * 他的作用是使该方法遵循线程同步机制
     * 线程同步机制:当一个线程A使用synchronized方法时,其他线程想使用这个synchronized方法时就必须等待,
     * 直到线程A使用完该synchronized方法
     */
    
    /**
     * 创建一个线程同步方法
     * 狗喝水
     */
    public synchronized void dogDrink() {
        water -= 1;
        System.out.println("dog drink water"+1+", remaining["+water+"]");
        try{
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 创建一个猫喝水的线程同步方法
     */
    public synchronized void catDrink() {
        water -= 1;
        System.out.println("cat drink water"+1+", remaining["+water+"]");
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//===========main部分
package thread_test1;


/**
 * 线程同步
 * @author Dell
 *
 */
public class Test2 {

    public static void main(String[] args) {
        
        House house = new House();
        Thread dog, cat;
        
        dog = new Thread(house);
        cat = new Thread(house);
        
        dog.setName("dog");
        cat.setName("cat");
        
        dog.start();
        cat.start();
     
    }
}

上述代码可以仅采用未注释的部分

线程的常用方法

线程中常用的方法如下:

1、start()启动线程的方法,创建的线程对象必须先调用此方法,使其开始争抢CPU时间片。线程在调用了start()方法后就不必再调用此方法了,否则将产生IllegalTHreadStateException异常

2、run()方法Thread中的run()与Runnable中的run()功能相同作用也一样,都是用来定义线程对象别调用后所执行的操作。当run()方法执行完毕后线程就进入了死亡状态,即将分配给线程的内存资源释放。

3、sleep(int millsecond)方法此方法传入一个以毫秒为单位的整数,使线程进入休眠状态,即放弃占用的CPU资源并在指定的时间内不再参加CPU时间片的争抢。

4、wait()方法此方法是Object类中的方法。此方法会使线程进入等待状态,该状态是无时间限制的等待状态,需要其他线程执行notify()notifyAll()方法是才会重新唤醒等待区的线程继续参加CPU的时间片争抢。

5、notify()唤醒在此对象监视器上等待的单个线程

6、notifyAll()唤醒在此对象监视器上等待的所有线程

...

临界资源问题

  我们知道线程与线程之间的资源是可以共享的,也就是说多个线程各一访问同一个资源,这个被多个线程访问的资源就叫做临界资源。

  那么什么是临界资源问题呢。我们举个栗子:

  采用上面第二段代码块举例,如果我将dogDrink()方法和catDrink()方法前的synchronized修饰符去掉。程序执行后的结果就会是这个样子

我们可以观察到

dog喝完剩下9,

cat喝完剩下8,

dog喝完剩下7,

cat喝完剩下7,

此时我们就发现问题了,刚刚dog喝完剩下7,怎么cat来喝还剩下7呢?这显然不符合逻辑。

其实程序是这样的,程序创建了两个线程一个dog 一个cat 从开始程序

dog抢到了CPU的时间片开始执行操作water-1=9,恰好将其打印再了控制台,这是cat线程抢到了CPU时间片执行了喝水的操作water-1=9,这时还是cat抢到了CPU时间片,但是这一次操作只进行了water-1,来没有来得及做赋值给water和打印操作就被dog抢走了CPU,此时water的值还是8 进行了water-1操作并赋值完成打印了出来,这个时候cat又抢回了CPU,这个时候cat接着上次被中断的位置继续执行操作将7赋值给water并进行了打印。于是就出现了上面的逻辑错误。

这就是一个典型的临界资源问题。为了解决这个问题。我们就在这些方法前面增加了一个修饰符synchronized,此修饰符的左右就是在该线程还没有完成这一项操作时,即使自己再休息,其他的线程也无法访问这个资源。这样理解比较抽象。我们可以吧synchronized当做是一把锁,线程比作是人,临界资源比作是房子,每个人(线程)的锁(synchronized)都是他自己特有的,当这个人(线程)在使用某个房子(资源)时,这个人想出去散散步,但是房子这个人还需要继续使用(中断);这个人(线程)就给这个房子(资源)上把锁(synchronized),其他人(线程)来就不能进入这个房子(访问这个资源)。知道正在使用这个房子(资源)的人(线程)使用完毕解锁之后,其他人(线程)才能继续访问。

线程池

1.、线程池的概念:

  线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

2、线程池的工作机制:

  在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。

  一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

3.、使用线程池的原因:

  多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过渡消耗系统资源,以及过渡切换线程的危险,从而可能导致系统资源的崩溃。这时,线程池就是最好的选择了。

 

 

------------个人学习总结,若有错误,欢迎指出,谢谢大佬们--------

 

posted on 2019-07-29 17:17  Gary757  阅读(215)  评论(0编辑  收藏  举报