当前位置 : 主页 > 编程语言 > java >

【尚硅谷复习笔记】多线程

来源:互联网 收集:自由互联 发布时间:2023-09-06
一、相关概念 1.1首先了解什么是程序、线程、进程 程序:指为完成特定任务,用某种语言编写的一组指令的集合,通俗易懂来讲程序就是一段静态的代码; 线程:程序组成线程,线是

一、相关概念

1.1首先了解什么是程序、线程、进程

程序:指为完成特定任务,用某种语言编写的一组指令的集合,通俗易懂来讲程序就是一段静态的代码;

线程:程序组成线程,线是程序内部的一条执行路径,对于程序而言线程的动态的,是CPU调度和执行的最小单位;

进程:线程组成进程,是程序的一次执行的过程,或是在在内存中运行的运用程序,如:打开的QQ/WX,进程至少由一个线程组成、是操作系统调度和分配资源的最小单位,一个进程若执行多个线程,就是支持多线程的。


1.2多线程优缺点

一个进程中多个线程共享相间的内存单元,他们从同一个堆中分配对象,可以访问相间的对象,提升了计算机系统CPU的利用率使得线程间的通信更加简洁、高效,但多个线程共享资源会带来安全隐患。·


1.3线程调度

分时调度:所有线程轮流使用CPU的使用权,并且平均分配每个线程占用CPU的时间。

抢占式调度:让优先级高的线程更大概率能够优先使用到CPU(java使用)


1.4并行与并发

并行:指两个或多个事件在同一时刻发生,指多个指令在多个CPU中同时执行,如:多个人同时在做不同的事情

并发:指两个或多个事件在同一时间段内发生,即一个时间段内有多条指令在单个CPU上快速切换,使得在宏观上具有多个进程在同时执行的效果


二、多线程的创建方式(四种)

2.1继承Thread类

(其实通过源码可以看到,继承的该Thread类也是实现了Runnable接口)

①定义Thread类的子类,并重写run()方法(方法体中代表了线程需要完成的任务)

②创建子类的实例(即创建了线程对象)

③通过子类调用线程对象的star()方法启动线程(启动线程后start方法会调用run方法,如果直接调用run方法线程未启动)

例子:创建分线程1遍历100内的偶数,线程2遍历100内的奇数

package Mon07.day0722;

public class ThreadTest{
 public static void main(String[]args){
     Thread1 t1 = new Thread1();
     Thread2 t2  = new Thread2();
     t1.start();
     t2.start();
     //注意:若还想建立新的R2线程,可以直接实例化并启动新线程
     Thread t3 = new Thread();
     t3.start();
 				}
}





class Thread1 extends Thread{
    @Override
    public void run(){
        for(int i = 0 ; i <= 100 ; i++){
            if(i%2 == 0){
                Thread.currentThread().setName("线程1");
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}


    class Thread2 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i <= 100; i++) {
                if (i % 2 != 0) {
                    Thread.currentThread().setName("线程2");
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }
        }
    }

2.2实现Runnable接口

①定义实现Runnable接口的子类,并重写run()方法(方法体中代表了线程需要完成的任务)

②创建子类的实例(即创建了线程对象)

③通过子类传入Thread类后再调用线程对象的star()方法启动线程(此子类和Thread类都实现了Runable接口,但是还是传入Thread类,是因为采用了代理)

案例

package Mon07.day0722;

public class RunnableTest {
    public static void main(String[] args) {
        Runnable1 R1 = new Runnable1();
        Thread t1 = new Thread(R1);
        t1.start();
        Runnable2 R2  = new Runnable2();
        Thread t2 = new Thread(R2);
        t2.start();
        //注意:若还想建立新的R2线程,可以直接实例化并启动新线程
        Thread t2 = new Thread(R2);
        t2.start();
    }

}



class Runnable1 implements Runnable{
    @Override
    public void run() {
        for(int i = 0 ; i <= 100 ; i++){
            if(i%2 == 0){
                Thread.currentThread().setName("线程1");//线程命名
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }

}class Runnable2 implements Runnable{
    @Override
    public void run() {
        for(int i = 0 ; i <= 100 ; i++){
            if(i%2 != 0){
                Thread.currentThread().setName("线程2");//线程命名
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }

}


拓展:继承Thread类和继承Runnable接口的区别

共同点:

①都要调用Thread类中定义的start()方法

②创建的线程对象都是Thread类或其子类的实例

不同点:一个是类的继承,一个是接口的实现

建议:常用Runnable接口实现

使用Runnable接口的好处在于:①该实现方法可以避免继承的局限性②更适合处理有共享数据的问题(继承需要通过静态共享数据,但接口数据天然 就会共享)


2.4实现Callable接口

(jdk5新增)

与之前的方法runnable()进行对比,有哪些好处?

1.call()可以有返回值,却因为callable使用了泛型参数,所以call()的返回值类型会更加灵活;

2.call()可以使用throws方式处理异常,更加灵活。

package Mon09.day0903;


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class NumThread implements Callable{

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            if (i%2 ==0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

public class CallableTest {
    public static void main(String[] args) {
        NumThread numThread = new NumThread();//创建callable接口实现类对象
        FutureTask futureTask = new FutureTask(numThread);//将对象传递到FutureTask构造器中,创建futureTask对象
        Thread thread = new Thread(futureTask);//将futureTask对象传递到Thread中,创建Thread对象并调用start()
        thread.start();

        try {
            Object sum = futureTask.get();
            //主线程带出,但如果此时主线程下、需要获取分线程call()的返回值,则此时主线程自带阻塞
            System.out.println("总和为:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

2.5 使用线程池

好处:

1.提高了程序执行的效率(因为线程已经提前创建好了)

2.提高了资源的复用率(因为执行完的线程并未销毁,而是可以继续执行其它的任务)

3.可以设置相关的参数,对线程池中的线程进行管理。



三、Thread的常用方法

3.1多线程的常用结构

3.1.1线程中的构造器

public Thread():分配一个新的线程对象;

public Thread(String name):分配一个指定名称的新线程对象;

public Thread(Runnale target):指定创建线程的目标对象,它实现了Runnable接口的run方法;

public Thread(Runnale target,String name):分配一个带有指定目标且指定名称的新线程对象;


3.1.2线程的常用方法

start():①启动线程 ②调用线程的run()

run():将线程要执行的操作,声明在run()中。

currentThread():获取当前执行代码对应的线程

getName():获取线程名

setName():设置线程名

sleep(long millis):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数

yield():静态方法,一旦执行即可能主动释放本次CPU的执行器

join():在线程a中通过线程b调用join(),以为着线程a进入阻塞状态,直到线程b执行结束,线程a才会结束阻塞状态继续执行

isAlive():判断当前线程的存活情况

getPriority():获取线程的优先级,未设置的默认为5级(1-10级)

三个优先级常量:MAX_PRIORITY(10),MIN_PRIORITY(1),NORM_PRIORITY(5)

setPriority(int newPriority):改变线程的优先级,范围[1,10]

过时方法(了解即可):

public final void stop(): 强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。

void suspend()/void resume():这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用resume()。已过时 ,不建议使用。


四、多线程的生命周期

了解即可。

【尚硅谷复习笔记】多线程_多线程

【尚硅谷复习笔记】多线程_多线程_02

五、如何使用同步机制解决线程安全问题(synchronized)

关于什么是线程安全问题:当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

5.1同步代码块

synchronize(同步监视器){
	//需要被同步的代码
}

说明:

1.需要被同步的代码,即为操作共享数据的代码。

2.共享数据:即多个线程多需要操作的数据。比如:ticket

3.需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其它线程必须等待,同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码。

4.同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器。

重点:在实现Runnable接口的方式中,同步监听器/锁 可以考虑使用this

    在继承Thread类的方式中,同步监听器慎用this(很可能多个实现类this不唯一)

package day0801;

import sun.plugin2.os.windows.Windows;


class wimdow extends Thread{

            static int ticket = 100;
            static Object obj = new Object();
    
            public void run(){
                while(true){
                    //synchronized(this){//实例化多个对象是,this代表t1 t2 t3,不能代表唯一性
                    // synchronized(obj){//obj若静态,则可以保证唯一性
                    synchronized(Windows.class){
                        if(ticket > 0) {
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                            System.out.println(Thread.currentThread().getName()+"售票,票号:"+ticket);
                            ticket--;
                        }else{break;}
                    }
                }
            }
}

public class Window{
    public static void main(String[] args) {
        wimdow w = new wimdow();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

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


5.2同步方法

如果操作共享数据的代码完整声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。

非静态的同步方法,默认同步监听器为this;

静态的同步方法,默认同步监听器为当前类本身。

package day0803;


import sun.plugin2.os.windows.Windows;

class wimdow1 extends Thread{

    static int ticket = 100;
    static Object obj = new Object();
    boolean isFlag = true;

    public synchronized void run(){
        while(isFlag){
               show();
            }
        }

    public void show(){
        if(ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName()+"售票,票号:"+ticket);
            ticket--;
        }else {isFlag = false;}

    }
}


public class WindowTest1 {
    public static void main(String[] args) {
        wimdow1 w = new wimdow1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

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

5.3synchronize好处与弊端

好处:解决了线程的安全问题

弊端:在操作共享数据时,多线程是串行执行的,意味着性能变低。

六、死锁

是什么

不同线程分别占用对方需要的同步资源不放弃,都在等对方放弃自己需要的同步资源,导致形成死锁。

死锁出现后,程序不会发生异常,不会给出提示,只是所有线程处于阻塞状态无法继续。


诱发死锁的原因

-条件互斥

-占用且等待

-不可抢夺

循环等待

以上四个条件,同时出现则会触发死锁。


如何避免死锁

针对条件1:互斥条件基本无法被破坏,因为线程需要通过互斥解决安全问题

针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待问题

针对条件3:占用部分资源的线程进一步申请其它资源时,如果申请不到则主动释放已经占用的资源

针对条件4:可以将资源改为线性顺序。申请资源时先申请序号较小的,避免循环等待问题。


七、Lock锁的使用

使用步骤:

1.创建lock的实例,确保多个线程公用一个lock实例(static final)

2.执行lock方法,锁定共享数据的方法

3.unlock()的调用,释放对共享数据的锁定

案例:

package day0828;

import java.util.concurrent.locks.ReentrantLock;


    class Window extends  Thread{
        static  int ticket = 100;

        //1.创建lock的实例,确保多个线程公用一个lock实例(static final)
        private static final ReentrantLock lock = new ReentrantLock();

        @Override
        public void run() {
            while (true){
                try {
                    //2.执行lock方法,锁定共享数据的方法
                    lock.lock();
                if (ticket > 0){


                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                            System.out.println(Thread.currentThread().getName()+"售票,售票号为:"+ticket);
                            ticket--;
                        }else {break;}


                }finally {
                    //3.unlock()的调用,释放对共享数据的锁定
                    lock.unlock();
                }
            }
        }
    }
package day0828;


public class LockTest {
    public static void main(String[] args) {
        Window window = new Window();
        window.start();
    }
}

面试题:

一、synchronized方法与lock的对比?

synchronized不管是同步代码块还是同步方法,都需要在结束一对{}后释放对同步监视器的调用。

lock是通过两个方法控制需要被同步的代码,更加灵活一些。

Lock作为接口,提供了多种实现类,适合更多复杂的场景,效率更高

二、wait和sleep方法的区别?

相同点:一旦执行,当前线程就会进入阻塞状态

不同点:

声明的位置:wait声明在Object中,sleep在Thread类中且为静态

使用的场景不同:wait只能在同步代码块或同步方法中,sleep在任何需要使用的场景中

使用在同步代码块中时:wait一旦执行则会释放同步监视器,sleep不会释放。

结束阻塞的方式:wait:到达指定时间自动结束或者notify、notifyAll唤醒,sleep通过指定时间自动结束


八、线程通信

当我们需要多个线程有规律的共同执行一个任务时,多线程间就需要一些通信机制来协调它们的工作。

比如:线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,此时B线程必须等到A线程完成后才能执行,那么线程A与线程B之间就需要线程通信,即——等待唤醒机制。

等待唤醒机制关键字:

wait:线程不再活动、参与调度,进入到wait set中,因此不会浪费CPU资源,不会竞争锁,此时线程的状态会转变为WAITING或者TMED_WAITING。只有当别的线程执行了一个特别动作:通知(notify)或者等待时间到,这个对象上等待的现场才会从wait set中释放出来,重新进入到调度队列(ready queue)中。

notify:选取所通知对象的wait set中一个线程释放;

notifyAll:释放所通知对象的wait set中所有线程;

注意:

1.此三个方法必须是在同步代码块或同步方法中。(需要配合condition实现线程间的通信)

2.此三个方法的调用者必须是同步监视器【synchronized (this)中的this】

例子:

package day0828;
/*
使用两个线程交替打印1-100
 */

import javafx.scene.control.TableRow;

class PrintNumber implements Runnable{
    private int number = 1;
    @Override
    public void run() {

        synchronized (this) {
            while (true){
                notify();
                if (number <= 100){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }else {break;}
            }
        }
    }
}

public class PrintNumberTest {
    public static void main(String[] args) {
        PrintNumber p = new PrintNumber();
        Thread t1 = new Thread(p, "线程1");
        Thread t2 = new Thread(p, "线程2");

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

九、案例

生产者Productor将产品交给店员Clerk,消费者从店员取走,店员最多持有20件产品,超过20件的话就会通知生产者停止生产,少于20件后才会恢复生产,若无产品店员则会让消费者稍等,等有产品后再通知消费者消费。

分析:多线程为消费者、生产者,共享数据为店员的产品(存在共享数据就会有线程安全问题)。


package Mon08.day0830;

/**
 *
 * 生产者Productor将产品交给店员Clerk,消费者从店员取走,店员最多持有20件产品,超过20件的话
 * 就会通知生产者停止生产,少于20件后才会恢复生产,
 * 若无产品店员则会让消费者稍等,等有产品后再通知消费者消费。
 */

//店员
class Clerk{
    private int productNum = 0;//产品初始数量

    public synchronized void addProductNum() {

        if (productNum >= 20){
            try {
                wait();//等待
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        System.out.println(Thread.currentThread().getName()+"生产了第"+productNum+"个产品");
        productNum++;
        notify();
    }

    public synchronized void minusProductNum() {

        if (productNum <= 0){
            try {
                wait();//等待
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        System.out.println(Thread.currentThread().getName()+"消费了第"+productNum+"个产品");
        productNum--;
        notify();
    }
}

//生产者
class Productor extends Thread{

    private Clerk clerk;

    public Productor(Clerk clerk){
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while (true){
            System.out.println("生产者生成产品……");


            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            clerk.addProductNum();

        }
    }
}

//消费者
class Comsumer implements Runnable{
    private Clerk clerk;

    public Comsumer(Clerk clerk){
        this.clerk = clerk;
    }


    @Override
    public void run() {

   while (true){
       System.out.println("消费者开始消费产品");
       try {
           Thread.sleep(50);
       } catch (InterruptedException e) {
           throw new RuntimeException(e);
       }
       clerk.minusProductNum();
   }
    }
    }



public class ProducerComsumerTest {

    public static void main(String[] args) {

        Clerk clerk = new Clerk();
        Productor p1 = new Productor(clerk);
        Comsumer c1 = new Comsumer(clerk);

        p1.start();
        Thread thread = new Thread(c1);
        thread.start();

        p1.setName("生产者1");
        thread.setName("消费者1");

    }

}



#在职重学JAVA基础

【文章原创作者:防ddos攻击 http://www.558idc.com/shsgf.html 复制请保留原URL】
上一篇:优化数据库性能:深入研究数据库索引
下一篇:没有了
网友评论