Java多线程(一篇从0讲透)

多线程

思维导图看天下:

image

1. 概述


并行与并发

并行 :指两个或多个事件在同一时刻发生(同时发生)

并发 :指两个或多个事件在同一个时间段内发生。(交替执行)


线程与进程

进程:是指一个内存中运行的程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程

记忆:进程的英文为Process,Process也为过程,所以进程可以大概理解为程序执行的过程。

(进程也是程序的一次执行过程,是系统运行程序的基本单位; 系统运行一个程序即是一个进程从创建、运行到消亡的过程)

线程:进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。【java默认有两个线程:main、GC】


进程与线程的区别

  • 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
  • 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多

2. 线程创建的五种方式

推荐使用Runnable接口的方式,因为Java是单继承的,所以使用Thread有OPP单继承局限性

image

2.1 背景介绍

线程类
Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例
每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码
Java使用线程执行体来代表这段程序流。


2.2 ① 继承Thread类

2.2.1 线程实现

1)实现步骤

  1. 继承Thread类的子类,并重写该类的run()方法(该run()方法的方法体就代表了线程需要完成的任务,因此run()方法称为线程执行体)
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

2)实现案例

自定义线程类:
image

主函数:
image

public static void main(String[] args) {
    MyThread myThread = new MyThread("MyThread");
    myThread.start();
    for (int i = 0;i<1000;i++){
        System.out.println("main"+i);
    }
}

执行结果:
image


3)执行过程分析

过程:程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建

随着调用Mt类的对象的start方法,另外一个新的线程也启动了 ,这样,整个应用就在多线程下运行。

运行时序图:
image

内存结构:
image


4)调用start和run方法的区别

image


2.2.2 构造方法

  1. public Thread()
    分配一个新的线程对象。
  2. public Thread(String name)
    分配一个指定名字的新的线程对象
  3. public Thread(Runnable target)
    分配一个带有指定目标新的线程对象
  4. public Thread(Runnable target,String name)
    分配一个带有指定目标新的线程对象并指定名字

2.2.3 常用方法

  1. public String getName() :获取当前线程名称。
  2. public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法
  3. public void run() :此线程要执行的任务在此处定义代码。
  4. public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  5. public static Thread currentThread() :返回对当前正在执行的线程对象的引用。

1)获取线程名称

  1. 可以使用Thread类中的方法getName,
    String getName() 返回该线程的名称。

  2. 可以先获取当前正在执行的线程,再调用getName方法获取线程名称

    static Thread currentThread() 返回对当前正在执行的线程对象的引用

    image

    //1.可以使用Thread类中的方法getName
    String name = getName();
    System.out.println(name);//创建时, 指定了名称,获取的就是指定的名称
    //如果没有指定名称,获取的就是Thread-0
    //2.可以先获取当前正在执行的线程
    Thread currentThread = Thread.currentThread();
    System.out.println(currentThread);//Thread[Thread-0,5,main]
    String name2 = currentThread.getName();
    System.out.println(name2);//Thread-0
    

2)设置线程名称

  1. 方法一:可以使用Thread类中的方法setName
    void setName(String name) 改变线程名称,使之与参数 name 相同。
MyThread myThread = new MyThread();
myThread.setName("myThreadName");
myThread.start();
  1. 方法二:添加一个带参构造方法,参数传递线程的名称;调用父类的带参构造方法,把名字传递给父类,让父亲给儿子起名字
    Thread(String name) 分配新的 Thread 对象。
public class MyThread extends Thread{
    //定义指定线程名称的构造方法
    public MyThread(String name) {
    super(name);
}

image


3)线程休眠

public static void sleep(long millis)

使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)睡醒了,继续执行

/*程序在执行第二秒时, 会暂停2秒,2秒后,继续执行后面程序*/
for (int i = 1; i <=60; i++) {
    System.out.println(i);
    /*让程序睡眠1秒钟   1秒=1000毫秒*/
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2.2.4 Thread构造方法底层原理——静态代理

只针对有参且参是Runnable类的构造方法:public Thread(Runnable target)

由于Thread和target顶层都是Runnable接口,所以Thread是使用了静态代理的方式代理参数target。


2.3 ② 实现Runnable接口

优势

  1. 避免单继承的局限性
    一个类继承了Thread类就不能继承其他的类
    一个类实现了Runnable接口,还可以继续继承别的类,实现其他的接口
  2. 增强了程序的扩展性,降低程序的耦合度
    使用Runnable接口把设置线程任务和开启线程相分离
    实现类当中,重写run方法,设置线程任务
    创建Thread类对象,调用 start方法,开启新线程

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享


2.3.1 实现步骤

1.创建一个RunnableImpl类实现Runnable接口
2.重写Runnable接口中的run方法,设置线程任务
3.创建Runnable接口的实现类RunnableImpl的对象t
4.创建Thread类对象,构造方法中传递Runnable接口的实现类RunnableImpl的对象t
5.调用Thread类中的start方法,开启新的线程,执行run方法
示例:

//实现Runnable接口
public class RunnableImpl implements Runnable{
    //2.重写Runnable接口中的run方法,设置线程任务
    @Override
    public void run() {
        //新线程执行的代码
        for (int i = 0; i <20; i++) {
            System.out.println(Thread.currentThread().getName()+"===>"+i);
        }
    }
}
public static void main(String[] args) {
        //3.创建Runnable接口的实现类对象
        RunnableImpl r = new RunnableImpl();
        //4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
        Thread t = new Thread(r);//打印20次i
        //5.调用Thread类中的start方法,开启新的线程,执行run方法
        t.start();  //【一般16-18行简写为:new Thread(r,"线程名").start();】
        //主线程开启新线程之后继续执行的代码
        for (int i = 0; i <20; i++) {
            System.out.println(Thread.currentThread().getName()+"===>"+i);
        }
    }

2.3.2 构造方法

  1. Thread(Runnable target) 分配新的 Thread对象
  2. Thread(Runnable target, String name) 分配新的 Thread对象【推荐该方法,因为可以自定义线程名】

2.4 ③ 实现Callable接口

十分重要,但本篇只简单介绍了一下,请去看下一篇JUC

2.4.1 实现步骤

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

代码:

public class MyCallableImpl implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        PictureCatch t = new PictureCatch();
        t.test(url,name);
        System.out.println("下载了文件名:"+name);
        return true;
    }

    String url; //网址
    String name;    //保存的文件名

    MyCallableImpl(String url,String name){
        this.url=url;
        this.name=name;
    }

    public static void main(String[] args) {
        MyCallableImpl t1 = new MyCallableImpl("https://img0.baidu.com/it/u=1151663768,725447312&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500","t4");
        MyCallableImpl t2 = new MyCallableImpl("https://img0.baidu.com/it/u=1648512719,1593015989&fm=253&fmt=auto&app=120&f=JPEG?w=891&h=500","t5");
        MyCallableImpl t3 = new MyCallableImpl("https://img2.baidu.com/it/u=863703859,746061395&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","t6");

        ExecutorService ser = Executors.newFixedThreadPool(3);
        Future<Boolean> result1 = ser.submit(t1);
        Future<Boolean> result2 = ser.submit(t2);
        Future<Boolean> result3 = ser.submit(t3);
        try {
            boolean r1 = result1.get();
            boolean r2 = result2.get();
            boolean r3 = result3.get();

            System.out.println(r1);
            System.out.println(r2);
            System.out.println(r3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        ser.shutdownNow();
    }


    class PictureCatch{
        void test(String url, String name){
            try {
                FileUtils.copyURLToFile(new URL(url), new File(name));
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("获取文件出错!");
            }
        }
    }
}

2.5 ④ 线程池Executor

这里就不讲了,放在了JUC并发编程那篇里详细讲解了


2.6 ⑤ Timer

使用 Timer 的方式如下:

public class MyTimer {

    public static void main(String[] args) {
        timer();
    }

    /**
     * 指定时间 time 执行 schedule(TimerTask task, Date time)
     */
    public static void timer() {
        Timer timer = new Timer();
        // 设定指定的时间time,此处为2000毫秒
        timer.schedule(new TimerTask() {
            public void run() {
                System.out.println("执行定时任务");
            }
        }, 2000);
    }

}

2.7 拓展:匿名内部类,实现Thread/Runnable多现程

2.7.1 匿名内部类

作用
把子类继承父类,重写父类的方法,创建子类对象,合成一步完成
把实现类实现接口,重写接口库的方法,创建实现类对象,合成一步完成
最终得要子类对象或实现类对象
格式

new 父类/接口(){
    重写父类/接口中的方法
};

2.7.2 Thread

image

public static void main(String[] args) {
    new Thread(){    //new 没有名称的类 继承Thread
        //重写run方法,设置线程任务
        @Override
        public void run() {
            for (int i = 0; i <20 ; i++) {
                System.out.println(Thread.currentThread().getName()+"==>"+i);
            }
        }
    }.start();
}

2.7.3 Runnable

image

new Thread(new Runnable() {  //new没有名称的类实现了Runnable接口
    //重写run方法,设置线程任务
    @Override
    public void run() { //实现接口当中run方法
        for (int i = 0; i <20 ; i++) {
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
}).start();

3. 线程使用

3.1 六种线程状态

  1. NEW(新建)
    线程刚被创建,但是并未启动。还没调用start方法
  2. Runnable(可运行)
    线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操 作系统处理器
  3. Blocked(锁阻塞)
    当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状 态;当该线程持有锁时,该线程将变成Runnable状态。
  4. Waiting(无限等待)
    一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。
    进入这个 状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
  5. Timed Waiting(计时等待)
    同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。
    这一状态 将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait
  6. Teminated(被终止)
    因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

image

image

3.2 线程的常用操作

线程方法

方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() 中断线程,别用这个方式
boolean isAlive() 测试线程是否处于活动状态

3.2.1 线程停止

  • 不推荐使用JDK提供的stop()、destory()方法。【已废弃】
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量,当flag=false(flag做while的条件),则终止线程运行

以下举例是使用一个标志位falg来终止变量

public class ThreadStopDemo implements Runnable {
    private boolean flag = true;
    @Override
    public void run() {
        int i = 1;
        while (flag) {
            System.out.println("run..."+(i++));
        }
    }
	
    //设置一个专门修改标志位的方法来停止线程
    public void stop(){
        flag = false;
    }

    public static void main(String[] args) {
        ThreadStopDemo demo = new ThreadStopDemo();
        new Thread(demo).start();
        for (int i = 1; i <= 500; i++) {
            System.out.println("main..."+i);
            if (i == 300) {
                demo.stop();
                System.out.println("线程该停止了");
            }
        }
    }
}

3.2.2 线程休眠_sleep

  • sleep(long millis) 指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptException,所以需要抛出异常
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时
  • 每一个对象都有一个锁,sleep不会释放锁

模拟倒计时+打印当前系统时间

public static void main(String[] args) {
    //模拟倒计时
    System.out.println("开始倒计时");
    int num = 10;
    while (true) {
        System.out.println(num--);
        Thread.sleep(1000);
        if (num <= 0) {
            break;
        }
    }
    System.out.println("开始报时");
    //打印当前系统时间
    int count = 10;
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
    while (true) {
        System.out.println(LocalDateTime.now().format(dateTimeFormatter));
        Thread.sleep(1000);
        count--;
        if (count <= 0) {
            break;
        }
    }
}

3.2.3 线程礼让_yield

礼让不一定成功,因为cpu重新调度,可能会再次选到之前的线程

  • Thread.yield();礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从内存中的运行状态转为就绪状态并拿出内存
  • 让cup重新调度选择线程进入内存,礼让不一定成功,看cup调度

代码演示:结果可能有三种:aabb,abab,abba

public class ThreadYieldDemo implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始!");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"结束!");
    }

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

3.2.4 线程强制执行_join

  • Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞(可以想象成插队)

代码演示:结果是正常排队执行到200后,得等强制执行走完200次后,才会继续执行正常排队201...

//插队线程
public class 线程强制执行 {

    public static void main(String[] args) {
        forceThread forceThread = new forceThread();
        Thread thread = new Thread(forceThread, "强制线程");


        for (int i = 0; i < 500; i++) {
            System.out.println("正常排队:"+i);
            if (i==200){
                thread.start();
                thread.join();
            }
        }
    }
}

class forceThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("强制执行——"+i);
        }
    }
}

3.2.5 线程状态查看_getState

六种线程状态看前面写的


代码演示:

public static void main(String[] args) {
    Thread thread = new Thread(()->{
        for (int i = 0; i < 20; i++) {
            Thread.sleep(500);
        }
    },"线程");


    System.out.println("线程start前的状态:"+thread.getState());
    thread.start();
    System.out.println("线程start后的状态:"+thread.getState());
    while (!Thread.State.TERMINATED.equals(thread.getState())){
        System.out.println("线程terminated之前的状态:"+thread.getState());
        Thread.sleep(500);
    }
    System.out.println("线程的状态:"+thread.getState());

}

结果:

线程start前的状态:NEW
线程start后的状态:RUNNABLE
线程terminated之前的状态:RUNNABLE
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:RUNNABLE
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:RUNNABLE
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:TIMED_WAITING
线程terminated之前的状态:RUNNABLE
线程terminated之前的状态:RUNNABLE
线程的状态:TERMINATED


3.2.6 线程优先级_Priority

源码中所有线程的优先级默认为5

优先级的设置建议在start()调度前

优先级低只是意味着获取调度的概率低,并不是优先级低就不会被调用了,这都是看cup的调度

  • java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程
  • 线程的优先级用数字表示,范围从1~10【越大优先级最高】
    • Thread.MIN_PRIORITY =1
    • Thread.MAX_PRIORITY =10
    • Thread.NORM_PRIORITY =5
  • 使用以下方式改变或获取优先级
    • getPriority() setPriority(int x)

代码演示:

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

        MyPriority myPriority = new MyPriority();

        Thread t1 = new Thread(myPriority,"线程1");
        Thread t2 = new Thread(myPriority,"线程2");
        Thread t3 = new Thread(myPriority,"线程3");
        Thread t4 = new Thread(myPriority,"线程4");
        Thread t5 = new Thread(myPriority,"线程5");
        Thread t6 = new Thread(myPriority,"线程6");

        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(7);
        t3.setPriority(Thread.MAX_PRIORITY);
        t4.setPriority(4);
        t5.setPriority(9);
        t6.setPriority(2);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
    }
}

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


3.2.7 守护线程_daemon

  • 线程分为用户线程和守护线程(daemon)

  • 虚拟机需要确保用户线程执行完毕

  • 虚拟机不用等待守护线程执行完毕(如,后台记录操作日志,监控内存,垃圾回收等)

    也就是说可以做到主线程结束了,但守护线程还没结束


代码演示:

public class 守护线程 {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread godThread = new Thread(god, "守护线程");
        Thread youThread = new Thread(you, "普通线程");
        godThread.setDaemon(true);	//设置为守护线程

        godThread.start();
        youThread.start();
    }

}

class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("我是上帝,是你的守护线程");
        }
    }
}

class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {
            System.out.println("我是普通人,只有三万多天的日子");
        }
        System.out.println("======一个月完美的结束了======");
    }
}

3.2.8 线程存储ThreadLocal

用于存储一个线程专有的值【对象方法】

ThreadLocal类,来创建工作内存中的变量,它将我们的变量值存储在内部(只能存储一个变量),不同的变量访问到ThreadLocal对象时,都只能获取到自己线程所属的变量。【每个线程的工作内存空间不同,所以线程之间相互独立,互不相关】

public static void main(String[] args) throws InterruptedException {
    ThreadLocal<String> local = new ThreadLocal<>();  //注意这是一个泛型类,存储类型为我们要存放的变量类型
    Thread t1 = new Thread(() -> {
        local.set("lbwnb");   //将变量的值给予ThreadLocal
        System.out.println("线程1变量值已设定!");
        try {
            Thread.sleep(2000);    //间隔2秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程1读取变量值:");
        System.out.println(local.get());   //尝试获取ThreadLocal中存放的变量
    });
    Thread t2 = new Thread(() -> {
        local.set("yyds");   //将变量的值给予ThreadLocal
        System.out.println("线程2变量值已设定!");
    });
    t1.start();
    Thread.sleep(1000);    //间隔1秒
    t2.start();
}

//结果:lbwnb。就算t2也设置了值,但不影响t1的值

拓展:子类线程也获得不了父类线程设置的值,但可以通过用InheritableThreadLocal方法来解决这个问题。(在InheritableThreadLocal存放的内容,会自动向子线程传递)

public static void main(String[] args) {
    ThreadLocal<String> local = new InheritableThreadLocal<>();
    Thread t = new Thread(() -> {
       local.set("lbwnb");
        new Thread(() -> {
            System.out.println(local.get());
        }).start();
    });
    t.start();
}

3.2.9 等待与唤醒

  1. 等待wait和唤醒notify、notifyall都需要在同步代码内(锁方法 or 锁代码块)
  2. 等待和唤醒只能由锁对象调用。(锁代码块的锁对象容易看出,锁方法的锁对象一般是this或方法所在的类)

public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用.

public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.会继续执行wait()方法之后的代码

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

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


示例:

顾客与老板线程:
创建一个顾客线程(消息者):告诉老板要吃什么 调用wait方法,放弃cpu的执行,进入wating状态(无限等待)
创建一个老板线程(生产者):花5秒做好 做好后 调用notify方法 唤醒顾客 开吃

注意

  • 顾客与老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行同步使用的锁必须要保证唯一,
  • 只有锁对象才能调用wait和notify方法

顾客线程
image

老板线程
image

Object obj = new Object();
new Thread(){
    @Override
    public void run() {
        synchronized (obj){
            System.out.println("告诉老板要吃饺子");
            try {
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("做好===开始吃饺子");
        }
    }
}.start();
new Thread(){
    @Override
    public void run() {
        synchronized (obj){
            try {
                Thread.sleep(3000);
                System.out.println("老板饺子已经做好");
                obj.notify();//唤醒当前锁对象上的等待线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}.start();

3.2.10 小结

  1. 进入计时等待状态的两种方式

    • 使用sleep(long m)方法,在毫秒值结束后,线程睡醒,进入Runnable/Blocked状态(抱着锁睡觉,不放锁)

    • 使用wait(long m)方法wait方法如果在毫秒值结束之后,还没有被唤醒,就会自动醒来,进入Runnable/Blocked状态(等待的时候会释放锁)

  2. 两种唤醒的方法

    • public void notify()
      随机唤醒1个

    • public void notifyall()
      唤醒锁对象上所有等待的线程.


4. 线程安全

4.0 线程同步机制

多个线程操作同一个资源

并发:同一个对象被多个线程同时操作

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步。线程同步其实就是一个等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程时候完毕,下一个线程再使用。

线程同步

  • 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题
    • 一个线程持有锁会导致其他所有需要此锁的线程挂起
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题

4.1 什么是线程安全

多线程访问了共享的数据,就会产生线程的安全

举例:

  1. 多个窗口,同时卖一种票. 如果不进行控制, 可以会出现卖重复的现象
  2. 多个窗口,同时在银行同一账户取钱,银行不进行控制就会亏钱
  3. ArrayList线程不安全

4.1.1 买票问题

解决措施:可锁代码块可锁方法,后面的解决方案是以买票问题为例

代码演示:

//买票问题
public class UnsafeTicket implements Runnable{

    private static Boolean falg = true;
    private int ticket =10;//票数

    public static void main(String[] args) {
        UnsafeTicket demo1 = new UnsafeTicket();
        new Thread(demo1,"小红").start();
        new Thread(demo1,"小明").start();
        new Thread(demo1,"黄牛").start();
    }

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

    //买票
    public void buy(){
        //没票了就停止线程
        if (ticket<=0) {
            falg = false;
            return;
        }
        //还有票就继续买
        System.out.println(Thread.currentThread().getName()+"买到了第"+ticket+"张票");
        ticket--;
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果:会出现三个人同时去了第5张票、也可能会出现有人取0 -1张票


4.1.2 银行取钱问题

解决措施:使用代码块锁account

代码演示:

//银行取钱问题
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100, "结婚基金");
        Bank drawMoney1 = new Bank(account, 50, "新一");
        Bank drawMoney2 = new Bank(account, 100, "小兰");

        drawMoney2.start();
        drawMoney1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("卡内余额:" + account.money);
    }

}

//账户
class Account{
    int money;//账户内的钱
    String name;//卡名

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

//银行
class Bank extends Thread{
    Account account;//操纵的账户
    int drawingMoney;//取了多少钱

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

    @Override
    public void run() {
        drawing();
    }
    //取钱
    private void drawing() {
        if (this.account.money - drawingMoney < 0) {
            System.out.println("余额不足," + Thread.currentThread().getName() + "取钱失败");
            return;
        }
        //sleep可以提高问题的发生的概率!
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.account.money = this.account.money - drawingMoney;
        System.out.println(Thread.currentThread().getName() + "取了" + drawingMoney);
    }
}

结果:
image


4.1.3 ArrayList问题

解决措施:锁代码块

代码演示:

public class UnsafeArrayList {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                int x = 10;
                list.add(x++);
            },"list线程不安全").start();
        }
        System.out.println(list.size());
    }
}

结果:9997,少了三个,是因为前面插入数据的时候有三个下标被重复赋值,导致有三次赋值被覆盖了。


4.2 解决线程安全

锁类模板 和 锁用该类模板创建出来的对象 两者之间互不影响!

4.2.1 synchronized锁代码块

同步代码块synchronized的格式

synchronized(锁对象obj){
            出现安全问题的代码(访问了共享数据的代码)
 }

注意

1.锁对象可以是任意对象 new Person new Student ...(一般是锁变化的对象,需要增删改的对象)
2.必须保证多个线程使用的是同一个锁对象
3.锁对象的作用:把{}中代码锁住,只让一个线程进去执行

1)锁实例对象

适用于使用同一个Runnable对象创建多个线程的情况,不适用于多个Runnable对象分别创建多个线程的情况

作用范围是对象实例,不可跨对象,所以多个线程不同对象实例访问此方法,互不影响,无法产生互斥。由于本题抢票中是多个线程使用同一个Runnable对象,所以得到的锁是同一个对象产生的obj,可以实现线程隔离。但银行例子中是多个线程分别使用不同的Runnable对象,所以使用锁实例对象是没用的。

示例
image

public class TicketRunnableImpl implements Runnable {
    //定义共享的票源
    private int ticket = 100;
    private Object obj = new Object(); //锁对象
    //线程任务:卖票
    @Override
    public void run() {
        synchronized (obj){
            while (ticket > 0) {
                /*为了提高线程安全问题出现的几率
                  让线程睡眠10毫秒,放弃cpu的执行权*/
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //卖票操作,ticket--
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
            }
        }
    }
    
    
    public static void main(String[] args) {
        UnsafeTicket demo1 = new UnsafeTicket();
        new Thread(demo1,"小红").start();
        new Thread(demo1,"小明").start();
        new Thread(demo1,"黄牛").start();
    }
}

总结:

同步监视器的执行过程(同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class)

  • 第一个线程访问,锁定同步监视器,执行其中的代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问,处于阻塞状态,一直等待
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

2)锁类

适用于使用同一个Runnable对象创建多个线程的情况,也适用于多个Runnable对象分别创建多个线程的情况

虽然是通过对象访问的此方法,但是加锁的代码块是类级别的跨对象的,所以锁的范围是针对类,多个线程访问互斥。

public class SynchronizedDemo {
    // 代码块锁(类):锁的应用对象是User类,可以称之为类锁
    public void method2() {
        synchronized (User.class) {
            // TODO 业务逻辑
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo obj1 = new SynchronizedDemo();
        SynchronizedDemo obj2 = new SynchronizedDemo();
        new Thread(() ->{
            obj1.method2(); //代码块锁,后面是类,多线程访问互斥
        }).start();
        new Thread(() ->{
            obj2.method2();
        }).start();
    }
}

4.2.2 synchronized锁方法

锁的是this,也就是主方法里调用该方法的对象

同步方法解决线程安全的格式:

修饰符 synchronized 返回值类型 方法名(参数列表){
    出现安全问题的代码(访问了共享数据的代码)
}

使用步骤

1.创建一个方法,方法的修饰符添加上synchronized
2.把访问了共享数据的代码放入到方法中
3.调用同步方法

1)锁普通方法(对象锁)

适用于使用同一个Runnable对象创建多个线程的情况,不适用于多个Runnable对象分别创建多个线程的情况

普通方法作用范围是对象实例,不可跨对象,所以多个线程不同对象实例访问此方法,互不影响,无法产生互斥。由于本题抢票中是多个线程使用同一个Runnable对象,所以得到的锁是同一个类对象this,可以实现线程隔离。但银行例子中是多个线程分别使用不同的Runnable对象最后锁的this也是不同类对象的this,所以使用锁普通方法是没用的。

示例
image

@Override
public void run() {
    ticketMethods();
}
public synchronized void ticketMethods(){
    while (ticket > 0) {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //卖票操作,ticket--
        System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
        ticket--;
    }
}

锁对象是谁???
锁对象为this
image

public  void ticketMethods(){
    synchronized(this){
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卖票操作,ticket--
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

2)锁静态方法(类锁)

适用于使用同一个Runnable对象创建多个线程的情况,也适用于多个Runnable对象分别创建多个线程的情况

静态方法是通过类访问,是类级别的跨对象的,所以锁的范围是针对类,多个线程访问互斥。

示例:变化的量记得也要static
image

public class TicketRunnableImpl implements Runnable {
    //定义共享的票源
    private static int ticket = 100;
    private Object obj = new Object(); //锁对象
    //线程任务:卖票
    @Override
    public void run() {
        ticketMethods();
    }
    public static synchronized void ticketMethods(){
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卖票操作,ticket--
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

锁对象是谁???
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)
image


4.2.3 Lock锁

概述

  • 从jdk5.0开始,java提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前先获得Lock对象
  • 。ReetrantLock(可重入锁)类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常见的是ReetrantLock,可以显示加锁、释放锁

Lock接口中的方法

void lock() 获取锁。
void unlock() 释放锁。//如果有try/catch的话,一般unlock是放在finally里

使用步骤

1.在成员位置创建一个Lock接口的实现类对象ReentrantLock
2.在可能会出现安全问题的代码前,调用lock方法获取锁对象
3.在可能会出现安全问题的代码后,调用unlock方法释放锁对象

示例
image

public class TicketRunnableImpl implements Runnable {
    //定义共享的票源
    private  int ticket = 100;
    //1.在成员位置创建一个Lock接口的实现类对象ReentrantLock
    Lock l = new ReentrantLock();
    //线程任务:卖票
    @Override
    public void run() {
        while (true) {
            l.lock();
            if (ticket > 0){
                try {
                    Thread.sleep(10);
                    //卖票操作,ticket--
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //3.在可能会出现安全问题的代码后,调用unlock方法释放锁对象
                    l.unlock(); //无论程序是否异常,都会把锁对象释放,节约内存提高程序的效率
                }
            }
        }
    }
}

4.2.4 synchronized与Lock对比

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

4.2.5 判断锁的对象是谁

8锁现象

1)标准情况下,一个对象 两个同步方法 第一个线程先拿到锁 谁先执行

2)一个对象 两个同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行

3)一个对象 一个同步方法一个普通方法 第一个线程先拿到锁 谁先执行

4)两个对象 两个同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行

5)一个对象 两个静态同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行

6)两个对象 两个静态同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行

7)一个对象 一个静态同步方法一个普通同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行

8)两个个对象 一个静态同步方法一个普通同步方法 第一个线程先拿到锁 第一个方法延迟4S 谁先执行

package com.ambition;
import java.util.concurrent.TimeUnit;
/**
 * 同一个对象  两个线程  两个同步方法 谁先执行?
 **/
public class Question1 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> phone.sendMsg()).start();
//        延迟的目的是控制哪个线程先拿到锁
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> phone.call()).start();
    }
}
class Phone {
//    synchronized 锁的对象是方法的调用者
//    两个方法用的是同一个锁 谁先拿到谁先执行
    public synchronized void sendMsg() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }
    public synchronized void call() {
        System.out.println("打电话");
    }
}

小结:

  • 对于普通同步方法,锁是当前new实例对象。
  • 对于static 静态同步方法,锁是当前类模板的Class对象。

5. 生产者与消费者

5.1 问题介绍与分析

1.线程通信

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

image

2.线程通讯-分析

  • 这个一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
    • 对于生产者,没有生产产品之前,要通知消费着等待,而生产了产品之后,有需要马上通知消费者消费
    • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
    • 在生产者消费者问题中,仅有synchronized是不够的,就需要用到之前讲的等待与唤醒。
      • synchronized可以阻止并发更新同一个共享资源,实现了同步
      • synchronized不能用来实现不同线程之前的消息传递(通信)

5.2 解决方法

5.2.1 管程法

生产者——缓存区——消费者

并发协作模型”生产者/消费者模式“-->管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)
  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”

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


代码:

//餐厅模式:生产者————厨师、消费者————顾客
public class 管程法 {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Cousumer(container).start();
    }
}

/**
 * 生产者
 */
class Productor extends Thread {
    /**
     * 缓冲区
     */
    private SynContainer container;

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

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

/**
 * 消费者
 */
class Cousumer extends Thread {
    /**
     * 缓冲区
     */
    private SynContainer container;

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

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

}

/**
 * 鸡(食物)
 */
class Chicken {
    /**
     * 鸡的编号
     */
    private int id;

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

    public int getId() {
        return id;
    }
}

/**
 * 缓冲区
 */
class SynContainer {
    /**
     * 缓冲区的容器大小(十只鸡)
     */
    private Chicken[] chickens = new Chicken[10];
    /**
     * 计数器
     */
    private int count = 0;
    /**
     * 生产者往容器中放入产品
     */
    public synchronized void push(Chicken chicken) {
        //如果容器满了,生产者就需要等待消费者消费
        if (count == 10) {
            //生产者开始等待消费者消费
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果没有满,生产者往容器中继续放入产品
        chickens[count] = chicken;
        count++;
        System.out.println("生产了" + chicken.getId() + "只鸡");
        //生产者通知消费者消费
        this.notifyAll();
        //模拟生产者要休息一下下
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 消费者消费容器中的产品
     */
    public synchronized Chicken pop() {
        //消费者判断容器中是否有产品
        if (count == 0) {
            //消费者等待生产者生产
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //消费者开始消费容器中的产品
        count--;
        Chicken chicken = chickens[count];
        System.out.println("消费了" + chicken.getId() + "只鸡");
        //消费者通知生产者继续生产
        this.notifyAll();
        return chicken;
    }
}

5.2.2 信号灯法(常用)

flag标志位来告诉消费者继续/停止消费,告诉生产者继续/停止生产

并发协作模型”生产者/消费者模式“-->信号灯法


public class 信号灯法 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Actor(tv).start();
        new Audience(tv).start();
    }
}

//演员
class Actor extends Thread{
    TV tv = new TV();

    public Actor(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            if (i % 2 == 0) {
                tv.push("快乐大本营");
            } else {
                tv.push("抖音");
            }
        }
    }
}

//听众
class Audience extends Thread{
    TV tv = new TV();

    public Audience(TV tv) {
        this.tv = tv;
    }

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

//电视节目
class TV {
    String name;//节目名称
    boolean flag=true; //标志位 T生产  F观看

    //生产节目
    public synchronized void push(String name){
        //判断要不要生产
        //不生产就等待
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //生产
        this.name=name;
        flag=!flag;
        System.out.println("我生产了"+name);
        //生产完就唤醒观众
        this.notifyAll();
    }

    //消费节目
    public synchronized void pop(){
        //判断有没有节目看
        //没有节目看就等待
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //有节目就消费
        flag=!flag;
        System.out.println("我看完了"+name);
        //看完就让演员再演
        this.notifyAll();
    }
}

posted @ 2023-04-01 15:24  不吃紫菜  阅读(1086)  评论(0编辑  收藏  举报