java之多线程

旧城冷巷雨未停 提交于 2020-02-25 22:23:52

前言:本章主要记载java多线程知识的一些沉淀~

一、线程的基础知识

1、线程和进程的区别:

进程:系统中一个执行的程序,可称为进程,进程中可以有多个执行的任务(多个顺利执行流)也就是一个线程

一个程序中至少有一个进程,一个进程可包含多个线程,但至少要有一个线程

2、进程的有哪些特性:

独立性:每个进程都是独立存在的实体,拥有自己独立的资源,在没有经过进程本身运行的情况下,其他线程不可以直接访问进程的地址空间

动态性:进程是一个在系统中获得的指令集合,进程加入了时间概念,拥有自己的生命周期和不同的状态,具有动态性

并发性:多个进程可以在多个处理器上并发执行,且进程直接互不影响

敲黑板:并发和并行的区别是什么呢?

并行:同一时间发布多条指令, 不同的处理器上进行运作

并发:同一时刻只发布一条指令 ,通过一些特定的指令,进行轮换执行,从而达到同时执行的效果

并发和并行看似相同,但是确实俩个不同的概念

3、面试经常会问到使用多线程有哪些优势,以及为什么要使用多线程?

先说后者,单线程的功能是非常有限的,随着项目的功能延伸和需求量的增加,单线程很难满足所有的用户需求

举个栗子:

单线程好比餐厅只有一个服务员,他只能做完第一件事才能做第二件事

多线程就像是餐厅有多个服务员,他们各干各的事情,互不打扰

优势:1、进程直接不能共享内存,线程之间可以共享

   2、多线程来实现任务并发比进程的效率更高

二、线程的创建和启动

 1、线程创建有3种方式

NO.1:继承Thread类,重写该类的run()方法

public class demo extends Thread{
    private int i;
    @Override
    public void run() {  //run方法线程体
        for(;i<100;i++){
            System.out.println(getName()+i);//1、继承Thread类时,直接使用this就可以换取当前线程,Tread对象的getname是返回当前线程的名字,所以这里可以直接调用getname
        }
    }
    public static void main(String[] args) {  //主线程体main
        for (int i = 0; i <100; i++) {
            System.out.println(Thread.currentThread().getName() + i + "个");
            if (i == 20) {
                new demo().start();//2、因继承了Tread类,子类直接创建可代表线程对象
                new demo().start();
            }
        }
    }
}//备注:3、继承Tread类来创建线程类时,多个线程之间无法共享线程中的实例变量也是代码中的i

NO.2:实现Runnable接口,重写run()方法

public class RunnableDemo implements Runnable  {
    private  int i;

    @Override
    public void run() {
        for (;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"11"+i);//1、通过Runable接口获取当前线程,只能使用Tread.currentTread().getname()
        }
    }
    public static void main(String[] args) {
        for (int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"22"+i);
            if(i==20){
                RunnableDemo runnableDemo=new RunnableDemo();//2、创建Runable对象只能作为线程对象的target
                new Thread(runnableDemo,"新线程1哈哈").start();
                new Thread(runnableDemo,"新线程2哈哈").start();
            }
        }
    }
}//备注:3、两个子线程的i变量是连续的,也就是说采用Runnable接口的方式,创建多线程是可以共享线程类的实例变量

小结:

继承Trread类

1、具备多线程能力

2、启动线程:子类对象.start()

3、不推荐使用:避免单继承的局限性

实现Runable接口

1、具备多线程能力

2、启动线程:传入目标对象+Thread对象.start()

3、推荐:灵活方便同一个对象被多个线程使用

//一份资源
RunnableDemo runnableDemo=new RunnableDemo();
//多个代理 
new Thread(runnableDemo,"张三).start();
new Thread(runnableDemo,"李四").start();
new Thread(runnableDemo,"王二").start();

NO.3:实现Callable接口,重写call()方法

1、实现Callable接口,需要有返回类型

2、重新call方法需要跑出异常

3、创建目标对象

4、创建执行服务

5、提交执行

6、获取结果

7、关闭服务

平常工作中用这种方式不是太多,这里只是了解一下~

实现Callable接口与Runable不同的最重要的是后4步

/**
 * 好处:
 *1、可以定义返回值
 *2、可以跑出异常 
 * */
public class CallableDemo implements Callable<Boolean>{

    @Override
    public Boolean call() throws Exception {
        //线程体
        for(int i=0;i<100;i++){
            System.out.println("我在看代码");
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException,InterruptedException {
        CallableDemo callableDemo=new CallableDemo();
        //创建执行服务
        ExecutorService executorService= Executors.newFixedThreadPool(1);
        //提交执行
        Future<Boolean> future=executorService.submit(callableDemo);
        //获取结
        Boolean result=future.get();
        System.out.println("打印返回结果"+result);
        //关闭服务
        executorService.shutdownNow();

    }

}

 

2、线程的生命周期

新建,就绪,运行,阻塞,死亡

 

 常见线程方法合集

 

 线程休眠sleep

sleep可以模拟网络延时,倒计时等

sleep时间达到后线程进入就绪状态

sleep可以设置当前阻塞的毫秒数

每一个对象都有一个锁,sleep不会释放锁

package com.snowy.snowy.controller;

import java.text.SimpleDateFormat;
import java.util.Date;

//sleep相关

public class TendownDemo {
    public static void main(String[] args) {
        Date date=new Date(System.currentTimeMillis());//获取系统当前时间
        while (true){
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
                date=new Date(System.currentTimeMillis());//更新当前时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

//        try {
//            tendown();
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
    }

    public static void tendown() throws InterruptedException {
        //使用for循环来模拟倒计时
//        for(int num=10;num<=10;num--){
//            Thread.sleep(1000);
//            System.out.println(num);
//            if (num<=1){
//                break;
//            }
//        }
        int i=10;
        while (true){
            Thread.sleep(1000);
            System.out.println(i--);
            if (i<=0){
                break;
            }
        }
    }
}

  线程停止

注意:不推荐使用jdk提供的stop()和destroy()方法 ,已废弃

可以使用一个标识变量当flag=false,则线程终止运行

package com.snowy.snowy.controller;

public class StopDemo implements Runnable {

    //设置一个标识
    private boolean flag=true;

    @Override
    public void run() {
        int i=0;
        //线程体使用该标识
        while(flag){
            System.out.println("运行起来了"+i++);
        }
    }

    //编写一个停止线程的方法
    public void  stop(){
        this.flag=false;
    }

    public static void main(String[] args) {
        StopDemo stopDemo=new StopDemo();
        new Thread(stopDemo).start();
        for(int i=0;i<1000;i++){
            System.out.println("main"+i);
            if (i==900){
                stopDemo.stop();
                System.out.println("线程需要停止了");
            }
        }
    }

}

  线程礼让yield

1、让当前正在执行的线程暂停,但不阻塞

2、执行yield()当前线程为就就绪状态

3、让cpu重新调度,具有随机性不一定礼让成功

例如:A,B俩个线程,cup正在执行A线程,然后A线程执行了yield方法礼让B线程,这时候A线程的状态为就绪状态(B线程也是继续状态),cpu会重新调度,如果调用B就礼让成功,若还是让A执行就说明没有礼让成功

public class YieldDemo implements Runnable{

    public static void main(String[] args) {
        YieldDemo yieldDemo=new YieldDemo();
        new Thread(yieldDemo,"A").start();
        new Thread(yieldDemo,"B").start();
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"结束");
    }
}

join合并线程

尽量少用,容易造成线程阻塞

合并线程,等此线程执行结束后,在执行其他的线程,其他的线程阻塞

例子:大家都在排队买东西,小明是老板熟人,直接插队,其他的人只能等小明买完东西才能轮着自己买

public class JoinDemo implements Runnable{
    @Override
    public void run() {
//        for(int i=0;i<1000;i++){
         //插队线程
            System.out.println(Thread.currentThread().getName()+"插队插队vip");
//        }
    }

    public static void main(String[] args) throws InterruptedException {
        JoinDemo joinDemo=new JoinDemo();
        Thread thread=new Thread(joinDemo);
        thread.start();

        //主线程
        for(int i=0;i<1000;i++){
            if (i==200){
                thread.join();
            }
            System.out.println("main"+i);
        }

    }
}

 附加: 获取线程的状态:Thread.State state=thread.getState();

线程的优先级Priority

 java提供了一个线程调度器来监控程序中启动后进入就绪的所有线程,线程调度器按照线程的优先级决定调度那个线程来执行

线程的优先级范围1--10:

Thread.max_priority=10

Thread.min_priority=1

Thread.norm_priority=5

获取、更改优先级

getPriority()、setPriority(int xx);

注意:线程优先级高不一定先执行,看cpu调度,但是优先级高权重也就高,执行的机会会增加

 

 守护线程daemon

setDaemon(true);//设置守护线程

isDaemon()//判断该线程是否是守护线程

线程分为守护线程和用户线程

虚拟机必须确保用户线程执行完毕---mian()就是一个用户线程

虚拟机不用等待守护线程执行完毕---GC垃圾回收线程,不用等待虚拟机执行完成就结束了

守护线程的作用:后台记录操作日志、监控内存、垃圾回收等待

当所有的前台线程死亡时,守护线程也会跟着死亡
 

线程同步

多个线程操作同一资源

并发:同一对象被多个线程同时操作

处理多线程问题时,多个线程访问同一对象,并且某些线程还想修改这个对象,这时候就需要用到线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一线程再使用,光有队列还是不够的还需要锁

为了保证数据在方法中被访问时的正确性,在访问时加入了锁机制sychronized,队列+锁才能保证线程同步的安全性

存在的问题:

1、一个线程只有锁可以导致其他需要此锁的线程挂起

2、在多线程的竞争下,加锁,释放锁会导致较多的上下文切换,和调度延时,引起性能问题

3、若一个优先级高的线程等待一个优先级低的线程释放锁,可能会引起优先级倒置,引起性能问题

同步方法:sychronized()

sychronized方法控制“对象”的访问,每个对象对应一把锁,每个sychronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到方法返回才释放锁,后面的线程获取这个锁,才能继续执行

例子:一堆人排队去面试,第一个先进去把门关上了相当于上锁,等他面试完了后,门开了相当于锁释放了,后面排队的人进去时在把门关上相当于上锁

从而达到加锁-->修改--->释放锁的机制

同步块:sychronized(obj){

    }

obj称之为同步监视器

obj可以是任何对象,但是推荐共享资源作为同步监听器

同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身或者是class

锁的对象是变化的量,需要增删改

 

附加:在 Java 5.0 提供了 java.util.concurrent(简称JUC)包,在此包中增加了在并发编程中很常用的工具类,并发包

CopyOnWriteArrayList集合是安全的

 

死锁

某一个同步块同事拥有“俩个以上对象的锁”时,就可能会发生死锁的问题

大白话:多个线程互相抱着对方需要的资源,然后形成僵持

例子:俩个女生化妆,需要镜子和梳子,A持有镜子,B持有梳子,他们都想要对方手里的东西,但是又不放自己手里的东西,这时候就会产生死锁

产生死锁的条件:

1、互斥条件:一个资源只能被一个进程使用(俩个女生都想拿镜子,俩个进程都想去访问同一个资源)

2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放(A拿了镜子想去拿梳子,但是我不想把镜子给你,而且我还想拿到梳子)

3、不剥夺条件:进程已获得的资源,在使用完之前,不能强行剥夺(A在用镜子,还没用完,B就想来拿镜子)

4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系(A想要B的东西,B想要A的)

以上4种可能产生死锁的条件,我们只要想办法破解其中一种或多种就可以避免死锁的发生

 

Lock(锁)

jdk5.0开始,java提供了线程同步机制通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当(sychronized是隐式,锁了谁不太好弄,也看不到他的开始和结束)

(可重复锁)ReentranLock类实现了lock,比较常用的是ReentranLock,可以显示加锁,释放锁

 

class A{
   
    private  final ReentrantLock lock=new ReentrantLock();

    @Override
    public void run() {
       
                lock.lock();//加锁,建议使用try catch
               
                    try {
              //保证线程安全的代码
                    }
                 finally {
                  lock.unlock();
            //如果有异常代码,要将unlock()写入finally语句块
          }
            }
    }

 

  总结:lock比对sychronized

1、lock是显式锁手动开启和关闭,sychronized是隐式锁出了作用域自己释放

2、lock只有代码块,sychronized有代码块锁和方法锁

3、使用lock锁,jvm花费较少的时间来调度线程,性能会更好一些,并且有扩展性因为提供更多的子类

4、优先使用顺序:

Lock-->同步代码块(已经进入方法体,分配了相应资源)-->同步方法(在方法体之外)

 

线程协作通信(生产者消费模式)

分析:这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者相互依赖,互为条件

在消费者问题中光有sychronized是不够的

  --sychronized可阻止并发更新同一个共享资源,实现了同步

  --sychronized不能用来实现不同线程之间的消息传递(通信)

java提供了以下几种方法解决线程之间的通信问题

wait()------->表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁

wait(long timeout)---->指定等待的毫秒数

notify()---->唤醒一个处于等待状态的线程

notifyall()---->唤醒同一个对象所有调用wait()方法的线程,优先级别高的线程优先调度

注意:都是Object类的方法,都只能在同步方法或者代码块中使用,否则会跑出异常

 解决线程通信方式一:

举例:小明(消费者)去KCF点餐,要了一份薯条,前台(数据缓存区),后厨(生产者)

1、对于生产者,没有生产产品之前,要等值消费者等待,生产了产品后要通知消费者消费(小明点的薯条没有了,后厨告诉前台,前台告诉小明需要等待,等后厨做好 ,通知小明来拿)

2、对于消费者,在消费之后,通知生产者已经结束消费,需要生产新的产品以供消费(小明拿走薯条后,告诉后厨我拿走了,后厨接着准备薯条以供其他消费者)

并发协作模式生产者/消费者--->管程法

生产者讲生产好的东西放入缓冲区,消费者从缓冲区拿东西(消费者不能直接使用生产者的数据)

解决线程通信方式二:

并发协作模式生产者/消费者--->信号灯法

通过一个标志位来判断,如果为true等待,为false唤醒

大白话:红灯停绿灯行~

 

线程池

背景:经常销毁和创建,使用量特别大的资源,比如并发情况下的线程,等性能要求影响比较大

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁,实现重复利用(类似于公共交通工具)

好处:提高响应速度,降低资源消耗,便于线程管理

 

 

//线程池
public class PoolTest {
    public static void main(String[] args) {
        //创建线程池
        //参数为线程的大小
        ExecutorService service= Executors.newFixedThreadPool(10);
        //执行---可回顾callable,用execute没有返回值,submit有返回值
        service.execute(new MyTread());
        service.execute(new MyTread());
        service.execute(new MyTread());
        service.execute(new MyTread());
        //关闭
        service.shutdown();
    }
}

class MyTread implements Runnable{

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

回顾:3种方式实现多线程

 

//回顾线程创建方式
public class TreadAllTest {
    public static void main(String[] args) {

        new Tread1().start();//Thread
        new Thread(new Tread2()).start();//Runnable
        FutureTask<Integer> futureTask=new FutureTask<Integer>(new Tread3());//Callable
        new Thread(futureTask).start();
        try {
            Integer integer=futureTask.get();
            System.out.println("1111"+integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

class Tread1 extends Thread{
    @Override
    public void run() {
        System.out.println("Tread1");
    }
}

class Tread2 implements Runnable{
    @Override
    public void run() {
        System.out.println("Tread2");
    }
}

class Tread3 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("Tread3");
        return 20;
    }
}

 

  

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!