【Java】多线程入门

Java多线程学习(入门)

前言

目前对于线程的了解仅仅停留在学习python的threading库,很多线程的概念没有真正弄清楚,所以选择来系统性的学习多线程。那么这次选择的是Java的多线程学习,等学完了分析一下Java和python使用多线程和底层实现的区别吧!

跟着【狂神说Java】多线程详解 学习的,笔记和代码跟着敲的,方便自己之后复习。

1、进程与线程

首先,我们做个简单的比喻:进程 = 火车,线程 = 车厢。

所以我们可以得到一个前提:线程是在进程下进行的

然后有以下特点:

  • 一个进程中可以有多线程

  • 不同进程很难共享数据

  • 相同进程的不同线程共享数据非常简单

  • 进程不会相互影响,但是一个线程挂了就导致整个进程挂了

  • 进程是资源分配的最小单位,线程是CPU调度的最小单位

  • 进程要比线程消耗更多的计算机资源

2、线程的创建

1.第一种方式创建线程

  • 用以下三个步骤来创建线程
  1. 继承Thread类
  2. 重写run方法
  3. 调用start开启线程

线程开启不一定立即执行,由cpu调度开启执行。

并且由于我们大部分电脑使用的都是单核的,所以实际上多线程就是多个线程交替执行,而非同时执行

我们看一个例子

// 继承Thread类
// 重写run方法
// 调用start开启线程
public class TestThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 20; i++) {
    
    
            System.out.println("114514"+ "--" + i) ;
        }
    }

    // main线程,主线程
    public static void main(String[] args) {
    
    
        TestThread testThread = new TestThread();
        testThread.start();
        for(int i = 0; i < 20; i++) {
    
    
            System.out.println("1919810" + "--" + i);
        }
    }
}

我们使用start方法来开启线程

得到的结果是

1919810–0
114514–0
1919810–1
114514–1
1919810–2
114514–2
1919810–3
1919810–4
1919810–5
114514–3
1919810–6
1919810–7
1919810–8
1919810–9
1919810–10
1919810–11
1919810–12
114514–4
1919810–13
114514–5
1919810–14
114514–6
1919810–15
114514–7
1919810–16
114514–8
1919810–17
114514–9
1919810–18
114514–10
114514–11
114514–12
114514–13
114514–14
114514–15
114514–16
114514–17
114514–18
114514–19
1919810–19

进程已结束,退出代码为 0

可以看到114514和1919810是在交替执行的

那么如果我们把start方法改为我们重写的run方法呢?

testThread.start();改为如下

testThread.run();

结果如下

114514–0
114514–1
114514–2
114514–3
114514–4
114514–5
114514–6
114514–7
114514–8
114514–9
114514–10
114514–11
114514–12
114514–13
114514–14
114514–15
114514–16
114514–17
114514–18
114514–19
1919810–0
1919810–1
1919810–2
1919810–3
1919810–4
1919810–5
1919810–6
1919810–7
1919810–8
1919810–9
1919810–10
1919810–11
1919810–12
1919810–13
1919810–14
1919810–15
1919810–16
1919810–17
1919810–18
1919810–19

进程已结束,退出代码为 0

这个明显就是单线程,将我们重写的run方法跑完再执行下面的for循环。

所以我们必须按照上面的三步走,我们重写完了run方法之后,使用start方法就是执行run方法中的代码,只不过是使用了另一个线程运行而已。

2.多线程图片下载测试

那么在知晓了如何创建线程之后,我们用多个线程来下载网络上的图片来测试线程

package com.woodwhale.demo01;

import org.apache.commons.io.FileUtils;

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

// 练习Thread,实现多线程同步下载图片
public class TestThread2 extends Thread{
    
    
    private String url;
    private String name;
    public TestThread2(String url, String name) {
    
    
        this.name = name;
        this.url = url;
    }
    // run方法下载
    @Override
    public void run() {
    
    
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下载了"+name);
    }

    public static void main(String[] args) {
    
    
        TestThread2 t1 = new TestThread2("https://api.woodwhale.top/random.php","1.jpg");
        TestThread2 t2 = new TestThread2("https://api.woodwhale.top/random.php","2.jpg");
        TestThread2 t3 = new TestThread2("https://api.woodwhale.top/random.php","3.jpg");
        t1.start();
        t2.start();
        t3.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.out.println("IO异常");
        }
    }
}

运行结果如下:

下载了3.jpg
下载了1.jpg
下载了2.jpg

进程已结束,退出代码为 0

再看看我们的目录,多了三张图片

image-20210719225752023

说明我们下载成功了!

而且从下载完的顺序来看,多线程是连续交替的,而非从1到3的连续下载。所以线程并非立刻执行,而是由cpu调度的!

3.第二种方式创建线程

用以下三个步骤来实现线程创建

  • 定义MyRunnable类实现Runnable接口
  • 实现run()方法 ,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
package com.woodwhale.demo01;


// 创建线程方式2,runnable接口
// 实现run方法
// 执行线程丢入runnable接口实现类,调用start方法
public class Test1 implements Runnable{
    
    

    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("114514!" + i);
        }
    }

    public static void main(String[] args) {
    
    
        Test1 test = new Test1();

//        Thread thread = new Thread(test);
//        thread.start();

        new Thread(test).start();

        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("1919810!" + i);
        }
    }
}

对于多线程的使用,我们推荐使用第二种方法——runnable接口

3、Callable接口

我们用callable接口来写一个多线程实现下载图片

callable接口构建的四个步骤

  1. 创建执行服务
  2. 提交执行
  3. 获取结果
  4. 关闭服务

callable接口和runnable接口的区别就是:

  • 可以定义返回值
  • 可以捕获异常
package com.woodwhale.demo02;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

// callable的好处
// 1、可以定义返回值
// 2、可以捕获异常

public class TestCallable implements Callable<Boolean> {
    
    

    private String url;
    private String name;

    public TestCallable(String url ,String name) {
    
    
        this.name = name;
        this.url = url;
    }
    @Override
    public Boolean call() throws Exception {
    
    
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载文件"+name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        TestCallable t1 = new TestCallable("https://cdn.jsdelivr.net/gh/Awoodwhale/photos/img/NTE5MTI0MzA0ODU1MDY3NTI4OF8xNjI2MDE0OTkzMzIx_4.jpg","1.jpg");
        TestCallable t2 = new TestCallable("https://cdn.jsdelivr.net/gh/Awoodwhale/photos/img/556062.jpg","2.jpg");
        TestCallable t3 = new TestCallable("https://cdn.jsdelivr.net/gh/Awoodwhale/photos/img/910923.png","3.jpg");

        // 创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3);

        // 提交执行
        Future<Boolean> r1 = ser.submit(t1);
        Future<Boolean> r2 = ser.submit(t2);
        Future<Boolean> r3 = ser.submit(t3);

        // 获取结果
        boolean rs1 = r1.get();
        boolean rs2 = r2.get();
        boolean rs3 = r3.get();

        // 关闭服务
        ser.shutdown();

    }

    static class WebDownloader{
    
    
        public void downloader(String url, String name) {
    
    
            try {
    
    
                FileUtils.copyURLToFile(new URL(url), new File(name));
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

4、静态代理

静态代理模式:

  • 真实对象和代理对象都要实现同一接口

  • 代理对象要代理真实角色

好处:

  • 代理对象可以做很多真实对象做不了的时期

  • 真实对象仅仅做自己的事情就好了

所以我们的Thread类就是一个静态代理

new Thread(() -> System.out.println(“114”)).start();

Thread就是代理,实现了runnalbe接口,这个输出语句真实对象

如果这个输出语句直接使用runnable接口也能完成自己的多线程任务,但是如果使用了静态代理Thread类,就可以使用代理完成更多事情

5、lambda表达式

  • 避免匿名内部类定义过多
  • 属于函数式编程

使用lambda表达式的前提是函数式接口

函数式接口的定义:只包含唯一一个抽象方法

我们多线程使用的runnable接口就是一个函数式接口

public interface Runnable{
    
    
    public abstract void run();
}

我们可以通过5种方法来写下面这段

package com.woodwhale.demo02;

public class TestLamda {
    
    
    // 3、静态内部类
    static class Like2 implements ILike{
    
    
        @Override
        public void lambda() {
    
    
            System.out.println("i like lambda2");
        }
    }

    public static void main(String[] args) {
    
    
        // 实现类
        ILike like = new Like();
        like.lambda();

        // 静态内部类
        like = new Like2();
        like.lambda();

        // 4、局部内部类
        class Like3 implements ILike{
    
    
            @Override
            public void lambda() {
    
    
                System.out.println("i like lambda3");
            }
        }
        like = new Like3();
        like.lambda();

        // 5、匿名内部类。没有类的名称,必须借助接口或者父类实现
        like = new ILike() {
    
    
            @Override
            public void lambda() {
    
    
                System.out.println("i like lambda4");
            }
        };
        like.lambda();

        // 6、用lambda简化
        like = ()->{
    
    
            System.out.println("i like lambda5");
        };
        like.lambda();

    }
}

// 1、定一个函数式接口
interface ILike{
    
    
    void lambda();
}

// 2、实现类
class Like implements ILike{
    
    
    @Override
    public void lambda() {
    
    
        System.out.println("i like lamda");
    }
}

我们把其中的lambda表达式拿出来看:

// 6、用lambda简化
        like = ()->{
    
    
            System.out.println("i like lambda5");
        };
        like.lambda();

上面的就是lambda表达式,因为前面实例化了like对象,所以可以直接写

我们再来看lambda的简化

package com.woodwhale.demo02;

interface ILove{
    
    
    void love(int a);
}

public class TestLambda2 {
    
    

    public static void main(String[] args) {
    
    
        ILove love = (int a)->{
    
    
            System.out.println("i love you" + a);
        };
        love.love(520);


    }
}

这个是一个类

ILove love = (int a)->{
    
    
            System.out.println("i love you" + a);
        };
        love.love(520);

可以简化为(去掉参数类型 )

ILove love = (a)->{
    
    
            System.out.println("i love you" + a);
        };
        love.love(520);

还能简化为(去掉括号)(如果多个参数则不能去掉参数括号)

ILove love = a->{
    
    
            System.out.println("i love you" + a);
        };
        love.love(520);

如果只有一行代码,还可以简化为(去掉大括号)

ILove love = a->System.out.println("i love you" + a);
love.love(520);

6、线程状态

这里用狂神说的多线程的课程中的图

image-20210726195416744

1.线程停止

不推荐使用jdk自带的stop();等方法,已经过时并且废弃了

推荐线程自己停下来

建议使用一个标志位进行终止变量:flag = true 那么终止线程

据个例子

package com.woodwhale.start;

import com.woodwhale.demo02.TestCallable;

// 测试停止线程
// 1、建议线程正常停止--->利用次数,不建议死循环
// 2、建议使用flag
// 3、不要使用stop(); jdk不建议使用的方法
public class TestStop implements Runnable{
    
    

    // 1、设置flag
    private boolean flag = true;

    // 2、重写run方法
    @Override
    public void run() {
    
    
        int i = 0;
        while (flag) {
    
    
            System.out.println("run ... Thread" + i++);
        }
    }

    // 3、设置一个公开的方法停止线程
    public void stop() {
    
    
        this.flag = false;
    }

    // 4、测试
    public static void main(String[] args) {
    
    
        TestStop testStop = new TestStop();
        new Thread(testStop).start();
        // 1s之后设置线程停止
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        testStop.stop();
        System.out.println("线程停止!");
    }
}

image-20210726201202589

2.线程休眠

Thread.sleep();方法

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

模拟倒计时

package com.woodwhale.start;

// 模拟倒计时
public class TestSleep {
    
    
    public static void tenDown() throws InterruptedException {
    
    
        int num = 10;
        do {
    
    
            System.out.println(num--);
            Thread.sleep(1000);
        } while (num > 0);
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        tenDown();
    }

}

image-20210726202101828

当然在我们程序运行时,我们有的时候需要模拟网络延迟,这个时候我们也可以使用Thread.sleep()方法

3.线程礼让

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

礼让使用yield()方法

举个例子:

package com.woodwhale.start;


// 测试礼让线程
// 礼让不一定成功
public class TestYield {
    
    
    public static void main(String[] args) {
    
    
        MyYield yield = new MyYield();
        new Thread(yield,"a").start();
        new Thread(yield,"b").start();
    }
}

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

出现这种结果就是礼让成功了

image-20210727103135903

4.join

  • join合并线程,等待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 可以想象成插队
package com.woodwhale.start;

import javax.swing.plaf.TableHeaderUI;

// 测试join方法
public class TestJoin 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 {
    
    
        // 启动线程
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        for (int i = 0; i < 1000; i++) {
    
    
            if (i == 200) {
    
    
                thread.join();
            }
            System.out.println("main"+i);
        }

    }
}

以上的例子,当main线程到了200,就让run线程运行完1000次,再让main线程完成

5.线程状态观测

Thread.State

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

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

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

死亡之后的线程不能再次启动

我们举个例子来观测线程:

package com.woodwhale.start;

public class TestState {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(()->{
    
    
            for (int i = 0; i < 5; i++) {
    
    
                try {
    
    
                    Thread.sleep(100);
                } 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);  // Run

        // 只要线程不终止,就一直输出
        while(state != Thread.State.TERMINATED) {
    
    
            Thread.sleep(100);
            state = thread.getState();  // 更新线程状态
            System.out.println(state);
        }
    }
}

image-20210727115039076

6.线程优先级

  • java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
  • 线程的优先级由数字来表示,范围1-10,1最低,10最高
  • 使用getPriority().setPoriority(int x)方法来改变或获取优先级
package com.woodwhale.start;

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

        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);

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

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

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

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

    }
}

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

及时设置了10的优先级,该线程也不一定第一个开始。这个完全看cpu的操作。我们设置线程优先级仅仅是增加概率而已!

7.守护线程

  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不需要等待守护线程执行完毕
  • 守护线程举例:
    • 后台操作日志
    • 监控内存
    • 垃圾回收

设置守护线程

Thread thread = new Thread(god);
        thread.setDaemon(true);

代码举例

package com.woodwhale.start;

public class TestDaemon {
    
    
    public static void main(String[] args) {
    
    
        God god = new God();
        You you = new You();

        Thread thread = new Thread(god);
        thread.setDaemon(true);

        thread.start();
        new Thread(you).start();
    }
}

class God implements Runnable{
    
    
    @Override
    public void run() {
    
    
        while(true) {
    
    
            System.out.println("stay with you");
        }
    }
}

class You implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 365; i++) {
    
    
            System.out.println("stay alive");
        }
        System.out.println("happy ending");
    }
}

当人过了365天之后,原本应该不停止的while(true),因为是守护线程,也随之结束

7、线程同步

1.并法问题的引入

当我们使用多线程处理一个对象时,由于线程过快的操作,如果不上一把锁,可能会造成变量冲突的错误

拿“抢票”据个例子,如果多线程处理抢票操作,不上锁,可能会造成不同的人抢到同一张票!

现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,最简单的解决问题方法就是——排队

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这个时候我们就需要线程同步

线程同步就是一种等待机制,多个需要同时访问的此对象的线程进入这个对象的等待池,形成队列。等待前面线程的使用完毕,下一个线程再次使用!

2.不安全线程的举例

(1)不安全的买票

package syn;

// 线程不安全,可能会出现负数
public class UnsafeBuyTicket {
    
    
    public static void main(String[] args) {
    
    
        BuyTicket station = new BuyTicket();
        new Thread(station,"woodwhale").start();
        new Thread(station,"wyh").start();
        new Thread(station,"wcx").start();
    }
}

class BuyTicket implements Runnable{
    
    
    // 票
    private int ticketNum = 10;
    // 外部停止方式flag
    private boolean flag = true;

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

    private void buy() {
    
    
        // 判断是否有票
        if (ticketNum <= 0) {
    
    
            flag = false;
            return;
        }
        // 买票
        System.out.println(Thread.currentThread().getName()+"拿到"+ticketNum--);
    }
}

上面这一段代码模拟了线程不安全的买票,3个人抢10张票

当票为最后一张时,如果三个线程同时抢这一张票,可能会出现票数为负数的情况

image-20210727204053874

(2)不安全的银行

package syn;

import jdk.swing.interop.DispatcherWrapper;

// 不安全的银行
// 两个人都去银行取钱
public class UnsafeBank {
    
    
    public static void main(String[] args) {
    
    
        // 账户
        Account account = new Account(100,"MarryMoney");
        Drawing you = new Drawing(account,50,"woodwhale");
        Drawing youGirlfriend = new Drawing(account,100,"girlfriend");
        you.start();
        youGirlfriend.start();
    }
}

// 账户
class Account{
    
    
    int money;
    String username;
    public Account(int money, String username) {
    
    
        this.money = money;
        this.username = username;
    }
}

// 银行:模拟取款
class Drawing extends Thread{
    
    
    Account account;    // 账户
    int drawingMoney;   // 取了多少钱
    int nowMoney;   // 现在手中的钱
    public Drawing(Account account, int drawingMoney, String name) {
    
    
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    // 取钱
    @Override
    public void run() {
    
    
        // 判断有无钱
        if (account.money - drawingMoney < 0) {
    
    
            System.out.println(Thread.currentThread().getName()+"钱不够,取不了");
            return;
        }
        // 模拟延迟,放大发生的可能性
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        // 卡内余额 = 余额 - 取出的钱
        account.money = account.money - drawingMoney;
        // 手里的钱
        nowMoney = nowMoney + drawingMoney;

        System.out.println(account.username+"余额为:"+account.money);
        System.out.println(this.getName()+"手里的钱:"+nowMoney);
    }
}

以上代码模拟了银行同一账户两个账户角色同时取钱。

如果一个账户只有100元,而两个线程分别取50和100,可能会出现账户钱为负数的情况

image-20210727190321946

(3)不安全的数组

package syn;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {
    
    
    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
    
    
            new Thread(()->{
    
    
                list.add(Thread.currentThread().getName());
            }).start();
        }
        System.out.println(list.size());
    }
}

我们使用多线程来给ArrayList数组增加元素时,非常可能发生添加失败的情况

比如这里,我们本来要加10000个,而实际只有9990个

image-20210727190519496

3.synchronized

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

synchronized有三种应用方式:

  1. 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
  2. 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
  3. 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

举个例子,比如我们上面写的不安全的ArrayList多现称添加,我们使用synchronized同步方法块就可以解决

package 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(()->{
    
    
                synchronized (list) {
    
    
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        System.out.println(list.size());
    }
}

image-20210727194937381

我们用synchronized监视的对象就是我们需要增删改的对象

4.死锁

某一个同步块同时拥有“两个以上对象的锁”,就可能发生“死锁”的问题

死锁,就是两个或者多个线程在等待对方释放资源,都停止执行的情况

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

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

我们举一个例子

package syn;

import java.nio.channels.MembershipKey;

// 死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
    
    
    public static void main(String[] args) {
    
    
        MakeUp g1 = new MakeUp(0,"wcx");
        MakeUp g2 = new MakeUp(1,"xxx");
        g1.start();
        g2.start();
    }
}

// 口红
class  Lipstick{
    
    

}

// 镜子
class  Mirror{
    
    

}

class MakeUp extends Thread{
    
    
    // 需要的资源只有一份,用static保证只有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice; // 选择
    String girlName;    // 使用化妆品的人

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

    // 化妆,互相持有对方的锁,就是需要拿到对方的资源
    private void makeup() throws InterruptedException {
    
    
        if (choice == 0) {
    
    
            synchronized (lipstick) {
    
    
                // 获得口红的锁
                System.out.println(this.girlName+"获得口红的锁");
                Thread.sleep(1000);

                synchronized (mirror) {
    
    
                    // 1s之后想获得镜子
                    System.out.println(this.girlName+"获得镜子的锁");
                }
            }
        }else {
    
    
            synchronized (mirror) {
    
    
                System.out.println(this.girlName+"获得镜子的锁");
                Thread.sleep(2000);
                synchronized (lipstick) {
    
    
                    System.out.println(this.girlName+"获得口红的锁");
                }
            }
        }
    }

    @Override
    public void run() {
    
    
        try {
    
    
            makeup();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

读这段代码,我们发现,这里的两个女生一个拿了镜子的锁,一个拿了口红的锁,形成了死锁

进程就会一直卡住

image-20210727202437818

解决方法就是,将两个锁分开

 if (choice == 0) {
    
    
            synchronized (lipstick) {
    
    
                // 获得口红的锁
                System.out.println(this.girlName+"获得口红的锁");
                Thread.sleep(1000);
            }
            synchronized (mirror) {
    
    
                // 1s之后想获得镜子
                System.out.println(this.girlName+"获得镜子的锁");
            }
        }else {
    
    
            synchronized (mirror) {
    
    
                System.out.println(this.girlName+"获得镜子的锁");
                Thread.sleep(2000);
            }
            synchronized (lipstick) {
    
    
                System.out.println(this.girlName+"获得口红的锁");
            }
        }

这样就不会形成死锁了

image-20210727202601827

5.Lock锁

这里放上狂神老师的课件解释

image-20210727202747743

我们举个例子

package syn;

import java.util.concurrent.locks.ReentrantLock;

public class TestLock {
    
    
    public static void main(String[] args) {
    
    
        TestLock2 testLock2 = new TestLock2();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }
}

class TestLock2 implements Runnable {
    
    

    int ticketNum = 10;
    // 定义lock锁
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
    
    
        while (true) {
    
    
            lock.lock();    // 加锁
            try{
    
    
                if (ticketNum > 0) {
    
    
                    try {
    
    
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(ticketNum--);
                }else {
    
    
                    break;
                }
            }finally {
    
    
                lock.unlock();  // 解锁
            }
        }
    }
}

这样买票就是一个个排队买票了,不会出现票为负数的情况

6.synchronized与Lock的对比

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

8、线程协作

生产者和消费者问题:

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

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

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

  • synchronized可以阻止并法更新同一个共享资源,实现了同步
  • 但是synchronized不能用来实现不同线程之间的消息传递

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

image-20210727211311921

1.解决方法一:管程法

  • 生产者:负责生产数据的模块
  • 消费者:负责处理数据的模块
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”
  • 生产者将生产好的数据放入缓冲区,消费者从缓冲区中拿出数据
  • image-20210727205739380
package connect;


// 测试:生产者消费者模型,利用管程法解决
// 生产者、消费者、产品、缓冲区
public class TestPC {
    
    

    public static void main(String[] args) {
    
    
        SynContainer container = new SynContainer();
        new Producer(container).start();
        new Consumer(container).start();
    }
}

// 生产者
class Producer extends Thread {
    
    
    SynContainer container;

    public Producer(SynContainer container) {
    
    
        this.container = container;
    }

    // 生产

    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("生产了"+i+"只鸡");
            container.push(new Chicken(i));
        }
    }
}

// 消费者
class Consumer extends Thread {
    
    
    SynContainer container;

    public Consumer(SynContainer container) {
    
    
        this.container = container;
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("消费了"+container.pop().id+"只鸡");
        }
    }
}

// 产品
class Chicken {
    
    
    int id;     // 产品编号
    public Chicken(int id) {
    
    
        this.id = id;
    }
}

// 缓冲区
class SynContainer{
    
    
    // 需要一个容器
    Chicken[] chickens = new Chicken[10];

    // 容器计数器
    int count = 0;

    // 生产者放入产品
    public synchronized void push(Chicken chicken) {
    
    
        // 如果容器满了需要等待消费者消费
        while (count == chickens.length) {
    
    
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        // 如果没满需要丢入产品
        chickens[count++] = chicken;
        // 可以通知消费者消费了
        this.notify();
    }

    public synchronized Chicken pop() {
    
    
        // 判断能否消费
        while (count == 0) {
    
    
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        // 如果可以消费
        Chicken chicken = chickens[--count];

        // 吃完了通知生产者生产
        this.notify();
        return chicken;
    }
}

2.解决方法二:信号灯法

信号号灯法,可以理解为缓存区为1的管程法。

我们使用信号灯(flag)来判断什么时候wait,什么时候notify

package connect;

// 测试生产者消费者:信号灯法,标志位解决
public class TestPC2 {
    
    
    public static void main(String[] args) {
    
    
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

// 生产者-》演员
class Player extends Thread{
    
    
    TV tv;
    public Player(TV tv) {
    
    
        this.tv = tv;
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 20; i++) {
    
    
            if (i % 2 == 0) {
    
    
                this.tv.play("快乐大本营");
            }else {
    
    
                this.tv.play("天天向上");
            }
        }
    }
}

// 消费者-》观众
class  Watcher extends Thread{
    
    
    TV tv;
    public Watcher(TV tv) {
    
    
        this.tv = tv;
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 20; i++) {
    
    
            tv.watch();
        }
    }
}

class TV{
    
    
    String voice;
    boolean flag = true;

    // 表演
    public synchronized void play(String voice) {
    
    
        if (!flag) {
    
    
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

        System.out.println("演员表演了:"+voice);
        // 通知观众观看
        this.notifyAll();

        this.voice = voice;
        this.flag = !this.flag;
    }

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

9、线程池

  • 背景:经常创建、销毁,会特别消耗资源,在并法情况下,对性能影响很大
  • 思路:提前创建好多个线程,放入线程池,使用时候直接获取,使用完毕放回池子中。可以避免频繁创建、销毁,实现了重复利用。类似于生活中的交通工具

java中的utils类中有写好的线程池工具,进阶的知识就去看看JUC并发编程吧。本人还没深入。

package connect;

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

public class TestPool {
    
    


    public static void main(String[] args) {
    
    
        // 创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        // 关闭服务
        service.shutdown();
    }
}

class MyThread implements Runnable{
    
    
    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName());
    }
}

猜你喜欢

转载自blog.csdn.net/woodwhale/article/details/119177069