Java自学(十二、Java的多线程详解)

记录自己在狂神说java中的学习情况,文章里有自己学习的理解和扩展,新手难免有理解偏差或者错误,恳请大佬指正。

Java 多线程详解及实战小例子

线程简介

现实中有特别多同时做多件事情的例子,比如上厕所是玩手机、吃饭的时候玩手机、边听音乐边跳舞。但是其实这些任务看起来是多个任务都在做,但是本质上大脑在同一时间依旧只做了一件事情。

原来是一条路,慢慢因为车太多了,道路堵塞,效率极低。为了提高使用效率,能够充分利用道路,于是加了多个车道。

普通方法调用和多线程

在这里插入图片描述

普通的方法调用是跳转到方法里执行方法,然后再返回调用的地方继续往下执行。相当于一个人在做饭—》送餐—》做饭。

多线程就是再另开一个线程去执行方法。每个线程都执行一部分工作,相当于一个人做饭,一个人送餐。

程序、进程、线程

程序是存放在磁盘中的可执行文件。进程就是程序的执行实例,通俗来说,在操作系统中执行着的程序,就是进程。

一个进程中可以有多个线程,使得可以同时执行一些任务。比如你在播放音乐的时候,能同时听到音乐和歌词以及对应的MV画面。

进程(Process)与线程(Thread)

说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。

而进程则是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位。

通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的单位。

进程是系统分配资源的单位,里面真正执行程序的是进程里的线程(即用CPU去处理程序的语句)。

注意点:很多多线程是模拟出来的,真正的多线程是指有多个CPU,即多核CPU。如果是模拟出来的多线程,即只在一个CPU的情况下,在同一时间点,CPU只能执行一个代码,但是因为切换的很快,所以就有了同时执行的错觉。但是事实上我们平时的任务数要远大于CPU核心数,这个时候的多线程都是模拟出来的。

并发:任务数大于CPU核心数,每个任务对应的线程要交替使用CPU。并发的关键是可以处理多个任务,但是多个任务并不是真正同时处理的。

并行:任务数小于CPU核心数,每个任务对应的线程可以真正意义上的同时执行。并行的关键是同时处理多个任务。

利用Erlang 之父 Joe Armstrong的图来解释,并发就是两个队伍去排队使用咖啡机,队伍1的人用了咖啡机后下一次就是队伍2的人去使用咖啡机。而并行就是两台咖啡机可以两个队伍分别使用。

在这里插入图片描述

小结

  • 线程就是独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程,gc线程
  • main()称为主线程,为系统的入口,用于执行整个程序
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如CPU调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程实现

线程有三种创建方式:继承Thread类、实现Runnable接口、实现Callable接口。

继承Tread类

启动线程的步骤如下:

  1. 自定义线程类继承Thread类
  2. 重写run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程
package Threads;

// 创建线程方法一:继承Thread类,重写run()方法,调用start开启线程
// 线程开启不一定立即执行!由CPU调度执行。
public class ThreadExtendsThread extends Thread {
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 5; i++) System.out.println("我在看代码---" + i);
    }

    public static void main(String[] args) throws InterruptedException {
        //main线程,主线程

        //创建一个线程对象
        ThreadExtendsThread tet = new ThreadExtendsThread();
        // 调用start()方法开启线程
        tet.start();
        for (int i = 0; i < 5; i++)
            System.out.println("我在学习多线程--" + i);
        /*
        我在学习多线程--0
        我在看代码---0
        我在学习多线程--1
        我在学习多线程--2
        我在看代码---1
        我在看代码---2
        我在看代码---3
        我在学习多线程--3
        我在看代码---4
        我在学习多线程--4
        我在看代码---5
        我在看代码---6
        我在学习多线程--5
        我在看代码---7
        我在学习多线程--6
        我在看代码---8
        我在学习多线程--7
        我在看代码---9
        我在看代码---10
        我在学习多线程--8
        我在看代码---11
        我在学习多线程--9
        我在看代码---12
        我在学习多线程--10
        我在看代码---13
        我在看代码---14
        我在看代码---15
        我在学习多线程--11
        我在看代码---16
        我在学习多线程--12
        我在看代码---17
        我在学习多线程--13
        我在学习多线程--14
        我在看代码---18
        我在学习多线程--15
        我在看代码---19
        我在学习多线程--16
        我在学习多线程--17
        我在学习多线程--18
        我在学习多线程--19
        * */
        //可以看到start()方法会让线程同时运行,运行结果是随机的,每次运行的结果不一定一样,因为线程的调度是CPU分配的。
        //但是如果用run()方法
        Thread.sleep(2000);//延时2秒等待上面的线程执行完,再开始下一个实验
        System.out.println("-------------------------------------------------------");
        ThreadExtendsThread tet2 = new ThreadExtendsThread();
        tet2.run();//start才开启了线程,而run没有,run只是线程的执行体
        for (int i = 0; i < 5; i++)
            System.out.println("我在学习多线程--" + i);
        //因为run没有开启线程,所以还是按顺序执行
        /*
        我在看代码---0
        我在看代码---1
        我在看代码---2
        我在看代码---3
        我在看代码---4
        我在学习多线程--0
        我在学习多线程--1
        我在学习多线程--2
        我在学习多线程--3
        我在学习多线程--4
        */
    }
}
继承Thread实战案例:下载图片

首先要下载jar包来获得FileUtils类。

下载地址:http://commons.apache.org/proper/commons-io/download_io.cgi

将其解压,然后把commons-io-2.8.0.jar这个包导入到项目中去。具体的导入方法有两种:

第一种为IDEA导入:

在这里插入图片描述

在这里插入图片描述

然后找到你的jar包的路径,然后添加进去。打钩就好了。

在这里插入图片描述

然后就是在代码中import了,import org.apache.commons.io.FileUtils;

第二种为:

在项目中创建一个lib文件夹,然后将jar包存入lib文件夹中

在这里插入图片描述

然后右键lib文件夹

在这里插入图片描述

点击OK,然后就可以直接import了,import org.apache.commons.io.FileUtils;

下载器代码实现:

package Threads;
// Commons IO是针对开发IO流功能的工具类库
// FileUtils文件工具,复制url到文件
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
//练习Thread,实现多线程同步下载图片
public class PictureDownload extends Thread {
    private String url;//网络图片地址
    private String name;//保存的文件名

    public PictureDownload(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) {
        PictureDownload pd = new PictureDownload("https://pic1.zhimg.com/80/v2-674f0d37fca4fac1bd2df28a2b78e633_720w.jpg?source=1940ef5c", "图片1.jpg");
        PictureDownload pd2 = new PictureDownload("https://pic1.zhimg.com/80/2886e9751c3df175ecdfc423dfe18493_720w.jpg?source=1940ef5c", "图片2.jpg");
        PictureDownload pd3 = new PictureDownload("https://pic2.zhimg.com/80/4e509eaa6a6445c87af5ac335abbb090_720w.jpg?source=1940ef5c", "图片3.jpg");

        pd.start();
        pd2.start();
        pd3.start();
    }
}

//下载器
class WebDownloader {
    //下载方法
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));//把url变成文件,文件名为name的值
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO,异常,downloader方法出现问题");
        }
    }
}

运行结果为:(每次运行可能不一样,说明线程是同时执行的)

下载了文件名为:图片3.jpg
下载了文件名为:图片2.jpg
下载了文件名为:图片1.jpg

实现Runnable接口(十分推荐,因为Java是单继承,继承了Thread就没办法继承别的了)

启动线程的步骤如下:

  1. 定义MyRunnable类实现Runnable接口
  2. 实现run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程
package Threads;
//创建线程方法2:实现runnable接口,重写run方法,执行线程需要丢入runnable接口类,调用start方法
public class ThreadRunable implements Runnable {
    public void run() {
        // run方法线程体
        for (int i = 0; i < 10; i++) {
            System.out.println("我在看代码--" + i);
        }
    }

    public static void main(String[] args) {
        //创建runnable接口的实现类对象
        ThreadRunable tr = new ThreadRunable();
        //创建线程对象,通过线程对象来开启我们的线程,代理,这里先不用知道代理是什么,就理解为你给我东西,我帮你执行就好了
        //Thread thread = new Thread(tr);
        //thread.start();
        //可以用匿名对象简化为
        new Thread(tr).start();

        for (int i = 0; i < 10; i++) {
            System.out.println("我在学习多线程--" + i);
        }
        /*
        我在学习多线程--0
        我在看代码--0
        我在学习多线程--1
        我在学习多线程--2
        我在看代码--1
        我在学习多线程--3
        我在看代码--2
        我在学习多线程--4
        我在看代码--3
        我在学习多线程--5
        我在看代码--4
        我在学习多线程--6
        我在看代码--5
        我在学习多线程--7
        我在看代码--6
        我在学习多线程--8
        我在看代码--7
        我在学习多线程--9
        我在看代码--8
        我在看代码--9
         */
    }
}

实现Runnable类与继承Thread类实现多线程的对比

继承Thread类

  • 子类继承Thread类具备多线程能力
  • 启动线程:子类对象.start()
  • 不建议使用,因为Java中单继承有局限性,继承了Thread类就不好继承别的类了

实现Runnalbe接口

  • 实现接口Runnable具有多线程能力
  • 启动线程:传入目标对象+Thread对象.start()
  • 推荐使用,避免了单继承局限性,灵活方便,方便同一个对象被多个线程使用

利用实现Runnable接口让一个对象被多个线程使用

package Threads;
import com.hbq.collectionsuse.TreeMapUse;
//多个线程同时操作一个对象
//买火车票

//发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱,像这个例子,小明先拿了第二张票,小红才拿第三张
public class ManyThread implements Runnable {
    //有几张票
    private int ticketNum = 10;

    public void run() {
        while (true) {
            if (ticketNum <= 0) break;
            //Thread.currentThread().getName()拿到当前线程的名字
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "张票");
        }
    }
    public static void main(String[] args) {
        ManyThread mt = new ManyThread();
        // 传入Thread时可以给线程起名字
        new Thread(mt, "小明").start();
        new Thread(mt, "小红").start();
        new Thread(mt, "黄牛").start();
        /*
        小红拿到了第10张票
        小明拿到了第9张票
        黄牛拿到了第8张票
        小明拿到了第6张票
        小红拿到了第7张票
        小明拿到了第4张票
        黄牛拿到了第5张票
        小明拿到了第2张票   这里就发现问题了,小明先拿了第2张,然后小红才拿了第三张
        小红拿到了第3张票
        黄牛拿到了第1张票
        */
    }
}
实现Runnable类实战:龟兔赛跑
package Threads;
public class TurtleAndRabbit implements Runnable {
    private static String winner;
    public void run() {
        for (int i = 0; i < 100; i++) {
            //模拟兔子睡觉
            if (Thread.currentThread().getName().equals("兔子")){
                try {
                    Thread.sleep(1);//睡1毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                i+=10;//睡醒后一下子跑十步
            }

            //判断比赛是否结束
            boolean flag = gameOver(i);
            if (flag) break;//比赛结束,停止程序
            System.out.println(Thread.currentThread().getName() + "跑了" + i + "步");
        }

    }
    //判断是否有胜利者,即游戏是否结束
    private boolean gameOver(int step) {
        if (winner != null) { //先判断是否有胜利者,如果有就不需要再判断了
            return true;
        }
        if (step == 99) {//如果之前没有胜利者,而且现在这个线程到了终点,则这个线程就是胜利者
            winner = Thread.currentThread().getName();
            System.out.println(winner + "获胜了");
            return true;
        }
        return false;
    }
    public static void main(String[] args) {
        TurtleAndRabbit tr = new TurtleAndRabbit();//同一个对象(赛道)
        new Thread(tr, "兔子").start();
        new Thread(tr, "乌龟").start();

    }
}

实现Callable接口

启动线程的步骤如下:

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行:Future result1 = ser.submit(t1);
  6. 获取结果:boolean r1 = result1.get();
  7. 关闭服务:ser.shutdownNow();

改写图片下载器

package Threads;
// Commons IO是针对开发IO流功能的工具类库
// FileUtils文件工具,复制url到文件
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
// 线程创建方式三:实现callable接口
/*
callable的好处
1.可以定义返回值
2.可以抛出异常
*/
public class PictureDownloadCallable implements Callable<Boolean> {
    private String url;//网络图片地址
    private String name;//保存的文件名

    public PictureDownloadCallable(String url, String name) {
        this.url = url;
        this.name = name;
    }

    //下载图片线程的执行体
    public Boolean call() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载了文件名为:" + name);
        return true;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        PictureDownloadCallable pd = new PictureDownloadCallable("https://pic1.zhimg.com/80/v2-674f0d37fca4fac1bd2df28a2b78e633_720w.jpg?source=1940ef5c", "图片1.jpg");
        PictureDownloadCallable pd2 = new PictureDownloadCallable("https://pic1.zhimg.com/80/2886e9751c3df175ecdfc423dfe18493_720w.jpg?source=1940ef5c", "图片2.jpg");
        PictureDownloadCallable pd3 = new PictureDownloadCallable("https://pic2.zhimg.com/80/4e509eaa6a6445c87af5ac335abbb090_720w.jpg?source=1940ef5c", "图片3.jpg");
        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3);
        //提交执行
        Future<Boolean> r1 = ser.submit(pd);
        Future<Boolean> r2 = ser.submit(pd2);
        Future<Boolean> r3 = ser.submit(pd3);
        //获取结果
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();
        //关闭服务
        ser.shutdownNow();
    }
    //下载器
    class WebDownloader {
        //下载方法
        public void downloader(String url, String name) {
            try {
                FileUtils.copyURLToFile(new URL(url), new File(name));//把url变成文件,文件名为name的值
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("IO,异常,downloader方法出现问题");
            }
        }
    }
}

线程状态

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  1. 最开始是线程创建阶段,也就是在Thread t = new Thread()语句执行以后,线程对象就进入了创建状态。转2。
  2. 然后调用start()方法,线程进入就绪状态。转3。
  3. 如果此时CPU调用了该线程,那么该线程就进入运行状态。开始真正执行线程体的代码块。如果中途有阻塞,则到4,否则到6。
  4. 当调用了sleep、wait或者锁定的时候,线程就进入了阻塞状态。此时等待阻塞结束。转5。
  5. 阻塞结束后,会进入到就绪状态,返回3。
  6. 线程中断或者结束,线程进入死亡状态,一旦进入了死亡状态,就不能再次启动。

线程的一些方法:

方法说明
setPriority(int newPriority)更改线程的优先级
static void sleep(long milis)在指定的毫秒数内让当前正在执行的线程休眠
void join()等待该线程终止
static void yield()暂停当前正在执行的线程对象,并执行其他线程
void interrupt()中断线程,已经不太被使用了
boolean isAlive()测试线程是否处于活动状态(就绪状态或运行状态)

如何让线程停止?

  • 不推荐使用JDK提供的stop()、destroy()方法。
  • 推荐让线程自己停止下来
  • 建议使用一个标志位进行终止变量,比如当flag==false的时候,则终止线程的运行。
package Threads;
// 1.建议线程正常停止-->尽量利用次数,不建议死循环,如果非要死循环,则可以加点时延,避免cpu卡死
// 2.建议使用标志位-->设置一个标志位,在死循环里利用标志位停止线程
// 3.不要使用官方自带的stop或者destroy等过时或者JDK不建议使用的方法停止线程
public class StopThread implements Runnable {
    // 1.设置一个标志位
    private boolean flag = true; //标志位
    @Override
    public void run() {
        while (flag) {
            System.out.println("线程执行");
        }
    }
    //2.设置一个公开的方法停止线程,转换标志位
    public void stop() { //调用该方法就可以让标志位为false,然后就可以停止线程
        this.flag = false;
    }
}

线程休眠(阻塞,sleep)

  • sleep(时间)指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间到达后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep不会释放锁

使用方法:

try{
	Thread.sleep(要休眠的毫秒数);
} catch(InterruptedException e) {
    e.printStackTrace();
}

实例:

package Threads;
import java.text.SimpleDateFormat;
import java.util.Date;
// 模拟网络延时:放大问题的发生性,要不然代码是发现不了问题的
// 模拟倒计时:
public class ThreadSleep implements Runnable {
    //有几张票
    private int ticketNum = 10;
    public void run() {
        while (true) {
            if (ticketNum <= 0) break;
            // 模拟网络延时
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum-- + "张票");
        }
    }
    //模拟倒计时
    public void tenDown() throws InterruptedException {
        int num = 10;
        while (num >= 0) {
            Thread.sleep(1000);//每一秒输出一个数字
            System.out.println(num--);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadSleep ts = new ThreadSleep();
        new Thread(ts, "小明").start();
        new Thread(ts, "小红").start();
        new Thread(ts, "黄牛").start();

        ts.tenDown();
        
        //打印当前系统时间
        Date startTime = new Date(System.currentTimeMillis());
        while (true){
            Thread.sleep(1000);
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
            startTime = new Date(System.currentTimeMillis());
        }
    }
}

线程礼让(yield)

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度,礼让不一定成功!看CPU心情

使用方法:

Thread.yield();

实例:

package Threads;

//测试礼让线程
//礼让不一定成功,看CPU心情
public class ThreadYield {
    public static void main(String[] args) {
        MyYield my = new MyYield();
        new Thread(my,"线程1").start();
        new Thread(my,"线程2").start();
        /*
        线程1线程开始执行
        线程2线程开始执行 礼让成功,会让线程2去跑
        线程1线程停止执行
        线程2线程停止执行
        */
    }
}

class MyYield implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始执行");
        Thread.yield();//礼让
        System.out.println(Thread.currentThread().getName() + "线程停止执行");
    }
}

线程的强制运行(join)

  • join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 可以理解成插队

比如线程A中有线程B的join指令,则线程A要等到线程B执行完之后,才能执行join指令下面的操作。

在这里插入图片描述

使用方法:

try {
	thread.join();//插队
} catch (InterruptedException e) {
	e.printStackTrace();
}

实例:

package Threads;

//测试join方法
public class ThreadJoin implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("线程vip插队" + i);
        }
    }

    public static void main(String[] args) {
        // 启动线程
        ThreadJoin tj = new ThreadJoin();
        Thread thread = new Thread(tj);
        thread.start();

        // 主线程
        for (int i=0;i<100;i++){
            System.out.println("main线程"+i);
            if (i==20){ //从主线程到20之前,都是两个线程一起执行,数到20后,主线程要等tj执行完了才继续执行
                try {
                    thread.join();//插队
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

线程的状态观测

Thread.State

线程状态。线程可以处于以下状态之一:

  • New:尚未启动的线程处于此状态
  • RUNNABLE:在Java虚拟机中执行的线程处于此状态
  • BLOCKED:被阻塞等待监视器锁定的线程处于此状态
  • WAITING:正在等待另一个线程执行特定动作的线程处于此状态
  • TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
  • TERMINATED:已退出的线程处于此状态

一个线程可以在给定时间点处于一个状态,这些状态是不反映任何操作系统线程状态的虚拟机状态。

package Threads;
//观察测试线程的状态
public class ThreadState {
    public static void main(String[] args) {
        // 这里使用了lambda实现了Thread里的Runnable接口,因为Runnable接口只有一个抽象方法run,所以满足函数式接口的条件,可以使用lambda方法简化成()->{//run里的线程体具体实现}
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);//会导致线程进入TIMED_WAITING状态
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("..............");
        });
        
        // 观察状态
        Thread.State state = thread.getState();
        System.out.println(state); //NEW
        //观察启动后
        thread.start();
        state = thread.getState();
        System.out.println(state); //RUNNABLE
        while (state != Thread.State.TERMINATED) {//只要线程不死,就一直输出状态
            try {
                Thread.sleep(500);
                state = thread.getState();//更新线程状态
                System.out.println(state);//输出状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //thread.start();// 会报错,死亡后的线程不能再启动了。Exception in thread "main" java.lang.IllegalThreadStateException
    }
}

线程的优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

线程的优先级用数字表示,范围从1~10

  • Thread.MIN_PRIORITY=1
  • Thread.MAX_PRIORITY=10
  • Thread.NORM_PRIORITY=5

使用getPriority()来获取优先级,使用setPriority(int xxx)来改变优先级

注意!线程优先级高不一定先执行,只是更有可能先执行而已。线程执行是看CPU决定的。

实例:

package Threads;

public class ThreadPriority implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {
        //主线程默认优先级
        System.out.println(Thread.currentThread().getPriority());

        ThreadPriority tp = new ThreadPriority();

        Thread t1 = new Thread(tp);
        Thread t2 = new Thread(tp);
        Thread t3 = new Thread(tp);
        Thread t4 = new Thread(tp);
        Thread t5 = new Thread(tp);
        Thread t6 = new Thread(tp);

        //先设置优先级,再启动
        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(10);
        t4.start();

        /*
        5
        Thread-0-->5
        Thread-1-->1
        Thread-3-->10
        Thread-2-->4
        */
//        t5.setPriority(-1); //Exception in thread "main" java.lang.IllegalArgumentException
//        t5.start();
//
//        t6.setPriority(11); //Exception in thread "main" java.lang.IllegalArgumentException
//        t6.start();
        t5.setPriority(8);
        t5.start();

        t6.setPriority(7);
        t6.start();
        /* 
        5
        Thread-0-->5
        Thread-1-->1
        Thread-2-->4
        Thread-3-->10 这回优先级高的也不一定先执行了
        Thread-4-->8
        Thread-5-->7
         */
    }
}

有可能出现性能倒置问题:也就是优先级低的运行了,优先级高的在那等待。

守护线程(daemon)

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如,后台记录操作日志、监控内存、垃圾回收等待

注意线程的守护线程是守护用户线程的线程,只要用户线程一死,守护线程也得陪葬。可以理解为:用户线程是皇帝,守护线程是皇帝的爱臣,皇帝一死,爱臣陪葬!

  • thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。

  • 在Daemon线程中产生的新线程也是Daemon的。

  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

实例:

package Threads;
// 测试守护线程
// 上帝守护你
public class ThreadDaemon {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();
        Thread thread = new Thread(god);
        thread.setDaemon(true);//默认是false表示是用户线程,正常的都是用户线程
        thread.start();//上帝守护线程启动

        new Thread(you).start(); //用户线程启动
        //可以看到理论上这个上帝线程while(true)是不会停止的,但是设置了守护线程以后,会跟着用户线程一起陪葬
    }
}

//上帝
class God implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("上帝保佑着你");
        }
    }
}

// 你
class You implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("开心的活着");
        }
        System.out.println("开心地嗝屁了,goodbye,world");
    }
}

线程同步(*)

现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如食堂排队打饭,每个人都想吃饭,最天然的解决方法就是,排队,一个个来。

处理多线程问题时,多个线程访问同一个对象(并发问题),并且某些线程还想修改这个对象,这个时候我们就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。

队列和锁

进厕所排队形成队列,但是如果厕所门开着,还是不安全,万一有人冲进来呢,所以还需要锁门。我们需要队列和锁来解决线程不安全的问题,这就叫线程的同步。

线程的同步

由于同一个进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他需要这个资源的线程就必须等待,使用后再释放锁。

但是这样可能会存在以下的问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

线程不安全的三大案例

案例一:买票

package Threads.syn;

//不安全的买票
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket bt=new BuyTicket();
        new Thread(bt,"李四").start();
        new Thread(bt,"张三").start();
        new Thread(bt,"王麻子").start();
        /*
        线程不安全
        张三拿到了id为9的票
        王麻子拿到了id为10的票
        李四拿到了id为8的票
        李四拿到了id为7的票
        张三拿到了id为6的票
        王麻子拿到了id为7的票
        王麻子拿到了id为5的票
        李四拿到了id为3的票
        张三拿到了id为4的票
        张三拿到了id为2的票 买了重复的id的票
        李四拿到了id为2的票
        王麻子拿到了id为1的票
        因为没有让他们排队买票,他们都看到系统中还有id为2的票,所以都买了,这就导致了重复买
         */
    }
}

class BuyTicket implements Runnable {
    //票
    private int ticketNums = 10;
    boolean flag = true;

    @Override
    public void run() {
        //买票
        while (flag) {
            buy();
        }
    }

    public void buy() {
        //判断是否有票
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        // 模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //买票
        System.out.println(Thread.currentThread().getName() + "拿到了id为" + ticketNums--+"的票");
    }
}

案例二:银行取钱

package Threads.syn;

import com.hbq.oop.Polymorphism.A;

//不安全的取钱
//两个人去银行取同一个账户的钱
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100, "结婚基金");
        Drawing you = new Drawing(account, 50, "张三", 0);
        Drawing wife = new Drawing(account, 100, "李四", 0);

        you.start();
        wife.start();

        /*
        结婚基金余额为:50
        结婚基金余额为:50
        张三手里的钱:50
        李四手里的钱:100 无中生钱
         */
    }
}

//账户
class Account {
    int money;//账户余额
    String card;//卡名

    public Account(int money, String card) {
        this.money = money;
        this.card = card;
    }
}

//银行:模拟取款
class Drawing extends Thread {
    Account account;//账户
    int drawingMoney;//取了多少钱
    int curmoney;//现在手里有多少钱

    public Drawing(Account account, int drawingMoney, String name, int curmoney) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
        this.curmoney = curmoney;
    }

    //取钱
    public void run() {
        // 判断钱够不够
        if (account.money - drawingMoney < 0) {
            System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
            return;
        }
        // 模拟延时,sleep可以放大问题的发生性,如果现实中这里发生了时延,就会出问题
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 卡内余额=余额-你取的钱
        account.money = account.money - drawingMoney;
        // 手里的钱
        curmoney = curmoney + drawingMoney;
        System.out.println(account.card + "余额为:" + account.money);
        // Thread.currentThread().getName() 等价于 this.getName(),因为我们是继承Thread类的,有Thread类的getName方法
        System.out.println(this.getName() + "手里的钱:" + curmoney);

    }

}

案例三:列表不安全

package Threads.syn;
import java.util.ArrayList;
import java.util.List;
//线程不安全的集合
public class UnsafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
            },"Thread-"+i).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
        //输出9999,而不是10000,是不安全的
    }
}

出现这些问题的根本原因还是因为在判断能否进行操作的时候,看到的是可以,但是期间被另一个线程修改了对象的数据,导致不满足进行操作的条件,但是此时每个线程还以为可以操作。

同步方法

  • 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制来解决同步问题,这套机制就是synchronized关键字,它包含synchronized方法和synchronized块。
//同步方法
public synchronized void method(int args){}
  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会被阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
  • 缺陷:如果将一个大的方法声明为synchronized将会影响效率。

将之前的买票案例的buy方法中增加synchronized关键字,解决同步问题。

package Threads.syn;
//安全的买票
public class SafeBuyTicketSynMethod {
    public static void main(String[] args) {
        BuyTicket2 bt = new BuyTicket2();
        new Thread(bt, "李四").start();
        new Thread(bt, "张三").start();
        new Thread(bt, "王麻子").start();
        /*
        李四拿到了id为10的票
        王麻子拿到了id为9的票
        张三拿到了id为8的票
        王麻子拿到了id为7的票
        李四拿到了id为6的票
        王麻子拿到了id为5的票
        张三拿到了id为4的票
        王麻子拿到了id为3的票
        李四拿到了id为2的票
        王麻子拿到了id为1的票
         */
    }
}
class BuyTicket2 implements Runnable {
    //票
    private int ticketNums = 10;
    boolean flag = true;

    @Override
    public void run() {
        //买票
        while (flag) {
            buy();
            // 模拟延时
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //增加synchronized同步方法,锁的是this
    private synchronized void buy() {
        //判断是否有票
        if (ticketNums <= 0) {
            flag = false;
            return;
        }
        // 模拟延时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //买票
        System.out.println(Thread.currentThread().getName() + "拿到了id为" + ticketNums-- + "的票");
    }
}

但是这种同步方法默认锁的是this,如果放在银行取钱的案例中,synchronized锁的对象应该是Account,但是将run方法中使用同步方法,锁的是Drawing这个对象,所以不能解决多人取同一个账户的钱的不安全问题。

其中you和wife是两个对象,synchronized同步方法锁run的话只会锁住当前实例对象,也就是分别锁住了you和wife,锁的对象都不同,就没办法实现同步。

同步方法弊端

在这里插入图片描述

方法里面需要修改的内容才需要锁,锁的太多很浪费资源。所以更建议使用同步块。

同步块

  • 同步块:synchronized(Obj){}

  • Obj称之为同步监视器

    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this,就是这个对象本身,或者是class
  • 同步监视器的执行过程

    • 第一个线程访问,锁定同步监视器,执行其中代码
    • 第二个线程访问,发现同步监视器被锁定,无法访问
    • 第一个线程访问完毕,解锁同步监视器
    • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

将之前的银行取钱案例取钱代码放到synchronized同步块里锁住account对象,让多个线程不能同时操作这同一个account对象。

package Threads.syn;
//安全的取钱
//两个人去银行取同一个账户的钱
public class safeBankSyn {
    public static void main(String[] args) {
        Account2 account = new Account2(100, "结婚基金");
        Drawing2 you = new Drawing2(account, 50, "张三", 0);
        Drawing2 wife = new Drawing2(account, 100, "李四", 0);

        you.start();
        wife.start();

        /*
        结婚基金余额为:50
        张三手里的钱:50
        李四钱不够,取不了
         */
    }
}
//账户
class Account2 {
    int money;//账户余额
    String card;//卡名

    public Account2(int money, String card) {
        this.money = money;
        this.card = card;
    }
}
//银行:模拟取款
class Drawing2 extends Thread {
    Account2 account;//账户
    int drawingMoney;//取了多少钱
    int curmoney;//现在手里有多少钱

    public Drawing2(Account2 account, int drawingMoney, String name, int curmoney) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
        this.curmoney = curmoney;
    }
    //取钱
    public void run() {
        // 锁的对象是要变化的量,需要增删改查的对象
        synchronized (account) { //锁住account这个对象,让不同的线程不能同时操作这个对象
            // 判断钱够不够
            if (account.money - drawingMoney < 0) {
                System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
                return;
            }
            // 模拟延时,sleep可以放大问题的发生性
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 卡内余额=余额-你取的钱
            account.money = account.money - drawingMoney;
            // 手里的钱
            curmoney = curmoney + drawingMoney;
            System.out.println(account.card + "余额为:" + account.money);
            // Thread.currentThread().getName() 等价于 this.getName(),因为我们是继承Thread类的,有Thread类的getName方法
            System.out.println(this.getName() + "手里的钱:" + curmoney);
        }
    }
}

同理,不安全的集合,在list添加的时候锁住list对象,也能实现线程同步。

package Threads.syn;
import java.util.ArrayList;
import java.util.List;
//线程安全的集合
public class safeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                synchronized (list){ //加锁就安全了
                    list.add(Thread.currentThread().getName());
                }
            },"Thread-"+i).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
        //10000,是安全的
    }
}

小结:

  • 对于普通同步方法,锁是当前实例对象。 如果有多个实例,那么锁对象必然不同无法实现同步。
  • 对于静态同步方法,锁是当前类的Class对象。有多个实例,但是锁对象是相同的 ,可以完成同步。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。对象最好是只有一个的,如当前类的class是只有一个的,锁的对象相同也能实现同步。

扩充内容CopyOnWriteArrayList

这个类本身就是线程安全的list,可以直接使用。

package Threads.syn;
import java.util.concurrent.CopyOnWriteArrayList;
// 测试JUC安全类型的集合
public class JUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> cowal = new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                cowal.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(cowal.size());
    }
}

死锁

多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

死锁实例:

package Threads.syn;
// 死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
    public static void main(String[] args) {
        MakeUp girl1 = new MakeUp(0, "灰姑娘");
        MakeUp girl2 = new MakeUp(1, "白雪公主");
        girl1.start();
        girl2.start();
    }
}

//口红
class LipStick {}
//镜子
class Mirror {}
class MakeUp extends Thread {
    // 需要的资源只有一份,可以使用static
    static LipStick lipStick = new LipStick();
    static Mirror mirror = new Mirror();

    int choice;//选择
    String girlName;

    public MakeUp(int choice, String girlName) {
        super(girlName);
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        super.run();
//        makeup();
        makeup2();
    }

    //化妆。互相持有对方的锁,就是需要拿到对方的资源,这种方式会导致死锁
    private void makeup() {
        if (choice == 0) {
            synchronized (lipStick) {//获得口红的锁
                System.out.println(this.girlName + "获得口红的锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror) {//一秒后想获得镜子
                    System.out.println(this.girlName + "获得镜子的锁");
                }
            }
        } else {
            synchronized (mirror) {//获得镜子的锁
                System.out.println(this.girlName + "获得镜子的锁");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipStick) {//一秒后想获得口红
                    System.out.println(this.girlName + "获得口红的锁");
                }
            }
        }
    }
    //化妆。修改后破坏死锁的条件,使得不死锁
    private void makeup2() {
        if (choice == 0) {
            synchronized (lipStick) {//获得口红的锁
                System.out.println(this.girlName + "获得口红的锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //破坏请求与保持条件,上面的口红用完后就把口红锁释放,这样另一个人就可以用口红了,然后再用镜子。
            synchronized (mirror) {//一秒后想获得镜子
                System.out.println(this.girlName + "获得镜子的锁");
            }
        } else {
            synchronized (mirror) {//获得镜子的锁
                System.out.println(this.girlName + "获得镜子的锁");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
            synchronized (lipStick) {//一秒后想获得口红
                System.out.println(this.girlName + "获得口红的锁");
            }
        }
    }
}

产生死锁的四个必要条件

  • 互斥条件:一个资源一次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不可剥夺条件:进程获得的资源,在未使用完之前,不能被强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

上述条件只要破坏其中一个以上,就可以避免死锁的发生。

锁(Lock)

从jdk5开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象。

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

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

实例:

package Threads.syn;
import java.util.concurrent.locks.ReentrantLock;
//测试Lock锁
public class LockUse {
    public static void main(String[] args) {
        Lock2 l2 = new Lock2();
        new Thread(l2).start();
        new Thread(l2).start();
        new Thread(l2).start();
    }
}
class Lock2 implements Runnable {
    int ticetNums = 10;
    //定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //加锁
                lock.lock();
                if (ticetNums > 0) {
                    System.out.println(Thread.currentThread().getName()+"获得了:"+ticetNums--);
                } else {
                    break;
                }
            } finally {//建议放在finally里保证要解锁
                //解锁
                lock.unlock();
            }
            try { //抢完票给个时延,避免黄牛疯抢
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

synchronized和Lock的对比

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

线程通信

线程之间有的时候也需要互相传递数据,互相进行通信。

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

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。

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

  • 对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又需要马上通知消费者消费
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费。
  • 在生产者消费者问题中,仅有synchronized是不够的
    • synchronized可阻止并发更新同一个共享资源,实现了同步
    • synchronized不能用来实现不同线程之间消息传递(通信)

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

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

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

解决方法1:并发协作模型“生产者/消费者模式”——>管程法

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

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据,如果数据缓冲区没满,则生产者才可以继续生产。如果数据缓冲区有数据,则消费者才可以继续消费。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nehASKtd-1618110069947)(E:\硕士毕业计划\java学习\生产者消费者.png)]

尝试实战一个生产者消费者的例子,我们看看只使用synchronized会怎么样:

package Threads.syn;
//测试:生产者消费者模型——>利用缓冲区解决:管程法
public class PC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start(); //生产者总共生产了97个产品
        new Customer(container).start(); //消费者总共消费了97个产品
    }
}
//生产者
class Productor extends Thread {
    SynContainer container;
    int counts = 0;//记录自己生产了几个产品

    public Productor(SynContainer container) {
        this.container = container;
    }
    //生产
    public void run() {
        for (int i = 0; i < 100; i++) {
            boolean flag = container.push(new Product(i));
            if (flag) counts++;
            System.out.println("生产了第" + i + "个产品");
        }
        System.out.println("生产者总共生产了" + counts + "个产品");
    }
}
//消费者
class Customer extends Thread {
    SynContainer container;
    int counts = 0;//记录自己消费了几个产品

    public Customer(SynContainer container) {
        this.container = container;
    }
    public void run() {
        for (int i = 0; i < 100; i++) {
            Product flag = container.pop();
            if (flag != null) {
                System.out.println("消费了id为" + flag.id + "的产品");
                counts++;
            }
        }
        System.out.println("消费者总共消费了" + counts + "个产品");
    }
}
// 产品
class Product {
    int id;
    public Product(int id) {
        this.id = id;
    }
}
//缓冲区
class SynContainer {
    Product[] products = new Product[10];
    int count = 0;
    //生产者放入产品
    public synchronized Boolean push(Product product) {
        //如果容器满了,就需要等待消费者消费
        if (count == products.length) {
            return false;
        }
        //如果没有满,就需要丢入产品
        products[count++] = product;

        return true;
        //生产好产品了,通知消费者消费
    }
    //消费者消费产品
    public synchronized Product pop() {
        //判断能否消费
        if (count == 0) {
            return null;
        }
        count--;
        Product product = products[count];
        //消费完了,通知生产者生产
        return product;
    }
}

可以看到,这样会导致最终生产者没有生产够100个商品,只生产了97个商品(每次运行可能都不一样)。

所以仅仅使用synchronized是不够的,还必须使用wait和notify方法。

package Threads.syn;
//测试:生产者消费者模型——>利用缓冲区解决:管程法
public class PC1 {
    public static void main(String[] args) {
        SynContainer1 container = new SynContainer1();
        new Productor1(container).start(); //生产者总共生产了100个产品
        new Customer1(container).start(); //消费者总共消费了100个产品
    }
}
//生产者
class Productor1 extends Thread {
    SynContainer1 container;
    int counts = 0;//记录自己生产了几个产品

    public Productor1(SynContainer1 container) {
        this.container = container;
    }
    //生产
    public void run() {
        for (int i = 0; i < 100; i++) {
            boolean flag = container.push(new Product1(i));
            if (flag) counts++;
            System.out.println("生产了第" + i + "个产品");
        }
        System.out.println("生产者总共生产了" + counts + "个产品");
    }
}
//消费者
class Customer1 extends Thread {
    SynContainer1 container;
    int counts = 0;//记录自己消费了几个产品
    public Customer1(SynContainer1 container) {
        this.container = container;
    }
    public void run() {
        for (int i = 0; i < 100; i++) {
            Product1 flag = container.pop();
            if (flag != null) {
                System.out.println("消费了id为" + flag.id + "的产品");
                counts++;
            }
        }
        System.out.println("消费者总共消费了" + counts + "个产品");
    }
}
// 产品
class Product1 {
    int id;

    public Product1(int id) {
        this.id = id;
    }
}
//缓冲区
class SynContainer1 {
    Product1[] products = new Product1[10];
    int count = 0;
    //生产者放入产品
    public synchronized boolean push(Product1 product) {
        //如果容器满了,就需要等待消费者消费
        if (count == products.length) {
            //通知消费者消费,生产等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                return false;
            }
        }
        //如果没有满,就需要丢入产品
        products[count++] = product;
        //生产好产品了,通知消费者消费
        this.notifyAll();
        return true;
    }
    //消费者消费产品
    public synchronized Product1 pop() {
        //判断能否消费
        if (count == 0) {
            //等待生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                return null;
            }
        }
        count--;
        Product1 product = products[count];
        //消费完了,通知生产者生产
        this.notifyAll();

        return product;
    }
}

注意:这里面的数组其实是有问题的,真正的解决方法应该自己再写一个能固定长度的队列来实现。

解决方法2:并发协作模型“生产者/消费者模式”——>信号灯法

如果信号为true则生产,否则就等待。消费者也一样。这有点像缓存区大小为1的管程法。

package Threads.syn;

//测试生产者消费者问题2:信号灯法,标志位解决
public class PC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        Player p = new Player(tv);//同一台电视播放
        Audience a = new Audience(tv);//同一台电视播放
        new Thread(p).start();
        new Thread(a).start();
    }
}
//生产者——>演员
class Player extends Thread {
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                tv.play("快乐大本营");
            } else {
                tv.play("广告");
            }
        }
    }
}
//消费者——>观众
class Audience extends Thread {
    TV tv;

    public Audience(TV tv) {
        this.tv = tv;
    }

    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}
//产品——>节目
class TV {
    //演员表演,观众等待
    //观众观看,演员等待
    String show;//表演的节目
    boolean flag = true;

    //表演
    public synchronized void play(String show) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了:" + show);
        //通知观众观看
        this.notifyAll();
        this.show = show;
        this.flag = !this.flag;
    }
    //观看
    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了" + show);
        //通知演员表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}

线程池

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

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具。

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
  • 便于线程管理
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

JDK5起提供了线程池相关API:ExecutorServiceExecutors

ExecutorService:真正的线程池接口。常见子类:ThreadPoolExecutor

  • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
  • <T>Future <T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
  • void shutdown():关闭连接池

Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池

实例:

package Threads.syn;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//测试线程池
public class ThreadPool {
    public static void main(String[] args) {
        //1.创建服务,创建线程池
        // newFixedThreadPool 参数:线程池大小
        ExecutorService service = Executors.newFixedThreadPool(3);
        //2.执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        /*
        pool-1-thread-1
        pool-1-thread-3
        pool-1-thread-2
        pool-1-thread-3 //重复利用线程
        pool-1-thread-1
         */
        //3.关闭连接
        service.shutdown();
    }
}
class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Java 中的多线程可以通过创建 Thread 类的实例来实现。具体步骤如下: 1. 创建一个实现 Runnable 接口的类,该类实现 run() 方法。 ```java public class MyRunnable implements Runnable { public void run() { // 执行线程的操作 } } ``` 2. 创建一个 Thread 实例,并将实现了 Runnable 接口的类作为构造函数的参数。 ```java MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); ``` 3. 启动线程。 ```java thread.start(); ``` 此外,还可以通过继承 Thread 类来实现多线程。具体步骤如下: 1. 创建一个继承自 Thread 类的类,重写 run() 方法。 ```java public class MyThread extends Thread { public void run() { // 执行线程的操作 } } ``` 2. 创建 MyThread 的实例。 ```java MyThread myThread = new MyThread(); ``` 3. 启动线程。 ```java myThread.start(); ``` 在多线程的使用中,需要注意线程安全的问题,比如共享变量的访问、同步操作等。可以使用 synchronized 关键字或者 Lock 接口来保证线程安全。 ### 回答2: 在Java中,多线程的使用是指程序同时运行多个线程,每个线程执行自己的任务。Java多线程的使用可以带来以下几点好处: 1. 提高程序的效率:多线程可以对多个任务进行并发处理,提高程序的运行效率。例如,可以将网络请求和UI界面分别放在两个不同的线程中,这样即使网络请求比较耗时,UI界面也能进行响应,不会出现界面假死的情况。 2. 充分利用系统资源:多线程可以充分利用系统的处理器资源,提高系统的利用率。在多核处理器上运行多个线程,可以让每个核心都得到充分利用,提高系统的整体性能。 3. 实现异步编程多线程可以实现异步编程,即一个线程执行后续操作,不需要等待另一个线程的完成。这样可以提高程序的响应速度。例如,可以使用多线程来进行文件下载,下载过程中可以同时进行其他操作。 4. 处理复杂的并发情况:在一些需要处理多个并发操作的场景中,多线程可以提供更好的解决方案。例如,在并发访问共享资源的情况下,使用线程锁可以保证对共享资源的安全访问,避免数据冲突和一致性问题。 Java中使用多线程可以通过创建Thread类的实例或者实现Runnable接口来实现。通过继承Thread类来创建线程,需要重写run方法,在run方法中定义线程要执行的任务。通过实现Runnable接口来创建线程,需要实现run方法,并将实现了Runnable接口的对象作为参数传递给Thread类的构造方法。 总之,Java多线程的使用使得程序可以同时执行多个任务,提高了程序的效率和用户体验,并且能够处理复杂的并发情况。但需要注意多线程的安全性和线程之间的协作,避免出现数据冲突和一致性问题。 ### 回答3: Java多线程的使用是指在一个程序中同时执行多个任务或者同时处理多个请求。多线程可以提高程序的并发性和响应性,可以将耗时的操作和任务分配给不同的线程来执行,从而提高程序的运行效率。 在Java中,多线程的使用主要依靠Thread类或者实现Runnable接口来创建线程。可以通过继承Thread类创建一个线程类,并重写run方法,在run方法中定义需要执行的任务;也可以实现Runnable接口,创建一个Runnable对象,然后将该对象作为参数传递给Thread类的构造方法,创建一个线程对象。 使用多线程的好处是可以充分利用处理器的多核特性,同时进行多个任务,提高程序的运行效率。多线程还可以提高程序的响应性,当程序中有耗时的操作时,可以将其放在一个独立的线程中执行,防止主线程被阻塞,提高用户体验。 然而,多线程的使用也存在一些问题。首先是线程安全问题,多个线程同时访问共享资源可能导致数据不一致或者数据损坏。为解决这个问题,可以使用同步机制,如synchronized关键字或Lock接口,保证在同一时间只有一个线程能够访问共享资源。其次,多线程的创建和销毁会消耗系统资源,如果线程数量过多,可能会影响系统性能。因此,在使用多线程时应该合理控制线程的数量。另外,线程之间的协调和通信也是一个值得关注的问题,可以使用wait、notify、join等方法来实现线程间的协作。 总之,Java多线程的使用可以提高程序的并发性和响应性,但需要注意线程安全、资源消耗以及线程协调和通信等问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值