线程

1. 认识线程

1.1 什么线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS 中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win 32 线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

一个进程可以有很多线程,每条线程并行执行不同的任务。

在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见的,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

1.2 进程与线程之间的关系

  • 线程与进程的定义;

    • 进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
    • 线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位
    • 一个程序至少有一个进程,一个进程至少有一个线程
  • 线程进程的区别体现在几个方面:

    • 因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。

    • 体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。。

    • 属于同一个进程的所有线程共享该进程的所有资源,包括文件描述符。而不同的进程相互独立。

    • 线程又称为轻量级进程,进程有进程控制块,线程有线程控制块;

    • .线程必定也只能属于一个进程,而进程可以拥有多个线程而且至少拥有一个线程;

  • 线程的优缺点

    线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在 SMP 机器上运行,而进程则可以跨机器迁移。

帮助理解 : 一个进程可以开启多个线程, 线程是并发执行的,他们之间快速的切换, 如: 我们在使用 qq 的时候可以一边打字聊天一边打qq电话。

1.3 进程与线程之间的选择

  • 需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。
  • 线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应
  • 因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
  • 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;
  • 需要更稳定安全时,适合选择进程;需要速度时,选择线程更好

小结:多线程提升CPU的利用率。

补充:协程:称之为微进程,在Java中是不研究的,在python研究

2. java 基本获取线程

2.1 获取线程的方式

  1. 继承Thread类,重写run方法
  2. 实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
  3. 通过Callable和Future/FutureTask创建多线程
  4. 通过线程池创建多线程

java中获取线程的方式到底有几种?

​ 网上说有4种实现方法,但是也有一部分人是持但对意见的,前面两种可以归结为一类:无返回值,原因很简单,通过重写run方法,run方式的返回值是void,所以没有办法返回结果。 后面两种可以归结成一类:有返回值,通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中

2.2 继承 Thread 获取线程

继承Thread类

package cn.guokeB;

public class ThreadA extends Thread  {

    @Override
    public void run() {
        System.out.println("新线程 :" + Thread.currentThread().getName());

    }
}

测试

package cn.guokeB;

public class TestA {
    public static void main(String[] args) {
        System.out.println("main: " + Thread.currentThread().getName());
        //开启三个线程 开启时调用 start() 方法;
        new ThreadA().start();
        new ThreadA().start();
        new ThreadA().start();
    }
}

执行结果:
    main: main
    新线程 :Thread-0
    新线程 :Thread-1
    新线程 :Thread-2

2.3 实现 Runnable 接口

实现 Runnable 接口

package cn.guokeB;

public class ThreadB implements Runnable  {

    @Override
    public void run() {
        System.out.println("新线程 :" + Thread.currentThread().getName());

 

测试

package cn.guokeB;

public class TestB {
    public static void main(String[] args) {
        System.out.println("main: " + Thread.currentThread().getName());
         Thread t1  = new Thread(new ThreadB());
        t1.start();
        System.out.println("当前线程的名字: "+ t1.getName());

    }
}

执行结果:
    main: main
    当前线程的名字: Thread-0
    新线程 :Thread-0

2.4 继承 Thread 与实现 Runnable 的区别

看起来继承 Thread 与实现 Runnable 都是重写了run 方法 但是他们之间有很大的区别:

  • 实现 Runnable 接口适合多个相同的程序代码的线程去处理同一个资源
  • 实现 Runnable 接口可以避免java中的单继承的限制
  • 实现 Runnable 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
  • 实现 Runnable 线程池只能放入实现 Runable 或 callable类线程,不能直接放入继承Thread的类

2.5 一个线程的经典买票案例(引入锁)

使用 Thread 实现

package cn.guokeB;

public class BuyTickets  extends  Thread{
    //定义一个公开的私有的 票数
    static  Integer tickets = 15;
    @Override
    public void run() {
        while (true){
            if (tickets<=0) return;
            synchronized (this){
                tickets--;
                System.out.println(Thread.currentThread().getName() + " : 当前的票数为"+tickets);
            }
        }
    }
}

测试

package cn.guokeB;

public class TestBuy {
    public static void main(String[] args) {
        //开启三个线程同时买票
        new BuyTickets().start();
        new BuyTickets().start();
        new BuyTickets().start();
    }
}

执行结果:
	Thread-1 : 当前的票数为12
Thread-0 : 当前的票数为12
Thread-2 : 当前的票数为13
Thread-0 : 当前的票数为10
Thread-1 : 当前的票数为11
Thread-0 : 当前的票数为8
Thread-2 : 当前的票数为9
Thread-0 : 当前的票数为6
Thread-0 : 当前的票数为4
Thread-0 : 当前的票数为3
Thread-0 : 当前的票数为2
Thread-0 : 当前的票数为1
Thread-1 : 当前的票数为7
Thread-0 : 当前的票数为0
Thread-2 : 当前的票数为5

使用 Runnable 实现

package cn.guokeB;

public class BuyTicketsI implements   Runnable{
    //定义一个公开的私有的 票数
   Integer tickets = 15;
    @Override
    public void run() {
        while (true){
            if (tickets<=0) return;
            synchronized (this){
                tickets--;
                System.out.println(Thread.currentThread().getName() + " : 当前的票数为"+tickets);
            }
        }
    }
}

测试

package cn.guokeB;

public class TestBuy {
    public static void main(String[] args) {
        BuyTicketsI buyTicketsI = new BuyTicketsI();
        //开启三个线程同时买票
        new Thread(buyTicketsI).start();
        new Thread(buyTicketsI).start();
        new Thread(buyTicketsI).start();

    }
}
执行结果;
	Thread-0 : 当前的票数为14
    Thread-0 : 当前的票数为13
    Thread-2 : 当前的票数为12
    Thread-2 : 当前的票数为11
    Thread-2 : 当前的票数为10
    Thread-2 : 当前的票数为9
    Thread-2 : 当前的票数为8
    Thread-2 : 当前的票数为7
    Thread-2 : 当前的票数为6
    Thread-2 : 当前的票数为5
    Thread-2 : 当前的票数为4
    Thread-2 : 当前的票数为3
    Thread-2 : 当前的票数为2
    Thread-2 : 当前的票数为1
    Thread-2 : 当前的票数为0
    Thread-1 : 当前的票数为-1
    Thread-0 : 当前的票数为-2

2.6 案例的分析

1. 继承 Thread 操作票数为什么要加 static

因为 继承了Thread 的类之间是独立的 要想访问同一个数据 就得把这个数据定义为公共的 。 这就体现出了 实现 Runnable 的优势 适合多个相同的程序代码的线程去处理同一个资源

2. synchronized 是什么 ??

synchronized 是 Java 中的关键字,是利用锁的机制来实现同步的。

锁机制有如下两种特性:

  • 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
  • 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。

3. 为什么要加锁

因为不加锁的话可能会出现脏读的现象

脏读 : 在数据库技术中,脏数据在临时更新( 脏读)中产生。事务A更新了某个数据项X,但是由于某种原因,事务A出现了问题,于是要把A回滚。但是在回滚之前,另一个事lijie务B读取了数据项X的值(A更新后),A回滚了事务,数据项恢复了原值。事务B读取的就是数据项X的就是一个“临时”的值,就是脏数据。通俗的讲,当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。

3. 线程池

3.1 什么是线程池

什么是线程池,线程我们都知道,那么什么是池呢?在现实世界中我们有池塘,池塘中有许多的动物,这是现实事件中的池子,我们类比的去理解,java中的线程池中就相当于现实世界中的池塘,池塘里的生物可以类比成线程但是线程有回收利用的特点;通俗点说就是线程池就是一个装线程的容器。且其中的线程可以回收再次利用。

3.2 使用线程池的优点

  • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

4. 在java中使用线程池

导读:

Java通过Executors提供四种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

4.1 使用 newCachedThreadPool 创建线程池

newCachedThreadPool 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

使用 newCachedThreadPool 创建一个缓存的线程

package cn.guoke.xcc;

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

public class ThreadA {
    public static void main(String[] args) throws InterruptedException {
        //创建一个可以缓存的线程池
        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000); //当执行第二个要任务时且第一个任务已经完成,会重复使用执行第一个任务的线程,而不用每次新建线程
            //关于  executorService.isShutdown() : 如果此执行者已关闭,则返回 true 。 这里取反当执行者不结束的时候
            if (!executorService.isShutdown()){
                //创建一个线程任务
                executorService.execute(()-> System.out.println("当前线程的名字是:" +Thread.currentThread().getName()));
            }

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

    }
}


执行结果:
    当前线程的名字是:pool-1-thread-1
    当前线程的名字是:pool-1-thread-1
    当前线程的名字是:pool-1-thread-1
    当前线程的名字是:pool-1-thread-1
    当前线程的名字是:pool-1-thread-1
    当前线程的名字是:pool-1-thread-1
    当前线程的名字是:pool-1-thread-1
    当前线程的名字是:pool-1-thread-1
    当前线程的名字是:pool-1-thread-1
    当前线程的名字是:pool-1-thread-1

4.2 使用 newFixedThreadPool 创建一个定长的线程

newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

创建一个定长的线程池

package cn.guoke.xcc;

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

public class ThreadB {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
            if (!executorService.isShutdown()){
                executorService.submit(()-> System.out.println("当前线程认为: "+ Thread.currentThread().getName()));
            }
        }

        executorService.shutdown();
    }
}

执行结果:
    当前线程认为: pool-1-thread-1
    当前线程认为: pool-1-thread-4
    当前线程认为: pool-1-thread-5
    当前线程认为: pool-1-thread-1
    当前线程认为: pool-1-thread-1
    当前线程认为: pool-1-thread-3
    当前线程认为: pool-1-thread-2
    当前线程认为: pool-1-thread-1
    当前线程认为: pool-1-thread-5
    当前线程认为: pool-1-thread-4

4.3 newSingleThreadExecutor 创建一个单线程化的线程池

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。

  •     如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行
    
  •     顺序按照任务的提交顺序执行 
    

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

newSingleThreadExecutor 创建一个单线程化的线程池

定义一个 MyThread

package cn.guoke.xcc;

public class MyThread implements  Runnable {
    @Override
    public void run() {
        System.out.println("当前线程为: " +Thread.currentThread().getName());
        //定义一个异常
        int a = 5/0;

    }
}

测试类

package cn.guoke.xcc;

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

public class ThreadC {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        MyThread m3 = new MyThread();
        MyThread m4 = new MyThread();
        //向线程池中添加任务
        executorService.execute(m1);
        executorService.execute(m2);
        executorService.execute(m3);
        executorService.execute(m4);

        //关闭
        executorService.shutdown();
    }
}

执行结果:
    当前线程为: pool-1-thread-1
    ---异常
    当前线程为: pool-1-thread-2
    ---异常
    当前线程为: pool-1-thread-3
    ---异常    
    当前线程为: pool-1-thread-4

4.4 newScheduledThreadPool 创建一个周期性的池

newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

1. 定时器

导读 ;

​ 相信大家都在现实世界中见过定时器吧,例如:一年有四个季节,每个季节是3个月;每天日出则做,日落则休
​ 那么我们的编程世界中是不是也要有定时器呢,答案是肯定的,JDK官方给我们提供了一个最基本的定时器Timer&TimerTask

​ Timer:用来执行任务的类,所以称之为定时器
TimerTask:每次具体做什么内容,所以称之为定时任务类

java实现定时器

package cn.guoke.xcc;

import java.util.TimerTask;

public class MyTimerTask extends TimerTask {

    //继承 TimerTask类 重写 run 方法 即线程任务
    @Override
    public void run() {
        System.out.println("你好!!");
    }
}

测试

package cn.guoke.xcc;

import java.util.Timer;

public class MyThreadD {
    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        MyTimerTask  myTimerTask=new MyTimerTask();
        timer.schedule(myTimerTask,  0, 2000);
        Thread.sleep(4000);
        timer.cancel();//取消定时器
    }
}

执行结果 :
	你好!!
    你好!!
    你好!!

2. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

实现 newScheduledThreadPool

package cn.guoke.xcc;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ThreadD {
    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
        scheduledExecutorService.scheduleAtFixedRate(()-> System.out.println("当前线程的名字是: " +Thread.currentThread().getName()),
                0,
                1,
                TimeUnit.SECONDS);
        Thread.sleep(5000);
        scheduledExecutorService.shutdown();
    }
}
执行结果:
	当前线程的名字是: pool-1-thread-1
    当前线程的名字是: pool-1-thread-1
    当前线程的名字是: pool-1-thread-2
    当前线程的名字是: pool-1-thread-1
    当前线程的名字是: pool-1-thread-3

5. 线程递增打印数字

4个线程打印100

package cn.guoke.xc;

public class ProGerThread {
    //输出的值
    private  Integer  i = 1;
    //控制线程
    private  Integer  stop = 0;

    public  synchronized  void  print(String threadName){
        //把线程名转为数字
        Integer con =  Integer.parseInt(threadName);
        //用数字控制线程的是否执行
        if (con!=stop){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.print(Thread.currentThread().getName()+":");
        for (int j = 0; j < 5; j++) {
            System.out.print(i+",");
            i++;
        }

        //换行
        System.out.println();
        // 让stop 值永远为 0-2
        stop = (stop+1)%4;
        //开启其他线程
        this.notifyAll();

    }

}

测试

package cn.guoke.xc;

public class Test {
    public static void main(String[] args) {
        ProGerThread proGerThread = new ProGerThread();
        //开启3个线程
        for (int i = 0; i < 4; i++) {
            new Thread(()->{
                for (int j = 0; j < 5; j++) {
                    proGerThread.print(Thread.currentThread().getName()
                            .charAt(Thread.currentThread().getName().length()-1)+""
                            );
                }
            }).start();
        }

    }
}

结果:
	Thread-0:1,2,3,4,5,
    Thread-1:6,7,8,9,10,
    Thread-2:11,12,13,14,15,
    Thread-3:16,17,18,19,20,
    Thread-1:21,22,23,24,25,
    Thread-1:26,27,28,29,30,
    Thread-0:31,32,33,34,35,
    Thread-1:36,37,38,39,40,
    Thread-3:41,42,43,44,45,
    Thread-2:46,47,48,49,50,
    Thread-2:51,52,53,54,55,
    Thread-3:56,57,58,59,60,
    Thread-1:61,62,63,64,65,
    Thread-0:66,67,68,69,70,
    Thread-3:71,72,73,74,75,
    Thread-3:76,77,78,79,80,
    Thread-2:81,82,83,84,85,
    Thread-0:86,87,88,89,90,
    Thread-2:91,92,93,94,95,
    Thread-0:96,97,98,99,100,
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值