Java多线程

1. 线程概述

1.1 线程和进程

  • 进程是处于运行过程中的程序,并且具有一定的独立功能
  • 并发性:同一个时刻只能有一条指令执行,但多个进程指令被快速轮换执行
  • 并行:多条指令在多个处理器上同时执行
  • 线程是进程的执行单元

1.2 多线程的优势

  • 进程之间不能共享内存,但线程之间非常容易
  • 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程效率更高
  • Java语言内置了多线程功能

2. 线程创建与启动

2.1 继承Thread

public class FirstThread extends Thread {
    private int i;

    @Override
    public void run() {
        for(i = 0; i < 50; i ++){
            System.out.println(this.getName() + "" + i);
        }
    }

    public static void main(String[] args){
        FirstThread ft = new FirstThread();
        for(int i =0; i < 100;i ++){
            System.out.println(Thread.currentThread().getName() + "" + i);
            if(i == 20) {
                ft.run();
            }
        }
    }
}

2.2 实现Runnable接口

public class FirstThread implements java.lang.Runnable {
    private int i;

    public void run() {
        for(i = 0; i < 50; i ++){
            System.out.println(Thread.currentThread().getName()+ "" + i);
        }
    }

    public static void main(String[] args){
        FirstThread ft = new FirstThread();
        for(int i =0; i < 100;i ++){
            System.out.println(Thread.currentThread().getName() + "" + i);
            if(i == 20) {
                ft.run();
            }
        }
    }
}

2.3 使用Callable和Future

  • Callable接口提供了一个call()方法可以作为线程执行体,call()方法有返回值且可以声明抛出异常
  • Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类
  • Future接口定义的方法:
方法名 作用
boolean cancel(boolean mayInterruptIfRunning) 试图取消该Future里关联的Callable任务
V get() 返回Callable任务里call方法的返回值,该方法会造成线程阻塞,等子线程执行完才能获得
V get(long timeout, TimeUnit unit) 返回Callable任务里call方法的返回值。该方法让程序最多阻塞timeoutunit指定的时间,如果经过指定时间Callable任务还没有返回值则抛出TimeoutException异常
boolean isCancelled() Callable中的任务是否取消
boolean isDone() Callable中的任务是否完成
public class CallableDemo {
    public static void main(String[] args){
        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)() -> {
            int i = 0;
            for( ; i < 100; i++){
                System.out.println(i);
            }
            return i;
        });
        new Thread(task).start();
        try {
            System.out.println(task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

2.4 创建线程的三种方式对比

RunnableCallable优劣势:

  • 线程类只是实现了RunnableCallable接口,还可以继承其他类
  • Runnable和Callable情况下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况
  • 编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()

Thread优劣势:

  • 线程类已经继承了Thread类,所以不能再继承其他父类
  • 编写简单,如果需要访问当前线程,用this使用

3. 线程生命周期

3.1 新建和就绪状态

  • new语句仅仅由Java虚拟机为其分配内存,并没有表现出任何线程的动态特征
  • 如果直接调用继承类的run方法,则只会有MainActivity,而且不能通过getName获得当前执行线程的名字,而需用Thread.currentThread().getName()
  • 调用了run方法后,该线程已经不再处于新建状态

3.2 运行和阻塞状态

  • 当线程数大于处理器数时,存在多个线程在同一个CPU上轮换的现象
  • 协作式调度策略:只有当一个线程调用了sleep()yield()方法才会放弃所占用的资源——即必须线程主动放弃所占用的资源
  • 抢占式调度策略:系统给每个可执行的线程分配一个小的时间段来处理任务,当任务完成后,系统会剥夺该线程所占用的资源
  • 被阻塞的线程会在合适的时候重新进入就绪状态
线程状态转换图

3.3 死亡状态

  • 测试线程死亡可用isAlive()
  • 处于死亡的线程无法再次运行,否则引发IllegalThreadStateException异常

4. 控制线程

4.1 join线程

  • MainActivity调用了A.join(),则MainActivity被阻塞,A线程执行完后MainActivity才执行

4.2 后台线程(Daemon Thread)

  • 如果所有的前台线程都死亡,后台线程会自动死亡
public class DaemonThread extends Thread {
    @Override
    public void run() {
        for(int i = 0; i< 1000; i++){
            System.out.println("DaemonActivity" + i);
        }
    }

    public static void main(String[] args){
        DaemonThread thread = new DaemonThread();
        thread.setDaemon(true);
        thread.start();
        for(int i = 0; i < 10; i ++ ){
            System.out.println("MainActivity" + i);
        }
    }
}
运行结果

4.3 线程睡眠sleep

 try {
      Thread.sleep(200);
} catch (InterruptedException e) {
       e.printStackTrace();
}
  • sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程优先级;但yield方法只会给优先级相同或更高的线程
  • sleep方法将转入阻塞状态,直到经过阻塞时间才会转入就绪;yield强制当前线程转入就绪状态
  • sleep方法抛出了InterruptedException,yield方法没抛出异常

4.4 改变线程优先级

  • 优先级高的线程获得较多的执行机会,优先级低的线程获得较少的执行机会
  • setPrioritygetPriority方法来设置和返回指定线程的优先级

5. 线程同步

  • run()方法不具有同步安全性
  • java引入了同步监视器来解决多线程同步问题,sychronized(obj)obj就是共享资源

5.1 同步方法

  • 同步方法就是使用synchronized来修饰某个方法
  • 实例方法的同步监视器默认是this
  • Java中不可变类总是线程安全的,可变类对象需要额外的方法来保证其线程安全
public class DaemonThread extends Thread {

    static int balance = 100;
    int drawAmount;
    String name;

    public DaemonThread(int drawAmount, String name){
        this.drawAmount = drawAmount;
        this.name = name;
    }

    @Override
    public void run() {
        this.draw(drawAmount);
    }

    public synchronized void draw(int amount){
        if(balance  >= amount){
            System.out.println(this.name + "取出了" + amount);
            try{
                Thread.sleep(1);
            } catch (InterruptedException e){
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println("\t余额为" + balance);

        } else{
            System.out.println(this.name + "取现失败");
        }
    }
    public static void main(String[] args){
        new DaemonThread(50, "A").start();
        new DaemonThread(100, "B").start();
    }
}

5.2 释放同步监视器的锁定

下列情况下,线程会释放对同步监视器的锁定

  • 当前线程的同步方法、同步代码块执行结束
  • 遇到了breakreturn
  • 遇到异常
  • 程序执行了同步监视器对象的wait()方法

下列情况下不会释放:

  • 执行同步方法时,程序调用Thread.sleep() Thread.yield()方法
  • 其他线程调用了该线程的suspend方法

5.3 同步锁

  • Java5开始,提供了一种功能更强大的同步锁机制,可以通过显式定义同步锁对象来实现同步
  • Lock提供了比synchronized更广泛的锁定操作,并且支持多个相关的Condition对象
  • Lock类型:
    Lock
    ReadWriteLock
    ReentrantLock:常用,可以对一个加锁的对象重新加锁
    ReentrantReadWriteLock
    StampedLock
方法名 作用
lock 加锁
unlock 解锁

5.4 死锁

A等B,B等A

5.5 线程通信

5.5.1 传统的线程通信

方法名 作用
wait 导致当前线程等待,直到其他线程调用该同步监视器的notify()notifyAll()方法
notify 唤醒在此同步监视器等待的单个线程
notifyAll 唤醒在此同步监视器等待的所有线程
  • wait()必须在加锁的情况下执行

5.5.2 使用Condition

  • 如果系统中不适用synchronized来保证线程同步,而使用Lock对象来保证同步,那么无法使用waitnotifynotifyAll()来进行线程通信
  • 当使用Lock对象,Java提供Condition保证线程协调
  • Condition方法如下
方法名 作用
await 导致当前线程等待,直到其他线程调用该同步监视器的signal()signalAll()方法
signal 唤醒在此Lock对象的单个线程
signalAll 唤醒在此Lock对象的所有线程

5.5.3 使用阻塞队列

  • Java提供了一个BlockingQueue接口
  • 当生产者线程试图向BlockingQueue放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue取出元素时,如果该队列已空,则该线程被阻塞
方法名 作用
put(E e) 尝试把E元素放入BlockingQueue
take() 尝试从BlockingQueue的头部取出元素
public class BlockingQueueThread extends Thread {

    private BlockingQueue<String> bq;

    public BlockingQueueThread(BlockingQueue<String> bq){
        this.bq = bq;
    }

    @Override
    public void run() {
        String[] strColl = new String[]{
                "Java",
                "Kotlin",
                "JavaScript"
        };



        for(int i = 0; i < 1000; i ++){
            try {
                System.out.println(getName() + "开始动工" + i);
                bq.put(strColl[i % 3]);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(getName() + "工作结束");
    }

    public static void main(String[] args){
        BlockingQueue<String> bq = new ArrayBlockingQueue<>(5);
        new BlockingQueueThread(bq).start();
    }
}
结果展示

可以看到,当Thread-0运行到第6次时就已经被阻塞,不能往里添加内容

6. 线程组和未处理的异常

  • ThreadGroup表示线程组,可以对一批线程进行分类管理
  • 子线程和创建它的父线程在同一个线程组内
  • ThreadGroup方法
方法名 作用
int activeCount 返回线程组中活动线程的数目
interrupt 中断此线程组中所有活动线程的数目
isDaemon 线程组是否是后台线程组
setDaemon 设置后台线程
setMaxPriority 设置线程组的最高优先级

7. 线程池

  • 线程池在系统启动时即创建大量空闲的线程
  • 程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动一个空闲线程来执行他们
  • 线程结束不死亡,而是回到空闲状态
  • Java8之后新增了一个Executors工厂类来生产线程池

7.1 ThreadPool

public class ThreadPoolTest {

    public static void main(String[] args){
        ExecutorService pool = Executors.newFixedThreadPool(2);

        java.lang.Runnable target = () -> {
           for (int i = 0; i < 100 ; i ++){
               System.out.println(Thread.currentThread().getName() + "的i为" +i);
           }
        };

        pool.submit(target);
        pool.submit(target);
        pool.shutdown();
    }
}
结果展示

7.2 ForkJoinPool

  • 将一个任务拆分成多个小任务并行计算,再把多个小任务的结果合并成总的计算结果
  • ForkJoinPoolExecutorService的实现类
public class PrintTask extends RecursiveAction {

    public static int THREADSH_HOLD = 50;

    private int start;

    private int end;

    public PrintTask(int start, int end){
        this.start = start;
        this.end = end;
    }

    @Override
    protected void compute() {
        if(end - start < THREADSH_HOLD){
            for(int i = start; i < end; i ++){
                System.out.println(Thread.currentThread().getName() + "的i为" + i);
            }
        } else {
            PrintTask left = new PrintTask(start, (start + end) / 2);
            PrintTask right = new PrintTask((start + end) / 2 , end);
            left.fork();
            right.fork();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        PrintTask printTask = new PrintTask(0 , 300);
        ForkJoinPool pool = new ForkJoinPool();
        pool.submit(printTask);
        pool.awaitTermination(2, TimeUnit.SECONDS);
        pool.shutdown();

    }
}
结果展示
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容