小白看这篇多线程就够了

多线程

一、多线程概述

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();
        }
    }
}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值