Java 并发--线程

前言

谈到并发,一个基础的概念就是java线程了。线程是CPU调度的基本单位。在单核CPU上,多个线程的“同时”执行其实就是把一段时间切成若干时间片。cpu在某个具体的时间片里面运行某个线程,当这个时间片用完之后,CPU会进行上下文切换(暂停当前运行的线程,运行另外一个线程)。

CPU进行上下文切换需要开销,而且线程的创建和销毁同样需要开销,所以有人可能会说多线程只是对多核CPU运行有意义,其实不然。如果在一个系统里面是单线程执行所有的任务,那么我们一次只可以做一件事,比如:我们听歌曲的时候不能再去上网(音乐播放和浏览网页是单线程的话)。除此之外,如果我们当前操作的任务是IO操作,或者是网络交互的话,那么我们的CPU将会被阻塞,直到IO操作或者网络交互的完成。很明显,这样大大降低了CPU的利用率,也给用户带来很糟糕的体验。

一、线程

1、线程的基本使用方法

1、实现Runnable接口

class Task1 implements Runnable{

    private int countDown = 5;

    public Task1(int countDown){
        this.countDown = countDown;
    }


    @Override
    public String toString() {
        return Thread.currentThread()+ " countDown = "+countDown;
    }

    @Override
    public void run() {
        while(countDown -- > 0){
            System.out.println(this);
        }
    }
}

2、实现Callable

callable相对于Runnable的区别是callable带有返回值

public class AddTask implements Callable<Integer> {

    private int a = 0,b = 0;

    public AddTask(int a1,int b1) {
        a = a1;
        b = b1;
    }

    @Override
    public Integer call() throws Exception {
        TimeUnit.SECONDS.sleep(2);
        return a+b;
    }

}


    public static void main(String[] args) {
        FutureTask<Integer> addFuture = new FutureTask<Integer>(new AddTask(100, 200));
        new Thread(addFuture).start();
        try {
            //addFuture.get() 是阻塞的,直到有结果返回为止
            System.out.println(addFuture.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }

3、继承Thread类

class Task2 extends Thread{
    private int countDown = 5;

    public Task2(int countDown){
        this.countDown = countDown;
    }

    @Override
    public String toString() {
        return Thread.currentThread() + " countDown = "+countDown;
    }


    @Override
    public void run() {
        while(countDown -- > 0){
            System.out.println(this);
        }
    }
}

开启线程

public class Main {

    public static void main(String[] args) {
        for (int i = 0; i < 2;i++){
            new Thread(new Task1(i+5)).start();
        }
//        for (int i = 0;i < 2; i++){
//            new Task2(i).start();
//        }

    }

}
输出样例
Thread[Thread-0,5,main] countDown = 4
Thread[Thread-1,5,main] countDown = 5
Thread[Thread-1,5,main] countDown = 4
Thread[Thread-1,5,main] countDown = 3
Thread[Thread-1,5,main] countDown = 2
Thread[Thread-0,5,main] countDown = 3
Thread[Thread-1,5,main] countDown = 1
Thread[Thread-0,5,main] countDown = 2
Thread[Thread-1,5,main] countDown = 0
Thread[Thread-0,5,main] countDown = 1
Thread[Thread-0,5,main] countDown = 0
注意

start方法开启线程,那么开启的结果是另起线程来运行run内容。而通过run方法开启的线程,这个线程是依附于开启它的父线程。也可以这么理解,start是真正意义上的多线程。

把start换成Run
public class Main {

    public static void main(String[] args) {
        for (int i = 0; i < 2;i++){
            new Thread(new Task1(i+5)).run();
        }
//        for (int i = 0;i < 2; i++){
//            new Task2(i).start();
//        }

    }

}
输出样例
Thread[main,5,main] countDown = 4
Thread[main,5,main] countDown = 3
Thread[main,5,main] countDown = 2
Thread[main,5,main] countDown = 1
Thread[main,5,main] countDown = 0
Thread[main,5,main] countDown = 5
Thread[main,5,main] countDown = 4
Thread[main,5,main] countDown = 3
Thread[main,5,main] countDown = 2
Thread[main,5,main] countDown = 1
Thread[main,5,main] countDown = 0

2、线程池

在Java中,如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在处理实际的用户请求的时间和资源要多得多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个JVM里创建太多的线程,可能会导致系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。

Java 提供了4种线程池

newFixedThreadPool
newCachedThreadPool
newScheduledThreadPool
newSingleThreadExecutor

newFixedThreadPool

线程数目固定的线程池,线程池里面的线程不会被回收,除非这个线程池被销毁。

newCachedThreadPool

数目不定的线程池,当新任务来临的时候,如果线程池里面有空闲的线程就使用空闲的线程来执行新任务,如果没有的话就新建线程去执行
一般使用的时候我们都是使用这个线程池,因为当它的任务执行完毕之后,如果一段时间内还没有新任务来,那么在它里面的线程就会被销毁,不会一直占用系统资源。

newScheduledThreadPool

主要用来执行定时任务和周期性任务

singleThreadExecutor

这个线程池里面只有一个线程,交给它的任务会按顺序执行,不用考虑线程之间的同步问题

用法示例(四种线程池的用法)

public class ThreadPool {

    public ThreadPool(){

        Runnable demo = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        //singleThreadExecutor的用法
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        singleThreadExecutor.execute(demo);

        //fixedThreadPool用法,我们开启的是一个大小为2的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
        fixedThreadPool.execute(demo);

        //cachedThreadPool用法
        ExecutorService cacheThreadPool = Executors.newCachedThreadPool();
        cacheThreadPool.execute(demo);

        //scheduledThreadPool的延迟执行,核心线程数为2的scheduledThreadPool
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
        //两秒后执行
        scheduledThreadPool.schedule(demo,1000, TimeUnit.MILLISECONDS);


        //间隔时间重复执行
        ScheduledExecutorService scheduledThreadPool2 = Executors.newScheduledThreadPool(2);
        //100ms后每个1秒钟执行一次demo
        scheduledThreadPool2.scheduleAtFixedRate(demo,100,1000,TimeUnit.MILLISECONDS);



    }

}

3、得到任务的返回值

Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务完成时会有结果返回,那么应该实现Callable接口,而不是Runnable接口。在Java SE5引入的Callable是一种具有类型参数的泛型,它的类型参数表示的是从call方法中返回的值,并且必须使用ExcutorService.submit()调用它。

示例

import java.util.concurrent.Callable;

/**
 * 这是一个有返回值得线程
 */
public class Task3 implements Callable<String> {
    @Override
    public String call() throws Exception {
        int i = 3;
        while (i -- > 0){
            System.out.println(Thread.currentThread() + " i= "+ i);
        }
        return Thread.currentThread() + " done!";
    }
}
import java.util.ArrayList;
import java.util.concurrent.*;

public class Main {

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newCachedThreadPool();
        ArrayList<Future<String>> list = new ArrayList<>();
        for (int i = 5; i > 0; i--){
            list.add(executorService.submit(new Task3()));
        }
        for (Future<String> s : list){
            try{
                System.out.println(s.get());
            }catch (InterruptedException | ExecutionException e){
                e.printStackTrace();
            }finally {
                executorService.shutdown();
            }
        }
    }

}

样例输出

Thread[pool-1-thread-1,5,main] i= 2
Thread[pool-1-thread-2,5,main] i= 2
Thread[pool-1-thread-1,5,main] i= 1
Thread[pool-1-thread-2,5,main] i= 1
Thread[pool-1-thread-1,5,main] i= 0
Thread[pool-1-thread-2,5,main] i= 0
Thread[pool-1-thread-1,5,main] done!
Thread[pool-1-thread-2,5,main] done!
Thread[pool-1-thread-3,5,main] i= 2
Thread[pool-1-thread-3,5,main] i= 1
Thread[pool-1-thread-3,5,main] i= 0
Thread[pool-1-thread-3,5,main] done!
Thread[pool-1-thread-4,5,main] i= 2
Thread[pool-1-thread-4,5,main] i= 1
Thread[pool-1-thread-4,5,main] i= 0
Thread[pool-1-thread-4,5,main] done!
Thread[pool-1-thread-5,5,main] i= 2
Thread[pool-1-thread-5,5,main] i= 1
Thread[pool-1-thread-5,5,main] i= 0
Thread[pool-1-thread-5,5,main] done!

submit方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。你可以使用isDone方法来查询Future是否已经完成。任务完成后可以用get方法来获取结果。也可以不用isDone直接就调用get,这种情况下get将阻塞,直到结果完成。
(个人觉得对于future对象应该有个监听接口,以阻塞的方式来实施的话体现不出多线程的优势了)。

4、线程休眠、优先级设定

线程休眠

直接在run函数里面添加:

try{
    Thread.sleep(10000);
}catch (Exception e){
    e.printStackTrace();
}

或者:

try{
    TimeUnit.MILLISECONDS.sleep(1000);
}catch(InterruptedException e){
    System.err.println("Interrupted"+e);
}

优先级

线程的优先级是调度器对线程进行调度时的一个重要参考标准。CPU处理现有线程集的顺序是不确定的。但是调度器倾向于让优先级高的线程先执行。这并不意味着低优先级线程会饿死,它们只是执行的频率较低而已。

示例
public class Task4 implements Runnable {

    private int countDown = 5;
    private int priority = 1;

    public Task4(int priority){
        this.priority = priority;
    }

    @Override
    public String toString() {
        return Thread.currentThread()+ " countDown = "+countDown;
    }
    @Override
    public void run() {
        Thread.currentThread().setPriority(priority);
        while(countDown -- > 0){
            System.out.println(this);
        }
    }
}
import java.util.concurrent.*;

public class Main {

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new Task4(Thread.MAX_PRIORITY));
        executorService.execute(new Task4(Thread.NORM_PRIORITY));
        executorService.execute(new Task4(Thread.MIN_PRIORITY));
    }

}
样例输出
Thread[pool-1-thread-1,10,main] countDown = 4
Thread[pool-1-thread-1,10,main] countDown = 3
Thread[pool-1-thread-1,10,main] countDown = 2
Thread[pool-1-thread-1,10,main] countDown = 1
Thread[pool-1-thread-1,10,main] countDown = 0
Thread[pool-1-thread-2,5,main] countDown = 4
Thread[pool-1-thread-2,5,main] countDown = 3
Thread[pool-1-thread-2,5,main] countDown = 2
Thread[pool-1-thread-2,5,main] countDown = 1
Thread[pool-1-thread-2,5,main] countDown = 0
Thread[pool-1-thread-3,1,main] countDown = 4
Thread[pool-1-thread-3,1,main] countDown = 3
Thread[pool-1-thread-3,1,main] countDown = 2
Thread[pool-1-thread-3,1,main] countDown = 1
Thread[pool-1-thread-3,1,main] countDown = 0

Java的线程有10个优先级,windows系统有7个优先级,sun的solaris有2^31个优先级。优先级对应并不好对应,所以大多数时候直接设置

Thread.MAX_PRIORITY
Thread.NORM_PRIORITY
Thread.MIN_PRIORITY

5、后台线程

所谓的后台线程是指在程序运行的时候,在后台提供一种通用服务的线程。不过这种线程并不属于程序中不可或缺的部分。一个程序如果没有非后台进程运行的话,那么这个程序就会被终止,而且所有的后台线程也会被杀死。

将一个线程设置为后台线程只需要在start之前先加下面一行代码就行。

t.setDaemon(true);

t是线程的名字。但是请注意,在开始就已经说了,进程如果不存在非后台线程,那么这个进程就会终止,所以当你想要测试这个后台线程的时候要保留main进程。

错误用法
import thread_learn.Task4;

import java.util.concurrent.*;

public class Main {

    public static void main(String[] args) {
        Thread t = new Thread(new Task4(Thread.NORM_PRIORITY));
        t.setDaemon(true);
        t.start();
    }

}

这样不会有任何输出,因为main线程将Task4开启之后,main就退出了。main退出之后,Task4是后台线程,所以会被系统杀死。我们需要保留main线程以观察后台线程。

正确示例
import thread_learn.Task4;

import java.util.concurrent.*;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Task4(Thread.NORM_PRIORITY));
        t.setDaemon(true);
        t.start();
        TimeUnit.MILLISECONDS.sleep(1000);
    }

}
Thread[Thread-0,5,main] countDown = 4
Thread[Thread-0,5,main] countDown = 3
Thread[Thread-0,5,main] countDown = 2
Thread[Thread-0,5,main] countDown = 1
Thread[Thread-0,5,main] countDown = 0

Process finished with exit code 0

6、捕获异常

由于线程的本质特性,使得你不能捕获从线程中逃离的异常。一旦异常逃出任务的run()方法,它就会向外传播到控制台,除非你使用特殊的方法来捕获这种错误异常。在Java SE5之前,你可以使用线程组来捕获这些异常,JavaSE5之后,就可以用Executor来解决这个问题。

示例
package thread_learn;

public class Task5 implements Runnable{


    @Override
    public void run() {
        throw new RuntimeException("Task5 Test!");
    }
}
import thread_learn.Task5;

import java.util.concurrent.*;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new Task5());
    }

}
样例输出
Exception in thread "pool-1-thread-1" java.lang.RuntimeException: Task5 Test!
    at thread_learn.Task5.run(Task5.java:8)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

修改main函数的主体代码,为其加上try-catch异常捕获,如下:

try {
      ExecutorService executorService = Executors.newCachedThreadPool();
      executorService.execute(new Task5());
}catch (RuntimeException e){
      e.printStackTrace();
}

运行之后依然报同样的错误,这说明,通过为main主体增加try-catch并没有作用。

为了解决这个问题,我们要修改Excutor产生线程的方式。Thread.UncaughtExceptionHandler是java SE5中的新接口,它允许你在每个Thread线程上都附着一个异常处理器。为了使用它,我们创建了一个新类型的ThreadFactory,它将在每个线程上都附着一个Thread.UncaughtExceptionHandler。我们将这个工厂传递给Executors创建新的ExecutorService方法。

自定义未捕获异常处理MUncaughtExceptionHandler
class MUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{

        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("Exception caught "+e);
        }
    }
自定义executorService的线程工厂HandlerThreadFactory
class HandlerThreadFactory implements ThreadFactory{

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler(new MUncaughtExceptionHandler());
            System.out.println("" +
                    "eh = " +t.getUncaughtExceptionHandler());
            return t;
        }
    }
Task6
public class Task6 implements Runnable {

    @Override
    public void run() {
        Thread t = Thread.currentThread();
        System.out.println("run() by "+t);
        System.out.println("" +
                "eht = " +t.getUncaughtExceptionHandler());
        throw new RuntimeException("Task6 Test!");
    }
}
main
import thread_learn.Task6;

import java.util.concurrent.*;

public class Main {

    public static void main(String[] args){

        ExecutorService executorService = Executors.newCachedThreadPool(new HandlerThreadFactory());
        executorService.execute(new Task6());
    }
}

参考:

Thingking in Java

Java四种线程池的使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值