java之多线程
前言:本章主要记载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; } }