JAVA多线程详解(超详细)


一、线程简介

1、进程、线程

  • 程序开发写的代码称之为程序。程序就是一堆代码一组数据和指令集是一个静态的概念。
  • 进程(Process)将程序运行起来我们称之为进程。进程是执行程序的一次执行过程它是动态的概念。进程存在生命周期也就是说程序随着程序的终止而销毁。进程之间是通过TCP/IP端口实现交互的。
  • 线程(Thread)线程是进程中的实际运作的单位是进程的一条流水线是程序的实际执行者是最小的执行单位。通常在一个进程中可以包含若干个线程当然一个进程中至少有一个线程。线程是CPU调度和执行的最小单位。
    注意很多多线程都是模拟出来的真正的多线程是指有多个CPU即多核如服务器如果是模拟出来的多线程即一个CPU的情况下在同一个时间点CPU只能执行一个代码因为切换的很快所以就有同时执行的错觉。

2、并发、并行、串行

  • 并发同一个对象被多个线程同时操作。这是一种假并行。即一个CPU的情况下在同一个时间点CPU只能执行一个代码因为切换的很快所以就有同时执行的错觉。
  • 并行多个任务同时进行。并行必须有多核才能实现否则只能是并发。
  • 串行一个程序处理完当前进程按照顺序接着处理下一个进程一个接着一个进行。

3、进程的三态

进程在运行的过程中不断的改变其运行状态。通常一个运行的进程必须有三个状态就绪态、运行态、阻塞态。

  • 就绪态当进程获取出CPU外所有的资源后只要再获得CPU就能执行程序这时的状态叫做就绪态。在一个系统中处于就绪态的进程会有多个通常把这些排成一个队列这个就叫就绪队列。
  • 运行态当进程已获得CPU操作权限正在运行这个时间就是运行态。在单核系统中同一个时间只能有一个运行态多核系统中会有多个运行态。
  • 阻塞态正在执行的进程等待某个事件而无法继续运行时便被系统剥夺了CPU的操作权限这时就是阻塞态。引起阻塞的原因有很多比如等待I/O操作、被更高的优先级的进程剥夺了CPU权限等。

二、线程实现

1、继承Thread类

 步骤
 - 自定义线程类继承Thread类
 - 重写run()方法编写线程执行体
 - 创建线程对象调用start()方法启动线程启动后不一定立即执行抢到CPU资源才能执行

代码如下示例

// 自定义线程对象继承Thread重写run()方法
public class MyThread extends Thread {

    public MyThread(String name){
        super(name);
    }

    @Override
    public void run() {
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }
    }

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

        // 创建线程实现类对象
        MyThread thread = new MyThread("线程1");
        MyThread thread2 = new MyThread("线程2");
        // 调用start()方法启动线程
        thread.start();
        thread2.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("我是主线程--" + i);
        }
    }
}
执行结果
我是自定义线程2--0
我是自定义线程2--1
我是主线程--0
我是自定义线程1--0
我是主线程--1
我是主线程--2
我是自定义线程2--2
我是主线程--3
我是自定义线程1--1
我是主线程--4
我是主线程--5
我是主线程--6
我是主线程--7
我是主线程--8
我是主线程--9
我是自定义线程2--3
我是自定义线程1--2
我是自定义线程2--4
我是自定义线程1--3
我是自定义线程1--4
我是自定义线程1--5
我是自定义线程1--6
我是自定义线程1--7
我是自定义线程1--8
我是自定义线程1--9
我是自定义线程2--5
我是自定义线程2--6
我是自定义线程2--7
我是自定义线程2--8
我是自定义线程2--9

2、实现Runnable接口

 步骤
 - 自定义线程类实现Runnable接口
 - 实现run()方法编写线程体
 - 创建线程对象调用start()方法启动线程启动后不一定立即执行抢到CPU资源才能执行

代码如下示例


// 自定义线程对象实现Runnable接口重写run()方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }
    }

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

        // 创建实现类对象
        MyRunnable myRunnable = new MyRunnable();
        // 创建代理类对象
        Thread thread = new Thread(myRunnable,"线程1");
        Thread thread2 = new Thread(myRunnable,"线程2");
        // 调用start()方法启动线程
        thread.start();
        thread2.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("我是主线程--" + i);
        }
    }
}

执行结果

我是主线程--0
我是自定义线程1--0
我是自定义线程2--0
我是自定义线程1--1
我是主线程--1
我是自定义线程1--2
我是自定义线程2--1
我是自定义线程1--3
我是主线程--2
我是主线程--3
我是自定义线程1--4
我是自定义线程2--2
我是自定义线程2--3
我是自定义线程2--4
我是自定义线程1--5
我是自定义线程1--6
我是主线程--4
我是自定义线程1--7
我是自定义线程1--8
我是自定义线程1--9
我是自定义线程2--5
我是自定义线程2--6
我是自定义线程2--7
我是自定义线程2--8
我是主线程--5
我是自定义线程2--9
我是主线程--6
我是主线程--7
我是主线程--8
我是主线程--9

3、实现Callable接口不常用

 步骤
 - 实现Callable接口先要返回值类型
 - 重写call()方法需要抛出异常
 - 创建目标对象
 - 创建执行服务ExecutorService ser = Executor.newFixedThreadPool(1);
 - 提交执行Future<Boolean> res = ser.submit(t1);
 - 获取结果boolean r1 = res.get();
 - 关闭服务ser.shutdownNow();

代码如下示例

import java.util.concurrent.*;

// 自定义线程对象实现Callable接口重写call()方法
public class MyThread implements Callable<Boolean> {

    @Override
    public Boolean call() throws Exception {
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }

        return true;
    }

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

        // 创建线程实现类对象
        MyThread thread = new MyThread();
        MyThread thread2 = new MyThread();

        // 创建执行服务参数是线程池线程数量
        ExecutorService ser = Executors.newFixedThreadPool(2);
        // 提交执行
        Future<Boolean> res = ser.submit(thread);
        Future<Boolean> res2 = ser.submit(thread2);
        // 获取结果
        boolean r1 = res.get();
        boolean r2 = res2.get();
        // 关闭服务
        ser.shutdownNow();
    }
}
执行结果
我是自定义pool-1-thread-1--0
我是自定义pool-1-thread-2--0
我是自定义pool-1-thread-1--1
我是自定义pool-1-thread-1--2
我是自定义pool-1-thread-1--3
我是自定义pool-1-thread-1--4
我是自定义pool-1-thread-1--5
我是自定义pool-1-thread-2--1
我是自定义pool-1-thread-1--6
我是自定义pool-1-thread-2--2
我是自定义pool-1-thread-2--3
我是自定义pool-1-thread-2--4
我是自定义pool-1-thread-2--5
我是自定义pool-1-thread-2--6
我是自定义pool-1-thread-2--7
我是自定义pool-1-thread-2--8
我是自定义pool-1-thread-2--9
我是自定义pool-1-thread-1--7
我是自定义pool-1-thread-1--8
我是自定义pool-1-thread-1--9

三、线程常用方法

1、线程的状态

  • 新建状态NEW线程已创建尚未调用start()方法启动之前。
  • 运行状态RUNNABLE线程对象被创建后调用该对象的start()方法并获取CPU权限进行执行。
  • 阻塞状态BLOCKED线程在获取synchronized同步锁失败(因为锁被其它线程所占用)它会进入同步阻塞状态。
  • 等待状态WAITING 等待状态。正在等待另一个线程执行特定动作来唤醒该线程的状态。
  • 超时等待状态TIME_WAITING有明确结束时间的等待状态。
  • 终止状态TERMINATED 当线程结束完成之后就会变成此状态。
    线程状态图

2、线程常用方法

1、 sleep(Long time)方法
 - 让线程阻塞的指定的毫秒数。
 - 指定的时间到了后线程进入就绪状态。
 - sleep可研模拟网络延时倒计时等。
 - 每一个对象都有一个锁sleep不会释放锁。

代码如下示例

public class MyThread implements Runnable {

    @Override
    public void run() {
        // 模拟倒计时
        for (int i = 10; i >= 0; i--) {
            try {
                System.out.println(i);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
    }

}
执行结果
10
9
8
7
6
5
4
3
2
1
0
2、yield()方法
 - 提出申请释放CPU资源至于能否成功释放取决于JVM决定。
 - 调用yield()方法后线程仍然处于RUNNABLE状态线程不会进入阻塞状态。
 - 调用yield()方法后线程处于RUNNABLE状态就保留了随时被调用的权利。

代码如下示例

public class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程开始执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName() + "线程结束执行");
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"a");
        Thread thread2 = new Thread(myThread,"b");
        thread.start();
        thread2.start();
    }

}
执行结果从结果1看a释放CPU成功后b就抢到了CPU执行权接着b也释放CPU成功a抢到了CPU执行权从结果2看a并没有成功释放CPU。
结果1
a线程开始执行
b线程开始执行
a线程结束执行
b线程结束执行
结果2
a线程开始执行
a线程结束执行
b线程开始执行
b线程结束执行
3、join()方法
 - 将当前的线程挂起当前线程阻塞待其他的线程执行完毕当前线程才能执行。
 - 可以把join()方法理解为插队谁插到前面谁先执行。

代码如下示例

public class MyThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "join()线程执行" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"a");
        thread.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程执行" + i);
            if (i == 2) {
                thread.join(); //主线程阻塞等待thread一口气执行完主线程才能继续执行
            }
        }
    }

}
执行结果
主线程执行0
a线程join()执行0
主线程执行1
主线程执行2
a线程join()执行1
a线程join()执行2
a线程join()执行3
a线程join()执行4
a线程join()执行5
a线程join()执行6
a线程join()执行7
a线程join()执行8
a线程join()执行9
主线程执行3
主线程执行4
主线程执行5
主线程执行6
主线程执行7
主线程执行8
主线程执行9
4、setPriority (int newPriority)、getPriority()
- 改变、获取线程的优先级。
- Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程线程调度器按照优先级决定应该调度哪个线程来执行。
- 线程的优先级用数据表示范围1~10。
- 线程的优先级高只是表示他的权重大获取CPU执行权的几率大。
- 先设置线程的优先级在执行start()方法。

代码如下示例

public class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程优先级"
            + Thread.currentThread().getPriority());
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"a");
        Thread thread2 = new Thread(myThread,"b");
        Thread thread3= new Thread(myThread,"c");
        Thread thread4= new Thread(myThread,"d");
        thread3.setPriority(Thread.MAX_PRIORITY);
        thread.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.NORM_PRIORITY);
        thread4.setPriority(8);
        thread.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

执行结果优先级高的线程不一定先执行

c线程优先级10
b线程优先级5
a线程优先级1
d线程优先级8
5、stop()、destroy()。【已废弃】
- JDK提供的上述两种方法已废弃不推荐使用。
- 推荐线程自动停止下来建议使用一个标识位变量进行终止当flag=false时则终止线程运行。

代码如下示例

public class MyThread implements Runnable {
    /**
     * 标识位为false时线程结束
     */
    private boolean flag = true;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    int i = 0;
    @Override
    public void run() {
        while (flag) {
            System.out.println(Thread.currentThread().getName() + "线程" + i ++);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"a");
        thread.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程" + i);
            if (i == 5) {
                // 当主线程 i== 5时标识位变为false控制子线程停止
                myThread.setFlag(false); 
            }
        }
    }
}

执行结果主线程 i== 5 之后子线程就停止运行了

主线程0
主线程1
a线程0
a线程1
a线程2
a线程3
a线程4
a线程5
a线程6
主线程2
a线程7
主线程3
a线程8
主线程4
a线程9
主线程5
a线程10
主线程6
主线程7
主线程8
主线程9

四、多线程

线程同步就是线程排队就是操作共享资源要有先后顺序一个线程操作完之后另一个线程才能操作或者读取。

  • 防止线程同步访问共享资源造成冲突。
  • 变量需要同步常量不需要同步常量存放于方法区。
  • 多个线程访问共享资源的代码即线程执行体有可能是同一份代码也有可能是不同的代码无论是否执行同一份代码只要这些线程的代码访问同一份可变的共享资源这些线程之间就需要同步。

1、守护Deamon线程

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕。
  • 虚拟机不用等待守护线程执行完毕。如后天记录操作日志、监控内存、垃圾回收等线程。
  • Thread.setDeamon(booean on)方法true守护线程fasle用户进程。默认是false。

代码如下示例

public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        DeamonThread deamon = new DeamonThread();
        UserThread user = new UserThread();

        Thread deamonThread = new Thread(deamon);
        deamonThread.setDaemon(true); // 设置为守护进程
        deamonThread.start();

        Thread userThread = new Thread(user);
        userThread.start();
    }
}

// 模拟守护线程
class DeamonThread implements Runnable{

    @Override
    public void run() {
        // 验证虚拟机不用等待守护线程执行完毕只要用户线程执行完毕程序就结束。
        // 如果成功怎下面的打印不会一直输出如果成功则下面的打印会一直输出
        while (true) {
            System.out.println("我是守护线程");
        }
    }
}

// 用户线程
class UserThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("我是用户线程 :" + i);
        }
    }
}

执行结果守护进程不会一直打印

我是守护线程
我是用户线程 :0
我是守护线程
...
我是用户线程 :1
我是守护线程
...
我是用户线程 :2
我是用户线程 :3
我是用户线程 :4
我是用户线程 :5
我是用户线程 :6
我是用户线程 :7
我是用户线程 :8
我是用户线程 :9
我是守护线程
...
我是守护线程

2、多线程并发与同步

(1)、多线程并发
在多线程场景下如果多个线程修改同一个资源或者一个线程修改共享资源另一个线程读取共享资源可能会导致结果不对的问题这就导致线程不安全即并发。导致线程并发的原因

  • 原子性一个或多个操作在CPU执行过程中被中断。即一个操作或者多个操作要么全部执行并且执行的过程中不会被任何因素打断要么就不执行。原子性就像数据库里面的事务一样他们是一个整体同存亡。
  • 可见性一个线程对共享变量的修改另一个线程不能立马看到。
  • 有序性程序执行的顺序没有按照代码的先后顺序执行。

下面以两个例子演示线程不安全问题。
示例1买票问题

代码如下示例

// 模拟线程不安全问示例1买票
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        BuyTicker ticker = new BuyTicker();

        Thread person1Thread = new Thread(ticker, "person1");
        Thread person2Thread = new Thread(ticker, "person2");
        Thread person3Thread = new Thread(ticker, "person3");
        person1Thread.start();
        person2Thread.start();
        person3Thread.start();
    }
}

class BuyTicker implements Runnable{
    // 车票
    private int tickerNum = 10;
    // 停止线程标识
    boolean flag = true;

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

    private void buyTicker() throws InterruptedException {
        // 判断是否还有票
        if (tickerNum <= 0) {
            flag = false;
            return;
        }
        // 模拟延时
        Thread.sleep(100);
        // 买票
        System.out.println(Thread.currentThread().getName() + "买到第" + tickerNum -- + "张票");
    }
}

执行结果可以看到第4、3张票卖了两次还有人买到了第0张票

person3买到第8张票
person2买到第10张票
person1买到第9张票
person1买到第7张票
person3买到第5张票
person2买到第6张票
person1买到第4张票
person2买到第4张票
person3买到第3张票
person1买到第2张票
person2买到第2张票
person3买到第1张票
person1买到第0张票

示例2银行取钱

代码如下示例

// 模拟线程不安全示例2银行取钱
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account(1000, "旅游基金");

        new Bank(account, 500, "你").start();
        new Bank(account, 600, "女朋友").start();
    }
}

// 账户
class Account {
    // 账户总余额
    int money;
    // 账户名
    String name;

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

// 银行
class Bank extends Thread{
    // 客户账户
    Account account;
    // 取得钱数
    int drawingMoney;

    public Bank(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(account.name+ "钱不够了取不了了");
            return;
        }

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 卡内余额 = 余额 - 取得钱
        account.money = account.money - drawingMoney;

        System.out.println(Thread.currentThread().getName()  + "取了" + drawingMoney);
        System.out.println(account.name + "余额为" + account.money);

    }
}

执行结果当你取500时线程执行到account.money = account.money - drawingMoney之前另一个线程抢到了CPU执行权也执行到account.money = account.money - drawingMoney之前现在余额还是1000继续执行1000-500-900=-400.

你取了500
女朋友取了900
旅游基金余额为-400
旅游基金余额为-400

(2)、同步解决并发问题

解决线程并发问题的方法是线程同步线程同步就是让线程排队就是操作共享资源要有先后顺序一个线程操作完之后另一个线程才能操作或者读取。

  • 防止线程同步访问共享资源造成冲突。
  • 变量需要同步常量不需要同步常量存放于方法区。
  • 多个线程访问共享资源的代码即线程执行体有可能是同一份代码也有可能是不同的代码无论是否执行同一份代码只要这些线程的代码访问同一份可变的共享资源这些线程之间就需要同步。

解决并发问题的两种方法
同步方法public synchronized void method(int args){执行体…}

  • synchronized 方法控制对(synchronized修饰的方法所在的对象就是this)“对象”的访问每个对象对应一把锁每个synchronized方法都必须获得调用该方法的锁才能执行否则线程会阻塞synchronized所在方法一旦执行就独占该锁直到方法执行完
    才释放锁后面被阻塞的线程才能获得这个锁继续执行。
  • 缺陷若将一个大的方法声明为synchronized 将会影响效率。需要修改的内容才需要锁锁的太多浪费资源。

同步代码块synchronized (Obj){执行体…}

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

示例1买票问题使用同步方法改造成线程安全

代码如下示例

// 模拟线程不安全问示例1买票
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        BuyTicker ticker = new BuyTicker();

        Thread person1Thread = new Thread(ticker, "person1");
        Thread person2Thread = new Thread(ticker, "person2");
        Thread person3Thread = new Thread(ticker, "person3");
        person1Thread.start();
        person2Thread.start();
        person3Thread.start();
    }
}

class BuyTicker implements Runnable{
    // 车票
    private int tickerNum = 10;
    // 停止线程标识
    boolean flag = true;

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

    private synchronized void buyTicker() throws InterruptedException {
        // 判断是否还有票
        if (tickerNum <= 0) {
            flag = false;
            return;
        }
        // 模拟延时
        Thread.sleep(100);
        // 买票
        System.out.println(Thread.currentThread().getName() + "买到第" + tickerNum -- + "张票");
    }
}

执行结果

person1买到第10张票
person1买到第9张票
person1买到第8张票
person1买到第7张票
person1买到第6张票
person1买到第5张票
person3买到第4张票
person3买到第3张票
person3买到第2张票
person2买到第1张票

示例2银行取钱使用同步代码块改造成线程安全

代码如下示例

// 模拟线程不安全示例2银行取钱
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        Account account = new Account(1000, "旅游基金");

        new Bank(account, 500, "你").start();
        new Bank(account, 600, "女朋友").start();
    }
}

// 账户
class Account {
    // 账户总余额
    int money;
    // 账户名
    String name;

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

// 银行
class Bank extends Thread{
    // 客户账户
    Account account;
    // 取得钱数
    int drawingMoney;

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

    @Override
    public void run() {
        synchronized (account) {
            if (account.money- drawingMoney < 0) {
                System.out.println(account.name+ "钱不够了取不了了");
                return;
            }

            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 卡内余额 = 余额 - 取得钱
            account.money = account.money - drawingMoney;
            System.out.println(Thread.currentThread().getName()  + "取了" + drawingMoney);
            System.out.println(account.name + "余额为" + account.money);
        }
    }
}

执行结果你取了500后你女朋友取600就提示余额不足不会出现余额为负数的情况了。这里的同步监视器是accountaccount才是操作的共享资源而不是bank。

你取了500
旅游基金余额为500
旅游基金钱不够了取不了了

示例3模拟集合ArrayList<>()是线程不安全的

代码如下示例

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

// 模拟集合ArrayList<>()是线程不安全的
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
            }).start();
        }
		// sleep可研模拟网络延迟
        Thread.sleep(5000);
        System.out.println(list.size());
    }
}

执行结果list的大小应该是1000结果是999

999

使用同步代码块改造成线程安全的

代码如下示例

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

// 模拟线程不安全示例2银行取钱
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                // 加锁
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }

        Thread.sleep(5000);
        System.out.println(list.size());
    }
}

执行结果

1000

3、死锁

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

代码如下示例

// 死锁例子鱼和熊不可兼得
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        Person personA = new Person(0, "猎人A");
        Person personB = new Person(1, "猎人B");
        personA.start();
        personB.start();
    }
}

// 熊掌
class Bear {

}

// 鱼
class Fish {

}

// 人
class Person extends Thread {
    // 保证资源只有一份
    public static Bear bear = new Bear();
    public static Fish fish = new Fish();

    int choose;
    String personName;

    public Person (int choose, String personName) {
        this.choose = choose;
        this.personName = personName;
    }

    @Override
    public void run() {
        // 捕猎
        try {
            this.hunting();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 捕猎方法
    private void hunting() throws InterruptedException {
        if (choose == 0) {
            synchronized (bear) {
                System.out.println(personName + "想捕捉熊");
                Thread.sleep(1000);
                synchronized (fish) {
                    System.out.println(personName + "想捕捉鱼");
                }
            }
        } else {
            synchronized (fish) {
                System.out.println(personName + "想捕捉鱼");
                Thread.sleep(1000);
                synchronized (bear) {
                    System.out.println(personName + "想捕捉熊");
                }
            }
        }
    }
}

执行结果两个线程一直阻塞都在等在对方释放锁结果导致死锁。

猎人A想捕捉熊
猎人B想捕捉鱼

2解决死锁的方法同步代码块中不要相互嵌套即不要相互嵌套锁。

代码如下示例

// 死锁例子鱼和熊不可兼得
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        Person personA = new Person(0, "猎人A");
        Person personB = new Person(1, "猎人B");
        personA.start();
        personB.start();
    }
}

// 熊掌
class Bear {

}

// 鱼
class Fish {

}

// 人
class Person extends Thread {
    // 保证资源只有一份
    public static Bear bear = new Bear();
    public static Fish fish = new Fish();

    int choose;
    String personName;

    public Person (int choose, String personName) {
        this.choose = choose;
        this.personName = personName;
    }

    @Override
    public void run() {
        // 捕猎
        try {
            this.hunting();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 捕猎方法
    private void hunting() throws InterruptedException {
        if (choose == 0) {
            synchronized (bear) {
                System.out.println(personName + "想捕捉熊");
                Thread.sleep(1000);
            }
            // 把嵌套的代码块拿到外面两个代码块并列
            synchronized (fish) {
                System.out.println(personName + "想捕捉鱼");
            }
        } else {
            synchronized (fish) {
                System.out.println(personName + "想捕捉鱼");
                Thread.sleep(1000);
            }
            // 把嵌套的代码块拿到外面两个代码块并列
            synchronized (bear) {
                System.out.println(personName + "想捕捉熊");
            }
        }
    }
}

执行结果两个线程即捕到了熊有捕到了鱼解决了死锁问题。

猎人B想捕捉鱼
猎人A想捕捉熊
猎人A想捕捉鱼
猎人B想捕捉熊

4、Lock(锁)

Lock 锁也称同步锁java.util.concurrent.locks.Lock 机制提供了⽐ synchronized 代码块和 synchronized ⽅法更⼴泛的锁定操作同步代码块 / 同步⽅法具有的功能 Lock 都有除此之外更强⼤更体现⾯向对象。
创建对象 Lock lock = new ReentrantLock() 加锁与释放锁⽅法如下
public void lock() 加同步锁
public void unlock() 释放同步锁

synchronized和Lock的对比

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

代码如下示例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 测试 Lock锁
public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        TestLock testLock = new TestLock();
        new Thread(testLock,"a").start();
        new Thread(testLock,"b").start();
        new Thread(testLock,"c").start();
    }
}

class TestLock implements Runnable {

    // 车票
    private static int tickerNum = 10;

    // 创建一个Lock锁
    private final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock(); // 加锁
            try {
                // 判断是否还有票
                if (tickerNum > 0) {
                    // 模拟延时
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 买票
                    System.out.println(Thread.currentThread().getName() + "线程买到第" + tickerNum -- + "张票");
                } else {
                    break;
                }
            } finally {
                lock.unlock(); // 解锁
            }
        }
    }
}

执行结果

a线程买到第10张票
a线程买到第9张票
a线程买到第8张票
a线程买到第7张票
a线程买到第6张票
a线程买到第5张票
b线程买到第4张票
b线程买到第3张票
b线程买到第2张票
b线程买到第1张票

5、线程协作

  • 线程之间需要进行通信通信有数据共享1、文件共享2、网络共享3、变量共享和线程协作两种方式。
  • 线程协作指不同线程驱动的任务相互依赖依赖一般就是对共享资源的依赖。有共享就有竞争有竞争就会有线程安全问题即并发解决并发问题就用线程同步。

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

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

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

  • 对于生产者没有生产产品之前要通知消费者等待。而生产了产品之后又需要马上通知消费者消费。
  • 对于消费者在消费之后要通知生产者已经结束消费需要生产新的产品以供消费。

在生产者消费者问题中没生产出产品之前消费者是不能消费的反之消费者没消费完之前生产者是不能生产的。这就需要来实现线程之间的同步。仅有同步还不行还要实现线程之间的消息传递即通信

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

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

注意均是Object的方法均只能在同步方法或者同步代码块中使用否则会抛出异常IIIegalMonitorStageException。

解决线程之间通信的方式管程法信号灯法

管程法生产者把生产好的数据放入缓存区消费者从缓存区中拿出数据。
在这里插入图片描述

代码如下示例

// 线程通信生产消费模式-管程法
public class MyThread{
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}

// 产品
class Chicken {
    int id;

    public Chicken (int id) {
        this.id = id;
    }
}

// 生产者
class Productor extends Thread {
    SynContainer synContainer;

    public Productor(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            synContainer.pushTo(new Chicken(i));
        }
    }
}

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

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

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

// 容器
class SynContainer {
    // 定义一个大小为10的容器
    Chicken[] chickens = new Chicken[10];
    // 容器计数器
    int count;

    // 生产者生产产品方法
    public synchronized void pushTo(Chicken chicken) {
        // 如果容器满了就停止生产
        if (chickens.length == count) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果容器没满就往容器中放入产品
        chickens[count] = chicken;
        System.out.println("生产了" + chicken.id + "个鸡腿");
        count ++;

        // 通知消费者消费
        this.notifyAll();
    }

    // 消费者消费产品方法
    public synchronized Chicken popTo() {
        // 如果容器中没有产品了就停止消费
        if (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果容器有产品就可以消费
        count --;
        Chicken chicken = chickens[count];
        System.out.println("消费了第" + chicken.id + "个鸡退");

        //只要消费了就通知生产者生产
        this.notifyAll();
        return chicken;
    }
}

执行结果

生产了0个鸡腿
生产了1个鸡腿
生产了2个鸡腿
生产了3个鸡腿
生产了4个鸡腿
生产了5个鸡腿
生产了6个鸡腿
生产了7个鸡腿
生产了8个鸡腿
生产了9个鸡腿
消费了第9个鸡退
生产了10个鸡腿
消费了第10个鸡退
生产了11个鸡腿
消费了第11个鸡退
生产了12个鸡腿
消费了第12个鸡退
生产了13个鸡腿
消费了第13个鸡退
生产了14个鸡腿
消费了第14个鸡退
生产了15个鸡腿
消费了第15个鸡退
生产了16个鸡腿
消费了第16个鸡退
生产了17个鸡腿
消费了第17个鸡退
消费了第8个鸡退
消费了第7个鸡退
消费了第6个鸡退
消费了第5个鸡退
消费了第4个鸡退
消费了第3个鸡退
消费了第2个鸡退
消费了第1个鸡退
消费了第0个鸡退
生产了18个鸡腿
生产了19个鸡腿
消费了第19个鸡退
消费了第18个鸡退

信号灯法设置一个标识位标识位来判断线程是等待还是执行。

代码如下示例

// 线程通信生产消费模式-信号灯法
public class MyThread{
    public static void main(String[] args) {
        CCTV cctv = new CCTV();
        new Player(cctv).start();
        new Watcher(cctv).start();
    }
}

// 演员
class Player extends Thread {
    CCTV cctv;

    public Player(CCTV cctv) {
        this.cctv = cctv;
    }

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

// 观众
class Watcher extends Thread {
    CCTV cctv;

    public Watcher(CCTV cctv) {
        this.cctv = cctv;
    }

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

// 电视
class CCTV {
    // 表演的节目
    String voice;
    // 标识
    boolean flag = true;

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

    // 观看节目
    public synchronized void watch () {
        if (flag) {
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 如果容器有产品就可以消费
        System.out.println("观众观看了" + voice);
        // 通知演员表演节目
        this.notifyAll();
        this.flag = !flag;
    }
}

执行结果

演员表演了快乐大本营
观众观看了快乐大本营
演员表演了天天向上
观众观看了天天向上
演员表演了快乐大本营
观众观看了快乐大本营
演员表演了天天向上
观众观看了天天向上
演员表演了快乐大本营
观众观看了快乐大本营
演员表演了天天向上
观众观看了天天向上
演员表演了快乐大本营
观众观看了快乐大本营
演员表演了天天向上
观众观看了天天向上
演员表演了快乐大本营
观众观看了快乐大本营
演员表演了天天向上
观众观看了天天向上

6、线程池

背景经常创建和销毁线程消耗特别大的资源比如并发的情况下的线程对性能影响很大。线程池就是问题为了解决这个问题提前创建好多个线程放在线程池中使用时直接获取使用完放回线程池中可以避免频繁的创建、销毁实现重复利用。
优点

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

线程池相关的API

  • ExecutorService线程池接口。

  • 常见的实现类ThreadPoolExecutor。
    void execute(Runnable command)执行任务命令没有返回值一般用来执行Runnable.
    <T> Future<T> submit(Callable<T> task)执行任务有返回值一般用来执行Callable

  • void shutdown()关闭连接池

  • Executors工具类线程池的工厂类用来创建并返回不同类型的线程池

代码如下示例

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

// 线程池
public class ThreadPool {
    public static void main(String[] args) {
        // 1、创建服务创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        MyThread myThread = new MyThread();
        // 执行
        service.execute(myThread);
        service.execute(myThread);
        service.execute(myThread);
        service.execute(myThread);
        // 关闭连接
        service.shutdown();
    }
}

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

执行结果

pool-1-thread-2
pool-1-thread-4
pool-1-thread-3
pool-1-thread-1

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java