还在看java多线程?这一篇就够了

1、多线程简介

程序是指令和数据的有序集合,其本身配如有任何运行的含义,是一个静态的概念。

进程(Process)

进程是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。操作系统调度的最小任务单位不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。

线程(Thread)

线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

进程和线程的关系

进程和线程是包含关系,一个进程可以包含一个或多个线程,但至少会有一个线程。多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。

多线程

Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。对于大多数Java程序来说,多任务,就是如何使用多线程实现多任务。

特点

  • 多线程模型是Java程序最基本的并发模型;
  • 后续读写网络、数据库、Web开发等都依赖Java多线程模型。

多任务模式

同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:

  • 多进程模式(每个进程只有一个线程)
    在这里插入图片描述
  • 多线程模式(一个进程有多个线程)
    在这里插入图片描述
  • 多进程+多线程模式(复杂度最高)

在这里插入图片描述

注意

很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器。如果是模拟出来的多线程,即使在一个CPU下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所有就有同时执行的错觉。

2、线程的创建

2.1、三种创建方式

2.1.1、Thread:继承Thread类(重点)

不推荐使用,避免OOP单继承的局限性

package com.longxin;

//创建线程方法一:继承Thread类,重写run方法,调用start开启线程
/**
 * 注意:线程开启不代表立即执行,线程的执行不受人为的控制,全权由CPU调度执行。
 * */
public class TestThread extends Thread{
    @Override
    public void run() {
        //run方法线程体
        super.run();
        for (int i = 1; i < 2000; i++) {
            System.out.println("-------"+i);
        }
    }
    //main方法主线程
    public static void main(String[] args) {
        //创建一个线程对象
        TestThread testThread = new TestThread();
        //调用start方法开启线程
        testThread.start();

        for (int i = 1; i < 2000; i++) {
            System.out.println("%%%%%%%"+i);
        }
    }
}

run和start的区别

run:直接调用run()方法,就是把run()方法当做普通方法来调用,没有多线程的概念,根据程序逻辑去执行。

start:调用start()方法,来启动线程真正实现多线程运行。

Thread下载图片练习:看下载图片的顺序是否是安装程序步骤执行的?

package com.thread.test;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

//练习thread,实现多线程同步下载图片
public class ThreadTest02 extends Thread{
    //网络图片地址
    private String url;
    //文件名
    private String name;
    public ThreadTest02(String url,String name){
        this.url = url;
        this.name = name;
    }
    //下载图片线程的执行体
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("名为:"+name+" 的文件已经下载完成");
    }

    public static void main(String[] args) {
        ThreadTest02 th1 = new ThreadTest02
            ("http://pic.sc.chinaz.com/files/pic/pic9/202006/bpic20613.jpg","马吃草.jpg");
        ThreadTest02 th2 = new ThreadTest02
            ("http://pic.sc.chinaz.com/files/pic/pic9/202006/bpic20616.jpg","方向盘.jpg");
        ThreadTest02 th3 = new ThreadTest02
            ("http://pic.sc.chinaz.com/files/pic/pic9/202006/hpic2610.jpg","企鹅.jpg");

        th1.start();
        th2.start();
        th3.start();
    }
}

//下载器
class WebDownloader {
    //下载方法
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));

        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("IO异常,downloader方法出现问题");
        }
    }
}

2.1.2、Runnable接口:实现Runnable接口(重点、推荐)

实现runnable接口

package com.thread.test;

/**
 * 创建线程2:实现runnable接口,重写run方法,执行线程需要在Thread构造器中写入runnable接口实现类,调用start方法
 * */
public class RunnableTest01 implements Runnable{

    @Override
    //实现Runnable接口的run方法
    public void run() {
        System.out.println("用Runnable建立的新线程");
    }

    public static void main(String[] args) {
        //创建runnable接口实现类的对象
        RunnableTest01 runnable = new RunnableTest01();
        //通过线程对象来开启线程代理
        new Thread(runnable).start();
    }
}

优点

  • 避免单继承的局限性
  • 灵活方便
  • 方便同一个对象被多个线程使用
2.1.3、Callable接口:实现Callable接口(了解)

实现Callable接口

/*测试Callable*/
public class PoolTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //可以使用很多实现类来操作Callable,举例使用FutureTask
        FutureTask<Integer> futureTask = new FutureTask(new MeThread());
        //启动线程
        new Thread(futureTask).start();
        //可接收返回值,一定要在线程执行后获取返回值,否则程序会卡死
        Integer result = futureTask.get();
        System.out.println(result);
    }
}
class MeThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 1+1;
    }
}

优点

  • 可以设置返回值
  • 可以抛出异常

使用多线程留下的问题:

多个线程操作同一个资源,线程不安全,数据紊乱。

3、代理模式

3.1、静态代理

代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。

package com.thread.test3;

public class StaticProxy2 {
    public static void main(String[] args) {
        /*观众看电影来电影院*/
        /*Cinema cinema = new Cinema(new PlayMovie());
        cinema.playMovie();*/
        new Cinema(new PlayMovie()).playMovie();
    }
}
/*电影*/
interface Movie{
    /*播放电影*/
    void playMovie();
}

/*电影公司要让这个电影播放*/
class PlayMovie implements Movie{
    @Override
    public void playMovie() {
        System.out.println("播放电影《阿甘正传》");
    }
}
/*电影公司就去让电影院去代理播放任务
* 电影院为了多赚钱,就在电影片头和片尾增加了广告
* */
class Cinema implements Movie{
    private Movie movie;
    public Cinema(Movie movie){
        this.movie = movie;
    }
    @Override
    public void playMovie() {
        before();
        this.movie.playMovie();
        after();
    }
    private void before() {
        System.out.println("片头广告");
    }
    private void after() {
        System.out.println("片尾广告");
    }
}

输出结果

片头广告
播放电影《阿甘正传》
片尾广告

4、JDK1.8 Lambda表达式

为什么要用Lambda表达式?

  • 避免内部类定义过多
  • 让代码看起来更整洁
  • 去掉一堆没有意义的代码,只留下核心逻辑

Functional Interface(函数式接口)

  • 理解函数式接口是学习Java8,Lambda表达式的关键
  • 函数式接口的定义:
    • 任何接口如果只包含唯一一个抽象方法,那么它就是一个函数式接口
    • 对于函数式接口,我们可以通过Lambda表达式来创建该接口的对象。
package com.thread.lambdaTest;
/*
* 推导Lambda表达式
*   注释的序号,是一个逐步简化的过程
*   1.定义一个函数式接口
*   2.用实现类实现接口
*   3.用静态内部类实现接口
*   4.用局部内部类实现接口
*   5.用匿名内部类实现接口
*   6.用lambda表达式简化实现接口
* */

//1.定义一个函数式接口
interface Love{
    void showLove();
}
//2.实现类
class Person implements Love{
    @Override
    public void showLove() {
        System.out.println("I love you");
    }
}

public class LambdaTest {
    //3.静态内部类
    static class Person2 implements Love{
        @Override
        public void showLove() {
            System.out.println("I love you 2");
        }
    }
    public static void main(String[] args) {
        //4.局部内部类
        class Person3 implements Love{
            @Override
            public void showLove() {
                System.out.println("I love you 3");
            }
        }
        //5.匿名内部类,没有类的名称,必须借助接口或者父类
        Love love4 = new Love() {
            @Override
            public void showLove() {
                System.out.println("I love you 4");
            }
        };

        //6.用lambda表达式简化
        Love love5 = ()->{
            System.out.println("I love you 5");
        };

        //测试实现类输出
        Love love = new Person();
        love.showLove();
        //测试静态内部类输出
        Love love2 = new Person2();
        love2.showLove();
        //测试局部内部类输出
        Love love3 = new Person3();
        love3.showLove();
        //测试匿名内部类输出
        love4.showLove();
        //测试lambda表达式简化后输出
        love5.showLove();
    }
}

对于lambda表达式的简化

package com.thread.lambdaTest;
/*
* 继续简化lambda表达式
* */
//还是先定义接口
interface Kiss{
    void kissYou(String name);
}
public class LambdaTestTwo {
    public static void main(String[] args) {
        //初始态
        Kiss kiss = (String name)->{
            System.out.println("I want to kiss you "+name);
        };
        kiss.kissYou("漂亮");
        //简化方式一:参数类型 (两种写法:一种写在参数括号内部(只限单参数);另一种在参数括号外部)
        Kiss kiss2 = (name -> {
            //括号内
            System.out.println("I want to kiss you "+name);
        });
        kiss2.kissYou("漂亮X2");

        kiss2 = (name)->{
            //括号外
            System.out.println("I want to kiss you "+name);
        };
        kiss2.kissYou("漂亮X2.1");

        //简化方式二:简化参数括号
        Kiss kiss3 = name -> {
            System.out.println("I want to kiss you "+name);
        };
        kiss3.kissYou("漂亮X3");

        //简化方式三:简化代码外层花括号
        Kiss kiss4 = name -> System.out.println("I want to kiss you "+name);
        kiss4.kissYou("漂亮X4");
    }
}

总结注意:

  • 如若使用lambda表达式,必须保证接口是函数式接口(即该接口只有一个方法)

  • 在对lambda表达式的简化方式一中,在参数括号内的写法,如果有大于一个以上的参数时,会报错。请使用括号外写法

  • 简化方式三中,在只有一行代码的情况下可以缩减为一行。如果有多行代码,请使用简化方式一或二,用代码块包裹。

  • 在有多个参数的情况下,也可以去掉参数类型。如果去掉,就都去掉。前提是所有参数必须写在括号内

5、多线程的五个状态

  • 创建状态

    线程对象一经创建就进入创建状态,此时它已经获取了相应的资源,但还没有处于可运行的状态。

  • 就绪状态

    当调用start()方法启动线程,线程就进入就绪状态,等待CPU调度执行

  • 阻塞状态

    调用sleep、wait或同步锁定时,线程就进入阻塞状态,该线程的代码将不会执行。当解除锁定后,该线程代码重新进入就绪状态等待CPU调度执行

  • 运行状态

    当就绪状态的线程获得CPU资源时,即可进入运行状态,线程才真正开始执行线程体的代码块

  • 死亡状态

    线程中断或者执行完毕,线程则进入死亡状态。处于死亡状态的线程不具有继续运行的能力

6、对于线程的操作

6.1停止线程的建议方法

package com.thread.stop;
/*
 * 测试停止线程
 * 1.建议线程正常停止--->利用次数,不建议死循环
 * 2.建议使用标志位
 * 3.不要使用stop(停止)或者destroy(破坏)等过时或者JDK不建议使用的方法
 * */
public class StopThreadTest implements Runnable{
    //1.设置一个标志位
    private boolean flag = true;
    @Override
    public void run() {
        int i = 1;
        while (flag){
            System.out.println("thread---run--->"+i++);
        }
    }
    //2。设置一个公开的方法停止线程
    public void stop() {
        this.flag = false;
    }
    public static void main(String[] args) {
        StopThreadTest stopThreadTest = new StopThreadTest();
        new Thread(stopThreadTest).start();

        for (int i = 1; i <= 1000; i++) {
            System.out.println("main--run-->"+i);
            if (i==500){
                //调用stop方法切换标志位,让线程停止
                stopThreadTest.stop();
                System.out.println("Thread stop!!!=============================");
            }
        }
    }
}

6.2、线程休眠sleep的使用

  • sleep(时间) 指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException,如果任何线程中断了当前线程,当这个异常被抛出是,当前线程的中断状态被清除。
  • sleep时间结束后,线程进入就绪状态
  • sleep可以模拟网络延时,倒计时
  • 每个对象都有一个锁,sleep不会释放锁
用线程休眠模拟倒计时
/*模拟倒计时*/
public static void main(String[] args) {
    int num = 10;
    while (true){
        System.out.println(num);
        if (num==0){
            break;
        }
        num--;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
用线程休眠模拟系统时间
/*模拟打印系统时间*/
public static void main(String[] args) {
    //获取当前系统时间
    Date date = new Date(System.currentTimeMillis());
    int i = 1;
    while (true){
        i++;
        if (i>10){
            break;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
        //更新
        date = new Date(System.currentTimeMillis());
    }
}

7、线程礼让yield

  • 礼让线程,暂停当前线程执行,允许其他具有相同优先级的线程获得运行机会
  • 将当前线程从运行状态转换为就绪状态
  • 线程的礼让只是提供一种可能,不保证一定礼让成功。看CPU的心情进行重新调度
package com.thread.yield;
/**
 * 测试线程礼让
 *  礼让不一定成功,看CPU心情
 * */
public class TestYield {
    public static void main(String[] args) {
        new Thread(new ThreadYield(),"A").start();
        new Thread(new ThreadYield(),"B").start();
    }
}
class ThreadYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"--->start");
        //线程礼让
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"--->end");
    }
}
/**
 * 礼让成功会连续输出两个start   失败会输出开始和结束
 * A--->start                B--->start
 * B--->start                B--->end
 * A--->end                  A--->start
 * B--->end                  A--->end
 * */

8、线程强制执行join

  • join强制执行,待此线程执行完成后,在执行其他线程,其他线程阻塞。
package com.thread.join;
/*线程强制执行*/
public class JoinTest implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("VIP线程执行"+i+"次");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new JoinTest());
        thread.start();
        for (int i = 0; i < 500; i++) {
            if (i==200){
                System.out.println("===============================================");
                thread.join();
            }
            System.out.println("普通线程执行"+i+"次");
        }
    }
}

9、线程状态state

  • NEW (新生)

    • 尚未启动的线程处于此状态。
  • RUNNABLE

    • 在Java虚拟机中执行的线程处于此状态。
  • BLOCKED

    • 被阻塞等待监视器锁定的线程处于此状态。
  • WAITING

    • 正在等待另一个线程执行特定动作的线程处于此状态。
  • TIMED_WAITING

    • 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
  • TERMINATED

    • 已退出的线程处于此状态。
package com.thread.state;
/*观察线程状态*/
public class StateTest {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("/");
        });
        //观察状态
        Thread.State state = thread.getState();
        System.out.println(state);
        //启动后观察
        thread.start();
        state = thread.getState();
        System.out.println(state);

        //只要线程不终止就一直输出状态
        while (state != Thread.State.TERMINATED){
            try {
                //每隔100毫秒输出一次状态
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //更新线程状态
            state = thread.getState();
            //输出状态
            System.out.println(state);
        }
    }
}

10、线程的优先级Priority

  • MAX_PRIORITY 最大优先级 = 10
  • NORM_PRIORITY 默认优先级 = 5
  • MIN_PRIORITY 最小优先级 = 1

线程的优先级高,只意味着获得调度的概率高,并不一定优先执行。小概率可能会出现性能倒置

package com.thread.priority;
/*测试线程优先级*/
public class PriorityTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Priority(),"一");
        Thread t2 = new Thread(new Priority(),"二");
        Thread t3 = new Thread(new Priority(),"三");
        Thread t4 = new Thread(new Priority(),"四");
        Thread t5 = new Thread(new Priority(),"五");
        System.out.println(
            Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
        /*先设置优先级,在启动*/
        //设置优先级为10
        t1.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        //设置优先级为6
        t2.setPriority(6);
        t2.start();
        //设置优先级为5
        t3.setPriority(Thread.NORM_PRIORITY);
        t3.start();
        //设置优先级为4
        t4.setPriority(4);
        t4.start();
        //设置优先级为3
        t5.setPriority(3);
        t5.start();
    }
}
class Priority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
    }
}

11、守护线程daemon

  • 守护线程是为其他线程服务的线程;
  • 所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出;
  • 守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。
package com.thread.daemon;
/*测试守护线程*/
//上帝守护人类
public class DaemonTest {
    public static void main(String[] args) {
        //把上帝放入线程中
        Thread god = new Thread(new God());
        //把上帝所在的线程设置为守护线程,默认为false
        god.setDaemon(true);
        //启动守护线程
        god.start();
        //启动用户线程
        new Thread(new Person()).start();
    }
}
//上帝,神
class God implements Runnable{
    @Override
    public void run() {
        int i = 1;
        while (true){
            System.out.println("上帝保佑你"+(i++)+"年");
        }
    }
}
//人类
class Person implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            System.out.println("我来到这个世界的第"+i+"年");
        }
        System.out.println("GoodBye,world!");
    }
}

12、线程同步synchronized

  • 多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
  • 同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 注意加锁对象必须是同一个实例;
  • 对JVM定义的单个原子操作不需要同步。
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 安全和性能不可兼得

使用synchronized的两种方式

修饰方法
  • synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。
  • 如果对一个静态方法添加synchronized修饰符,它锁住的是哪个对象?
    • 对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例。
package com.thread.syn;
/*安全的买票*/
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket,"黄牛").start();
        new Thread(buyTicket,"张三").start();
        new Thread(buyTicket,"李四").start();
    }
}
//买票
class BuyTicket implements Runnable{
    //票数
    private int ticketNums = 10;
    //标识符
    private boolean flag = true;
    @Override
    public void run() {
        while (flag){
            buy();
        }
    }
    private synchronized void buy(){
        //判断是否有票
        if (ticketNums<=0){
            flag = false;
            return;
        }
        ticketNums--;
        System.out.println(Thread.currentThread().getName()+"拿到第"+(10-ticketNums)+"张票,还有"+ticketNums+"张票");
    }
}
修饰代码块
  • 一个方法里的代码分为只读代码和进行数据操作的修改代码,只需要给涉及到数据操作的代码上锁就可以

  • 方法里面需要修改的内容才需要锁,锁太多影响性能,浪费资源

public class ListTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

13、拓展 CopyOnWriterArrayList

  • 在同一时间多个线程无法对同一个List进行读取和增删,否则就会抛出并发异常
  • CopyOnWriteArrayList完美解决的开发工作中的多线程的并发问题。
  • 在读多写少的情况下使用
  • 缺点:
    • **内存占有问题:**两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大
    • **数据一致性:**CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器
/*CopyOnWriteArrayList的应用*/
public class TestGUC {
    public static void main(String[] args) {
        //定义一个CopyOnWriteArrayList
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        //多线程循环遍历添加线程名
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            //让线程休眠一秒,保证数据的最终一致性
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //输出线程名的个数,看是否为10000个
        System.out.println(list.size());
    }
}

14、死锁

  • 产生死锁的四个必要条件

    1. **互斥条件:**一个资源每次只能被一个线程使用
    2. **请求与保持条件:**一个进程引请求资源而阻塞时,对已获得的资源保持不放
    3. **不剥夺条件:**进程已获得的资源,在未使用完之前,不能强行剥夺
    4. **循环等待条件:**若干进程之间形成一种头尾相接的循环的等待资源关系。
  • 只要破解以上四个条件中的任意一项或多项就可避免死锁。

案例:化妆

两个姑娘化妆时,在口红和镜子都只有一个的情况下。一个持有镜子,另一个持有口红。两人都需要化妆,然后都拿着自己所持有的资源,并且想要对方的资源,但对方都不愿意先交出自己的资源,就会形成如下死锁。

package com.thread.deadLock;
/*死锁:多个线程互相占用对方所需资源,形成僵持*/
public class DeadLock {
    public static void main(String[] args) {
        Makeup g1 = new Makeup(0,"白雪公主");
        Makeup g2 = new Makeup(1,"灰姑娘");
        g1.start();
        g2.start();
    }
}
//镜子
class Mirror{}
//口红
class Lipstick{}

//化妆
class Makeup extends Thread{
    //用static来保证,所需的资源只有一份
    static Mirror mirror = new Mirror();
    static Lipstick lipstick = new Lipstick();
    //选择
    int select;
    //女孩姓名
    String girlName;
    public Makeup(int select,String girlName){
        this.select = select;
        this.girlName = girlName;
    }
    @Override
    public void run() {
        if (select==0){
            synchronized (mirror){
                System.out.println(this.girlName+"获得镜子的使用权");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick){
                    System.out.println(this.girlName+"获得口红的使用权");
                }
            }
        }else {
            synchronized (lipstick){
                System.out.println(this.girlName+"获得口红的使用权");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror){
                    System.out.println(this.girlName+"获得镜子的使用权");
                }
            }
        }
    }
}

run方法内的代码修改为如下,即可解决问题

@Override
public void run() {
    if (select==0){
        synchronized (mirror){
            System.out.println(this.girlName+"获得镜子的使用权");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (lipstick){
            System.out.println(this.girlName+"获得口红的使用权");
        }
    }else {
        synchronized (lipstick){
            System.out.println(this.girlName+"获得口红的使用权");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (mirror){
            System.out.println(this.girlName+"获得镜子的使用权");
        }
    }
}

15、终极lock锁

  • ReentrantLock 可重入锁

  • JDK5.0开始,java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现,同步锁使用Lock对象充当

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显方式加锁、释放锁

package com.thread.ultimate;

import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    public static void main(String[] args) {
        BuyTickets buyTickets = new BuyTickets();
        new Thread(buyTickets,"黄牛").start();
        new Thread(buyTickets,"张三").start();
        new Thread(buyTickets,"李四").start();
    }
}
//买票
class BuyTickets implements Runnable{
    //票数
    int tickets = 10;
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        lock.lock();
        while (true){
            //如果同步代码有异常,需要用finally块包裹lock.unlock();
            try {
                if (tickets>0){
                    Thread.sleep(500);
                    tickets--;
                    System.out.println
                       (Thread.currentThread().getName()+"买到第"+(10-tickets)+"张票,还剩"+tickets+"张票");
                }else {
                    break;
                }
            } catch (InterruptedException e) {
                //此处异常为Thread.sleep产生
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

synchronized与lock的对比

  • Lock是显式锁(需要手动开启和关闭锁,切记关闭锁)synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程性能更好。并且具有更好的拓展性(提供更多的子类)
  • 建议优先使用顺序:
    • Lock>同步代码块(以及进入方法体,分配了相应资源)>同步方法(在方法体之外)
代码块锁方法锁更好的拓展性性能更好
Lock(显式锁)
synchronized(隐式锁)

16、生产者和消费者问题

应用场景:生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将产品生产出来放入仓库中,消费者将从仓库中消费取走商品
  • 如果仓库中没有产品,生产者需将产品放入仓库。否则停止生产并等待,等待消费者取走产品
  • 如果仓库中有产品,消费者可以将产品取走消费。否则停止消费并等待,等待仓库中放入产品

这是一个线程同步,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件

在生产者消费者问题中,仅有synchronized是不够的的

  • synchronized可阻止并发更新同一个共享资源,实现了同步
  • synchronized不能用来实现不同线程之间的消息传递(通信)

java提供了几个方法解决线程之间的通信问题

方法名作用
wait()表示线程一直等待,直到其他线程通知。与sleep不同,wait会释放锁
wait(long timeout)制定等待的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用wait等待的线程,优先级高的线程,优先调度

注意:以上方法都是Object类的方法,都只能在同步方法或同步代码块中使用,否则会抛出异常IllegalMonitorStateException

解决方式一、管程法

  • 生产者:负责生产数据的模块(可能是方法、对象、线程、进程)
  • 消费者:负责处理数据的模块(可能是方法、对象、线程、进程)
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

package com.thread.producerConsumerProblem;
/*测试生产者与消费者模型 缓冲区解决,管程法*/
public class PCTest {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();
        new Producer(synContainer).start();
        new Consumer(synContainer).start();
    }
}
//生产者
class Producer extends Thread{
    SynContainer synContainer;
    public Producer(SynContainer synContainer){
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            synContainer.setProduct(new Phone(i));
            System.out.println("生产了第"+i+"部手机");
        }
    }
}
//消费者
class Consumer extends Thread{
    SynContainer synContainer;
    public Consumer(SynContainer synContainer){
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println("消费者购买了第"+synContainer.getProduct().id+"部手机");
        }
    }
}
//产品
class Phone{
    //产品编号
    int id;
    public Phone(int id){
        this.id = id;
    }
}
//缓冲区
class SynContainer{
    //容器大小
    Phone [] phones = new Phone[5];
    //容器计数器
    int count  = 0;
    //生产者放入产品
    public synchronized void setProduct(Phone phone){
        //如果产品满了就需要等待消费者消费
        if (count==phones.length){
            //通知消费者消费,生产者等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果没有满就放入产品
        phones[count] = phone;
        count++;
        //通知消费者消费

    }
    //消费者消费产品
    public synchronized Phone getProduct(){
        //判断能否消费
        if(count==0){
            //等待生产者生产,消费者等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果可以消费
        count--;
        //通知生产者生产
        return phones[count];
    }
}

解决方式二、信号灯法

package com.thread.producerConsumerProblem;

public class PCTest2 {
    public static void main(String[] args) {
        Tv tv = new Tv();
        new Actor(tv).start();
        new Watcher(tv).start();
    }
}
//演员
class Actor extends Thread{
    Tv tv;
    public Actor(Tv tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i%2==0){
                this.tv.performance("快乐大本营");
            }else {
                this.tv.performance("抖音");
            }
        }
    }
}
//观众watcher
class Watcher extends Thread{
    Tv tv;
    public Watcher(Tv tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            this.tv.watch();
        }
    }
}
//电视台
class Tv{
    //节目
    String program;
    //标志位,信号灯
    boolean flag = true;
    //表演
    public synchronized void performance(String program){
        if (!flag){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员在表演:"+program);
        //通知观众观看,唤醒
        this.notifyAll();
        this.program = program;
        flag = !flag;
    }
    //观看
    public synchronized void watch(){
        if (flag){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众在看:"+program);
        //提示演员表演
        this.notifyAll();
        flag = !flag;
    }
}

17、线程池

线程池背景:经常创建和销毁使用量特别大的资源比如 并发情况下的线程,对性能影响很大。

使用线程池的优势:

(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的真正实现类是ThreadPoolExecutor,其构造方法有如下4种:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

可以看到,其需要如下几个参数:

  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
  • unit(必需):指定keepAliveTime参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • workQueue(必需):任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该参数中。其采用阻塞队列实现。
  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

线程池的使用流程如下:

// 创建线程池
Executor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// 向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

上述线程池设置太麻烦?Executors已经为我们封装好了4种常见的功能线程池

定长线程池(FixedThreadPool)

创建方法的源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

使用示例:

/*测试线程池*/
//创建线程池   参数为线程池大小
ExecutorService service = Executors.newFixedThreadPool(5);
//执行
service.execute(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
});
//关闭链接
service.shutdown();
  • 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:控制线程最大并发数。

定时线程池(ScheduledThreadPool )

创建方法的源码:

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
 
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}
 
public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}

使用示例:

// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
  • 特点:核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延时阻塞队列。
  • 应用场景:执行定时或周期性的任务。

可缓存线程池(CachedThreadPool)

创建方法的源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

使用示例:

// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
  • 特点:无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列。
  • 应用场景:执行大量、耗时少的任务。

单线程化线程池(SingleThreadExecutor)

创建方法的源码:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

使用示例:

// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
  • 特点:只有1个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作、文件操作等。

参考

线程池部分

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值