黑马程序员--多线程知识点总结

时间:2023-01-27 12:49:55



——- android培训java培训、期待与您交流! ———-

多线程的概念

进程:进程是资程序在操作系统上运行的过程,他是系统进行资源分配和调度的独立单位。

线程:在一个进程内又可以执行多个任务,一般将进程内部的任务称为线程。线程是程序中单个顺序的控制流,是程序使用CPU的基本单位。

线程与进程的主要区别

1.地址空间和其他资源:每个进程占有独立的地址空间,包括代码、数据、及其他资源。然而,属于同一个进程中的线程只能享受这些资源,而不具有独立的地址空间和其他资源。

2.通信:进程之间的通信(简称IPC)开销较大且受到诸多限制,必须具有明确的对象或操作接口并采用统一的通信协议;而线程间可以直接读取数据段(如全局变量)来进行通信,需要进程同步和互斥手段的辅助,以保证数据的统一性,开销较少且比较简单。

3.切换:进程间的切换开销也比较大,而线程间的切换开销较小。

4.控制表:与进程的控制表PCB相似,线程也有自己的控制表TCP,但是TCP中保存的线程状态远少于PCB。

提示
并发不是并行,前者是逻辑上的同时发生,是指在某一段时间内同时运行的多个线程;而后者是物理上同时发生的,是指在某一时间点同时运行多个线程,即进程是轮换执行的。

线程的创建
方式一:继承Thread类

public static void main(String[] args) {
        //第四步创建子类对象
        Mythead my = new Mythead();
        //第五步开启线程
        my.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println(my.getName() + "。。。第二条线程");
        }
    }
}   
//第一步 继承Thread

class Mythead extends Thread {
    //第二步 重写run方法
    public void run(){
        //第三步 把要做的事写入run方法中
        for (int i = 0; i < 1000; i++) {
            System.out.println("新实现的线程");
        }
    }
}

方式二:实现Runnable接口

public static void main(String[] args) {
        //第四步:创建定义类的对象
        MyRunnable myr = new MyRunnable();
        //第五步:创建Thread对象,将定义类的对象当作参数传入Thread的构造中,线程开启后后自动调用Runnable的run方法
        Thread t = new Thread(myr);
        //第六步:开启线程
        t.start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("……main……");
        }

    }
}
//第一步:定义类实现Runnable接口
class MyRunnable implements Runnable{

    @Override//第二步:重写run方法
    public void run() {
        //第三步:将要做的方法放在run方法中
        for (int i = 0; i < 1000; i++) {
            System.out.println("……MyRunnable……");
        }

    }

匿名内部类实现线程的两种方式

public static void main(String[] args) {

        //继承Thread
        new Thread(){//第一步:new 类(){}继承类
            public void run(){//第二步:重写run方法
                for (int i = 0; i < 10000; i++) {//第三步:将要执行的代码放在方法中

                    System.out.println( "第一种");
                }
            }
        }.start();//第四步:调用start方法开启线程

        //实现Runnable接口
        new Thread(new Runnable(){//第一步:new 接口(){}实现接口
            @Override
            public void run() {//第二步:重写run方法
                for (int i = 0; i < 10000; i++) {//第三步:将要实现的代码放在方法中
                    System.out.println("这是第二种");

                }
            }
        }).start();//第四步:开启线程
    }
}

比较继承Thread类和实现Runnable接口
继承Thread类和实现Runnable接口都可以实现多线程,但是在一般情况下都会选择后者。与继承Thread类相比,实现Runnable接口有如下好处:

1.可以避免由于java单继承的特性带来的局限。一个java只能继承一个类,但是可以实现多个接口。

2.适合多个相同程序代码的线程去处理同一个资源的情况;把线程同程序的代码、数据有效分离,较好的体现了面向对象的设计思想。

获取和设置线程的名字

1.使用SetName方法设置名字:

public static void main(String[] args) {
        Thread t1 = new Thread(){
            public void run(){
                for (int i = 0; i < 100; i++) {
                    System.out.println(getName() + "....运行线程");
                }
            }
        };


        Thread t2 = new Thread(){
            public void run(){
                for (int i = 0; i < 100; i++) {
                    System.out.println(getName() + "。。。。.......运行线程");//使用getName获取名字
                }
            }
        };

        t1.setName("第一条");//使用set方法设置名字

        t2.setName("第二条");

        t1.start();
        t2.start();
    }

}

2.传入参数设置名字:

public static void main(String[] args) {
        new Thread("参数设置名字1"){                        //向Thread构造内传入String类型的名字
            public void run(){
                for (int i = 0; i < 100; i++) {
                    System.out.println(getName()+ "...线程运行");//通过getName方法获取名字
                }
            }
        }.start();
        new Thread("参数设置名字2"){                           //向Thread构造内传入String类型的名字
            public void run(){
                for (int i = 0; i < 100; i++) {
                    System.out.println(getName()+ "...线程运行");//通过getName方法获取名字
                }
            }
        }.start();
    }

休眠线程

//Thread.sleep(毫秒,纳秒), 控制当前线程休眠若干毫秒1秒= 1000毫秒 1秒 = 1000 * 1000 * 1000纳秒 1000000000
    public static void main(String[] args) {
         new Thread(new Runnable(){
            @Override
            public void run() {

                for (int i = 0; i < 10; i++) {
                    try {                  //子类继承父类方法,父类中没有抛出异常子类遇有异常不能抛,只能处理
                    Thread.sleep(10);//休眠10毫秒
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }
                    System.out.println(Thread.currentThread().getName() + "输出一个");
                }
            }
        }).start();


         new Thread(new Runnable(){
             @Override
             public void run() {
                 try {

                    Thread.sleep(1000);
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }
                 for (int i = 0; i < 10; i++) {
                     System.out.println(Thread.currentThread().getName() + "。。。。输出另一个");
                 }
             }
         }).start();


    }

注意:使用sleep()方法时,有以下几点需要注意:

1.当线程休眠结束后,该线程并不会立即进入运行状态,而是进入就绪状态,等待JVM为其分配CPU。

2.当线程处于休眠状态时,如果该线程被中断,则会抛出InterrupedException异常。中断线程可以使用Thread类提供的interrupt()方法。

3.slep()是静态方法。在主方法内,无论是通过线程对象t调用sleep()还是直接通过Thread.sleep()形式去调用sleep()方法,都是休眠主线程,若想要线程实例休眠,sleep()方法应该放在run()内。

线程的优先级


public class ThreadDemo {

    public static void main(String[] args) {
        myThread mt = new myThread();        //创建myThread类的实例
        Thread t1 = new Thread(mt,"线程1");  //实例化Thread对象t1,并设置线程名称
        Thread t2 = new Thread(mt,"线程2");  //实例化Thread对象t2,并设置线程名称
        Thread t3 = new Thread(mt,"线程3");  //实例化Thread对象t3,并设置线程名称
        Thread t4 = new Thread(mt,"线程4");  //实例化Thread对象t4,并设置线程名称

        t1.setPriority(Thread.MIN_PRIORITY);  //设置线程t1的优先级为最低
        t2.setPriority(2);                      //设置线程t2的优先级为2
        t3.setPriority(Thread.NORM_PRIORITY);  //设置线程t3的优先级为5
        t4.setPriority(Thread.MAX_PRIORITY);   //设置线程t4的优先级为最高

        t1.start();  //启动线程
        t2.start();  //启动线程
        t3.start();  //启动线程
        t4.start();  //启动线程
    }

}

class myThread implements Runnable{

    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"……"+i);
        }
    }

}

线程的加入

public static void main(String[] args) {
        final Thread t = new Thread (){
            public void run() {
                for (int i = 0; i < 100; i++) {

                    System.out.println(getName() + "生日快乐");
                }
            }
        };


        Thread t2 = new Thread (){
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if(i==3){
                        try {
                            t.join();//插入线程当前线程暂停,当插入线程运行完后再继续
                                           //插入线程,当前线程停30毫秒
                            //t.join(30);//匿名内部类在访问这个方法 的局部变量时,变量要用final修饰
                        } catch (InterruptedException e) {

                            e.printStackTrace();
                        }
                    }

                    System.out.println(getName() + "。。。祝你自己生日快乐");
                }
            }
        };


        t.start();
        t2.start();
    }

提示:对于join()方法的使用有如下几点需要注意
(假设在当前运行线程为A中调用线程B的join()方法)

1.如果在没有join()方法中指定时间长度,则线程A 会等到B运行结束后,才会从阻塞状态转变为就绪状态,等待获取CPU。

2.如果在join()方法中指定了时间长度,线程B没有运行结束,则线程A也会在时间结束时,从阻塞状态转变为就绪状态。

3.如果在join()方法中指定了时间长度,且时间长度超过线程B的运行时间,则线程A会在线程B运行结束后,从阻塞状态转变为就绪状态。

守护线程

public static void main(String[] args) {
        Thread t = new Thread(){
            public void run() {
                for (int i = 0; i < 50; i++) {
                    //Thread.sleep(10);
                    System.out.println(getName() + "第一次输入");
                }
            }
        };


        Thread t1 = new Thread(){
            public void run() {
                for (int i = 0; i < 2; i++) {
                    //Thread.sleep(10);
                    System.out.println(getName() + "。。。。。。。。。。第一次输入");
                }
            }
        };
        t.setDaemon(true);//设置守护线程
        //setDaemon(), 设置一个线程为守护线程, 该线程不会单独执行, 当其他非守护线程都执行结束后, 自动退出
        t.start();
        t1.start();
    }

线程的礼让


public class ThreadDemo {

    public static void main(String[] args) {
        myThread1 mt1 = new myThread1();        //创建myThread1类的实例
        myThread2 mt2 = new myThread2();        //创建myThread2类的实例
        Thread t1 = new Thread(mt1,"线程1");  //实例化Thread对象t1,并设置线程名称
        Thread t2 = new Thread(mt2,"线程2");  //实例化Thread对象t2,并设置线程名称

        t1.setPriority(Thread.MIN_PRIORITY);  //设置线程t1的优先级为最低
        t2.setPriority(2);                      //设置线程t2的优先级为2

        t1.start();  //启动线程
        t2.start();  //启动线程
    }

}

class myThread1 implements Runnable{

    @Override
    public void run() {
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"……"+i);
            Thread.currentThread().yield();
        }
    }

}

class myThread2 implements Runnable{

    @Override
    public void run() {
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"……"+i);
            Thread.currentThread().yield();
        }
    }

}

提示:sleep()方法和yield()方法都可以让出运行权,但他们还是有很大不同的

1.sleep()方法让出运行权时,不考虑其他线程的优先级;而yield()方法只将运行权让给与其具有相同的优先级或比其具有更高的线程。

2.使用sleep()方法会让当前线程转到阻塞状态;而调用yield()方法则将当前线程转到就绪状态。

3.sleep()方法声明抛出InterruptedException异常;而yield()方法不抛出任何异常。

4.sleep()方法比yield()方法具有更好的移植性,不能依靠yield()方法来提高程序的并发性。

同步代码块

同步代码块的使用时机:

当多线程并发, 有多段代码同时执行时, 如果希望某一段代码执行的过程中CPU不要切换到其他线程工作. 这时就需要同步.

如果两段代码是同步的, 那么同一时间只能执行一段, 在一段代码没执行结束之前, 不会执行另外一段代码.

怎样实现代码块的同步:

public static void main(String[] args) {
        final Printer1 p = new Printer1();
        new Thread(){//匿名内部类在使用方法中的局部变量时必须用final修饰
            public void run() {
                while(true){
                    p.print1();
                    p.print2();
                }
            }
        }.start();
    }

}
/*使用synchronized关键字加上一个锁对象来定义一段代码, 这就叫同步代码块 * 多个同步代码块如果使用相同的锁对象, 那么他们就是同步的*/
class Printer1{
        T fd = new T();
    public  void print1(){
        synchronized(fd){//获取锁,,任意对象可以当锁传递
            //synchronized(new T()){//匿名对象不可以可以当锁传递,,因为不能确定匿名对象是同一对象也就不能确定是否同步

        System.out.print("print1……加油!");
        }              //释放所
    }


    public  void print2(){
        synchronized(fd){
            //synchronized(new T()){
            System.out.print("print2……加油!");
        }
    }
    }
    class T{}

synchronized修饰的方法:

public static void main(String[] args) {
        final Printer3 p = new Printer3();
        new Thread(){
            public void run(){
                p.print1();
                p.print1();
            }
        }.start();
    }

}

class Printer3{
    //synchronized 修饰的方法内全部代码都是同步的
    //如果想方法中的部分代码那就用synchronized修饰代码块
    //如果想用所有的代码就修饰方法

public synchronized void print1(){


    System.out.print("print1……加油!");

}


public synchronized void print2(){

    System.out.print("print2……加油!");

}
}

静态方法的同步

public static void main(String[] args) {
        final Printer p = new Printer();
        new Thread(){
            public void run(){
                p.print1();
                p.print1();
            }
        }.start();
    }

}
class Printer{
    //T fd = new T();
public static void print1(){
    synchronized(Printer.class){

    System.out.print("print1……加油!");
    }              //释放锁
}


public static void print2(){
    synchronized(Printer.class){//静态的方法中代码块要同步插入 字节码对象当锁 静态的同步函数的锁是:字节码对象
                                                               //非静态同步函数的锁是:this
    System.out.print("print2……加油!");
    }
}
}

死锁

//多线程同步的时候, 如果同步代码嵌套, 使用相同锁, 就有可能出现死锁
    //开发时尽量避免出现同步代码块的嵌套
    private static String s1 = "筷子一";
    private static String s2 = "筷子一";
    public static void main(String[] args) {
        new Thread(){
            public void run(){
                while(true){
                    synchronized(s1){
                        System.out.println(getName() +"获取" + s1 + "等待" + s2 );
                        synchronized(s2){
                            System.out.println(getName() +"获取" + s2 + "等待用餐" );
                        }
                    }

                }
            }
        }.start();


    new Thread(){
        public void run(){
            while(true){
                synchronized(s2){
                    System.out.println(getName() +"获取" + s2 + "等待" + s1 );
                    synchronized(s1){
                        System.out.println(getName() +"获取" + s1 + "等待用餐" );
                    }
                }

            }
        }
    }.start();
}

单例设计模式

作用是保证类在内存中只有一个对象。

代码:

public static void main(String[] args) {
        /*Singleton s1 = Singleton.getS();
        Singleton s2 = Singleton.getS();

        System.out.println(s1==s2);*/

        Singleton s1 = Singleton.s;
        Singleton s2 = Singleton.s;

        System.out.println(s1==s2);
    }

}//
/*class Singleton{
    private Singleton(){}//私有构造方法,不让其他类创建本类对象

    private static Singleton s = new Singleton();//创建本类对象

    public static Singleton getS(){//对外提供公共访问方式,
        return s;//返回本类对象
    }
}*/
//懒汉式 单例的延迟加载
/*class Singleton{
    private Singleton(){}//私有构造方法 ,不让其他类创建本类对象

    private static Singleton s ;//声明本类对象

    public static Singleton getS(){//对外提供公共访问方式
        if(s==null){
            s=new Singleton();//判断
        }
        return s;
    }
}*/
//饿汉式和懒汉式的区别
 // 都是单线程时
  //饿汉是空间换时间
  //懒汉式是时间换空间
  //都是多线程时
  //饿汉式没有安全隐患
  //懒汉式是可能出现安全隐患的,因为有可能创建多个对象
class Singleton{//第三种方法
    private Singleton(){}//私有构造方法
    public final static Singleton s = new Singleton();
}//创建本类公共的静态的final修饰的对象

Runtime类:是单例类

    public static void main(String[] args) throws IOException {
        Runtime r = Runtime.getRuntime();//

        //r.exec("shutdown -s -t 300");//设置关机时间
        r.exec("Shutdown -a");         //取消关机

Timer类:是一个计时器类在指定的时间执行指定的任务

public static void main(String[] args) throws InterruptedException {
        Timer t = new Timer();//创建计时器对象
        t.schedule(new Myclass(),new Date(115, 3, 9, 11, 57, 30),3000);//安排指定时间,重复执行任务
        //t.schedule(new Myclass(),new Date(115, 3, 9, 11, 57, 30),);//安排指定时间,执行指定任务
        while(true){
            Thread.sleep(1000);
            System.out.println(new Date());
        }
    }

}
class Myclass extends TimerTask{
    public void run(){
        System.out.println("叫我起床");
    }
}

线程间的通信

使用时机:

多个线程并发执行时, 在默认情况下CPU是随机切换线程的
如果我们希望他们有规律的执行, 就可以使用通信.

public static void main(String[] args) {
        final Demo2 d = new Demo2();
        new Thread(){
            public void run(){
                while(true){
                    try {
                        d.print();
                    } catch (InterruptedException e) {

                        e.printStackTrace();
                    }

                }

            }
        }.start();
        new Thread(){
            public void run(){
                while(true){
                    try {
                        d.print2();
                    } catch (InterruptedException e) {

                        e.printStackTrace();
                    }

                }

            }
        }.start();

    }

}
/*等待唤醒机制 * 在同步代码块中所对象是谁,就用哪个对象来调用wait * * 面试题一 * 为什么wait方法和notify方法需要定义在Object * 因为所有的对象都是Object的子类,而所有的对象都可以当做所对象 * * 面试题二 * 1,sleep方法必须传入参数,参数就是时间,当时间到了,会自动醒来 * wait方法不是必须传入参数,如果没有参数,遇到wait就等待,如果传入参数等传入的时间到了等待 * 2,sleep方法在同步中不释放锁 * wait方法在同步中释放锁*/
class Demo {
    private int flag = 1;//定义变量
    private Object obj = new Object();
    public void print() throws InterruptedException{
        synchronized(this){
            while(flag != 1){
                this.wait();//当前线程等待
            }
            System.out.print("print……加油!");
            flag = 2;
            this.notify();//唤醒等待的单个线程
        }

    }
    public void print2() throws InterruptedException{
        synchronized(this){
            while(flag != 2){
                this.wait();
            }
            System.out.print("print2……加油!");
            flag = 1;
            this.notify();
            //如果多个线程之间通信, 需要使用notifyAll()通知所有线程, 用while来反复判断条件
        }

    }

1.5新特性 互斥锁

同步
使用ReentrantLock类的lock()和unlock()方法进行同步

通信
使用ReentrantLock类的newCondition()方法可以获取Condition对象需要等待的时候使用Condition的await()方法, 唤醒的时候用signal()方法,不同的线程使用不同的Condition, 这样就能区分唤醒的时候找哪个线程了

public static void main(String[] args) {
        final Demo2 d = new Demo2();
    new Thread(){
        public void run(){
            while(true){
                try {
                    d.print();
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }

            }

        }
    }.start();
    new Thread(){
        public void run(){
            while(true){
                try {
                    d.print2();
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }

            }

        }
    }.start();
    new Thread(){
        public void run(){
            while(true){
                try {
                    d.print3();
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }

            }

        }
    }.start();

    }

}



class  Demo2{
    private ReentrantLock r = new ReentrantLock();
    private Condition c1 = r.newCondition();
    private Condition c2 = r.newCondition();
    private Condition c3 = r.newCondition();
private int flag = 1;
private Object obj = new Object();
public void print() throws InterruptedException{
    r.lock();//同步 ,上锁
        while(flag != 1){
            c1.await();//当前线程等待,需要等待的时候使用Condition的await()方法
        }
        System.out.print("print……加油!");
        flag = 2;
        c2.signal();//唤醒等待的单个线程,唤醒的时候用signal()方法
        r.unlock();//释放锁

}
public void print2() throws InterruptedException{
    r.lock();
        while(flag != 2){
            c2.await();
        }
        System.out.print("print2……加油!");
        flag = 3;
        c3.signal();
    r.unlock();

}
public void print3() throws InterruptedException{
    r.lock();
    while(flag != 2){
        c3.await();
    }
    System.out.print("print3……加油!");
    flag = 1;
    c1.signal();
    r.unlock();

}

线程池

概述

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池

内置线程池的使用步骤

public static void main(String[] args) {


        ExecutorService pool = Executors.newFixedThreadPool(3);//创建线程池对象
        MyRunnable m1 = new MyRunnable();//创建Runnable实例
        MyRunnable m2 = new MyRunnable();
        MyRunnable m3 = new MyRunnable();

        pool.submit(m1);//提交Runnable实例
        pool.submit(m2);
        pool.submit(m3);

        //pool.shutdown();//关闭线程池
    }
public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "我是线程池");
        }



    }

}

简单工厂设计模式

简单工厂模式 又叫静态工厂方法模式,它定义一个具体的工厂类负责创建一些类的实例
优点
客户端不需要在负责对象的创建,从而明确了各个类的职责
缺点
这个静态工厂类负责所有对象的创建,如果有新的对象增加,或者某些对象的创建方式不同,就需要不断的修改工厂类,不利于后期的维护

工厂方法模式

工厂方法模式中抽象工厂类负责定义创建对象的接口,具体对象的创建工作由继承抽象工厂的具体类实现。
优点
客户端不需要在负责对象的创建,从而明确了各个类的职责,如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可,不影响已有的代码,后期维护容易,增强了系统的扩展性

缺点
需要额外的编写代码,增加了工作量