Java中多线程的学习

1、多线程概述

1.1、线程与进程的概念

进程

  • 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间

线程

  • 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程,当进程中没有线程时进程就会结束
  • 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程

简单讲进程就犹如一个运行的音乐播放器,播放音乐和显示歌词就是里面的线程。两个音乐播放器都有自己的存储空间显示进程都有自己的内存空间,每个音乐播放器都有歌词、歌曲,也可以收索歌曲,播放歌曲可以查看歌词也可以搜索其他歌曲,可以简单指代理解线程共享一个内存空间,线程之间可以自由切换并且可以并发执行

1.2、线程的调度

分时调度

  • 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

抢占式调度

  • 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java中使用的为抢占式调度
  • CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

1.3、同步与异步

同步: 线程排队执行,效率低但是线程安全
异步: 线程同时执行,效率高但是数据不安全

同步和异步可以理解为一群人去上网学习,同步就是排好队等前面学习完后面补上,这样效率低但是安全不会发生冲突,异步就是一堆人围上去谁先抢到谁先学,这样效率高了但是容易出现安全隐患

1.4、并发与并行

并发: 指两个或多个事件在同一个时间段内发生。
并行: 指两个或多个事件在同一时刻发生(同时发生)。

1.5、线程的实现

1.5.1、实现线程的两种方法

  1. 继承Thread类
  2. 实现Runnable接口
实现Runnable比继承Thread好的优势所在
 1. 通过创建任务然后给线程分配的方式来实现多线程,更适多个线程执行相同任务的情况
 2. 可以避免Java中单继承带来的局限
 3. 任务与线程是分离的,提高了程序的健壮性
 4. 线程池只接收Runnable类型的任务,不接收Thread类型的线程

1.5.2、线程阻塞

线程阻塞不只是指sleep()方法和wait()方法,只要线程处于等待状态我们就称之为线程阻塞。常见的有:文件读取、用户输入等

1.5.3、线程中断

一个线程是一个独立的执行路径,它是否结束应该由其自身决定。如果要进行中断我们可以用interrupt()方法对其打上一个标记,它自己读取这个标记进行判断是否进行中断
触发中断标记的方法:
Object.wait(), Object.wait(long), Object.wait(long, int), Thread.sleep(long), Thread.interrupt(), Thread.interrupted()

	Thread t=new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    //当线程发现中断标记后进入catch语句块
                    //System.out.println("发现中断标记,但我们就是不进行死亡,就是玩");
                    //中断线程
                    System.out.println("发现中断标记,我们这个线程自杀了");
                    return;
                }
            }
        }
    });
    t.start();
    //给t线程添加中断标记
    t.interrupt();

1.5.4、守护线程

线程分为两种:

  • 用户线程:当一个进程不包含任何存活的线程时,进程结束
  • 守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡
	Thread t=new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    //设置t为守护线程
    t.setDaemon(true);
    t.start();
	//当主线程死亡时守护线程即使没有完成它的任务也会跟着死亡
    for (int i = 0; i < 5; i++) {
        System.out.println(Thread.currentThread().getName()+":"+i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

1.5.5、线程安全与线程不安全

同步是线程安全的,异步是线程不安全的
解决线程不安全的方法:

  1. 同步代码块

格式:synchronized (锁对象){}
注意:同步代码块中的锁对象必须是同一个,不然各自线程看各自的锁对象是一直都未上锁不能实现排队

  1. 同步方法

格式:public synchronized boolean xxx(){}
注意:同步方法也是用的锁,当方法没有被static修饰时,此时的锁是this;当方法被static修饰时,此时的锁是 类名.class

  1. 显示锁

同步代码块和同步方法都是隐式锁
格式: 先new 一个锁对象Lock l=new ReentrantLock();然后再用l.lock();上锁和l.unlock();解锁

1.5.6、显示锁和隐式锁的区别

  1. synchronized(隐式锁):Java中的关键字,是由JVM来维护的,是JVM层面的锁;Lock(显示锁):是JDK5以后才出现的具体的类,使用lock是调用对应的API,是API层面的锁
  2. 用法不一样,参考上面1.5.5线程安全与线程不安全解决线程不安全的方法
  3. synchronized等待不可中断,除了抛出异常和正常运行完毕;Lock可以中断

Lock的中断方式:
1.使用tryLock(long time, TimeUnit unit)方法,参数1表示等待时间,参数2表示时间单位
2.在加锁的方法中调用lockInterruptibly()并try-catch捕捉异常,然后用Thread中的interrupt()方法来打中断标记;中断标记触发请查看1.5.3、线程中断

  1. synchronized是非公平锁;Lock默认是非公平锁,通过创建对象时new ReentrantLock()传递一个Boolean值来改变,true是公平锁

1.5.7、公平锁和非公平锁

公平锁是线程排队使用资源,非公平锁是抢占式获取资源

1.5.8、线程的状态

线程的六种状态:
NEW(尚未启动的线程处于该状态)
RUNNABLE(在Java虚拟机中执行的线程处于此状态)
BLOCKED(被阻塞等待监视器锁定的线程处于此状态)
WAITING(无限期等待另一个线程执行特定操作的线程处于此状态)
TIMED_WAITING(正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态)
TERMINATED(已退出的线程处于此状态)

2、线程池

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

2.1、线程池的好处

  • 降低资源消耗。
  • 提高响应速度。
  • 提高线程的可管理性。

2.2、Java中的四种线程池(ExecutorService)

2.2.1、缓存线程池

缓存线程池无长度限制
执行流程:

  1. 判断线程池中是否有空闲的线程
  2. 存在空闲的线程则使用空闲的线程
  3. 如不存在空闲的线程,则新建一个线程并放入线程池,然后使用。根据规则会释放长时间未使用的线程
	ExecutorService service = Executors.newCachedThreadPool();
    //向线程池中加入新的任务
    service.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("线程的名称:" + Thread.currentThread().getName());
        }
    });
    service.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("线程的名称:" + Thread.currentThread().getName());
        }
    });
    service.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("线程的名称:" + Thread.currentThread().getName());
        }
    });
    //使线程休眠1秒后新建的任务就会获取上面已经创建并空闲线程执行
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    service.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("线程的名称:" + Thread.currentThread().getName());
        }
    });

2.2.2、定长线程池

定长线程池的长度是由自己指定的长度
执行流程:

  1. 判断线程池是否存在空闲线程
  2. 存在空闲线程就使用空闲线程
  3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
  4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
	ExecutorService service = Executors.newFixedThreadPool(2);
   service.execute(new Runnable() {
       @Override
       public void run() {
           System.out.println("线程的名称:" + Thread.currentThread().getName());
       }
   });
   service.execute(new Runnable() {
       @Override
       public void run() {
           System.out.println("线程的名称:" + Thread.currentThread().getName());
       }
   });
   service.execute(new Runnable() {
       @Override
       public void run() {
           System.out.println("线程的名称:" + Thread.currentThread().getName());
       }
   });

2.2.3、单线程线程池

当定长线程池的长度为1时效果就与单线程线程池一致
执行流程:

  1. 判断线程池中的线程是否空闲
  2. 如空闲则使用
  3. 如不空闲则等待上一个线程执行完后再执行
	 ExecutorService service = Executors.newSingleThreadExecutor();
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:" + Thread.currentThread().getName());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:" + Thread.currentThread().getName());
            }
        });

2.2.4、周期性任务定长线程池

执行流程:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
  4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

周期性任务执行时:定时执行, 当某个时机触发时, 自动执行某任务

	ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
	/** 定时执行
	* 参数1. runnable类型的任务
	* 参数2. 时长数字
	* 参数3. 时长数字的单位
	*/
	service.schedule(new Runnable() {
	  @Override
	  public void run() {
	      System.out.println("线程的名称:" + Thread.currentThread().getName());
	  }
	}, 5, TimeUnit.SECONDS);
	/**
	* 周期执行
	* 参数1. runnable类型的任务
	* 参数2. 时长数字(延迟执行的时长)
	* 参数3. 周期时长(每次执行的间隔时间)
	* 参数4. 时长数字的单位
	*/
	service.scheduleAtFixedRate(new Runnable() {
	  @Override
	  public void run() {
	      System.out.println("线程的名称:" + Thread.currentThread().getName());
	  }
	}, 3, 2, TimeUnit.SECONDS);

2.3、callable(了解)

2.3.1、callable的使用

  1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> { 
	@Override 
	public <T> call() throws Exception { 
		return T; 
	} 
}
  1. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
    FutureTask<Integer> future = new FutureTask<>(callable);
  2. 通过Thread,启动线程
    new Thread(future).start();

2.3.2、Runnable与Callable的异同

相同点:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

不同点:

  • Runnable没有返回值;Callable可以返回执行结果
  • Callable接口的call()允许抛出异常;Runnable的run()不能抛出

2.3.3、Callable返回值获取

Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

3、Java8中的新特性lambda表达式

lambda表达式是函数式编程思想

注意:在使用lambda时,使用lambda表达式的接口只有一个未实现的抽象方法

Thread t =new Thread(new Runnable(){
	@Override
	public void run(){
		System.out.println("当前正在执行的线程名称:" + Thread.currentThread().getName());
	}
});

/**
 * 可以用lambda表达式简化
 *  () -> System.out.println("当前正在执行的线程名称:" + Thread.currentThread().getName())  
 * 小括号里面可以根据调用接口需要实现的方法里面是否有参数来传递参数,如接口需要实现的方法有返回值可以在大括号里面return返回
 * () -> {
 * 		System.out.println("当前正在执行的线程名称:" + Thread.currentThread().getName();
 * 		//如有需要可以在这里面return
 * })
 */

Thread t=new Thread(() -> System.out.println("当前正在执行的线程名称:" + Thread.currentThread().getName()));

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麋鹿不知归途

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值