多线程笔记

目录

1. 概念

进程与线程的区别:

多线程的概念

 多线程优缺点

​编辑创建多线程的几种方式

1、继承Thread类

2、实现Runnable接口

3、实现Callable接口

实现Runnable和Callable有什么区别:

相同点:

不同点:

注意点:

4、线程池实现多线程

创建线程的方式的比较

实现runnable和继承thread的区别

案例:

ping通(用java写windows的操作)

以上方法如果多个会很慢 一个地址*18秒,所以会用多线程

综合案例:

线程安全问题:

线程休眠:

同步代码块

同步 、异步

同步锁:

同步方法:

synchronized

Lock

Synchronized和Lock区别(面试题)

多线程的常用方法:

currentThread():

getName():

run():

start():

线程优先级:

setPriority()

线程礼让:

yield 当前线程让出cpu-参与下次的竞争

线程插队:

join加入当前线程上

守护线程:

setDaemon()设置线程为守护线程

死锁

多线程的状态

五种状态:

线程通信:

生产者、消费者


1. 概念

① 进程:是一次执行程序的过程,是系统资源分配的单位。
② 线程:一个进程通常包括若干个线程,一个进程中至少有一个线程,线程是CPU调度和执行的单位。
③ Main()为主线程,为系统的入口。

进程与线程的区别:

线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;进程是系统资源分配的单位,线程是系统调度的单位

一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;

进程之间相互独立,进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。同时线程还有自己的栈和栈指针,程序计数器等寄存器。

调度和切换:线程上下文切换比进程上下文切换要快得多

多线程的概念

       多线程(Multi-Threading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能(好处)。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作线程Thread),利用它编程的概念就叫作多线程处理

 多线程优缺点

优点:

   1)可以让程序运行速度更快     用户在频繁切换运行进程的界面,界面下运行进程底层都有多个线程不断运行着,CPU一直处于运行状态。

    

   2)  提高CPU利用率    大量线程同时处于运行状态,让CPU不间断运行

缺点:

   1)   多线程情况,多个线程可能共享一个资源,如果资源数量有限,多线程竞争会产生等待问题。如多台电脑共享一个打印机。

   2) 多线程,不断进行线程(上下文)切换,线程切换消耗资源的。

3) 多线程,有可能造成死锁。(后面专门讲多线程死锁现象,实例,解决死锁方法

打勾的地方为代码写的

创建多线程的几种方式

1、继承Thread类

自定义类继承Thread类,

重写run方法,

创建线程对象,

调用start方法启动

public class MyTicket extends Thread{

    public MyTicket(){}

    public MyTicket(String name){

    }

    public void run(){
        for (int i=1;i<=10;i++){
            //currentThread()返回对当前正在执行的线程对象的引用。
            //  public static native Thread currentThread(); static 类加载时方法被执行
            //  native  底层代码不是java编写是c或者C++编写
            System.out.println(Thread.currentThread().getName() + "正在运行:"+i);
        }
    }
}

text类

 //其中mian也是一个多线程方式
    public static void main(String[] args) {

        //创建几个多线程类
        //其中如果不给多线程命名,系统会自动命名
        MyTicket t1=new MyTicket();
        MyTicket t2=new MyTicket();

        //启动线程执行任务
        //使用run方法执行线程任务是错误的执行方式,这种是普通的方法调用,不是多线程的执行方式
//        t1.run();
//        t2.run();

        //使用start方法,表示该线程准备就绪,可以让调度器进行调度
        t2.start();
        t1.start();

        System.out.println(Thread.currentThread().getName()+":在执行....");

    }

2、实现Runnable接口

Runnabel是线程任务,不能使用直接像继承Thread一样使用start方法

创建MyThreadRunnable类

public class MyTicketRunnable implements Runnable {
    /**
     * 通过实现Runnable,定义要执行的线程任务
     */
    @Override
    public void run() {
        for (int i=1;i<=10;i++){
            System.out.println(Thread.currentThread().getName()+":执行了任务"+i+"....");
        }
    }
}

测试类

 public static void main(String[] args) {
        //定义线程任务
        MyTicketRunnable m1=new MyTicketRunnable();
        MyTicketRunnable m2=new MyTicketRunnable();
        MyTicketRunnable m3=new MyTicketRunnable();

        //创建线程主体(施工小组),关联要执行的任务
        Thread t1=new Thread(m1);
        Thread t2=new Thread(m2);
        Thread t3=new Thread(m3);

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

        System.out.println(Thread.currentThread().getName()+"执行了....");
    }

3、实现Callable接口

Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的(都可以实现多线程)。 然而,  Runnable不返回结果,也不能抛出被检查的异常。 Callable可以返回结果,也可以检测获取异常。

Callable需要使用FutureTask类帮助执行,FutureTask类结构如下:

判断任务是否完成:isDone()

能够中断任务:cancel()

能够获取任务执行结果get()  

public class MyCallable implements Callable {

    @Override
    public Object call() throws Exception {
        for (int i=1;i<=100;i++){
            System.out.println(Thread.currentThread().getName()+"运行了"+i+"...");
        }
        return null;
    }
}
public class Text {
    public static void main(String[] args) {
        //创建线程任务对象
        MyCallable myc=new MyCallable();
        //创建异步任务对象
        FutureTask f1=new FutureTask(myc);
        FutureTask f2=new FutureTask(myc);

        //定义线程对象
        Thread t1=new Thread(f1);
        Thread t2=new Thread(f2);

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

小案例:求分段和

public class SumCallable implements Callable {
    private int begin;
    private int end;

    public SumCallable(int begin,int end){
        this.begin=begin;
        this.end=end;
    }

    /**
     * 求指定范围的数据和
     * @return
     * @throws Exception
     */
    @Override
    public Object call() throws Exception {
        int sum=0;
        for (int i=begin;i<end;i++){
            System.out.println(Thread.currentThread().getName()+"进行累加"+i);
            sum+=i;
        }
        System.out.println(Thread.currentThread().getName()+"计算结束,和为"+sum+" ...");
        return sum;
    }
}
public class Text {
    public static void main(String[] args) throws Exception{
        //创建线程对象
        SumCallable sc1=new SumCallable(1,40);
        SumCallable sc2=new SumCallable(51,10);

        //创建异步任务对象
        FutureTask ft1=new FutureTask(sc1);
        FutureTask ft2=new FutureTask(sc2);

        //创建线程对象
        Thread t1=new Thread(ft1);
        Thread t2=new Thread(ft2);

        //让线程处于就绪状态,等待调度器调度
        t2.start();
        t1.start();

        //通过异步任务对象,获取关联任务的返回结果
        //get():会等待线程任务执行结束
        int sum1=(int) ft1.get();
        int sum2=(int) ft2.get();
    }
}

实现Runnable和Callable有什么区别:

相同点:

两者都是接口;

两者都可用来编写多线程程序;

两者都需要调用Thread.start()启动线程;

不同点:

实现Callable接口的线程能返回执行结果;而实现Runnable接口的线程不能返回结果;

Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的不允许抛异常;

实现Callable接口的线程可以调用Future.cancel取消执行 ,而实现Runnable接口的线程不能

注意点:

Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!  

4、线程池实现多线程

Executors工厂和工具类Executor , ExecutorService , ScheduledExecutorService , ThreadFactory和Callable在此包中定义的类。

Executor接口:

声明了execute(Runnable runnable)方法,执行任务代码

ExecutorService接口:

继承Executor接口,声明方法:submit、invokeAll、invokeAny以及shutDown等

AbstractExecutorService抽象类:

实现ExecutorService接口,基本实现ExecutorService中声明的所有方法

ScheduledExecutorService接口:

继承ExecutorService接口,声明定时执行任务方法

ThreadPoolExecutor类:

继承类AbstractExecutorService,实现execute、submit、shutdown、shutdownNow方法

ScheduledThreadPoolExecutor类:

继承ThreadPoolExecutor类,实现ScheduledExecutorService接口并实现其中的方法

  1. 该类支持以下几种方法: 
    • 创建并返回一个ExecutorService设置的常用的配置设置的方法。  
    • 创建并返回一个ScheduledExecutorService的方法,  其中设置了常用的配置设置。  
    • 创建并返回“包装”ExecutorService的方法,通过使实现特定的方法无法访问,来禁用重新配置。 
    • 创建并返回将新创建的线程设置为已知状态的ThreadFactory的方法。  
    • 创建并返回一个方法Callable,这样他们就可以在需要的执行方法使用Callable  。
package com.Ticket7;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i=1;i<=20;i++) {
            System.out.println(Thread.currentThread().getName() + "执行任务了:"+i+".......");
        }
    }
}
import java.util.concurrent.Executors;

public class Text {
    public static void main(String[] args) {
        //创建固定数量线程的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        //通过线程池,获取其中创建好的线程,执行线程任务
        MyRunnable r = new MyRunnable();

        executorService.execute(r);
        executorService.execute(r);
        executorService.execute(r);
        executorService.execute(r);
        executorService.execute(r);
        executorService.execute(r);

        //关闭线程池
        executorService.shutdown();

    }
}

这里会在线程池中先创造固定的条数,然后如果来一个直接调,但是当超过了固定条数,就需要等待。

创建线程的方式的比较
  1. 实现接口和继承Thread类比较

 接口更适合多个相同的程序代码的线程去共享同一个资源。

 接口可以避免java中的单继承的局限性

 接口代码可以被多个线程共享,代码和线程独立

 线程池只能放入实现Runable或Callable接口的线程,不能直接放入继承Thread的类。  

  1. 扩充:

  在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。

实现runnable和继承thread的区别

       受java单继承影响,继承thread无法实现成员变量共享(不用特殊手段,如加static),实现Runnable接口时,线程运行可以共享成员变量。

案例:

为了更清楚知道区别,这里有一个卖票的案例

继承Thread

//各卖各的
public class SaleThread extends Thread{
    //定义票数
    private int ticket=10;

    public SaleThread(String  name){
        super(name);
    }
    @Override
    public void run() {
              while (true){
           if (ticket>0){
               System.out.println(Thread.currentThread().getName()+"卖票一张,还剩"+(--ticket)+"张");
           }else {
               break;
           }
       }

    }
}
public class Text {
    public static void main(String[] args) {
        //创建两个线程对象,卖票
        SaleThread s1 = new SaleThread("张三");
        SaleThread s2 = new SaleThread("李四");
        //启动卖票线程
        s1.start();
        s2.start();
    }
}

Runnabel接口

public class SaleRunnable implements Runnable{
    //定义票数
    private int ticket=10;
    @Override
    public void run() {
        //由于这里的一些目前解决不了的问题,会导致超卖的一个事情
        //两个线程的时间定的嘛,然后会出现一个线程进入if循环了但是时间到了,下个线程也进去了就出现超卖的结果
        while (true){
            if (ticket>0){
                System.out.println(Thread.currentThread().getName()+"卖票一张,还剩"+(--ticket)+"张");
            }else{
                break;
            }
        }
    }
}
public class text {
    public static void main(String[] args) {
        //创建共享任务实例
        SaleRunnable sr = new SaleRunnable();
        //创建两个线程
        Thread t1 = new Thread(sr,"张三");
        Thread t2 = new Thread(sr,"李四");

        //线程就绪,准备卖票
        t1.start();
        t2.start();
    }
}

上面会出现超卖的情况,后期会学到解决方法。

ping通(用java写windows的操作)

就是对于每台电脑进行一个访问,得到数据什么的一般在cmd中进行运行。

public class Test {
    public static void main(String[] args) throws IOException {
        //性能统计
        //获取当前系统时间的毫秒数
        long start = System.currentTimeMillis();


        //使用java,启动windows程序,执行特定命令
        Runtime runtime = Runtime.getRuntime();
        //通过runtime执行windows程序,返回程序的进程对象
        Process exec = runtime.exec("ping 192.168.1.142");
        //获取ping之后的输出信息
        InputStream is = exec.getInputStream();
        BufferedReader br = new BufferedReader(
                new InputStreamReader(is)
        );
        String res = null;
        while ((res = br.readLine())!=null){
            if(res.contains("TTL")){
                System.out.println("能够ping通...");
                break;
            }
        }

        long end = System.currentTimeMillis();
        System.out.println((end-start)+"毫秒");
    }
}
以上方法如果多个会很慢 一个地址*18秒,所以会用多线程
综合案例:

PingUtil类

public class PingUtil {
    public static void ping(String ip,long start){
        InputStream is=null;
        BufferedReader br=null;
        try {
            //创建运行时对象,用于执行windows程序
            Runtime r = Runtime.getRuntime();
            Process process = r.exec("ping " + ip);
            //获取ping结果
            is = process.getInputStream();
            //通过缓冲字符流接受返回结果
            br = new BufferedReader(new InputStreamReader(is));
            //逐行读取ping的结果
            String str = null;
            boolean isPingSuc = false;
            while ((str=br.readLine())!=null){
                if(str.contains("TTL")){
                    isPingSuc=true;
                    break;
                }
            }

            //结束的时间
            long end =System.currentTimeMillis();

            if(isPingSuc){
                System.out.println("ping "+ip+" 通了("+(end-start)+"ms)******************");
            }else{
                System.out.println("ping "+ip+" 没有通了("+(end-start)+"ms)");
            }



        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(is!=null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(br!=null){
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class PingRunnable implements Runnable{
    //目标ip
    private String ip;
    //程序启动时间
    private long start;

    public PingRunnable(String ip,long start){
        this.ip=ip;
        this.start=start;
    }

    @Override
    public void run() {
        PingUtil.ping(ip,start);
    }
}

public class PingTest {
    public static void main(String[] args) {
        //获取程序开始时间
        long start = System.currentTimeMillis();

        ExecutorService executorService = Executors.newFixedThreadPool(50);

        //多线程并发执行: 测试192.168.1.100 - 192.168.1.120
        for (int i=1;i<=254;i++){
            //创建线程任务
            PingRunnable pr = new PingRunnable("10.8.6."+i,start);
            //创建线程主体
            //Thread t =new Thread(pr);
            //让线程就绪
            //t.start();
            executorService.execute(pr);

            //new Thread(new PingRunnable("192.168.1."+i,start)).start();
        }

        // 串行执行: 测试192.168.1.100 - 192.168.1.120
//        for (int i=100;i<=120;i++){
//            PingUtil.ping("192.168.1."+i,0);
//        }

        long end = System.currentTimeMillis();
        System.out.println("主方法执行时间:"+(end-start));
    }
}

线程安全问题:

线程休眠:

sleep:

//Thread.sleep:让当前线程休眠指定时间
Thread.sleep(3000);

前面卖票的时候出现的一种问题,如果加上休眠会更加明显

同步代码块
同步 、异步

同步:多个任务情况下,一个任务A执行结束,可以执行另一个任务B。

异步:多个任务情况下,一个任务A正在执行,同时可以执行另一个任务B。任务B不用等待任务A结束才执行。

并发和并行其实是异步线程实现的两种形式。并行其实是真正的异步,多核CUP可以同时开启多条线程供多个任务同时执行,互补干扰,如上图的并行,其实和异步图例一样。但是并发就不一样了,是一个伪异步。在单核CUP中只能有一条线程,但是又想执行多个任务。这个时候,只能在一条线程上不停的切换任务,比如任务A执行了20%,任务A停下里,线程让给任务B,任务执行了30%停下,再让任务A执行。这样我们用的时候,由于CUP处理速度快,你看起来好像是同时执行,其实不是的,同一时间只会执行单个任务。

同步锁:

同步锁是谁?

 对于非static方法,同步锁就是this

 对于static方法,同步锁是当前方法所在类的字节码对象(类名.class) 

将整个运行类创建一个木门,让每次门里面的只有一个线程,避免以上问题

  //定义锁对象
    Object obj = new Object();
//通过锁定一个共享对象,构建同步代码块(门)

 synchronized (obj) {}
public class SaleTicket implements Runnable{
    //定义全局变量
    private int ticket = 20;

    //定义锁对象
    Object obj = new Object();

    @Override
    public void run() {

        while (true){
                //通过锁定一个共享对象,构建同步代码块
                synchronized (obj) {
                //票数大于0,才能卖票
                if (ticket > 0) {
                    //卖票一张,进行出票
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //输出卖票一张
                    System.out.println(
                            Thread.currentThread().getName() + "卖票一张,还剩(" + (--ticket) + "张)"
                    );
                } else {
                    break;
                }
            }
        }
    }
}
同步方法:

就是将运行主体,单独设置一个方法,然后加入synchronized

让方法变为同步方法,同一时间,只能有一个线程访问该方法内容

public class SaleTicket implements Runnable{
    //定义全局变量
    private  int ticket = 20;

    //定义锁对象
    Object obj = new Object();

    @Override
    public void run() {
        while (true){
            if(!sale()){
                break;
            }
        }
    }

    /**
     * synchronized让方法变为同步方法,同一时间,只能有一个线程访问该方法内容
     */
    public synchronized  boolean sale(){
        //票数大于0,才能卖票
        if (ticket > 0) {
            //卖票一张,进行出票
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //输出卖票一张
            System.out.println(
                    Thread.currentThread().getName() + "卖票一张,还剩(" + (--ticket) + "张)"
            );
            return true;
        }
        return false;
    }
}
synchronized
  1. 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
  1. 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  1. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

Lock
public class SaleTicet implements Runnable{
    //定义全局变量
    private int ticket =20;

    //定义可重入公平锁,实现同步
    private Lock lock=new ReentrantLock(true);//公平锁
    @Override
    public void run() {
        //
        while (true){
            //开始锁定
            try {
                //如果票数大于0,才能卖票
                if (ticket>0){
                    //卖票一张,进行出票
                    try {
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }

                    //输出卖票一张
                    System.out.println(
                            Thread.currentThread().getName() + "卖票一张,还剩(" + (--ticket) + "张)"
                    );
                }else {
                    break;
                }
            }finally {
                //在finally代码块中解锁
                lock.unlock();
            }
        }
    }
}
public class SaleTest {
    public static void main(String[] args) {
        //定义卖票任务
        SaleTicket st = new SaleTicket();

        //创建两个线程,模拟两个人卖票。两个人一共卖20张票
        Thread t1 = new Thread(st,"悟空");
        Thread t2 = new Thread(st,"白骨精");

        //让线程就绪
        t1.start();
        t2.start();
    }
}

SynchronizedLock区别(面试题)

  1.  synchronized是java内置关键字,在jvm层面,Lock是个java类;
  1. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  1. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  1.  用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  1. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
  1.  Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。  

多线程的常用方法:

currentThread():

          返回对当前正在执行的线程对象的引用。

           Thread.currentThread()

getName():

       返回此线程的名称。

      Thread.currentThread().getName()

run():

       当前启动的线程,执行业务的地方

start():

      启动线程,让线程处于就绪状态。导致此线程开始执行; 底层Java虚拟机调用此线程的run方法。

线程优先级:
setPriority()

       更改此线程的优先级。 优先级是1-10 之间的任意数字,默认值为5

package com.aaa.mt.demo4;

public class SetPriorityUse {
    public static void main(String[] args) {
        //使用内部类,定义一个线程A
        Thread threadA = new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在运行AAA。。。。");
            }
        };
        //使用内部类,定义一个线程B
        Thread threadB = new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在运行BBB。。。。");
            }
        };
        //使用内部类,定义一个线程B
        Thread threadC = new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在运行CCC。。。。");
            }
        };
        //设置线程启动的优先级  不设置时优先级为5   优先级是是数字1-10
    /*    threadA.setPriority(Thread.NORM_PRIORITY); //5
        //threadA.setPriority(3); //可以写成1-10之间的任意数字  也可以使用常量
        threadB.setPriority(Thread.MIN_PRIORITY);//1
        threadC.setPriority(Thread.MAX_PRIORITY);//10*/
        threadA.setPriority(9); //5
        //threadA.setPriority(3); //可以写成1-10之间的任意数字  也可以使用常量
        threadB.setPriority(5);//1
        threadC.setPriority(2);//10
        //优先级的,有几率先执行,并不是一定执行
        threadA.start();
        threadB.start();
        threadC.start();
    }
}
线程礼让:
yield 当前线程让出cpu-参与下次的竞争
public class MyText extends Thread{
    @Override
    public void run() {
        for (int i=0;i<20;i++) {
            System.out.println(Thread.currentThread().getName() + "~~~~~~~~~~~~~~~" + i);

            Thread.yield();//当前线程让出cpu参与下次的竞争
            System.out.println("======");
        }
    }
}

使用yield线程出现交换执行的频率高了。

线程插队:
join加入当前线程上

插入的线程执行完毕后,当前线程才会执行。就是main主线程在最后

public class Test {
    public static void main(String[] args) throws Exception{
        MyText t1=new MyText();
        t1.setName("i小鹌鹑");
        MyText t2=new MyText();
        t2.setName("b");


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

        t1.join();//t1加入主线程上,主 线程需要等t1执行完毕后才会执行.
        // 如果主线程需要等带t1和t2线程的执行结果 做下一步操作时。
        for (int i = 0; i <20 ; i++) {
            Thread.sleep(10);
            System.out.println("main~~~~~~~~~~~~~~~~~~~~~"+i);
        }
    }
}
守护线程:
setDaemon()设置线程为守护线程

当所有线程执行完毕后,守护线程也会终止。也就相当于保镖一样

//JDK--默认就有一个守护线程.GC垃圾回收。
public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1=new MyThread();
        t1.setName("线程A");
        t1.setDaemon(true);//设置t1为守护线程
        t1.start();
        for (int i = 0; i <20 ; i++) {
            System.out.println("main~~~~~~~~~~~~~~~~~~~~~"+i);
        }
    }
}

死锁

线程A拥有锁资源a,希望获取锁资源b,线程B拥有锁资源b,希望获取锁资源a。 两个线程互相拥有对方希望获取的锁资源。可能会出现程序堵塞。从而造成死锁。

解决:

1. 不要使用锁嵌套。
2. 设置超时时间。--Lock类中tryLock.
3. 使用安全java.util.concurrent下的类。

多线程的状态

五种状态:

新建

 new关键字创建了一个线程之后,该线程就处于新建状态

 JVM为线程分配内存,初始化成员变量值  

 就绪

当线程对象调用了start()方法之后,该线程处于就绪状态

JVM为线程创建方法栈和程序计数器,等待线程调度器调度  

 运行

 就绪状态的线程获得CPU资源,开始运行run()方法,该线程进入运行状态  

阻塞

当发生如下情况时,线程将会进入阻塞状态

• 线程调用sleep()方法主动放弃所占用的处理器资源

• 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

• 线程试图获得一个同步锁(同步监视器),但该同步锁正被其他线程所持有。

• 线程在等待某个通知(notify)

•  程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法  

死亡

线程会以如下3种方式结束,结束后就处于死亡状态:

• run()或call()方法执行完成,线程正常结束。

• 线程抛出一个未捕获的Exception或Error。

•  调用该线程stop()方法来结束该线程,该方法容易导致死锁,不推荐使用。  

线程通信:

线程通讯方式
  休眠唤醒方式:

Objectwait、notify、notifyAll

Conditionawait、signal、signalAll

 CountDownLatch:用于某个线程A等待若干个其他线程执行完之后,它才执行

CyclicBarrier:一组线程等待至某个状态之后再全部同时执行

Semaphore[ˈseməˌfɔː):用于控制对某组资源的访问权限 

 Object类提供了线程间通信的方法:wait()、notify()、notifyAll(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。

注意:wait()/notify()/notifyAll() 必须配合 synchronized 使用,wait 方法释放锁,notify 方法不释放锁。

用案例来解释通信:连线程交替打印奇数跟偶数

匿名类:

用之前学习接口的在电脑里面插入usb接口干事件举例

创建一个接口IUsb

public interface IUsb {
    void work();
}

电脑

public class Mouse implements  IUsb{
    public void work() {
        System.out.println("将鼠标插入到电脑的iusb接口,,双击启动");
    }
}

测试类:运用匿名类进行调用

public class Test {
    public static void main(String[] args) {
        //方式一:多态调用
        IUsb usb2=new Mouse();

        //方式二:匿名类
        IUsb usb=new IUsb() {//匿名类(一次性实现接口)
            public void work() {
                System.out.println("将键盘插入到usb接口,用键盘编写文档");
            }
        };

        usb.work();
        usb2.work();
    }
}

同时线程中使用:

public class Test2 {
    public static void main(String[] args) {
        
        Runnable run=new Runnable() {
            public void run() {
                for (int i=1;i<10;i++){
                    System.out.println(Thread.currentThread().getName()+" "+i);
                }
            }
        };

        Thread t1=new Thread(run,"1");
        Thread t2 = new Thread(run,"线程B");

        t1.start();
        t2.start();
    }
}
案例:线程打印数字:

思路:1、创建一个锁,避免安全问题

           2、一个共享变量

           3、创建两个方法,进行打印方法(当数字为偶数进去,然后打印,最后添加变量,唤醒其他等待锁的线程)

唤醒;

//唤醒其他等待锁的线程
lock.notify();

等待:wait

wait 是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行

,只有其他线程调用了notify()notify并不释放锁,只是告诉调用过wait()的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放,调用 wait() 的一个或多个线程就会解除 wait 状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行

                       
lock.wait();
Object:
public class WaitNotifyTask {
    //定义锁
    Object lock=new Object();
    //定义共享变量
    int i=0;

    //输出基数
    public  void odd(){
        //通过枷锁,实现线程协同
        synchronized (lock) {
            while (i < 10) {
                //判断是否是奇数
                if (i % 2 != 0) {
                    System.out.println(Thread.currentThread().getName() + ": " + i);
                    i++;
                    //唤醒其他等待锁的线程
                    lock.notify();
                } else {
                    try {
                        //如果不是
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }


    //输出偶数
    public void even(){
        while (i<9){
            synchronized (lock){
                if(i%2==0){
                    System.out.println(Thread.currentThread().getName()+": "+i);
                    i++;
                    //唤醒其他线程
                    lock.notify();
                }else{
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

测试类:

  //执行任务
        final WaitNotifyTask wnt=new WaitNotifyTask();

        //创建两个线程
        Thread t1=new Thread(new Runnable() {
            public void run() {
                //
                wnt.odd();
            }
        }, "打印奇数");

        Thread t2=new Thread(new Runnable() {
            public void run() {
                wnt.even();
            }
        },"偶数");

        //
        t2.start();
        t1.start();
condition:

Object和Condition休眠唤醒区别:

object wait()必须在synchronized(同步锁)下使用,

object wait()必须要通过notify()方法进行唤醒

 condition await() 必须和Lock(互斥锁/共享锁)配合使用

 condition await() 必须通过 signal() 方法进行唤醒 

CountDownLatch:

这个类能够使一个线程等待其他线程完成各自的工作后再执行。

通过一个计数器来实现的,计数器的初始值为线程的数量

每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在锁上等待的线程就可以恢复执行任务。

使用场景

1: 某一线程在开始运行前等待n个线程执行完毕。

将CountDownLatch的计数器初始化为n:new CountDownLatch(n) ,每当一个任务线程执行完毕,就将计数器减1, countdownlatch.countDown(),当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。一个典型应用场景就是动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

// 5 个线程

// 4  -1

// 第五个线程 会在前四个线程执行完之后 再执行

一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检查。 只有等所有检查完毕后,引擎才能点火。

2:实现多个线程开始执行任务的最大并行性。

注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒

CyclicBarrier

实现让一组线程等待至某个状态之后再全部同时执行

使用场景:

比如说有五个线程,需要它们都到达了某一个点之后才能开始一起执行,也就是说假如其中只有四个线程到达了这个点,还差一个线程没到达,此时这四个线程都会进入等待状态,直到第五个线程也到达了这个点之后,这五个线程才开始一起进行执行状态

Semaphore:工人请求资源

生产者、消费者

案例:银行卡进出消费:

判断状态:

public class BankCard {
    //进行一共判断

    //余额
    private double balance;

    //银行卡状态
    private boolean flag;//true 有 flag 没有

    //synchronized处理,创造存方法
    public synchronized void  cun(double money){
        if (flag==true){
            try {
                //等待
                wait();属于Object类中。 进入等待队列 并且释放拥有的锁
            }catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //如果没有加余额
        balance+=money;

        //然后将状态转为有
        flag=true;

        //唤醒--唤醒等待队列中的某个线程
        notify();
        System.out.println(Thread.currentThread().getName()+"往卡中存入了:"+money+";卡中余额:"+balance);
    }

    //创建取的方法
    public synchronized  void  qu(double money){
        if (flag==false){
            try {
                wait(); //放入等待队列中。
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        balance-=money;
        flag=false;
        notify();
        System.out.println(Thread.currentThread().getName()+"从卡中取出了:"+money+";卡中余额:"+balance);
    }

    }

存跟取

public class CunThread extends Thread{
    private BankCard bankCard;

    public CunThread(BankCard bankCard){
        this.bankCard=bankCard;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            bankCard.cun(1000);
        }
    }
}
public class QuThread extends Thread{

    private BankCard bankCard;

    public QuThread(BankCard bankCard){
        this.bankCard=bankCard;
    }

    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            bankCard.qu(1000);
        }
    }
}

最后测试

public class Text {
    public static void main(String[] args) {
        BankCard bankCard=new BankCard();
        CunThread c=new CunThread(bankCard);
        c.setName("李晨");
        QuThread q=new QuThread(bankCard);
        q.setName("范冰冰");
        c.start();
        q.start();
    }
}

wait方法和notify方法。

案例2:鸡蛋灌饼

package com.deom08;

public class Box {

    //设置布尔值,用于控制当前要生产还是要消费
    //初始设置为ture,表示已经消费过
    private boolean flag=true;

    //生产者(Producer),像盒子里面生成鸡蛋灌饼
    public synchronized void  set(int i ){
        //如果已经生成过饼子,消费者还没有取走
        if(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //生产者准备生产
        System.out.println(Thread.currentThread().getName()+"准备生产饼子...");
        //生产过程
        try {
            Thread.sleep(90);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //生产者准备生产
        System.out.println(Thread.currentThread().getName()+"生产了一个饼子...:"+i);
        //修改标记,表示已经生产过,等待消费
        flag=false;
        //告诉消费者,可以取饼了
        this.notify();
    }

    //消费者(Consumer),从盒子里面去饼
    public synchronized void get(int i){
        //判断如果已经消费过,则等待生产
        if(flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //消费者被唤醒,可以消费饼子
        System.out.println(Thread.currentThread().getName()+"准备消费饼子...");
        //吃饼
        try {
            Thread.sleep(90);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"饼子吃完了....:"+i);

        //修改标记
        flag=true;
        //唤醒生产者,生产下一个饼子
        this.notify();
    }
}

生产、消费者

//生产者和消费者共享对象
    private Box box;
    public Producer(Box box){
        this.box=box;
    }
    public void run() {
        for (int i=1;i<=10;i++){
            this.box.set(i);
        }
    }

测试

   //创建盒子,被生产和消费者共享
        Box box=new Box();

        Thread t1=new Thread(new Producer(box),"生产者");
        Thread t2=new Thread(new Consumer(box),"消费者");

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

线程池

工作原理:

        当线程池中线程数量小于 corePoolSize 创建线程,并处理请求。

        当线程池中线程数量大于等于 corePoolSize 时,则把请求放入 workQueue 中,随着线程池中的核心线程们不断执行任务,只要线程池中有空闲的核心线程,线程池就从 workQueue 中取任务并处理。

       当 workQueue 已存满,放不下新任务时则新建非核心线程入池,并处理请求直到线程数目达到 maximumPoolSize(最大线程数量设置值)。

       如果线程池中线程数大于 maximumPoolSize 则使用 RejectedExecutionHandler 来进行任务拒绝处理。

在Java中创建线程池常用的类是ThreadPoolExecutor,该类的全参构造函数如下:

 public ThreadPoolExecutor(

            int corePoolSize, //  2 核心线程数量

            int maximumPoolSize,//  8 最大线程数

            long keepAliveTime, //    600 最大空闲时间
            TimeUnit unit,//        时间单位

            BlockingQueue<Runnable> workQueue, //   任务队列

            ThreadFactory threadFactory, // 线程工厂

            RejectedExecutionHandler handler //  饱和处理机制

         )

参数介绍:

1、corePoolSize:线程池中核心线程数的最大值

            如何设置核心的线程数

            1个任务0.1s 

            80% 1s处理任务是10

            1s处理100个任务  10

             核心线程数 8020原则 10 

2、maximumPoolSize:线程池中能拥有最多线程数

1000个任务

值=(最大的任务的个数-任务队列的长度)*单个任务执行的时间

(1000-200)*0.1=80

  1. workQueue:用于缓存任务的阻塞队列,对于不同的应用场景我们可能会采取不同的排队策略,这就需要不同类型的阻塞队列,在线程池中常用的阻塞队列有以下2种:
    • SynchronousQueue<Runnable>:([ˈsɪŋkrənəs kjuː])此队列中不缓存任何一个任务。向线程池提交任务时,如果没有空闲线程来运行任务,则入列操作会阻塞。当有线程来获取任务时,出列操作会唤醒执行入列操作的线程。从这个特性来看,SynchronousQueue是一个无界队列,因此当使用SynchronousQueue作为线程池的阻塞队列时,参数maximumPoolSizes没有任何作用。
    • LinkedBlockingQueue<Runnable>:顾名思义是用链表实现的队列,可以是有界的,也可以是无界的,但在Executors中默认使用无界的。

以上三个参数之间的关系如下:

  1. 如果没有空闲的线程执行该任务且当前运行的线程数少于corePoolSize,则添加新的线程执行该任务。
  1. 如果没有空闲的线程执行该任务且当前的线程数等于corePoolSize同时阻塞队列未满,则将任务入队列,而不添加新的线程。
  1. 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务。
  1. 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于maximumPoolSize,则根据构造函数中的handler指定的策略来拒绝新的任务。

 值的设置=(核心的线程数/单个任务执行的时间)*2    (10/0.1)*2=200

  1. keepAliveTime:表示空闲线程的存活时间。
  2. unit:表示keepAliveTime的单位。
  3. handler:表示当workQueue已满,且池中的线程数达到maximumPoolSize时,线程池拒绝添加新任务时采取的策略。一般可以采取以下四种取值。

ThreadPoolExecutor.AbortPolicy()

抛出RejectedExecutionException异常

ThreadPoolExecutor.CallerRunsPolicy()

由向线程池提交任务的线程来执行该任务

ThreadPoolExecutor.DiscardOldestPolicy()

抛弃最旧的任务(最先提交而没有得到执行的任务)fifo

ThreadPoolExecutor.DiscardPolicy()

抛弃当前的任务

4、threadFactory:指定创建线程的工厂

小结:

corePoolSize,maximumPoolSize,workQueue之间关系。 

1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。 

2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 

3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务 

4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理 

5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程 

6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭 

线程池四种常用方式:
1、newSingleThreadExecutor:

创建一个单线程的线程池。这个线程池只有一个线程工作。如果这个线程出现异常,会有一个新的线程来替代它。此线程保证所有的任务执行顺序是按照提交顺序执行

适用场景:

保证任务由一个线程串行执行 

package com.domo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class test {
    public static void main(String[] args) {
        //
        ExecutorService
                 executorService=Executors.newSingleThreadExecutor();

        //循环

        for (int i=1;i<=10;i++){
            final int index =i;
            //使用单一的线程池,执行任务
            executorService.execute(new Runnable() {
                public void run() {
                    //局部匿名内部类,访问外部变量,外部变量必须是常量
                    System.out.println(Thread.currentThread().getName()+"执行"+index);

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
        //关闭线程池
        executorService.shutdown();
    }
}
2、newFixedThreadPool:

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。 线程池的大小一旦达到最大值就会保持不变,如果某个线程出现异常,那么会补充一个新的线程

如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢出。是其劣势;

适用场景:

可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。

  //固定大小线程池:线程池中有固定个数的线程
        ExecutorService executorService= Executors.newFixedThreadPool(3);

3、newCachedThreadPool:

创建一个可缓存的线程池。如果线程池的大小超过处理任务所需要的线程数, 那么会回收部分空闲线程,当任务数 增加时,线程会智能添加新线程来处理任务。此线程不会对线程池的大小做限制,线程池大小完全依赖于操作系统(或 JVM)能够创建最大线程的大小。 (如果间隔时间长,下一个任务运行时,上一个任务已经完成,所以线程可以继续复用,如果间隔时间调短,那么部分线程将会使用新线程来运行。)

workQueue 为 SynchronousQueue 同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,因为CachedThreadPool线程创建无限制,不会有队列等待,所以使用SynchronousQueue;

适用场景:

快速处理大量耗时较短的任务  (大量耗时长的线程运行,会导致当前程序不可用 )

 //可缓存线程池
        ExecutorService executorService= Executors.newCachedThreadPool();

4、newScheduledThreadPool:

创建一个无限大小的线程池。此线程池支持定时及周期性执行任务的需求。

适应场景:

定时执行任务

//定时执行任务线程池
        ScheduledExecutorService executorService= Executors.newScheduledThreadPool(3);
延迟指定世界,执行任务
   executorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(LocalDateTime.now()+" 执行了任务....");
            }
        },15, TimeUnit.SECONDS);
结果:



执行指定时间之后,每隔指定时间执行一次任务
  executorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(
                        LocalDateTime.now() + "  " +
                                Thread.currentThread().getName() + "执行了任务..."
                );
            }
        },3,2, TimeUnit.SECONDS);

初始延迟指定时间3s之后开始任务,任务之间的延迟间隔是2s
 executorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                int taskCost=new Random().nextInt(10);
                try {
                    Thread.sleep(taskCost*1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(
                        LocalDateTime.now()+""+
                                Thread.currentThread().getName()+"执行任务"+taskCost+"s"
                );
            }
        },3,2,TimeUnit.SECONDS);

线程池submit和execute区别:

 两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。

 1,submit既支持Runnable还支持Callable  execute 只支持Runnable

 *                   2,submit 因为支持Callable,所以就可以支持获取返回值

 *                   3,submit 因为支持Callable,所以就可以支持获取异常处理

java多线程特性:

原子性:

可见性:
有序性:
volatile的说明

public class Task implements Runnable{
    //共享变量
    volatile  boolean stop=false;
    int i=0;

    @Override
    public void run() {
        long start=System.currentTimeMillis();
        while (!stop){
            i++;
        }
        long end=System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+"线程结束"+i+"次");
    }
}
 //创建任务对象
        Task task=new Task();

        //创建线程
        //线程1:执行死循环
        Thread t1=new Thread(task,"线程1");

        //线程2 通知线程,让线程1停下来
        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                //修改stop为ture,让t1停下来
                System.out.println(Thread.currentThread().getName()+"设置stop为ture");
                task.stop=true;
            }
        },"线程2");

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

ThreadLocal:

史上最全ThreadLocal 详解(一)-CSDN博客

ThreadLocal提供线程局部变量,即为使用相同变量的每一个线程维护一个该变量的副本。

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal,比如数据库连接Connection,每个请求处理线程都需要,但又不相互影响,就是用ThreadLocal实现。

ThreadLocal是Java中的一个非常有用的类,它用来创建线程局部变量。每个线程都会持有该变量的一个副本,因此每个线程可以独立地改变自己的副本,而不会影响其他线程所持有的副本。

下面是一个使用ThreadLocal的简单例子:

public class ThreadLocalExample {  
	    // 创建ThreadLocal变量  
	    public static final ThreadLocal<Integer> localVariable = new ThreadLocal<Integer>();  
	  
	    static void print(String str) {  
	        // 打印当前线程本地内存中localVariable变量的值  
	        System.out.println(str + ": " + localVariable.get());  
	        // 清除当前线程本地内存中的localVariable变量  
	        localVariable.remove();  
	    }  
	  
	    public static void main(String[] args) {  
	        Thread t1 = new Thread(new Runnable() {  
	            public void run() {  
	                // 设置线程1中本地变量的值  
	                localVariable.set(100);  
	                print("Thread 1");  
	                System.out.println("After remove: " + localVariable.get());  
	            }  
	        });  
	  
	        Thread t2 = new Thread(new Runnable() {  
	            public void run() {  
	                // 设置线程2中本地变量的值  
	                localVariable.set(200);  
	                print("Thread 2");  
	                System.out.println("After remove: " + localVariable.get());  
	            }  
	        });  
	  
	        t1.start();  
	        t2.start();  
	    }  
	}

5,扩展

ThreadPoolExecutor (Java Platform SE 7 )

详细解说java线程池_线程池为什么用有界队列-CSDN博客

面试题:什么叫做阻塞队列的有界和无界-CSDN博客

  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值