多线程相关知识详解!

进程和线程是什么?

  1. 进程:在我们的电脑上有很多单独运行的程序,这每一个程序都有一个独立的进程,进程之间相互独立,比如我们电脑上的qq,微信,电脑管家等。

  2. 线程:进程想执行任务就需要依赖线程,一个进程中至少有一个线程。那么什么是多线程呢?先来了解一下串行和并行。

    • 串行:就是一个线程执行执行多条任务时,必须先执行完A,然后再执行B,最后执行C。
    • 并行:比如在下载多个文件时开启多条线程,多个文件同时进行下载,这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的。


         那么再针对多线程来说,就好比我们使用训练下载文件的时候可以同时下载多个文件,按照单线程来讲就必须下载完一个文件后再开始下载另一个文件。

线程的生命周期

  • 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
  • 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;
  • 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。
  • 等待/阻塞/睡眠 :在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
  • 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。

创建线程的多种方式

1、继承Thread类:

  • 定义类继承Thread
  • 复写Thread类中的run方法;
    目的:将自定义代码存储在run方法,让线程运行
  • 调用线程的start方法:
    该方法有两步:启动线程,调用run方法。
public class ThreadDemo1 {
    public static void main(String[] args) {
        //创建两个线程
        ThreadDemo td = new ThreadDemo("zhangsan");
        ThreadDemo tt = new ThreadDemo("lisi");
        //执行多线程特有方法,如果使用td.run();也会执行,但会以单线程方式执行。
        td.start();
        tt.start();
        //主线程
        for (int i = 0; i < 5; i++) {
            System.out.println("main" + ":run" + i);
        }
    }
}
//继承Thread类
class ThreadDemo extends Thread{

    //设置线程名称
    ThreadDemo(String name){
        super(name);
    }
    //重写run方法。
    public void run(){
        for(int i = 0; i < 5; i++){
        System.out.println(this.getName() + ":run" + i);  //currentThread()  获取当前线程对象(静态)。  getName() 获取线程名称。
        }
    }
}

2、实现Runnable接口: 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run 的无参方法。

  • 定义类实现Runnable接口
  • 覆盖Runnable接口中的run方法
    将线程要运行的代码放在该run方法中。
  • 通过Thread类建立线程对象。
  • 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
    自定义的run方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run方法就要先明确run方法所属对象
  • 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
public class RunnableDemo {
    public static void main(String[] args) {
        RunTest rt = new RunTest();
        //建立线程对象
        Thread t1 = new Thread(rt);
        Thread t2 = new Thread(rt);
        //开启线程并调用run方法。
        t1.start();
        t2.start();
    }
}

//定义类实现Runnable接口
class RunTest implements Runnable{
    private int tick = 10;
    //覆盖Runnable接口中的run方法,并将线程要运行的代码放在该run方法中。
    public void run(){
        while (true) {
            if(tick > 0){
                System.out.println(Thread.currentThread().getName() + "..." + tick--);
            }
        }
    }
}

3、通过Callable和Future创建线程:

  • 创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
  • 创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
  • 使用FutureTask对象作为Thread对象启动新线程。
  • 调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
public class CallableFutrueTest {
    public static void main(String[] args) {
        CallableTest ct = new CallableTest();                        //创建对象
        FutureTask<Integer> ft = new FutureTask<Integer>(ct);        //使用FutureTask包装CallableTest对象
        for(int i = 0; i < 100; i++){
            //输出主线程
            System.out.println(Thread.currentThread().getName() + "主线程的i为:" + i);
            //当主线程执行第30次之后开启子线程
            if(i == 30){
                Thread td = new Thread(ft,"子线程");
                td.start();
            }
        }
        //获取并输出子线程call()方法的返回值
        try {
            System.out.println("子线程的返回值为" + ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
class CallableTest implements Callable<Integer>{
    //复写call() 方法,call()方法具有返回值
    public Integer call() throws Exception {
        int i = 0;
        for( ; i<100; i++){
            System.out.println(Thread.currentThread().getName() + "的变量值为:" + i);
        }
        return i;
    }
}

4、使用线程池创建:

public class ThreadTest {
 
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(50);
    
    public void dothing(){
        for(int i=0;i<50;i++){
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    //do something
                }
            });
        }
        fixedThreadPool.shutdown();//关闭线程池
 
        //此处不可以删除或注释,需要线程执行结束后再执行别的内容,即只有线程结束后才会继续向下执行
        while (!fixedThreadPool.isTerminated()) {
 
        }
    }
}
  • 线程池的特点:
    1.通过复用已存在的线程,降低线程创建和消耗带来的损耗。
    2.提高相应速度,通过复用已存在的新线程,无序等待便能立即执行
    3.提供更强大的功能:延时定时线程池

1.可缓存线程池 Executors.newCacheThreadPool():先查看池中有没有线程。如果有直接使用,没有则新建。(通常适用于执行一些生存期很短的线程)
线程池为无限大,当执行完上一个任务后,后面的任务会复用上一个任务的线程,而不用每次新建。

2.定长线程池Executors.newFixedThreadPool(int n):创建一个固定长度的线程池,以共享的无界队列方式来运行这些线程。

3.延时定时线程池Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持延时和定时。

4.单线程化的线程池Executors.newSingleThreadExecutor():它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

线程安全和线程锁

1、线程安全
既然提到了线程安全,我们就知道有的线程肯定是不安全的,那么来举两个例子,有如下代码:

Integer count = 0;
public void getCount() {
       count ++;
       System.out.println(count);
 }

当我们开启线程并且每个线程访问10次时,有如下结果:
在这里插入图片描述
这里出现了两个重复数字,可以看出这个线程并不是安全的,其中最常见的问题就是A线程进入后读取count还没开始加一时,线程B进来了,这样导致两个线程拿到的值时一样的。

再看下面这段代码:

public void threadMethod(int i) {
    i = i + 1;
}

这段代码很容易看出来,这个方法只对传入的参数进行操作,对别的线程参数没有不会有任何影响,所以说它时绝对安全的。

所以可以总结一下:当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

2、线程锁
     既然存在线程安全的问题,那么肯定得想办法解决这个问题,怎么解决?我们说说常见的几种方式

  1. synchronized关键字
    关键字synchronized的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。

     关键字synchronized可以有多张用法,这里做一个简单的整理:

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获取给定对象的锁。
  • 直接作用于实例方法:相当于给当前实例加锁,进入同步代码块前要获得当前实例的锁。
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获取当前类的锁。

下面来分别说一下上面的三点:
假设我们有下面这样一个Runnable,在run方法里对静态成员变量sCount自增10000次:

class Count implements Runnable {
    private static int sCount = 0;

    public static int getCount() {
        return sCount;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            sCount++;
        }
    }
}

假设我们在两个Thread里面同时跑这个Runnable:

Count count = new Count();
Thread t1 = new Thread(count);
Thread t2 = new Thread(count);
t1.start();
t2.start();
try {
    t1.join();
    t2.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.print(Count.getCount());

得到的结果并不是20000,而是一个比20000小的数,如14233。

这是为什么呢?假设两个线程分别读取sCount为0,然后各自技术得到sCount为1,并先后写入这个结果,因此,虽然sCount++执行了2次,但是实际sCount的值只增加了1。

我们可以用指定加锁对象的方法解决这个问题,这里因为两个Thread跑的是同一个Count实例,所以可以直接给this加锁:

class Count implements Runnable {
    private static int sCount = 0;

    public static int getCount() {
        return sCount;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            synchronized (this) {
                sCount++;
            }
        }
    }
}

synchronized直接作用于静态方法的用法和上面的给实例方法加锁类似,不过它是作用于静态方法:

class Count implements Runnable {
    private static int sCount = 0;

    public static int getCount() {
        return sCount;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }

    private static synchronized void increase() {
        sCount++;
    }
}
  1. lock
    先来说说它跟synchronized有什么区别吧,Lock是在Java1.6被引入进来的,Lock的引入让锁有了可操作性,什么意思?就是我们在需要的时候去手动的获取锁和释放锁,甚至我们还可以中断获取以及超时获取的同步特性,但是从使用上说Lock明显没有synchronized使用起来方便快捷。我们先来看下一般是如何使用的:
private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子类

   private void method(Thread thread){
       lock.lock(); // 获取锁对象
       try {
           System.out.println("线程名:"+thread.getName() + "获得了锁");
           // Thread.sleep(2000);
       }catch(Exception e){
           e.printStackTrace();
       } finally {
           System.out.println("线程名:"+thread.getName() + "释放了锁");
           lock.unlock(); // 释放锁对象
       }
   }

进入方法我们首先要获取到锁,然后去执行我们业务代码,这里跟synchronized不同的是,Lock获取的所对象需要我们亲自去进行释放,为了防止我们代码出现异常,所以我们的释放锁操作放在finally中,因为finally中的代码无论如何都是会执行的。

写个主方法,开启两个线程测试一下我们的程序是否正常:

public static void main(String[] args) {
       LockTest lockTest = new LockTest();

       // 线程1
       Thread t1 = new Thread(new Runnable() {

           @Override
           public void run() {
               // Thread.currentThread()  返回当前线程的引用
               lockTest.method(Thread.currentThread());
           }
       }, "t1");

       // 线程2
       Thread t2 = new Thread(new Runnable() {

           @Override
           public void run() {
               lockTest.method(Thread.currentThread());
           }
       }, "t2");

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

结果:
在这里插入图片描述
可以看出我们的执行,是没有任何问题的。
还有其他很多锁就不一一列举,详细介绍可以看下面这篇博客:https://www.jianshu.com/p/fa084227c96b

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值