我是靠谱客的博主 乐观宝马,最近开发中收集的这篇文章主要介绍小白看这篇多线程就够了,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

多线程

目录

    • 多线程
      • 一、多线程概述
        • 1.进程和线程
          • 1.1进程与线程的关系
        • 2.分析有几个线程
        • 3.实现线程的两种方式
          • 3.1 第一种方式
          • 3.2 第二种方式(建议使用)
          • 3.3 第三种方式(新特性)
        • 4.线程的生命周期
        • 5.获取线程信息
        • 6.sleep方法
          • 6.1 sleep方法的面试题
        • 7.interrupt方法
        • 8.终止线程
          • 8.1 强行终止线程
          • 8.1 合理的终止线程
        • 9.线程调度
          • 9.1 常见的线程调度
          • 9.2 与线程调度有关的方法
            • 9.2.1 getPriority
            • 9.2.2 setPriority
            • 9.2.3 yield
            • 9.2.4 join
        • 10.守护线程
          • 10.1 守护线程概述
          • 10.2 实现守护线程
        • 11.定时器
          • 11.1 定时器概述
          • 11.2 实现定时器
      • 二、线程安全
        • 1.线程同步机制
        • 2.编程模型
        • 3.账户取钱(多线程并发)
        • 4.synchronized(排他锁)理解
        • 5.synchronized面试题
        • 6.锁池(lockpool)
        • 7.死锁
        • 9.解决线程同步方法
        • 8.变量类型
      • 三、生产者和消费者
        • 1.wait方法
        • 2.notify方法
        • 3.生产者和消费者模式

一、多线程概述

1.进程和线程

​ 进程是一个应用程序,线程是一个进程中的执行场景/执行单元,一个进程可以启动多个线程,以下为一个例子:

​ 对于java程序来说,在DOS窗口中输入:java HelloWord回车之后:

​ 1.会启动JVM,JVM就是一个进程

​ 2.JVM再启动一个主线程调用mainfangfa

​ 3.JVM再启动一起垃圾回收线程负责看护,回收垃圾。

​ 对于以上这个例子中至少有两个线程,一个是主线程,另一个是垃圾回收线程。

1.1进程与线程的关系

​ 对于进程与线程的关系,可以举以下这个栗子:

阿里巴巴:进程
	马云:阿里巴巴的一个线程
	童文红:阿里巴巴的一个线程
	
京东:进程
	刘强东:京东的一个线程
	奶茶:京东的一个线程

​ 因此,进程可以看作现实生活中的一个公司,线程可以看作是公司中的某个员工,进程A和进程B的内存独立不共享。

​ 在java语言中,线程A和线程B的堆内存和方法区内存是共享的,栈内存独立不共享,一个线程一个栈

​ 假设一个进程中启动10个线程,则会有10个栈空间,每个栈之间独立工作,互不干扰,这就是多线程并发。

火车站可以看作是一个进程。
	火车站中的每一个窗口看作是一个线程,大家可以在不同的窗口买票,不需要排队,所以多线程并发可以提高效率。

​ java中之所以有多线程机制,目的就是为了提高程序的处理效率。

2.分析有几个线程

​ 在不考虑垃圾回收线程条件下,分析以下代码有几个线程:

public static void main(String[] args) {
    System.out.println("main方法begin");
    m1();
    System.out.println("main方法over");
}

private static void m1() {
    System.out.println("m1方法begin");
    m2();
    System.out.println("m1方法over");
}

private static void m2() {
    System.out.println("m2方法begin");
    m3();
    System.out.println("m2方法over");
}

private static void m3() {
    System.out.println("m3方法execute");
}
}

​ 这个程序除过垃圾回收线程之外,只有一个线程,就是主线程,主线程main调用m1方法,m1方法调用m2方法,m2方法调用m3方法,在一个栈中自上而下逐行执行。

3.实现线程的两种方式

​ java支持多线程机制,并且将多线程实现了,我们只需要继承即可。

3.1 第一种方式

​ 第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。

​ 在使用这个方法时,需要注意以下问题:

  • start方法作用:启动一个分支线程,在JVM中开辟一个新的栈空间,开辟完栈空间之后立即结束。
  • 要启动多线程必须使用start()方法,如果只使用run()方法,那么run方法也在主线程的主栈中进行压栈,是个单线程的
  • 启动成功分支线程会自动调用类中的run()方法,run()方法在分支栈中的底部(压栈)。
  • run方法在分支栈的底部,main方法在主栈的底部,run和main是评级的。

以下是代码:

public class Test01 {
//这里是main方法,这里的代码属于主线程,在主栈中运行。
public static void main(String[] args) {
    //新建一个分支线程
    myThread myThread=new myThread();
    //开启线程
    //start()方法的作用:启动一个分支线程,在JVM中开辟一个新的空间,这段代码完成之后就瞬间结束了。
    //这段代码的任务就是开辟一个新的栈空间,只要新的栈空间开出来,这个方法就立即结束了,线程就启动成功。
    //启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)
    //run方法在分支栈的最底部,main方法在主栈的最底部,run和main是平级的。
    myThread.start();
    //以下这个代码是运行在主线程中的
    for(int i=0;i<10;i++){
        System.out.println("主线程---->"+i);
    }
}
}

//启动多线程的类需继承java.lang.Thread类,并重写run方法
class myThread extends Thread{
@Override
public void run() {
    for (int i=0;i<100;i++){
        System.out.println("分支线程--->"+i);
    }
}
}

​ 由于start()方法很快就结束,只是开辟一个分支栈,然后主栈和分支栈开始同时执行(并发)在主栈中start()方法瞬间结束,往下走开始循环,分支线程需要压栈run方法开始执行,结果如下:
在这里插入图片描述

3.2 第二种方式(建议使用)

​ 第二种实现线程的方式是定义类实现Runnable接口,实现run方法,但这并不是线程类,而是普通的实现类,需要将实现类对象封装在Thread对象中,然后实现线程,代码如下:

public class Test01 {

    public static void main(String[] args) {
        //创建一个可实现类对象
        myRunnable mr=new myRunnable();
        //将可实现类对象封装到线程对象中
        Thread t=new Thread(new myRunnable());
        //开启线程
        t.start();

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

//这不是一个线程类,只是一个实现类
class myRunnable implements Runnable{
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println("支线程--->"+i);
        }
    }
}

​ 使用匿名内部类使用这个方法:

public static void main(String[] args) {
    //使用匿名内部类的方式进行构建线程对象
    Thread t=new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i=0;i<10;i++){
                System.out.println("分支线程--->"+i);
            }
        }
    });
    //启动线程
    t.start();

    for (int j=0;j<10;j++){
        System.out.println("主线程--->"+j);
    }
}
3.3 第三种方式(新特性)

​ 这个方式是JDK的新特性,实现Callable接口,这种方式可以获取线程的返回值,上面两种是没法获取线程的返回值,因为run()方法返回void。

​ 系统委派一个线程去执行一个任务,当任务执行完毕之后,可能会返回一个执行结果,而前两种实现线程的方式都不行,但是实现Callable接口可以返回线程执行结果。

​ 【注意】:当获取线程结果时可能会导致“当前线程”阻塞,效率比较低,“当前线程”要获取线程结果时会等待call()方法执行完毕之后!

public static void main(String[] args) throws Exception{
    //创建一个"未来任务"对象,参数非常重要,需要传参Callable类型对象
    //这块使用匿名内部类的方法传参
    FutureTask task=new FutureTask(new Callable() {
        @Override//这里的call()方法就相当于run()方法
        public Object call() throws Exception {
            System.out.println("call method begin!");
            Thread.sleep(1000*10);
            System.out.println("call method over!");
            int i=100;
            int j=100;
            return i+j;
        }
    });

    //创建一个线程对象
    Thread t=new Thread(task);
    //启动线程
    t.start();
    //目前是在main线程中,在main线程中怎么获取t线程的返回结果
    //get()方法可能会导致“当前线程”的阻塞
    Object result = task.get();

}
4.线程的生命周期

  • 新建状态:就是刚new出线程对象的时候。

  • 就绪状态:调用start()方法之后,进入就绪状态,就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺CPU时间片的权利(CPU时间片就是执行权)。当一个线程抢夺到CPU时间片之后,就开始执行run()方法,run()方法的开始执行标志着线程进入运行状态。

  • 运行状态:run()方法的开始执行,标志着这个线程进入运行状态,当之前占有的CPU时间片用完了之后,会重新回到就绪状态抢夺CPU时间片,当再次抢夺到CPU时间片之后,会重新进入run()方法接着上一次的代码接着执行。

  • 阻塞状态:当线程处在运行状态执行代码时,若有一个需要用户从键盘输入的代码,这个时候线程就出现了阻塞状态,此时线程会放弃自己的CPU时间片。阻塞解除之后,因为线程之前进入阻塞状态放弃了CPU时间片,所以此时线程会进入就绪状态抢夺CPU时间片。

  • 死亡状态:当一个线程的run()方法执行结束之后,这个线程就进入死亡状态。
    在这里插入图片描述

5.获取线程信息

​ 线程的默认名称规律为:Thread-0,Thread-1,Thread-2…

​ 获取线程信息包括:获取线程对象、获取线程名字、修改线程名字,以下代码均已实现:

public class Test01{
    public static void main(String[] args) {
        //创建线程对象t1
        Thread t1=new Thread(new myThread());
        //创建线程对象t2
        Thread t2=new Thread(new myThread());
        //修改线程t1对象名称为"t1"
        t1.setName("t1");
        //修改线程t2对象名称为"t2"
        t2.setName("t2");
        //分别输出线程t1和t2对象名称
        System.out.println("t1名称为:"+t1.getName()+",t2名称为:"+t2.getName());
        //开启线程t1
        t1.start();

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

class myThread implements Runnable{
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println("分支线程--->"+i);
        }
    }
}

​ 获取线程对象的方法使用currentThread(),这是一个静态方法,获取当前线程对象的引用。若此方法在main中,只会显示出main中的线程,即主线程,主线程的名字就叫做"main",以下为使用方法:

public class Test01{

    public static void main(String[] args) {
        //创建一个线程对象t1
        Thread t1=new Thread(new myRunnable());
        //创建一个线程对象t2
        Thread t2=new Thread(new myRunnable());
        //开启线程t1
        t1.start();
        //开启线程t2
        t2.start();
    }
}

class myRunnable implements Runnable{
    @Override
    public void run() {
        //获取当前线程的对象
        Thread currentThread=Thread.currentThread();
        for (int i=0;i<10;i++){
            System.out.println(currentThread.getName()+"--->"+i);
        }
    }
}

6.sleep方法

​ 方法全称为:void sleep(long millis),这是一个静态方法,参数为毫秒,作用是让当前线程进入休眠,进入“阻塞状态”,放弃占有的CPU时间片,让给其他线程使用。这行代码出现在A线程中,就会让A线程休眠,出现在B线程中,就会让线程休眠,以下是使用方法:

public static void main(String[] args) {
        //获取当前线程对象
        Thread currentThread=Thread.currentThread();
        for (int i=0;i<10;i++){
            try {
                //让当前线程休眠1秒钟
                //当前线程就是main线程
                currentThread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(currentThread.getName()+"--->"+i);
        }
    }
6.1 sleep方法的面试题

​ 问题:以下"t.sleep()"代码会让线程t休眠吗?

public class Test01{
    public static void main(String[] args) {
        //创建一个线程对象t
        Thread t=new myThread();
        //将线程t的名字改为"t"
        t.setName("t");
        //开启线程t
        t.start();
        try {
            //注意:这块休眠是主线程休眠,sleep方法是静态方法,使当前线程(即main线程)休眠
            t.sleep(5*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("hello world!");
    }


}

class myThread extends Thread{
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}

​ sleep()方法是Thread类中的静态方法,使当前线程进入休眠状态,以上程序的当前线程是”main线程“,故不会使t线程进入休眠。当sleep方法在myThread类中的run方法中时,这时会使t线程进入休眠

​ 以上程序执行之后,t线程正常进行,main线程中的输出5s之后才会输出。

7.interrupt方法

​ interrupt()方法的原理是用异常处理的,使用interrupt()方法会使sleep终止,sleep终止导致抛出异常,然后抓住异常,休眠终止。

​ 若一个线程休眠时间过长,想要终止其休眠状态,则可用interrupt()方法终止其休眠,直接进行下面代码,以下为代码示例

public class Test01 {
    public static void main(String[] args) {
        //创建线程t
        Thread t=new Thread(new myThread());
        //原本线程t要休眠一个小时,现在想要终止其休眠状态
        t.interrupt();
        System.out.println("你好呀!");
    }
}

class myThread implements Runnable{
    @Override
    public void run() {
        try {
            //这块不能直接抛出异常,因为子类异常不能比父类异常更多
            //线程t休眠1个小时
            Thread.sleep(5*1000*3600);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("hello world!");
    }
}
8.终止线程

8.1 强行终止线程

​ 强行终止线程的方法为:stop()方法,直接使线程终止,不再进行,以下为代码示例:

public class Test01{
    public static void main(String[] args) {
        //创建一个线程t
        Thread t=new Thread(new myThread());
        //修改线程t的名字
        t.setName("t");
        //开启线程
        t.start();
        try {
            //main线程休眠5s
            Thread.sleep(5*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //强行终止线程t,这个方法已过时,不建议使用
        t.stop();
    }
}

class myThread implements Runnable{
    @Override
    public void run() {
        try {
            for (int i=0;i<10;i++){
                System.out.println(Thread.currentThread().getName()+"--->"+i);
                //输出完t线程需要10s
                Thread.sleep(1000);
            }

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

​ stop()方法已过时,这个方法的缺点是直接杀掉线程,容易丢数据,损坏数据。

8.1 合理的终止线程

​ 利用if…else语句对线程进行控制,在主线程中对if…else语句的判断条件进行修改,代码如下:

public class Test01{
    public static void main(String[] args) {
        //创建线程t
        myThread mt=new myThread();
        Thread t=new Thread(mt);
        //修改线程t名字为"t"
        t.setName("t");
        //开启线程t
        t.start();
        try {
            //main线程休眠5s
            Thread.sleep(5*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //直接给run赋值为false,可以直接终止线程t
        mt.run=false;
    }
}

class myThread implements Runnable{

    boolean run=true;
    @Override
    public void run() {
        if (run){
            for (int i=0;i<10;i++){
                System.out.println(Thread.currentThread().getName()+"--->"+i);
                try {
                    //每次循环休眠1s
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }else {

        }
    }
}
9.线程调度

9.1 常见的线程调度
  • 抢占式调度模型:哪个线程的优先级比较高,抢到的CPU时间片的概率就会高一些/大一些,java采用的就是抢占式调度模型。
  • 均分式调度模型:平均分配CPU时间片,每个线程占有的CPU时间片的时间长度是一样的,平均分配,一切平等。有一些编程语言的调度模式就是这种的。
9.2 与线程调度有关的方法

​ 线程的优先级一共有10个级别,最高级别是10,最低级别是1,默认级别是5。优先级高的会抢夺CPU时间片多一些即处于运行状态的时间多一些而不是说运行的先后

9.2.1 getPriority

​ 方法全称为:int getPriority(),返回该线程的优先级别,为实例方法,使用方法如下:

public class test {
    public static void main(String[] args) {
        //创建线程对象
        Thread t=new Thread(new thread());
        //改变线程t的名称
        t.setName("t");
        //开启线程
        t.start();
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
        //输出t线程的名称
        System.out.println(t.getName()+"--->"+t.getPriority());
        //输出main线程的名称
        System.out.println(Thread.currentThread().getName()+"--->"+t.getPriority());
    }
}

class thread implements Runnable{
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}
9.2.2 setPriority

​ 方法全称为:void setPriority(int newPriority),为实例方法,为线程设定优先级,使用方法如下:

public class test {
    public static void main(String[] args) {
        //创建线程对象
        Thread t=new Thread(new thread());
        //改变线程t的名称
        t.setName("t");
        //给t线程设置优先级
        t.setPriority(3);
        //给main线程设置优先级
        Thread.currentThread().setPriority(5);
        //输出t线程的优先级
        System.out.println(t.getName()+"的优先级为:"+t.getPriority());
        //输出main线程的优先级
        System.out.println(Thread.currentThread().getName()+"的优先级为:"+Thread.currentThread().getPriority());
        //开启线程
        t.start();
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}

class thread implements Runnable{
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}
9.2.3 yield

​ 方法全称为:static void yield(),为静态方法,暂停当前正在执行的线程,并执行其他线程。yield()方法不是阻塞方法,让当前线程让位,从运行状态回到就绪状态,重新获取CPU时间片。

【注意】:回到就绪状态之后有可能还会再次抢到CPU时间片。

public static void main(String[] args) {
        System.out.println("main begin!");
        //创建一个新线程t
        Thread t=new Thread(new Runnable());
        //给线程t改名
        t.setName("t");
        //开启线程
        t.start();
        for (int i=0;i<100;i++){
            //每当main线程到i%10=0时,会让位一次
            if (i%10==0){
                Thread.yield();//静态方法,使当前线程让位,从运行状态到就绪状态,重新抢夺CPU时间片
            }
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
        System.out.println("main over!");
    }
}

class Runnable implements java.lang.Runnable{
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}
9.2.4 join

​ 方法全称为:void join(),实例方法,当前线程与使用此方法的线程合并,并让当前线程阻塞,合并不是线程的栈合并只剩下一个,而是发生了等待关系,使用方法如下:

public static void main(String[] args) {
    System.out.println("main begin!");
    //创建一个新线程t
    Thread t=new Thread(new Runnable());
    //给线程t改名
    t.setName("t");
    //开启线程
    t.start();

    //线程合并
    try {
        t.join();//使线程t合并到main线程中,使main线程阻塞
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("main over!");
}
}

class Runnable implements java.lang.Runnable{
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}
10.守护线程

10.1 守护线程概述

线程分类

  • 用户线程:平时使用的线程都是用户线程
  • 守护线程:守护线程就是后台线程,其中最具有代表性的就是垃圾回收线程

【注意】:main线程也是一个用户线程。

守护线程特点

​ 一般守护线程是一个死循环,所有的用户线程都结束了,守护线程会自动结束。

守护线程用处

​ 每天00:00的时候需要备份数据,这个需要使用定时器,并且我们可以将定时器设置为守护线程,所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。

10.2 实现守护线程

​ 实现守护线程只需要使用setDaemon(true)方法即可:

public static void main(String[] args) {

    //创建线程
    Thread t1=new BakDataThread();
    //改名
    t1.setName("t1");
    //实现线程t1为守护线程
    t1.setDaemon(true);
    //开启
    t1.start();

    for (int i=0;i<10;i++){
        System.out.println(Thread.currentThread().getName()+"--->"+i);

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

}

class BakDataThread extends Thread{

public void run(){
    int i=0;
    while (true){
        System.out.println(Thread.currentThread().getName()+"--->"+(++i));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
}
11.定时器

11.1 定时器概述

定时器的作用

间隔特定的时间,执行特定的程序。比如每天银行要进行数据的备份,每周银行要进行银行账户的总账操作。

11.2 实现定时器

使用sleep方法

​ 可以使用sleep()方法睡眠固定的时间,醒来之后执行任务,这是最原始的定时器,比较low。

使用库中写好的定时器

​ 在Java的类库中已经有一个写好的定时器:java.util.Timer,可以直接拿来用。Spring框架中提供的SpringTask框架,这个框架只需要进行简单的配置,就可以完成定时器的任务,SpringTask框架的底层也是使用的这个定时器

public class TimerTest {
public static void main(String[] args) throws ParseException {
    //创建定时器对象
    Timer timer=new Timer();
    //安排任务
    //timer.schedule(执行任务,开始时间,执行时间间隔)
    //设置时间格式
    SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    //获取当前时间
    Date firstTime = sdf.parse("2020-10-19 23:05:00");
    //执行schedule方法
    timer.schedule(new LogTimerTask(),firstTime,1000*10);
}
}

//编写一个日志定时器
class LogTimerTask extends TimerTask {
@Override
public void run() {
    SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String time = sdf.format(new Date());
    System.out.println(time+"已经完成了一次日志记录");
}
}

在这里插入图片描述

二、线程安全

什么时候数据在多线程并发的环境下会存在问题?

三个条件:

  • 条件一:多线程并发
  • 条件二:有共享数据
  • 条件三:共享数据有修改的行为
1.线程同步机制

怎么解决线程安全问题?

​ 当多线程并发的环境下,有共享数据,并且这个共享数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?

​ 线程排队执行(不能并发),用排队执行解决线程安全问题,这种机制被称为线程同步机制。

线程同步就是线程排队执行了,线程排队就会牺牲一部分的效率,为了数据的安全,只有在数据安全的前提下才能使效率更佳。

2.编程模型

异步编程模型(并发)

​ 线程t1和线程t2,各自执行各自的,t1不管t2,t2也不管t1,谁都不需要等谁,其实就是多线程并发,这种效率较高。

同步编程模型(排队)

​ 线程t1和线程t2,线程t1执行的时候需要等到线程t2结束,或者线程t2执行的时候需要等线程t1结束,两个线程之间发生了线程等待关系,线程排队执行,这种效率较低。

3.账户取钱(多线程并发)

Account类(不适用线程同步机制)

//顾客,不使用线程同步机制,多线程并发
public class Account {
private String account;
private String password;
private Integer balance;

public Account() {
}

public Account(String account, String password, Integer balance) {
    this.account = account;
    this.password = password;
    this.balance = balance;
}

public String getAccount() {
    return account;
}

public void setAccount(String account) {
    this.account = account;
}

public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}

public Integer getBalance() {
    return balance;
}

public void setBalance(Integer balance) {
    this.balance = balance;
}

//取钱函数,money为取的钱数
public void drawMoney(int money){
    int before=this.getBalance();
    int after=before-money;
    this.setBalance(after);
}
}

取款线程

//线程类
public class ThreadSafe extends Thread{

private Account account;

public ThreadSafe(Account account) {
    this.account = account;
}

//取款开始执行
@Override
public void run() {
    //假设取款5000
    int money=5000;
    //取款
    account.drawMoney(money);
    System.out.println(Thread.currentThread().getName()+"线程的"+account.getAccount()+"账户取钱,还剩"+account.getBalance()+"元");

}
}

多线程取款测试

public static void main(String[] args) {
    //创建一个用户
    Account account=new Account("acc-01","acc-01",10000);
    //创建两个线程,用户执行取钱操作
    Thread t1=new ThreadSafe(account);
    Thread t2=new ThreadSafe(account);
    //设置Name
    t1.setName("t1");
    t2.setName("t2");
    //启动线程
    t1.start();
    t2.start();
}

不使用线程同步机制运行结果

​ 以上为不使用线程同步机制的取款方式,线程t1和线程t2可能同时进入run方法,也可能t1或t2任一个完了之后另一个再执行。若为前者就会出现线程并发出问题,结果如下:
在这里插入图片描述

取款Account(使用线程同步机制)

//顾客,使用线程同步机制
public class Account {
private String account;
private String password;
private Integer balance;

public Account() {
}

public Account(String account, String password, Integer balance) {
    this.account = account;
    this.password = password;
    this.balance = balance;
}

public String getAccount() {
    return account;
}

public void setAccount(String account) {
    this.account = account;
}

public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}

public Integer getBalance() {
    return balance;
}

public void setBalance(Integer balance) {
    this.balance = balance;
}

//取钱函数,money为取的钱数
public void drawMoney(int money){
    /**
     * 在此之后要使用线程同步机制,即线程排队
     * 一个线程把这些代码执行完毕之后另一个代码才能够继续执行
     * 线程同步机制的语法是:
     *                 synchronized(){
     *                     //线程同步代码块
     *                 }
     *              synchronized后面小括号中传的这个“数据”是相当重要的
     *              这个数据必须是线程中共享的数据,才能达到线程同步
     *              在这块线程t1和线程t2共享的数据就是账户对象,这里的账户对象就是this
     */
    synchronized (this){
        int before=this.getBalance();
        int after=before-money;
        try {
            //假设这块网络拥堵,延缓1s
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
    }

}
}

使用线程同步测试结果
在这里插入图片描述

4.synchronized(排他锁)理解

synchronized代码块

synchronized代码块的小括号中要放入共享数据,在java中每一个对象都有一把锁,将此对象放入synchronized小括号中时会占有这把锁,当第二个线程过来要修改此共享数据时发现这个锁已经被占有,待同步代码块结束之后这个锁才会被释放,第二个线程在同步代码块外边等待t1的结束,t1同步代码块中的代码结束之后,t2继续修改此共享数据。

​ 这样就达到了线程排队执行。需要注意的是,共享数据对象要选好,这个共享对象一定是你需要排队执行的这些线程对象所共享的。

synchronized修饰实例方法

​ 以上使用synchronized代码块就是为了余额balance不出现并发安全问题,那如果直接给Account类中取钱(drawMoney)方法前加关键字synchronized会正常吗?一下为改变后的Account类代码:

//取钱函数,money为取的钱数
//在取钱方法前加关键字synchronized
public synchronized void drawMoney(int money){
    int before=this.getBalance();
    int after=before-money;
    try {
        //假设这块网络拥堵,延缓1s
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    this.setBalance(after);
}

​ 在取钱方法前加关键字synchronized也可以解决线程并发安全问题,结果如下:
在这里插入图片描述

【缺点】:

  • 当synchronized加到实例方法之前,锁的是"this",没得挑!所以这种方式不灵活。
  • 当synchronized加到实例方法之前,线程同步的是整个实例方法的代码块,可能会无故扩大同步的范围,导致效率降低。

synchronized修饰静态方法

​ 当synchronized修饰静态方法时,会加类锁。在一个类中,类锁只有一个,而对象锁取决于new了多少个对象。

5.synchronized面试题

面试题1:doOther方法的执行需要等待doSome方法的结束吗?

public class exam01 {

//测试
public static void main(String[] args) {
    MyClass myClass=new MyClass();

    //创建线程t1和线程t2
    Thread t1=new MyThread(myClass);
    Thread t2=new MyThread(myClass);
    //改名
    t1.setName("t1");
    t2.setName("t2");
    //启动
    t1.start();
    try {
        //保证t1先执行
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t2.start();
}
}

//线程类
class MyThread extends Thread{

private MyClass myClass;

public MyThread(MyClass myClass) {
    this.myClass = myClass;
}

public void run(){
    if (Thread.currentThread().getName().equals("t1")){
        myClass.doSome();
    }
    if (Thread.currentThread().getName().equals("t2")){
        myClass.doOther();
    }
}
}

class MyClass{
    
public synchronized void doSome(){
    System.out.println("doSome begin");
    try {
        Thread.sleep(1000*10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("doSome over");
}

public void doOther(){

    System.out.println("doOther begin");
    System.out.println("doOther over");

}
}

​ 不需要等待,因为doOther()方法没有synchronized,没有被锁,不需要排队。

面试题2:当doOther()方法前有synchronized时,和题1一样的问题

public class exam01 {

    public static void main(String[] args) {
        MyClass myClass=new MyClass();

        //创建线程t1和线程t2
        Thread t1=new MyThread(myClass);
        Thread t2=new MyThread(myClass);
        //改名
        t1.setName("t1");
        t2.setName("t2");
        //启动
        t1.start();
        try {
            //保证t1先执行
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }


}

class MyThread extends Thread{

    private MyClass myClass;

    public MyThread(MyClass myClass) {
        this.myClass = myClass;
    }

    public void run(){
        if (Thread.currentThread().getName().equals("t1")){
            myClass.doSome();
        }
        if (Thread.currentThread().getName().equals("t2")){
            myClass.doOther();
        }
    }
}

class MyClass{

    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public synchronized void doOther(){

        System.out.println("doOther begin");
        System.out.println("doOther over");

    }
}

​ 需要等待。因为先执行doSome()方法,doSome()方法有synchronized,先会拿到myClass对象的锁,当执行dothter()方法时,线程t2进入锁池没有等到myClass对象的锁,只能等待t1线程释放了对象锁之后才可以执行。

面试题3:与题2条件、问题一样,但new两次MyClass

public class exam01 {

public static void main(String[] args) {
    MyClass myClass1=new MyClass();
    MyClass myClass2=new MyClass();

    //创建线程t1和线程t2
    Thread t1=new MyThread(myClass1);
    Thread t2=new MyThread(myClass2);
    //改名
    t1.setName("t1");
    t2.setName("t2");
    //启动
    t1.start();
    try {
        //保证t1先执行
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t2.start();
}


}

class MyThread extends Thread{

private MyClass myClass;

public MyThread(MyClass myClass) {
    this.myClass = myClass;
}

public void run(){
    if (Thread.currentThread().getName().equals("t1")){
        myClass.doSome();
    }
    if (Thread.currentThread().getName().equals("t2")){
        myClass.doOther();
    }
}
}

class MyClass{

public synchronized void doSome(){
    System.out.println("doSome begin");
    try {
        Thread.sleep(1000*10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("doSome over");
}

public void doOther(){

    System.out.println("doOther begin");
    System.out.println("doOther over");

}
}

​ 不需要等待。因为线程t1和t2分别传入了不同的myClass对象,有两把锁,而synchronized锁的是"this",锁的是不同的myClass对象,所以不会等待。

面试题4:条件和问题与题3相同,但doSome和doOther方法变为静态方法

public class exam01 {

public static void main(String[] args) {
    MyClass myClass1=new MyClass();
    MyClass myClass2=new MyClass();

    //创建线程t1和线程t2
    Thread t1=new MyThread(myClass1);
    Thread t2=new MyThread(myClass2);
    //改名
    t1.setName("t1");
    t2.setName("t2");
    //启动
    t1.start();
    try {
        //保证t1先执行
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t2.start();
}
}

class MyThread extends Thread{

private MyClass myClass;

public MyThread(MyClass myClass) {
    this.myClass = myClass;
}

public void run(){
    if (Thread.currentThread().getName().equals("t1")){
        myClass.doSome();
    }
    if (Thread.currentThread().getName().equals("t2")){
        myClass.doOther();
    }
}
}

class MyClass{

public synchronized static void doSome(){
    System.out.println("doSome begin");
    try {
        Thread.sleep(1000*10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("doSome over");
}

public synchronized static void doOther(){

    System.out.println("doOther begin");
    System.out.println("doOther over");

}
}

​ 需要等待。当synchronized修饰静态方法时,会加类锁,一个类中,类锁只有一个,所以只有当线程t1的doSome()方法执行之后释放了类锁,线程t2才可以执行doOther()方法。

6.锁池(lockpool)

​ 在上面4中的synchronized代码块中,当线程t1进入synchronized代码块中时,会占有共享对象锁,线程t2发现共享对象锁被占,就会从运行状态进入锁池找共享对象锁,线程t2进入锁池找共享对象的锁时会释放掉原先抢夺的CPU时间片,在锁池中找这个共享对象锁,有可能找到了,有可能没找到,找到了就会进入就绪状态重新抢夺CPU时间片,没找到的话就在锁池中等待。

7.死锁

死锁程序

/**
* 创建死锁
*/
public class deadLock01 {

public static void main(String[] args) {
    Object o1=new Object();
    Object o2=new Object();

    //创建线程t1和t2
    Thread t1=new MyThread1(o1,o2);
    Thread t2=new MyThread2(o1,o2);
    //改名
    t1.setName("t1");
    t2.setName("t2");
    //开启两个线程
    t1.start();
    t2.start();
}

}

class MyThread1 extends Thread {
Object o1;
Object o2;

public MyThread1(Object o1, Object o2) {
    this.o1 = o1;
    this.o2 = o2;
}

public void run(){
    //线程t1进入run方法之后首先会锁o1对象锁
    synchronized (o1){
        try {
            //睡眠1s,确保线程t2进入run方法并锁住o2对象锁
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //将o1对象锁住之后锁o2锁,但是o2锁被线程t2锁住了,锁池中没有o2对象锁,线程t1进入等待
        synchronized (o2){

        }
    }

}
}

class MyThread2 extends Thread {
Object o1;
Object o2;

public MyThread2(Object o1, Object o2) {
    this.o1 = o1;
    this.o2 = o2;
}

public void run(){
    //线程t2进入run方法之后首先会锁o2对象锁
    synchronized (o2){
        try {
            //睡眠1s,确保线程t1锁住o1对象锁
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //o1对象锁被锁住,线程t2锁住o2对象锁之后进入锁池找不到o1对象锁,进入等待
        synchronized (o1){
        }
    }

}
}

死锁解释(重点)

​ 如死锁程序,对象o1和o2是线程t1和t2共享的,线程t1的run()方法中先对o1对象上锁,再对o2对象上锁线程t2的run()方法中先对o2对象上锁,再对o1对象上锁

执行main()方法,线程t1开启,执行t1中的run()方法,对o1对象加锁,线程t1睡眠1s;线程t2开启,执行t2中的run()方法,对o2对象加锁,线程t2睡眠1s。当线程t1继续往下执行时,在锁池中找不到o2对象锁,进入等待;线程t2往下执行时,在锁池中找不到o1对象锁,进入等待,这就成为死锁了

9.解决线程同步方法

​ 以后程序中不可能一上来就使用线程同步机制(synchronized),这会让程序效率变低,用户体验不好。

方案1:尽量使用局部变量代替"实例变量"和"静态变量"

​ 局部变量在栈中,每一个线程对象都有单独的栈空间,因此局部变量不会有线程安全问题。

方案2:new多个对象

​ 如果必须是实例变量,那么可以考虑new多个对象,这样实例变量的内存就不共享了。(一个线程对应一个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了)

方案3:synchronized

​ 如果不能使用局部变量,也不能new多个对象,就只能选择synchronized了,线程同步机制。

8.变量类型

​ 在Java中一共有三类变量,分别是实例变量、静态变量和局部变量。

  • 实例变量:Student s=new Student(),在堆中存储

  • 静态变量:final static int a=10,在方法区中存储

  • 局部变量:方法中自定义的变量,在栈中存储

    堆和方法区只有一个,变量会共享,而栈有多个,不会共享,所以栈中的数据在多并发时是安全的,没有线程安全问题

    【注意】:常量也没有线程安全问题,因为常量不可被修改

三、生产者和消费者

1.wait方法

​ wait()方法不是线程对象的方法,是java中任何一个对象都有的方法。

wait方法作用

​ 当一个o对象调用wait()方法时,在这个对象上活动的线程t进入无限期等待,直到调用notify()方法,notify()方法的调用可以让正在o对象上正在等待的线程被唤醒。

方法原理

​ wait()方法会让在o对象上活动的线程进入等待状态,并且释放之前占有o对象的锁

2.notify方法

​ notify()方法也不是线程对象的方法,是java中任何一个对象的方法。

notify方法作用

​ 使用notify()方法可以让之前使用wait()方法的对象o当前线程被唤醒。还有一个notifyAll()方法,这个方法唤醒o对象上处于等待的所有线程。

方法原理

​ notify()方法只会通知,不会释放之前占有o对象的锁。

3.生产者和消费者模式

概述

​ 生产者和消费者模式是为了专业解决某个特定需求的。

​ 在供求关系中,若有一个线程t1负责生产,另外一个线程t2负责消费,最终要达到平衡,当生产满了就不能再生产了,需要消费;当消费完了就不能再消费了,需要生产。

​ 生产和消费的对象需要调用wait()和notify()方法,调用这两个方法需要建立在synchronized线程同步基础之上。

代码实现生产者和消费者模式

/*
实现生产者和消费者模式线程
仓库为容量为1
生产满了之后就要消费,消费完了之后就要生产
*/
public class ThreadTest03 {
public static void main(String[] args) {
    List list=new ArrayList();
    //创建生产者 线程t1
    Thread t1=new Thread(new Producer(list));
    //创建线程t2
    Thread t2=new Thread(new Consumer(list));
    //改名
    t1.setName("生产者线程");
    t2.setName("消费者线程");
    //开启线程
    t1.start();
    t2.start();
}
}

//生产线程
class Producer implements Runnable{

private List list;

public Producer(List list){
    this.list=list;
}
@Override
public void run() {
    //一直生产
    while (true){
        //给仓库加锁
        synchronized (list){
            //说明仓库已经有1个元素了
            if (list.size()>0){
                try {
                    //当前线程进入等待状态,并且释放掉list集合的锁
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //程序能够执行到这里说明仓库空了
            Object add=new Object();
            list.add(add);
            System.out.println(Thread.currentThread().getName()+"--->"+add);
            //唤醒消费者进行消费
            list.notify();
        }
    }
}
}

//消费线程
class Consumer implements Runnable{

//创建一个List集合
private List list;

public Consumer(List list){
    this.list=list;
}

@Override
public void run() {
    //一直消费
    while (true){
        //加锁
        synchronized (list){
            //说明仓库空了
            if (list.size()==0){
                try {
                    //消费者线程等待,释放掉list集合的锁
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //程序能够执行到这块,说明仓库满了,进行消费
            Object remove = list.remove(0);
            System.out.println(Thread.currentThread().getName()+"--->"+remove);
            //唤醒生产者进行生产
            list.notify();
        }
    }
}
}

最后

以上就是乐观宝马为你收集整理的小白看这篇多线程就够了的全部内容,希望文章能够帮你解决小白看这篇多线程就够了所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部