多线程基础

一、概述

在生活中多线程无处不在,有没有思考过一个问题,为什么可以同时和多个人一起打游戏?为什么聊天时双方可以同时互发消息?这些都是多线程的体现!

多任务:
举几个例子:一边吃饭一边玩手机、看电影时一边看画面一边听音频,再比如一边上厕所一边玩手机
现实中太多这样同时做多件事的例子了,看起来是多个任务同时在做,其实本质上我们的大脑在同一时间只做了一件事!只是时间交替很快,拿吃饭玩手机举例子,一刹那你在吃饭下一刻你在玩手机你觉得是同时进行的,实际上只做了一件事!

多线程的好处:
还是举几个生活中的例子,道路狭窄车多就会出现堵塞,甚至出现事故,效率会很低。为了避免这种情况,提高使用效率,于是拓宽了车道,允许多辆车同时通过。再或者原来你和你朋友共用一个游戏账号,会造成很多麻烦,但是后来你也创了一个账号这样你们就可以同时玩游戏

在这里插入图片描述

单线程:主线程==>执行方法==>回到主线程==>结束
多线程:主线程==>执行方法==>两个线程同时进行

二、程序、进程、线程

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

进程(Process):在操作系统中运行的程序就是进程,比如QQ、游戏、IDEA等等。通俗点说进程就是程序的执行过程,他是一个动态的概念。是系统资源分配的单位

线程(Thread):进程非常大,一个进程里面可以有多个线程。举个例子:我们平时用播放器看电影,视频中同时有声音、图像、和字幕。当然一个进程至少有一个线程,否则没有存在的意义!线程是操作系统 能够进行运算、调度最小单位

注意:很多多线程都是模拟出来的,真正的多线程是指有多个CPU,即多核,如服务器,如果是模拟出来的多线程,即在一个CPU的情况下,在同一时间点,CPU只能执行一个代码。只是切换的很快,所以有种同时执行的错觉。就和我们边吃饭边玩手机一样,我们只是一个CPU同一时间只能干一件事!

核心概念:

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

三、线程的创建

线程的创建有以下三种方式
在这里插入图片描述

1.继承Thread类

查看jdk帮助文档,发现他实现了Runnable接口,即线程的第二种创建方式。
在这里插入图片描述
使用方法:

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

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("学习多线程的第"+i+"天");
        }
    }

    public static void main(String[] args) {

        TestThread1 testThread1 = new TestThread1();
        //这里注意调用start()开启线程,调用run()则按顺序执行!
        testThread1.start();

        for (int i = 0; i < 200; i++) {
            System.out.println("学习Java的第"+i+"天");
        }
    }
}

在这里插入图片描述
注意:线程开启不一定立即执行,而是由CPU调度执行!

案例:下载网图

这里需要导入一个jar包,apache下的commons.io这个包(官网自行下载),将它沾到IDEA中,右键点击Add as Library。
这个包下有FileUtils工具类,使用copyURLToFile(URL source, File destination)方法拷贝url到一个文件。

public class TestThread extends Thread{
    private String url;
    private String name;

//使用有参构造获取copyURLToFile所需的参数
    public TestThread(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public void run() {
        DownLoad downLoad = new DownLoad(url,name);
        System.out.println("下载的文件名为:"+name);


    }

    public static void main(String[] args) {
        TestThread testThread = new TestThread("图片url","图片名字");
        TestThread testThread1 = new TestThread("图片url","图片名字");
        TestThread testThread2 = new TestThread("图片url","图片名字");

        testThread.start();
        testThread1.start();
        testThread2.start();
    }
}


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

    }
}

2.实现 Runnable 接口

使用方法:

  1. 自定义线程类实现Runnable接口
  2. 重写run()方法,编写线程执行体
  3. 创建线程对象
  4. 将线程对象在创建Thread时作为参数传递,并启动
public class TestThread2 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("第"+i+"天学习多线程");
        }
    }

    public static void main(String[] args) {
        TestThread2 testThread2 = new TestThread2();
        
//        Thread thread = new Thread(testThread2);
//        thread.start();
        new Thread(testThread2).start();

        for (int i = 0; i < 200; i++) {
            System.out.println("第"+i+"天学习Java");

        }
    }
}

解释new Thread(testThread2).start()写的原因:无论我们以怎样的形式实现多线程,都需要调用Thread类中的start方法去向操作系统请求io,cup等资源,Tread中有一个构造方法是传入一个Runnable接口类型的参数来启动线程。
在这里插入图片描述

推荐使用实现Runnable接口!

原因:

  1. 继承有单继承的局限性,而接口可以多继承
  2. 实现Runnable接口启动线程是讲线程对象传入Thread构造方法中,灵活方便,方便同一个对象被多个线程使用

典型案例龟兔赛跑:
sleep(long millis)
当前正在执行的线程休眠(暂停执行)指定的毫秒数

public class Race implements Runnable {
   private static String winner;//winnerֻ胜利者 使用static的原因:胜利者只有一个

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {

            if (Thread.currentThread().getName().equals("兔子") && i==50){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            boolean flag = gameOver(i);
            if (flag){
               break;
            }
            System.out.println(Thread.currentThread().getName() + "==>跑了" + i + "步");
        }
    }

    private boolean gameOver(int steps) {
        if (winner != null) {
            return true;
        } else {
            if (steps == 100) {
                winner = Thread.currentThread().getName();
                System.out.println(winner+"获胜了");
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race,"兔子").start();
        new Thread(race,"乌龟").start();

    }
}

3.实现Callable接口

无论以上哪种方式实现多线程,都存在一个问题,就是线程run方法没有返回值,如果一个线程需要有返回值时,可以采用实现Callable接口来实现多线程!

首先看一下Callable接口的源码

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

首先@FunctionalInterface定义了这个接口为函数式接口,Callable接口接受一个泛型作为接口中call方法的返回值类型,因此我们在使用时需要传入一个返回值类型。

使用方法1:
由于Thread类中并没有定义任何构造方法可以直接接收Callable接口对象实例,并且需要接收call()方法返回值的问题,所以从JDK1.5开始提供有一个java.until.concurrent.FutureTask类。

public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

可以看出FutureTask类也是实现了Runnable接口的

FutureTask类的常用方法:

方法类型
public FutureTask(Callable callable)构造方法
public FutureTask(Runnable runnable, V result)构造方法
public V get() throws InterruptedException, ExecutionException普通方法

从第一个构造方法可以看出,FutureTask可以传入一个Callable接口类型的参数,而FutureTask实现了RunnableFuture,而 RunnableFuture 接口又继承了Runnable接口,因此我们可以将FutureTask 的一个实例当做是一个Runnable接口的实例传入Thread来启动我们新建的线程。

public class TestCallable implements Callable {
    @Override
    public Boolean call() {
        for (int i = 0; i < 11; i++) {
            System.out.println("java好难");
        }

        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        for (int i = 0; i < 11; i++) {
            System.out.println("学习多线程"+i);

            TestCallable testCallable = new TestCallable();
            FutureTask<Boolean> task = new FutureTask<Boolean>(testCallable);
            new Thread(task).start();
            //Boolean flag = task.get();
            //System.out.println(flag);
        }
    }
}

使用方法2:

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

ExecutorService 和 Executors会在下面线程池中讲

public class TestCallable implements Callable {
    @Override
    public Boolean call() {
        for (int i = 0; i < 11; i++) {
            System.out.println("java好难");
        }

        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        for (int i = 0; i < 11; i++) {
            System.out.println("学习Callable接口"+i);

            TestCallable testCallable = new TestCallable();
            //创建执行服务
            ExecutorService ser =  Executors.newFixedThreadPool(2);
            //提交执行
            Future<Boolean> result1 =ser.submit(testCallable);
            //获取结果
            boolean rs = result1.get();
            //关闭服务
            ser.shutdown();

        }
    }
}

四、线程的状态(五大状态)

线程有五大状态:创建 就绪 阻塞 运行 死亡

在这里插入图片描述
创建状态:Thread r = new Thread();线程对象一旦创建就进入了创建状态
就绪状态:当调用start()方法,线程立刻进入就绪状态,但不意味着立刻执行调度而是等待CPU调度
运行状态:CPU调度后进入运行状态,线程才真正执行线程体的代码块
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

死亡状态:线程中断或者结束,一旦进入死亡状态就不能继续启动

线程方法:

方法说明
setPriority(int newPriority)设置线程的优先级
static void sleep(long millis)让正在执行的线程休眠(暂停执行)指定的毫秒数
void join()等待该线程死亡
static void yield()暂停正在执行的线程对象
void interrupt()中断这个线程,不建议使用这个方法!
boolean isAlive()测试当前线程是否已被中断
Thread.State getState()得到当前线程的状态

停止线程

不推荐使用JDK提供的 stop()、 destroy()方法。已废弃!
在这里插入图片描述

推荐线程自己停下来!
怎么实现呢?
使用一个标志位进行终止变量 当flag=false,则终止线程运行

public class TestTreadStop implements Runnable{
    int i=0;
    boolean flag = true;

    @Override
    public void run() {
        while (flag){
            i++;
            System.out.println("子线程执行了"+i+"次");
        }
    }
//自己定义的停止方法非Thread类的
    private void stop(){
        flag = false;
    }

    public static void main(String[] args) {
        TestTreadStop testTreadStop = new TestTreadStop();
        new Thread(testTreadStop).start();

        for (int i = 0; i < 300; i++) {

            if (i==100){
                testTreadStop.stop();
                System.out.println("子线程该停止了!");
            }
            System.out.println("主线程执行了"+i+"次");
        }
    }
}

主线程和子线程共同抢占CPU,两者交替执行,当主线程的i==100时,调用了子线程的stop方法,那么子线程停止就不再执行,那么此时程序中就存在一个主线程在执行,直到停止。

线程休眠(sleep)

public static native void sleep(long millis) throws InterruptedException;

millis 为毫秒数,指定线程停止的毫秒数。
存在异常 InterruptedException
注意: 每一个对象都有一个锁,sleep不会释放锁;

sleep的使用:

1、模拟网络延时
还是用抢票这个例子

public class TestThread3 implements Runnable{
    private int num = 10;
    @Override
    public void run() {

        while (true){
            if (num<=0){
                break;
            }
           /* try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            */
            System.out.println(Thread.currentThread().getName()+"==>拿到了第"+num--+"张票");
        }
    }

    public static void main(String[] args) {
        TestThread3 thread3 = new TestThread3();
        new Thread(thread3,"小米").start();
        new Thread(thread3,"大米").start();
        new Thread(thread3,"黄牛").start();
    }
}

不加sleep方法,运行发现正常抢票
在这里插入图片描述
当我们增加了网络延时后,会发现很多问题!
在这里插入图片描述
以上例子通过三个线程抢票,模拟网络延迟,放大了问题的发生性,我们知道当我们定义多个线程共同抢夺同一个资源时,也没有使用锁是不安全的(两个人拿到同一张票),我们不模拟延时,很难发现问题。而当我们使用了sleep后,我们很容易看到问题发生的现状(两个人抢到了同一张票或者抢到一张不存在的票!)。

2、实现倒计时

public class TestSleep {

    private void tenDown(){
        int number = 10;
        try {
            while (true){
                System.out.println(number--);

                if (number<=0){
                    break;
                }
                Thread.sleep(1000);
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new TestSleep().tenDown();
    }
}

3、控制台输出当前时间

public class TestSleep2 {

    public static void main(String[] args) {
        Date date = new Date();

        try {
            while (true){

                System.out.println(new SimpleDateFormat("YYYY/MM/DD").format(date));
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));
                Thread.sleep(1000);
                date = new Date();
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

线程礼让(yield)

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

public class TestYield {

    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();
    }

}
class MyYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"执行");
        Thread.yield();//礼让
        System.out.println(Thread.currentThread().getName()+"停止");
    }
}
礼让成功:
a执行
b执行
b停止
a停止
礼让失败:
b执行
b停止
a执行
a停止

a,b两个线程,假设a线程进入了CPU正在执行,a礼让了b,两个同时处于就绪状态,等待CPU调度!可能礼让不成功,又是调度了a

线程插队(join)

Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞

join可以理解为插队,比如你在排队办理一些业务时,前面有个人插队说自己是vip可以优先办理,其他人只能等着他办理完才能继续。线程也是如此!使用了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) {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);


        for (int i = 0; i < 500; i++) {
            if (i==200){
                try {
                    thread.start();
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("路人"+i);
        }
    }
}

当i==200时路人线程要等待VIP全部执行完才继续执行

线程状态

在这里插入图片描述
可以使用getState()获得当前线程状态

public class TestSta {

    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 2; i++) {
                try {
                    Thread.sleep(1000);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        Thread.State state = thread.getState();
        System.out.println(state);

        thread.start();
        state = thread.getState();
        System.out.println(state);

        while (state != Thread.State.TERMINATED){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            state = thread.getState();
            System.out.println(state);

        }
    }
}
结果:
NEW
RUNNABLE
TIMED_WAITING
.
.这里省略多个TIMED_WAITING
.
TERMINATED

线程优先级

需要用到的方法

  • public final int getPriority() 获取优先级
  • public final void setPriority(int newPriority) 设置优先级

每个线程都有默认的优先级。主线程的默认优先级 Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。

注意优先级高的只是执行的概率更高了,并不代表一定会先执行优先级高的!看CPU的调度。线程属性的设置要在线程启动之前!

在这里插入图片描述
查看Thread类源码发现线程的最小优先级为1,最大优先级为10,默认优先级为5。如果超过最大或者低于最小会抛 IllegalArgumentException 异常

线程分为 用户线程(例如:main;虚拟机必须确保用户线程执行完毕) 和守护线程(例如:gc;虚拟机不用等待守护线程执行完毕)

守护线程与用户线程

守护线程:是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程
用户线程:可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。如我们使用 Thread 创建的线程在默认情况下都属于用户线程

怎么使线程成为用户线程与守护线程?

  • 通过 Thread.setDaemon(false) 设置为用户线程
  • 通过 Thread.setDaemon(true) 设置为守护线程
  • 如果不设置线程属性,那么默认为用户线程
public class TestDaemon {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread t1 = new Thread(god);
        t1.setDaemon(true);
        t1.start();

        Thread t2 = new Thread(you);
        t2.start();
    }
}
//守护线程
class God implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("祝你开心幸福"+i+"天");
        }
    }
}
//用户线程
class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Happy every day");
        }
        System.out.println("goodBye world");
    }
}

运行发现:用户线程全部执行,守护线程只执行了一部分。

这是因为 虚拟机必须确保用户线程执行完毕而不用等待守护线程执行完毕

上面代码还会出现:当用户线程死后,我们看到守护线程还在执行一段时间,原因是当我们销毁用户线程后也需要一段时间,因为cpu切换速度比较快,所以我们的守护线程也会运行一段时间。

守护线程的作用:如,后台记录操作日志,监控内存,垃圾回收等待…

五、线程同步

当多个线程同时操作同一个对象时会发生并发问题!

并行和并发

在学习多线程时,常常会遇到并行和并发两个名词,它们看起来是一个概念,实际上它们是有区别的!

举个例子理解一下:拿学校食堂打饭来说,并发就是只有一个窗口排了两队人,这些人交替打饭。而并行是两个窗口两队人,分别在两个窗口打饭。

并发(多线程操作同一个资源):CPU一核,模拟出多条线程,天下武功,唯快不破,快速交替

下图展示了两个任务并发执行的过程(交替执行):
在这里插入图片描述

并行:并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。

同样是执行两个任务,双核 CPU 的工作状态(也就是并行的执行过程)如下图所示:
在这里插入图片描述

并发+并行

执行任务的数量恰好等于 CPU 核心的数量,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,尤其是电脑和手机,开机就几十个任务,而 CPU 往往只有 4 核、8 核或者 16 核,远低于任务的数量,这个时候就会同时存在并发和并行两种情况:所有核心都要并行工作,并且每个核心还要并发工作。

例如一个双核 CPU 要执行四个任务,它的工作状态如下图所示:
在这里插入图片描述
每个核心并发执行两个任务,两个核心并行的话就能执行四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,这跟操作系统的分配方式,以及每个任务的工作状态有关系。

注意:
单核 CPU 只能并发,无法并行;换句话说,并行只可能发生在多核 CPU 中。

在多核 CPU 中,并发和并行一般都会同时存在。

线程同步

  • 现实生活中,我们会遇到 ” 同一个资源 , 多个人都想使用 ” 的问题 , 比如,食堂排队 打饭 , 每个人都想吃饭 , 最天然的解决办法就是 , 排队 . 一个个来.
    举个例子:购买火车票的过程
  • 处理多线程问题时 , 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 . 这时候我们就需要线程同步 . 线程同步其实就是一种等待机制 , 多个需要同时访问此对象的线程进入这个对象的等待池 形成队列, 等待前面线程使用完毕 , 下一个线程再使用
  • 由于同一进程的多个线程共享同一块存储空间 , 在带来方便的同时,也带来了访问 冲突问题 , 为了保证数据在方法中被访问时的正确性 , 在访问时加入 锁机制

synchronized , 当一个线程获得对象的排它锁 , 独占资源 , 其他线程必须等待 , 使用后释放锁即可 . 存在以下问题 :

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

线程同步需要 队列+锁
队列可以看作是排队。锁可以看作是排队上厕所,每个人进入厕所都会锁上门,然后解决完后打开锁,下一个人进去。这样使得线程变得安全,但是性能降低!

同步方法

在这里插入图片描述
synchronized实现原理就是 队列和锁

同步块

  • 同步块:synchronized(obj){ }
  • obj称之为 同步监视器
    • obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器 , 因为同步方法的同步监视器就是this , 就是这个对象本身 , 或者是 class
  • 同步监视器的执行过程
    • 第一个线程访问 , 锁定同步监视器 , 执行其中代码 .
    • 第二个线程访问 , 发现同步监视器被锁定 , 无法访问 .
    • 第一个线程访问完毕 , 解锁同步监视器 .
    • 第二个线程访问, 发现同步监视器没有锁 , 然后锁定并访问

同步方法的弊端

方法里面有只读和需要修改的代码,只读的代码可以多人同时访问,而修改代码不允许同时进行修改。这时就不建议使用同步方法,方法里需要修改的内容才需要锁,锁的太多会造成资源浪费性能降低!

代码示例

下面举几个线程不安全的例子,并通过同步方法和同步代码块解决

买票

这里多个线程资源抢占票,若不适用锁机制不同对象会抢到同一张票,还会造成票的负数,显然这是线程不安全的,那么我们使用synchronized方法实现线程安全

public class Unsafe1 {
    public static void main(String[] args) {
        station station = new station();
        Thread t1 = new Thread(station,"张三");
        Thread t2 = new Thread(station,"李四");
        Thread t3 = new Thread(station,"黄牛");
        t1.start();
        t2.start();
        t3.start();
    }
}

class station implements Runnable{
   private int ticket = 5;
   boolean flag = true;
   
    @Override
    public void run() {
        while (flag){
            buy();
        }
    }

    private  void buy(){
        if (ticket <= 0){
            flag = false;
            return;
        }
        try {
            //增大事情的发生性
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"获得了第"+ticket--+"张票");
    }
}

运行结果发现:同时抢到了一张票,现实生活中是不允许的!
在这里插入图片描述

解决:在buy方法前加修饰符synchronized,其他不变,实现队列加锁

	private synchronized void buy(){
        if (ticket <= 0){
            flag = false;
            return;
        }

分析:我们是对票进行操作,票属于station 这个类,而同步方法的同步监视器就是this , 就是这个对象本身!相当于把票放在一个房间里,来一个线程进房间上锁取走票后解锁,下一个继续这样的操作。

取钱

银行卡上有100万,你和你女朋友同时取钱,你取50万,女朋友取100万,如果不上锁不实现同步,账户将会出现负的50万(程序比较笨)

解决:

public class Unsafe2 {
    public static void main(String[] args) {
        Count count = new Count(100,"购房基金");
        bank zs = new bank(count,50,"你");
        bank ls = new bank(count,100,"女朋友");
        zs.start();
        ls.start();

    }
}

class Count {
     int money;
     String name;

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

class bank extends Thread {
     Count count;
     int drawingMoney;
     int nowMoney;

    public bank(Count count,int drawingMoney,String name){
        super(name);
        this.count = count;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        //取钱
        //synchronized默认锁this
        synchronized (count){
            if (count.money-drawingMoney < 0){
                System.out.println(this.getName()+"余额不足,取款未成功");
                return;
            }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count.money = count.money-drawingMoney;
                nowMoney = nowMoney+drawingMoney;
                System.out.println(count.name+"取走了"+count.money);
                System.out.println(this.getName()+"拥有"+nowMoney);

        }
    }
}

这里解释一下为什么使用同步块而不是锁方法:因为同步方法默认锁的是this,也就是本类(bank),而实际改变的是账户count,bank并没有改变。

ArrayList

ArrayList是线程不安全的,通过一个例子了解为什么说是不安全的

public class Unsafe3 {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {

                new Thread(()->{
                    list.add(Thread.currentThread().getName());
                }).start();
        }
        //Thread.sleep(1000);
        System.out.println(list.size());
    }
}

首先我们不加延时,发现输出的大小不为10000,你可能要说因为main线程跑完了,list这个线程还没有完,造成了结果不对。那我们加上延时多次运行发现还是有时候他不是10000。说明是不安全的!同一时间两个线程同时操作了一个位置,造成了数据的覆盖!

解决:在lambda表达式中加同步代码块

new Thread(()->{
    synchronized (list){
      list.add(Thread.currentThread().getName());
      }
 }).start();

CopyOnWriteArrayList

讲完线程不安全的,我们来了解一下线程安全的
CopyOnWriteArrayList 是JUC下的一个方法,在java.util.concurrent.CopyOnWriteArrayList包下

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

Lock锁

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock 接口 是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • 使用Lock锁通常使用它的实现类 ReentrantLock(可重入锁),ReentrantLock 类实现了 Lock,可以显式加锁、释放锁

用reentrantLock.lock();来加锁
reentrantLock.unlock(); 释放锁或者说关锁

我们在CopyOnWriteArrayList类的源码中也可以发现这个类

在这里插入图片描述

还是以买票为例

public class TestLock {
    public static void main(String[] args) {
        sta sta = new sta();
        Thread t1 = new Thread(sta,"张三");
        Thread t2 = new Thread(sta,"李四");
        Thread t3 = new Thread(sta,"黄牛");
        t1.start();
        t2.start();
        t3.start();

    }

}

class sta implements Runnable{
    private int ticket = 10;
    boolean flag = true;
    private final ReentrantLock lock = new ReentrantLock();

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

    private void buy(){

        try {
            Thread.sleep(400);
            //加锁
            lock.lock();
            if (ticket <= 0){
                flag = false;
                return;
            }
            System.out.println(Thread.currentThread().getName()+"获得了第"+ticket--+"张票");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //释放锁
            lock.unlock();
        }

    }
}

synchronized 与 Lock 的对比

  • Lock是显式锁(手动开启和关闭锁,不要忘记关锁!!!),synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,jvm将花费较短时间来调度线程,性能更好。并且具有更好的扩展性(有更多的子类)
  • 优先使用顺序:
    • Lock>同步代码块>同步方法

六、死锁

死锁:多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源,而导致两个线程或者多个线程都在等待对方释放资源,都停止执行的情形。若无外力作用他们都无法进行下去。

在这里插入图片描述

死锁形成条件:某一个同步块同时拥有两个以上对象的锁时,就可能发生“死锁问题”。也就是两个线程在各自拥有锁的情况下,又去尝试获取对方的锁,从而造成的一直阻塞的情况。

举个简单的例子:有A、B两个小孩,A小孩拥有一辆玩具车,B小孩拥有一把玩具枪,他们都想要对方手里的玩具但是不肯交换。这时就造成了死锁!

代码演示

a,b两个女孩抢 口红和镜子两个资源,一个拥有镜子想要口红,一个拥有口红想要镜子

public class DeadLock {
    public static void main(String[] args) {
        MakeUp g1 = new MakeUp(0, "a");
        MakeUp g2 = new MakeUp(1, "b");
        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;

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

    @Override
    public void run() {
        if (choice == 0) {
            synchronized (lipstick) {
                System.out.println(Thread.currentThread().getName() + "拿到了" + "口红");
               
                synchronized (mirror) {
                    System.out.println(Thread.currentThread().getName() + "拿到了" + "镜子");
                }
            }
        }else {
        synchronized (mirror) {
            System.out.println(Thread.currentThread().getName() + "拿到了" + "镜子");
           
            synchronized (lipstick) {
                System.out.println(Thread.currentThread().getName() + "拿到了" + "口红");
                }
            }
        }
    }
}

程序卡在这里,不停止。
在这里插入图片描述

解决:当互相想要对方手里的资源时,双方都释放锁。也就是互相交换!(获得了一把锁之后不再去请求获取另一把锁)

@Override
    public void run() {
        if (choice == 0) {
            synchronized (lipstick) {
                System.out.println(Thread.currentThread().getName() + "拿到了" + "口红");
            }

            synchronized (mirror) {
                System.out.println(Thread.currentThread().getName() + "拿到了" + "镜子");
            }
        }else {
            synchronized (mirror) {
                System.out.println(Thread.currentThread().getName() + "拿到了" + "镜子");
            }
            synchronized (lipstick) {
                System.out.println(Thread.currentThread().getName() + "拿到了" + "口红");
            }
        }
    }

在这里插入图片描述
现在的执行流程:

  • a拿到口红,b拿到镜子
  • a释放口红,b释放镜子
  • a拿到镜子,b拿到口红
  • 然后都释放程序结束

死锁产生的原因:

产生死锁的四个必要条件:
① 互斥条件:一个资源只能被一个线程占有,当这个资源被占用后其他线程就只能等待。
②不可剥夺条件:当一个线程不主动释放资源时,此资源一直被拥有线程占有。
③请求并持有条件:线程已经拥有一个资源后仍然不满足,又尝试请求新的资源。
④环路等待条件:产生死锁一定是发生了线程资源环路链。

改变死锁中的任意一个或多个条件就可以解决死锁问题!

七、线程协作(生产者消费者模式)

线程通讯

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

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

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

Java提供了几个方法解决线程之间的通信问题!
在这里插入图片描述
均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常IllegalMonitorStateException

生产者消费者问题

管程法:

生产者 : 负责生产数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;

消费者 : 负责处理数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;

缓冲区 : 消费者不能直接使用生产者的数据 , 他们之间有个 “ 缓冲区

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

代码示例:

//管程法
public class TestPC {
    public static void main(String[] args) {
        Cache cache = new Cache();
        Producer producer = new Producer(cache);
        Customer customer = new Customer(cache);
        producer.start();
        customer.start();
    }

}
//生产者
class Producer extends Thread{
    Cache cache ;

    public Producer(Cache cache){
        this.cache = cache;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产了"+i+"只鸡");
            cache.produce(new Foods(i));
        }
    }
}
//消费者
class Customer extends Thread{
    Cache cache ;

    public Customer(Cache cache){
        this.cache = cache;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了第"+cache.buy().id+"只鸡");
        }
    }
}

class Foods{
    int id;
    public Foods(int id){
        this.id = id;
    }
}

//缓冲区
class Cache{
    Foods[] foods = new Foods[10];
    int count = 0;

    public synchronized void produce(Foods food){
        while (count == foods.length){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        foods[count] = food;
        count++;
        //缓冲区有food了即可通知消费者消费
        this.notifyAll();
    }

    public synchronized Foods buy(){
        while (count == 0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;
        Foods food = foods[count];
        this.notifyAll();
        return food;
    }
}

信号灯法

就像日常生活中的红绿灯,红灯停绿灯行。当flag=true时生产者生产,消费者等待,flag=false时消费者消费,生产者等待

//信号灯法
public class TestPC2 {
    public static void main(String[] args) {
        Tv tv = new Tv();
        Player player = new Player(tv);
        Watcher watcher = new Watcher(tv);
        player.start();
        watcher.start();
    }

}
//生产者
class Player extends Thread{
    Tv tv;
    public Player(Tv tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i%2==0){
               this.tv.show("魔术");
            }else {
                this.tv.show("科学");
            }
        }
    }
}
//消费者
class Watcher extends Thread{
    Tv tv;
    public Watcher(Tv tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            this.tv.watch();
        }
    }
}
//产品
class Tv{
    String play;
    boolean flag =true;

    public synchronized void show(String play){
        while (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("表演者表演"+play);
        this.notifyAll();
        this.play = play;
        this.flag = !this.flag;


    }
    public synchronized void watch(){
        while (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众收看"+play);
        this.notifyAll();
        this.flag = !this.flag;
    }
}

注意对于生产者和消费者问题,最好不要用if判断,用if判断的话,唤醒后线程会从wait之后的代码开始运行,不会重新判断if条件,直接继续运行if代码块之后的代码,多个线程中会出现脏读的情况,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
if判断的叫虚假唤醒。

线程池法

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。(使用线程池可以提高性能!)
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。
  • 可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

线程池的好处:

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

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

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

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

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.shutdown();
    }
}
class MyThread implements Runnable{

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

八、补充

transient和volatile关键字是多线程看源码时见过很多所以补充一下

transient关键字

transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。

想了解关于序列化与反序列化的知识点这里

为什么要使用transient关键字

在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。

transient使用总结

  • 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法被访问。
  • transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
  • transient一般在实现了Serializable接口的类中使用。
  • 一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。

volatile关键字

问:请谈谈你对volatile的理解?
答:volatile是Java虚拟机提供的轻量级的同步机制,它有3个特性:
1)保证可见性
2)不保证原子性
3)禁止指令重排

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值