Java多线程学习总结

JAVA多线程技术:

线程简介:
  • 线程和进程的区别:
    (0) 程序:程序是指令和数据的有序集合,其本身没有任何运 行的含义,是一个静态的概念。
    (1) 进程: 是正在执行的程序,是系统进行资源分配和调度的独立单位。每一个进程都有它自己的内存空间和系统资源。
    (2) 线程: 在一个进程内部又可以同时执行多个任务,而每一个任务我们就可以看做是一个线程。线程是程序使用CPU的基本单位。(一个进程至少要包含一个线程)

  • 多线程和多进程意义:
    (1) 多进程:单进程计算机只能做一件事情,所以我们常见的操作系统都是多进程操作系统。它可以“同时”让我们做多个事情。其实,计算机在某个时间点上只能做一件事情(针对于单CPU单核心情况下),不同的进程在计算机中频繁的切换运行,而且速度很快(参考操作系统中程序运行时间片的概念:每个程序都会分配一个时间片,运行完就会切换下一个接着运行!)从而使我们感觉程序之间是并行的关系。多进程配合多线程能够达到更复杂的执行效果。多进程的作用不是提高执行速度,而是提高CPU的使用率。(争取到的时间片次数会随着进程数量而增多,但这块儿我个人认为只能说在单核模式下才成立)
    (2)多线程:线程作为真正的执行实体。即使是在多进程的模式下,每个进程依旧有一个主线程来进行工作。多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。(有歧义)

    多线程作用示例图:
    在这里插入图片描述

  • 如何理解多进程/多线程的提高了程序的使用率:
    (a). 我们程序在运行的使用,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大.那么也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的使用率.但是即使是多线程程序,那么他们中的哪个线程能抢占到CPU的资源呢,这个是不确定的,所以多线程具有随机性。(是操作系统调用程序具有动态的复杂性,在我们看来更像是一种随机事件!)
    (b). 上面的说法在单CPU单核心模式下,的确是这样理解的。但在多CPU多核心模式下,就不完全正确了。在多核模式下,多线程/多进程不仅仅能够增加系统调用相关程序的概率,而且能够达到提高某些特定任务程序完成速度(不能说叫运行速度,运行速度是硬件限制吧!)。

  • 多线程和多进程的区别:(参考JAVA核心技术卷I)
    本质的区别在于每个进程拥有自己的一整套变量, 而线程则共享数据。 这听起来似乎有些风险, 的确也是这样, 在本章稍后将可以看到这个问题。然而,共享变量使线程之间的通信比进程之间的通信更有效、 更容易。 此外, 在有
    些操作系统中,与进程相比较, 线程更“ 轻量级”, 创建、 撤销一个线程比启动新进程的开销要小得多。

  • 并行和并发:
    a. 并发:并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象。
    b. 并行:并行的"同时"是同一时刻可以多个进程在运行(处于running)。
    c. 实际上,当程序中写下多进程或多线程代码时,这意味着的是并发而不是并行。并发是因为多进程/多线程都是需要去完成的任务,不并行是因为并行与否由操作系统的调度器决定,可能会让多个进程/线程被调度到同一个CPU核心上。只不过调度算法会尽量让不同进程/线程使用不同的CPU核心,所以在实际使用中几乎总是会并行,但却不能以100%的角度去保证会并行。也就是说,并行与否程序员无法控制,只能让操作系统决定。

  • Java程序运行原理:
    a. Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。
    b. 该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。
    c. 所以 main方法运行在主线程中。(在JAVA代码编写过程中,利用主线程运行代码从而创建出来的线程都叫做用户线程!)

  • JVM的启动是多线程的吗?
    JVM启动至少启动了垃圾回收线程主线程,所以是多线程的。

线程创建:(三种方法)
  • 线程创建的三种方法:
    在这里插入图片描述
Runnable 的创建以及使用方法:
  • 流程:
    1、实现Runnable接口
    2、重新run()方法
    3、实例化对象
    4、调用start()方法
  • Runnable 示例:
    public class HelloRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println("我是子线程:" + i);
            }
        }
    
        /**
         * 主方法/主线程
         * @param args
         */
        public static void main(String[] args) {
            // 实例化子线程
            // (new Thread(new HelloRunnable())).start();
            // new Thread(new HelloRunnable()).start();
            HelloRunnable helloRunnable = new HelloRunnable();
            Thread thread = new Thread(helloRunnable);
            thread.start();
            for (int i = 0; i < 3; i++) {
                System.out.println("我是主线程main:" + i);
            }
        }
    }
    ------------
    输出;
    我是主线程main:0
    我是主线程main:1
    我是主线程main:2
    我是子线程:0
    我是子线程:1
    我是子线程:2
    
  • 小结:一般情况下,在无需使用返回值的情况下。推荐使用该方法创建多线程程序。
继承Thread类的创建以及使用方法:
  • 创建以及使用过程:
    1、继承Thread类
    2、c重写run()方法
    3、实例化类
    4、调用start()方法
  • 示例:
    public class HelloThread extends Thread {
        /**
         * 重写run()方法
         */
        @Override
        public void run() {
            // super.run();
            for (int i = 0; i < 3; i++) {
                System.out.println("我是子线程:" + i);
            }
        }
    
        /**
         * 主方法/主线程
         * @param args
         */
        public static void main(String[] args) {
            // 实例化子线程
            // (new HelloThread()).start();
            // new HelloThread().start();
            HelloThread helloThread = new HelloThread();
            helloThread.start();
            for (int i = 0; i < 3; i++) {
                System.out.println("我是主线程main:"+i);
            }
        }
    }
    ----------
    输出:(无序,是灰常正常滴!因为根本无法猜测到底是如何调度产生的此结果,而且向"屏幕输出"本身也是一个临界资源的使用!)
    我是子线程:0
    我是主线程main:0
    我是主线程main:1
    我是主线程main:2
    我是子线程:1
    我是子线程:2
    
  • 小结:不建议使用:避免OOP单继承局限性。(不太理解这句话?)
  • 理解:是因为Callable是一个接口,而Thread是一个类,让一个多线程类的对象是去实现接口从而达到自定义线程对象的工作任务,因为接口本身可以实现多继承的特性,其灵活性比直接通过继承Thread从而改写run方法来的更为灵活有效。
使用Callable类的创建以及使用方法:

特点:前面集中创建多线程的方法,都是让线程去执行某个功能,执行完毕,线程也就销毁了,什么都没留下。但是这种创建多线程的方法。每个线程在运行完都可以自定义一个返回值,并且返回给主线程的调用方。

  • 创建以及使用过程:
    1、实现Callable接口
    2、重写call方法
    3、实例化类
    4、调用start()
    5、获取返回值
  • 示例:
    public class HelloCallable implements Callable {
        /**
         * 重写call方法
         *
         * @return
         * @throws Exception
         */
        @Override
        public Object call() throws Exception {
            int j = 0;
            for (int i = 0; i < 10; i++) {
                System.out.println("我是子线程" + i);
                j++;
            }
            return j;
        }
    
        /**
         * 主方法
         *
         * @param args
         */
        public static void main(String[] args) {
            // 实例化类
            HelloCallable helloCallable = new HelloCallable();
            FutureTask futureTask = new FutureTask(helloCallable);
            Thread thread = new Thread(futureTask);
            thread.start();
            try {
                // 获取返回值
                System.out.println("返回值:" + futureTask.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    
  • Runnable接口方法推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用。
线程状态:(五大状态)
  • 线程状态一览图:
    在这里插入图片描述

  • 线程状态(详细)转换图:
    在这里插入图片描述

  • 线程状态转换图:(附带各种操作后的状态)
    在这里插入图片描述

线程方法一览表:
方法说明
setPriority(int newPriority)更改线程的优先级
static void sleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠(休眠时不释放所占有的锁)
void join()等待该线程终止(线程插队,使得调用该方法的线程对象插入到当前开始执行)
static void yield()暂停当前正在执行的线程对象,并执行其他线程(线程礼让)
void interrupt()中断线程,别用这个方式(一般不用,采用标志位的方法结束线程!)
boolean isAlive()测试线程是否处于活动状态
void setDaemon(boolean on)将该线程标记为守护线程或用户线程
线程优先级:
  • 说明:Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度 器按照优先级决定应该调度哪个线程来执行。

  • 线程的优先级用数字表示,范围从1~10
    (1)Thread.MIN_PRIORITY = 1;
    (2)Thread.MAX_PRIORITY = 10;
    (3)Thread.NORM_PRIORITY = 5

  • 使用以下方式改变或获取优先级:

    getPriority();
    setPriority(int xxx)
    
  • 注意:
    (1) 优先级的设定建议在start()调度前
    (2) 优先级低只是意味着获得调度的概率低.并不是优先级低就不会被调用了.这都是看CPU的复杂的调度算法相关。

  • 示例:

    public class ThreadPriority implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("线程名称:"
                        + Thread.currentThread().getName() + ",线程优先级:"
                        + Thread.currentThread().getPriority() + "循环次数:" + i);
            }
        }
    
        public static void main(String[] args) {
            ThreadPriority threadPriority = new ThreadPriority();
            Thread thread1 = new Thread(threadPriority, "子线程1");
            thread1.setPriority(Thread.MIN_PRIORITY);
            thread1.start();
    
            Thread thread2 = new Thread(threadPriority, "子线程2");
            thread2.setPriority(Thread.MAX_PRIORITY);
            thread2.start();
        }
    }
    --------------------
    输出:
    线程名称:子线程2,线程优先级:10循环次数:0
    线程名称:子线程2,线程优先级:10循环次数:1
    线程名称:子线程2,线程优先级:10循环次数:2
    线程名称:子线程2,线程优先级:10循环次数:3
    线程名称:子线程2,线程优先级:10循环次数:4
    线程名称:子线程2,线程优先级:10循环次数:5
    线程名称:子线程2,线程优先级:10循环次数:6
    线程名称:子线程2,线程优先级:10循环次数:7
    线程名称:子线程2,线程优先级:10循环次数:8
    线程名称:子线程2,线程优先级:10循环次数:9
    线程名称:子线程1,线程优先级:1循环次数:0
    线程名称:子线程1,线程优先级:1循环次数:1
    线程名称:子线程1,线程优先级:1循环次数:2
    线程名称:子线程1,线程优先级:1循环次数:3
    线程名称:子线程1,线程优先级:1循环次数:4
    线程名称:子线程1,线程优先级:1循环次数:5
    线程名称:子线程1,线程优先级:1循环次数:6
    线程名称:子线程1,线程优先级:1循环次数:7
    线程名称:子线程1,线程优先级:1循环次数:8
    线程名称:子线程1,线程优先级:1循环次数:9
    
线程休眠:
  • 概念:
    (1). sleep (时间) 指定当前线程阻塞的毫秒数;
    (2). sleep存在异常InterruptedException;
    (3). sleep时间达到后线程进入就绪状态;(只是回到就绪状态,而不是回到运行状态!)
    (4). sleep可以模拟网络延时,倒计时等。
    (5). 每一个对象都有一个锁,sleep不会释放锁;

  • 示例:

    public class ThreadSleep implements Runnable {
        @Override
        public void run() {
            String info[] = {
                    "窗外风景美如画,",
                    "本想吟诗赋天下。",
                    "奈何自己没文化,",
                    "一句卧槽雪好大。"
            };
            for (int i = 0; i < info.length; i++) {
                System.out.println(info[i]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            ThreadSleep threadSleep = new ThreadSleep();
            Thread thread = new Thread(threadSleep);
            thread.start();
        }
    }
    ------------------
    输出:
    窗外风景美如画,
    本想吟诗赋天下。
    奈何自己没文化,
    一句卧槽雪好大。
    
线程插队:
  • 概念:Join合并线程,待此线程(执行该方法的线程对象)执行完成后,再执行其他线程,其他线程阻塞。(其他线程阻塞完毕后都会回到就绪状态中去!)
  • 特点:特点就是跟我们日常生活中的插队很像。
  • 示例:
    public class ThreadJoin implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("线程名称:"+Thread.currentThread().getName()+
                        "循环次数:"+i);
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            ThreadJoin threadJoin = new ThreadJoin();
            Thread thread1 = new Thread(threadJoin,"线程1");
            Thread thread2 = new Thread(threadJoin,"线程2");
            thread1.start();
    
    //        thread2.start();
            Thread.currentThread().setName("主线程");
            for (int i = 0; i < 20; i++) {
                if(i==10){
                    thread1.join(); // 主线程执行到第10次的时候,让子线程1来插队执行!
                }
                System.out.println("线程名称:"+Thread.currentThread().getName()+
                        "循环次数:"+i);
            }
    
        }
    }
    -----------
    输出:
    线程名称:主线程循环次数:0
    线程名称:主线程循环次数:1
    线程名称:主线程循环次数:2
    线程名称:主线程循环次数:3
    线程名称:主线程循环次数:4
    线程名称:主线程循环次数:5
    线程名称:主线程循环次数:6
    线程名称:主线程循环次数:7
    线程名称:主线程循环次数:8
    线程名称:主线程循环次数:9
    线程名称:线程1循环次数:0  (开始插队!)
    线程名称:线程1循环次数:1
    线程名称:线程1循环次数:2
    线程名称:线程1循环次数:3
    线程名称:线程1循环次数:4
    线程名称:线程1循环次数:5
    线程名称:线程1循环次数:6
    线程名称:线程1循环次数:7
    线程名称:线程1循环次数:8
    线程名称:线程1循环次数:9
    线程名称:主线程循环次数:10
    线程名称:主线程循环次数:11
    线程名称:主线程循环次数:12
    线程名称:主线程循环次数:13
    线程名称:主线程循环次数:14
    线程名称:主线程循环次数:15
    线程名称:主线程循环次数:16
    线程名称:主线程循环次数:17
    线程名称:主线程循环次数:18
    线程名称:主线程循环次数:19
    
线程礼让:
  • 概念:
    (1). 礼让线程,让当前正在执行的线程暂停,但不阻塞
    (2). 将线程从运行状态转为就绪状态(礼让后的线程也会回到就绪状态,于被礼让的线程一同争夺CPU的使用权)
    (3). 让cpu重新调度,礼让不一定成功!要看CPU如何进行调度!
  • 示例:
    public class ThreadYield 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) {
            ThreadYield threadYield = new ThreadYield();
            Thread thread1 = new Thread(threadYield,"线程1");
            Thread thread2 = new Thread(threadYield, "线程2");
            thread1.start();
            thread2.start();
        }
    }
    --------------
    输出:
    线程1开始了!
    线程2开始了!
    线程1结束了!
    线程2结束了!
    
线程停止:
  • 概念:
    (1). 不推荐使用JDK提供的 stop()、 destroy()方法。【已废弃】
    (2). 推荐线程自己停止下来
  • 方法:
    建议使用一个标志位进行终止变量 当flag=false,则终止线程运行。
  • 示例:
    public class ThreadStop implements Runnable {
        private static boolean flag = true;
    
        @Override
        public void run() {
            int i =0 ;
            while (flag){
                System.out.println("我是子线程"+(i++));
            }
        }
    
        public static void main(String[] args) {
            ThreadStop threadStop = new ThreadStop();
            Thread thread = new Thread(threadStop);
            thread.setPriority(Thread.MAX_PRIORITY);
            thread.start();
            Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
            for (int i = 0; i < 100; i++) {
                System.out.println("我是主线程"+i);
                if(i==50){
                    ThreadStop.flag = false;
                }
            }
            
        }
    }
    
线程状态观测:
  • 概念:
    利用 thread对象.getState() 的方法来获取到当前对象的状态。
  • Thread对象的状态表:(参考JDK帮助文档)
状态含义
NEW尚未启动的线程处于此状态
RUNNABLE在Java虚拟机中执行的线程处于此状态
BLOCKED在被阻塞等待监视器锁定的线程处于此状态
WATTING正在等待另一个线程执行特定的动作处于此状态
TIME_WATTING正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
TERMINATED已经退出的线程处于此状态
  • 注意:一个线程可以在给定时间点处于一个状态。这些状态是不反映任何操作系统线程状态的虚拟机状态。
守护线程:
  • 守护线程的作用:
    (1). 线程分为用户线程和守护线程(个人以为:应该还有一个“虚拟机主线程”)
    (2). 虚拟机必须确保用户线程执行完毕(虚拟机会等到用户线程执行完毕就退出了,而不是管守护线程是否运行完毕)
    (3). 虚拟机不用等待守护线程执行完毕
    (4). 操作如:后台记录操作日志,监控内存,垃圾回收等待等。

  • 示例:

public class ThreadDaemon{
    public static void main(String[] args) {
        // 实例化守护线程:
        Daemon daemon = new Daemon();
        Thread thread1 = new Thread(daemon);
        thread1.setDaemon(true);
        thread1.start();

        //实例化用户线程
        User user = new User();
        Thread thread2 = new Thread(user);
        thread2.start();
    }
}

class User implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"我在努力奔跑"+i);
        }
    }
}

class Daemon implements Runnable{

    @Override
    public void run() {
        int i=0;
        while (true){
            System.out.println(Thread.currentThread().getName()+"我在守护你"+(i++));
        }
    }
}
-------------------
输出:
Thread-0我在守护你0
Thread-0我在守护你1
Thread-0我在守护你2
Thread-0我在守护你3
Thread-0我在守护你4
Thread-0我在守护你5
Thread-0我在守护你6
Thread-0我在守护你7
Thread-0我在守护你8
Thread-0我在守护你9
Thread-0我在守护你10
Thread-0我在守护你11
Thread-0我在守护你12
Thread-0我在守护你13
Thread-0我在守护你14
Thread-0我在守护你15
Thread-0我在守护你16
Thread-0我在守护你17
Thread-0我在守护你18
Thread-0我在守护你19
Thread-0我在守护你20
Thread-0我在守护你21
Thread-0我在守护你22
Thread-0我在守护你23
Thread-0我在守护你24
Thread-0我在守护你25
Thread-0我在守护你26
Thread-0我在守护你27
Thread-0我在守护你28
Thread-0我在守护你29
Thread-0我在守护你30
Thread-0我在守护你31
Thread-0我在守护你32
Thread-0我在守护你33
Thread-0我在守护你34
Thread-0我在守护你35
Thread-0我在守护你36
Thread-0我在守护你37
Thread-0我在守护你38
Thread-0我在守护你39
Thread-0我在守护你40
Thread-0我在守护你41
Thread-0我在守护你42
Thread-0我在守护你43
Thread-0我在守护你44
Thread-0我在守护你45
Thread-0我在守护你46
Thread-0我在守护你47
Thread-0我在守护你48
Thread-0我在守护你49
Thread-0我在守护你50
Thread-0我在守护你51
Thread-0我在守护你52
Thread-1我在努力奔跑0
Thread-0我在守护你53
Thread-1我在努力奔跑1
Thread-0我在守护你54
Thread-1我在努力奔跑2
Thread-0我在守护你55
Thread-1我在努力奔跑3
Thread-0我在守护你56
Thread-1我在努力奔跑4
Thread-0我在守护你57
Thread-0我在守护你58
Thread-0我在守护你59
Thread-0我在守护你60
Thread-0我在守护你61
Thread-0我在守护你62
Thread-0我在守护你63
Thread-0我在守护你64
Thread-0我在守护你65
Thread-0我在守护你66
Thread-0我在守护你67
Thread-0我在守护你68
Thread-0我在守护你69
Thread-0我在守护你70
Thread-0我在守护你71
Thread-0我在守护你72
Thread-1我在努力奔跑5
Thread-1我在努力奔跑6
Thread-1我在努力奔跑7
Thread-0我在守护你73
Thread-1我在努力奔跑8
Thread-0我在守护你74
Thread-1我在努力奔跑9
Thread-0我在守护你75
Thread-0我在守护你76
Thread-0我在守护你77
Thread-0我在守护你78
Thread-0我在守护你79
Thread-0我在守护你80
Thread-0我在守护你81
Thread-0我在守护你82
Thread-0我在守护你83
Thread-0我在守护你84
Thread-0我在守护你85
  • 小结:当一个线程被设置为守护线程的时候,他一般会先运行(有可能),最后结束(必然)。随着其他线程的执行完毕销往了以后,守护线程随后也会进行销往。

问题:现在这些现在做的示例,都是属于并发还是属于并行?还是并发和并行混合的???
回答:这个应该属于并发和并行混合的。

案例:龟兔赛跑:
  • 示例:
public class Race implements Runnable{

    /*
     * 1.实现两个多线程类
     * 2.判断谁是胜利者
     * 3.模拟兔子睡觉
     * */
    //胜利者:
    private static String winner = null;

    @Override
    public void run() {
        for (int i = 0; i <=100; i++) {
            boolean flag = gameOver(i);
            if(flag){
//                System.out.println("比赛已经结束!");
                break;
            }
            // 模拟兔子睡觉!
            if(Thread.currentThread().getName().equals("兔子") && i%10 ==0){
                try {
                    Thread.currentThread().sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 正常跑法:
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" "+i+"米");

        }
    }

    //判断比赛是否结束
    public Boolean gameOver(int meters){
        // 还未分出胜负
        if(winner != null){
            return true;
        }
        if(meters >= 100){
            winner = Thread.currentThread().getName();
            System.out.println("胜利者是:"+winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        // 实例化两个线程
        Race race = new Race();
        Thread thread1 = new Thread(race,"乌龟");
        Thread thread2 = new Thread(race,"兔子");
        thread1.start();
        thread2.start();

    }
}

--------------------
输出:
乌龟 0米
兔子 0米
乌龟 1米
兔子 1米
乌龟 2米
兔子 2米
乌龟 3米
兔子 3米
乌龟 4米
兔子 4米
乌龟 5米
兔子 5米
乌龟 6米
兔子 6米
乌龟 7米
兔子 7米
乌龟 8米
兔子 8米
乌龟 9米
兔子 9米
乌龟 10米
乌龟 11米
兔子 10米
乌龟 12米
兔子 11米
乌龟 13米
兔子 12米
乌龟 14米
兔子 13米
乌龟 15米
兔子 14米
乌龟 16米
兔子 15米
乌龟 17米
兔子 16米
乌龟 18米
兔子 17米
乌龟 19米
兔子 18米
乌龟 20米
兔子 19米
乌龟 21米
乌龟 22米
兔子 20米
乌龟 23米
兔子 21米
乌龟 24米
兔子 22米
乌龟 25米
兔子 23米
乌龟 26米
兔子 24米
乌龟 27米
兔子 25米
乌龟 28米
兔子 26米
乌龟 29米
兔子 27米
乌龟 30米
兔子 28米
乌龟 31米
兔子 29米
乌龟 32米
乌龟 33米
兔子 30米
乌龟 34米
兔子 31米
乌龟 35米
兔子 32米
乌龟 36米
兔子 33米
乌龟 37米
兔子 34米
乌龟 38米
兔子 35米
乌龟 39米
兔子 36米
乌龟 40米
兔子 37米
乌龟 41米
兔子 38米
乌龟 42米
兔子 39米
乌龟 43米
乌龟 44米
兔子 40米
乌龟 45米
兔子 41米
乌龟 46米
兔子 42米
乌龟 47米
兔子 43米
乌龟 48米
兔子 44米
乌龟 49米
兔子 45米
乌龟 50米
兔子 46米
乌龟 51米
兔子 47米
乌龟 52米
兔子 48米
乌龟 53米
兔子 49米
乌龟 54米
乌龟 55米
兔子 50米
乌龟 56米
兔子 51米
乌龟 57米
兔子 52米
乌龟 58米
兔子 53米
乌龟 59米
兔子 54米
乌龟 60米
兔子 55米
乌龟 61米
兔子 56米
乌龟 62米
兔子 57米
乌龟 63米
兔子 58米
乌龟 64米
兔子 59米
乌龟 65米
乌龟 66米
兔子 60米
乌龟 67米
兔子 61米
乌龟 68米
兔子 62米
乌龟 69米
兔子 63米
乌龟 70米
兔子 64米
乌龟 71米
兔子 65米
乌龟 72米
兔子 66米
乌龟 73米
兔子 67米
乌龟 74米
兔子 68米
乌龟 75米
兔子 69米
乌龟 76米
乌龟 77米
兔子 70米
乌龟 78米
兔子 71米
乌龟 79米
兔子 72米
乌龟 80米
兔子 73米
乌龟 81米
兔子 74米
乌龟 82米
兔子 75米
乌龟 83米
兔子 76米
乌龟 84米
兔子 77米
乌龟 85米
兔子 78米
乌龟 86米
兔子 79米
乌龟 87米
乌龟 88米
兔子 80米
乌龟 89米
兔子 81米
乌龟 90米
兔子 82米
乌龟 91米
兔子 83米
乌龟 92米
兔子 84米
乌龟 93米
兔子 85米
乌龟 94米
兔子 86米
乌龟 95米
兔子 87米
乌龟 96米
兔子 88米
乌龟 97米
兔子 89米
乌龟 98米
乌龟 99米
胜利者是:乌龟
兔子 90
线程同步:
  • 概念:
    (1). 现实生活中中,我们都会遇到“同一个资源,多个人都想使用”的问题,比如:食堂排队打饭,每个人都想吃饭,最天然的解决办法就是:排队,一个个来!
    (2). 处理多线程问题时,多个线程访问同一个对象(读操作,无需同步),并且某些线程还想修改这个对象(写操作,需要同步)。这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个**“对象的等待池”**形成队列,等待前面线程将临界资源使用完毕,下一个线程再使用!

  • 线程同步的形成条件:队列+锁

  • 线程同步的方法:
    a. 由于同一个进程的多个线程共享一块儿存储空间,再带来方便的同时,也带来了访问冲突的问题,为了保证数据在被访问同时的正确性,在访问时加入锁机制:Synchronized等,当一个线程获得对象的排他锁,这个线程将独占这个对象资源,其他的线程若需要对这个对象进行访问,则必须进行等待。
    b. 重点:每个对象都有一把锁,一旦某个线程获得了这个锁,其他需要访问该对象的资源都会因无法再获取到这个锁(因为另一个线程还未释放)而陷入被锁等待的状态。
    c. 当线程使用完毕,释放锁资源即可。

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

竞争条件的一个例子:(参考Java核心技术-卷I)
  • 示例:
    a. 为了避免多线程引起的对共享数据的说误,必须学习如何同步存取。在本节中,你会看到如果没有使用同步会发生什么。在下一节中, 将会看到如何同步数据存取。
    b. 在下面的测试程序中,模拟一个有若干账户的银行。随机地生成在这些账户之间转移钱款的交易。每一个账户有一个线程。每一笔交易中, 会从线程所服务的账户中随机转移一定数目的钱款到另一个随机账户。
    c. 模拟代码非常直观。我们有具有 transfer 方法的 Bank 类。该方法从一个账户转移一定数目的钱款到另一个账户(还没有考虑负的账户余额)。如下是 Bank类的 transfer 方法的代码。
    在这里插入图片描述
    这里是 Runnable 类的代码。它的 run 方法不断地从一个固定的银行账户取出钱款。在每一次迭代中,run 方法随机选择一个目标账户和一个随机账户,调用 bank 对象的 transfer 方 法,然后睡眠。
    在这里插入图片描述
    当这个模拟程序运行时,不清楚在某一时刻某一银行账户中有多少钱。但是,知道所有账户的总金额应该保持不变, 因为所做的一切不过是从一个账户转移钱款到另一个账户。

  • 现象:当程序过一段时间, 余额总量有轻微的变化。当长时间运行这个程序的时候, 会发现有时很快就出错了,有时很长的时间后余额发生混乱。

  • 竞争条件详解:
    a. 其中有几个线程更新银行账户余额。一段时间之后, 错误不知不觉地出现了,总额要么增加, 要么变少。
    b. 当两个线程试图同时更新同一个账户的时候,这个问题就出现了。假定两个线程同时执行指令:

    accounts[to] += amount;
    

    c. 问题在于这条语句本身并不是原子操作。 该指令可能被处理如下:
    (1). 将 accounts[to] 加载到寄存器。
    (2). 增加 amount。
    (3). 将结果写回 accounts[to]。
    (4). 现在,假定第 1 个线程执行步骤 1 和 2, 然后,它被剥夺了运行权。假定第 2 个线程被唤醒并修改了 accounts 数组中的同一项。然后,第 1 个线程被唤醒并完成其第 3 步。这样, 这一动作擦去了第二个线程所做的更新。于是, 总金额不再正确。

  • 注释:可以具体看一下执行我们的类中的每一个语句的虚拟机的字节码:

    javap -c -v Bank
    

    对 Bank.class 文件进行反编译。例如, 代码行:

    account[to] += amount;
    

    运行字节码转化程序后,被转换为下面的字节码:

    aload_0
    getfield 	#2; //Field accounts:®
    iload_2
    dup2
    daload
    dload_3
    dadd
    dastore
    

    结论:
    (1):这些代码的含义无关紧要。重要的是增值命令是由几条指令组成的, 执行它们的线
    程可以在任何一条指令点上被中断。

    (2):真正的问题是 transfer 方法的执行过程中可能会被中断。如果能够确保线程在失去控制
    之前方法运行完成, 那么银行账户对象的状态永远不会出现讹误。

    (3):如何确保transfer执行过程中不会中断呢?则是利用Java同步的方法!

同步方法和同步块:
  • 概念:
    由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出这一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和synchronized块

  • 同步方法用法:

    public synchronized void method(int args[])
    
  • 同步方法细节:
    synchronized方法控制“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则会引起调用该方法的线程进入到阻塞状态。方法一旦执行成功后,该线程对象就会独占该锁,直到方法返回才会释放锁,因此后面被阻塞的线程才能去争夺这个锁的拥有权,进而继续执行。

  • 注意:
    a. 如果同步方法何声明在一个类中,声明在一个方法上,其实会使得锁加在这个类的某个对象上(因为每个对象都有一把锁),所以如果这个对象被众多线程调用,则这些调用该对象的线程都会被拥有这个对象的锁的线程所阻塞。
    b. 如果同步方法声明在一个类的static方法上,则会使得该类的所有对象都被锁住(所以static方法一定要慎用synchronized)
    c. synchronized方法有可能会影响效率,使用的时候需要注意!

  • 示例:

    public class SynchronizedMethods implements Runnable{
    
        //票数100张
        private static int ticket = 100;
    
        @Override
        public void run() {
            while (this.ticket > 0){
                if(ticket >0 ){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                sellTicket();
            }
        }
    
        private synchronized void sellTicket() {
            if(ticket <=0){
                    return;
                }
            System.out.println(Thread.currentThread().getName()+"售卖了1张,还剩"+(--ticket)+"张!");
        }
    
        public static void main(String[] args) {
            SynchronizedMethods synchronizedMethods = new SynchronizedMethods();
            Thread thread1 = new Thread(synchronizedMethods,"售票员1");
            Thread thread2 = new Thread(synchronizedMethods,"售票员2");
            Thread thread3 = new Thread(synchronizedMethods,"售票员3");
            thread1.start();
            thread2.start();
            thread3.start();
        }
    }
    ----
    输出:(顺序和数值都正确了!)
    售票员1售卖了1张,还剩99张!
    售票员1售卖了1张,还剩98张!
    售票员1售卖了1张,还剩97张!
    售票员1售卖了1张,还剩96张!
    售票员1售卖了1张,还剩95张!
    售票员1售卖了1张,还剩94张!
    售票员1售卖了1张,还剩93张!
    售票员1售卖了1张,还剩92张!
    售票员1售卖了1张,还剩91张!
    售票员1售卖了1张,还剩90张!
    售票员1售卖了1张,还剩89张!
    售票员1售卖了1张,还剩88张!
    售票员1售卖了1张,还剩87张!
    售票员1售卖了1张,还剩86张!
    售票员1售卖了1张,还剩85张!
    售票员1售卖了1张,还剩84张!
    售票员1售卖了1张,还剩83张!
    售票员1售卖了1张,还剩82张!
    售票员1售卖了1张,还剩81张!
    售票员1售卖了1张,还剩80张!
    售票员1售卖了1张,还剩79张!
    售票员1售卖了1张,还剩78张!
    售票员1售卖了1张,还剩77张!
    售票员1售卖了1张,还剩76张!
    售票员1售卖了1张,还剩75张!
    售票员1售卖了1张,还剩74张!
    售票员1售卖了1张,还剩73张!
    售票员1售卖了1张,还剩72张!
    售票员1售卖了1张,还剩71张!
    售票员1售卖了1张,还剩70张!
    售票员1售卖了1张,还剩69张!
    售票员3售卖了1张,还剩68张!
    售票员3售卖了1张,还剩67张!
    售票员3售卖了1张,还剩66张!
    售票员3售卖了1张,还剩65张!
    售票员3售卖了1张,还剩64张!
    售票员3售卖了1张,还剩63张!
    售票员3售卖了1张,还剩62张!
    售票员3售卖了1张,还剩61张!
    售票员3售卖了1张,还剩60张!
    售票员3售卖了1张,还剩59张!
    售票员3售卖了1张,还剩58张!
    售票员3售卖了1张,还剩57张!
    售票员3售卖了1张,还剩56张!
    售票员3售卖了1张,还剩55张!
    售票员3售卖了1张,还剩54张!
    售票员3售卖了1张,还剩53张!
    售票员3售卖了1张,还剩52张!
    售票员3售卖了1张,还剩51张!
    售票员3售卖了1张,还剩50张!
    售票员3售卖了1张,还剩49张!
    售票员3售卖了1张,还剩48张!
    售票员3售卖了1张,还剩47张!
    售票员3售卖了1张,还剩46张!
    售票员3售卖了1张,还剩45张!
    售票员3售卖了1张,还剩44张!
    售票员3售卖了1张,还剩43张!
    售票员3售卖了1张,还剩42张!
    售票员3售卖了1张,还剩41张!
    售票员3售卖了1张,还剩40张!
    售票员3售卖了1张,还剩39张!
    售票员2售卖了1张,还剩38张!
    售票员2售卖了1张,还剩37张!
    售票员2售卖了1张,还剩36张!
    售票员2售卖了1张,还剩35张!
    售票员2售卖了1张,还剩34张!
    售票员2售卖了1张,还剩33张!
    售票员2售卖了1张,还剩32张!
    售票员2售卖了1张,还剩31张!
    售票员2售卖了1张,还剩30张!
    售票员2售卖了1张,还剩29张!
    售票员2售卖了1张,还剩28张!
    售票员2售卖了1张,还剩27张!
    售票员2售卖了1张,还剩26张!
    售票员2售卖了1张,还剩25张!
    售票员2售卖了1张,还剩24张!
    售票员2售卖了1张,还剩23张!
    售票员2售卖了1张,还剩22张!
    售票员2售卖了1张,还剩21张!
    售票员2售卖了1张,还剩20张!
    售票员2售卖了1张,还剩19张!
    售票员2售卖了1张,还剩18张!
    售票员2售卖了1张,还剩17张!
    售票员2售卖了1张,还剩16张!
    售票员2售卖了1张,还剩15张!
    售票员2售卖了1张,还剩14张!
    售票员2售卖了1张,还剩13张!
    售票员2售卖了1张,还剩12张!
    售票员2售卖了1张,还剩11张!
    售票员2售卖了1张,还剩10张!
    售票员2售卖了1张,还剩9张!
    售票员2售卖了1张,还剩8张!
    售票员2售卖了1张,还剩7张!
    售票员2售卖了1张,还剩6张!
    售票员2售卖了1张,还剩5张!
    售票员2售卖了1张,还剩4张!
    售票员2售卖了1张,还剩3张!
    售票员2售卖了1张,还剩2张!
    售票员2售卖了1张,还剩1张!
    售票员2售卖了1张,还剩0张!
    
  • 小结:个人认为,并行程序的同步方法在访问临界资源的时候,其执行速度依然会比串行的程序快一点,因为准备工作也需要时间(比如:在本程序中的sleep()的时间),所以其节省的时间就是准备的时间。

  • 同步方法的缺点:
    a. 因为同步方法会使得一个方法内的所有资源都被锁住,而我们有时候只是在修改临界资源的时候才需要用到这把锁。所以,如果synchronized锁的太多,会浪费资源,白白让其他众多线程处于阻塞期间。
    b. 所以引入了同步块这个概念!

  • 同步块的用法:

    synchronized(Obj){}
    
  • 用法中的Obj 称之为同步监视器
    a. Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
    b. 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者说是class(与反射有关)

  • 同步监视器的执行过程:
    (1). 第一个线程访问,锁定同步监视器,执行其中的代码。
    (2). 第二个线程访问,发现同步监视器被锁定,无法访问。
    (3). 第一个线程访问完毕,解锁同步监视器。
    (4). 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。

  • 示例:

    public class SynchronizedStatements implements Runnable{
        //票数100张
        private static int ticket = 100;
    
        @Override
        public void run() {
            while (this.ticket > 0){
    
                if(ticket >0 ){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                synchronized (""){
                    if(ticket <=0){
                    	return;
                	}
                    System.out.println(Thread.currentThread().getName()+"售卖了1张,还剩"+(--ticket)+"张!");
                }
            }
        }
    
        public static void main(String[] args) {
            SynchronizedStatements synchronizedStatements = new SynchronizedStatements();
            Thread thread1 = new Thread(synchronizedStatements,"售票员1");
            Thread thread2 = new Thread(synchronizedStatements,"售票员2");
            Thread thread3 = new Thread(synchronizedStatements,"售票员3");
            thread1.start();
            thread2.start();
            thread3.start();
        }
    }
    -------------------
    输出:
    售票员1售卖了1张,还剩99张!
    售票员2售卖了1张,还剩98张!
    售票员3售卖了1张,还剩97张!
    售票员1售卖了1张,还剩96张!
    售票员3售卖了1张,还剩95张!
    售票员2售卖了1张,还剩94张!
    售票员1售卖了1张,还剩93张!
    售票员3售卖了1张,还剩92张!
    售票员2售卖了1张,还剩91张!
    售票员1售卖了1张,还剩90张!
    售票员2售卖了1张,还剩89张!
    售票员3售卖了1张,还剩88张!
    售票员1售卖了1张,还剩87张!
    售票员1售卖了1张,还剩86张!
    售票员2售卖了1张,还剩85张!
    售票员3售卖了1张,还剩84张!
    售票员1售卖了1张,还剩83张!
    售票员2售卖了1张,还剩82张!
    售票员3售卖了1张,还剩81张!
    售票员1售卖了1张,还剩80张!
    售票员2售卖了1张,还剩79张!
    售票员3售卖了1张,还剩78张!
    售票员2售卖了1张,还剩77张!
    售票员1售卖了1张,还剩76张!
    售票员3售卖了1张,还剩75张!
    售票员2售卖了1张,还剩74张!
    售票员1售卖了1张,还剩73张!
    售票员3售卖了1张,还剩72张!
    售票员1售卖了1张,还剩71张!
    售票员2售卖了1张,还剩70张!
    售票员3售卖了1张,还剩69张!
    售票员1售卖了1张,还剩68张!
    售票员2售卖了1张,还剩67张!
    售票员3售卖了1张,还剩66张!
    售票员1售卖了1张,还剩65张!
    售票员2售卖了1张,还剩64张!
    售票员1售卖了1张,还剩63张!
    售票员3售卖了1张,还剩62张!
    售票员2售卖了1张,还剩61张!
    售票员1售卖了1张,还剩60张!
    售票员3售卖了1张,还剩59张!
    售票员2售卖了1张,还剩58张!
    售票员1售卖了1张,还剩57张!
    售票员3售卖了1张,还剩56张!
    售票员2售卖了1张,还剩55张!
    售票员1售卖了1张,还剩54张!
    售票员2售卖了1张,还剩53张!
    售票员1售卖了1张,还剩52张!
    售票员3售卖了1张,还剩51张!
    售票员2售卖了1张,还剩50张!
    售票员1售卖了1张,还剩49张!
    售票员3售卖了1张,还剩48张!
    售票员3售卖了1张,还剩47张!
    售票员2售卖了1张,还剩46张!
    售票员1售卖了1张,还剩45张!
    售票员3售卖了1张,还剩44张!
    售票员2售卖了1张,还剩43张!
    售票员1售卖了1张,还剩42张!
    售票员3售卖了1张,还剩41张!
    售票员2售卖了1张,还剩40张!
    售票员1售卖了1张,还剩39张!
    售票员3售卖了1张,还剩38张!
    售票员2售卖了1张,还剩37张!
    售票员1售卖了1张,还剩36张!
    售票员2售卖了1张,还剩35张!
    售票员1售卖了1张,还剩34张!
    售票员3售卖了1张,还剩33张!
    售票员2售卖了1张,还剩32张!
    售票员1售卖了1张,还剩31张!
    售票员3售卖了1张,还剩30张!
    售票员2售卖了1张,还剩29张!
    售票员1售卖了1张,还剩28张!
    售票员3售卖了1张,还剩27张!
    售票员3售卖了1张,还剩26张!
    售票员1售卖了1张,还剩25张!
    售票员2售卖了1张,还剩24张!
    售票员2售卖了1张,还剩23张!
    售票员1售卖了1张,还剩22张!
    售票员3售卖了1张,还剩21张!
    售票员1售卖了1张,还剩20张!
    售票员2售卖了1张,还剩19张!
    售票员3售卖了1张,还剩18张!
    售票员2售卖了1张,还剩17张!
    售票员1售卖了1张,还剩16张!
    售票员3售卖了1张,还剩15张!
    售票员2售卖了1张,还剩14张!
    售票员1售卖了1张,还剩13张!
    售票员3售卖了1张,还剩12张!
    售票员1售卖了1张,还剩11张!
    售票员2售卖了1张,还剩10张!
    售票员3售卖了1张,还剩9张!
    售票员2售卖了1张,还剩8张!
    售票员1售卖了1张,还剩7张!
    售票员3售卖了1张,还剩6张!
    售票员2售卖了1张,还剩5张!
    售票员1售卖了1张,还剩4张!
    售票员3售卖了1张,还剩3张!
    售票员1售卖了1张,还剩2张!
    售票员2售卖了1张,还剩1张!
    售票员3售卖了1张,还剩0张!
    
Lock锁:
  • 概念:
    a. 从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
    b. java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
    c. ReentrantLock 类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

  • 用法:

    	Class TA{
    		private final ReentrantLock lock = new ReenTrantLock();
    		public void m(){
    			lock.lock();
    			try{
    				//保证线程安全的代码;
    			}
    			finally{
    				lock.unlock();
    				// 这样的写法有助于程序的稳定性,使得锁一定能被释放,而不是卡死!
    			}
    		}
    	}
    
  • 示例:

    package Test2;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ThreadLock implements Runnable{
        //票数100张
        private static int ticket = 100;
        ReentrantLock reentrantLock = new ReentrantLock();
    
        @Override
        public void run() {
            while (this.ticket > 0){
    
                if(ticket >0 ){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                reentrantLock.lock();
                if(ticket <=0){
                    reentrantLock.unlock();
                    return;
                }
                ticket--;
                System.out.println(Thread.currentThread().getName()+"售卖了1张,还剩"+(ticket)+"张!");
                reentrantLock.unlock();
            }
        }
    
        public static void main(String[] args) {
            ThreadLock threadConflict = new ThreadLock();
            Thread thread1 = new Thread(threadConflict,"售票员1");
            Thread thread2 = new Thread(threadConflict,"售票员2");
            Thread thread3 = new Thread(threadConflict,"售票员3");
            thread1.start();
            thread2.start();
            thread3.start();
        }
    }
    --------------
    .....
    售票员3售卖了1张,还剩6张!
    售票员1售卖了1张,还剩5张!
    售票员2售卖了1张,还剩4张!
    售票员1售卖了1张,还剩3张!
    售票员3售卖了1张,还剩2张!
    售票员2售卖了1张,还剩1张!
    售票员3售卖了1张,还剩0张!
    问题:为何程序最后到0停止不了?(已经解决)
    
  • Synchronized与Lock的对比:
    a. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,除了自动域会自动释放锁。
    b. Lock只有代码块儿锁,synchronized有代码块锁和方法锁。
    c. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
    d. 优先使用顺序:(更侧重于性能方面)
    Lock > 同步代码块(已经进入了方法体,分配了相应的资源) > 同步方法(在方法体外)
    但是,根据java核心编程书中记载,为了减少出错和方便起见,最好优先使用Synchronized方法。

死锁以及避免方法:
  • 概念:
    a. 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源被释放(然后自己获取后)才能继续运行下去,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。
    b. 某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

  • 死锁的示例:

    public class ThreadDeadLock  {
        public static void main(String[] args) {
            DeadLockA deadLockA = new DeadLockA();
            DeadLockB deadLockB = new DeadLockB();
            Thread thread1 = new Thread(deadLockA);
            Thread thread2 = new Thread(deadLockB);
            thread1.start();
            thread2.start();
        }
    }
    
    class DeadLockA implements Runnable{
    
        @Override
        public void run() {
            synchronized ("A"){
                System.out.println(Thread.currentThread().getName()+"获取了A锁");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized ("B"){
                    System.out.println(Thread.currentThread().getName()+"获取了A锁和B锁");
                }
            }
        }
    }
    
    class DeadLockB implements Runnable{
    
        @Override
        public void run() {
            synchronized ("B"){
                System.out.println(Thread.currentThread().getName()+"获取了B锁");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized ("A"){
                    System.out.println(Thread.currentThread().getName()+"获取了B锁和A锁");
                }
            }
        }
    }
    ---------------------------------
    输出:
    Thread-0获取了A锁
    Thread-1获取了B锁
    (程序未停止!)
    
  • 死锁产生的四个必要条件:
    (1). 互斥条件:一个资源每次只能被一个进程使用。
    (2). 请求于保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    (3). 不剥夺条件:进程已获得的资源,在未使用完钱,不能强行剥夺。
    (4). 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
    上面列出了死锁的四个必要条件,我们只要想办法破除其中的任意一个或多个条件,就可以避免死锁的发生。

  • 破解死锁的示例:(利用了线程间通信的手段!)

    public class ThreadCorindience {
        public static void main(String[] args) {
            ThreadCorindienceA threadCorindienceA = new ThreadCorindienceA();
            ThreadCorindienceB threadCorindienceB = new ThreadCorindienceB();
            Thread thread = new Thread(threadCorindienceA);
            Thread thread1 = new Thread(threadCorindienceB);
            thread1.start();
            thread.start();
        }
    }
    
    class ThreadCorindienceA implements Runnable{
    
        @Override
        public void run() {
            synchronized ("A"){
                System.out.println(Thread.currentThread().getName()+"获取了A锁");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized ("B"){
                    System.out.println(Thread.currentThread().getName()+"获取了A锁和B锁");
                    "B".notify();
                }
            }
        }
    }
    
    class ThreadCorindienceB implements Runnable{
    
        @Override
        public void run() {
            synchronized ("B"){
                System.out.println(Thread.currentThread().getName()+"获取了B锁");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    "B".wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized ("A"){
                    System.out.println(Thread.currentThread().getName()+"获取了B锁和A锁");
                }
            }
        }
    }
    ------------------------------
    输出:
    Thread-1获取了B锁
    Thread-0获取了A锁
    Thread-0获取了A锁和B锁
    Thread-1获取了B锁和A锁
    

    注意:由于死锁的产生条件是:互相争取资源。只要有一方主动释放已占有的资源,就会使得死锁的条件不满足,从而破除死锁!(不需要两方都释放资源)

客户端锁定:(参考Java核心技术)
  • 概念:
    正如刚刚讨论的,每一个 Java 对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:

    synchronized(obj)
    {
    	critical section
    }
    

    有时会发现“ 特殊的” 锁,例如:

    public class Bank
    {
    	private doublet] accounts;
    	private Object lock = new Object;
    	public void transfer(int from, int to, int amount) {
    	synchronized (lock) // an ad-hoc lock
    	{
    		accounts[from] -= amount;
    		accounts[to] += amount; 
    		System.out.print1n(...);
    	}
    }
    

    在此,lock 对象被创建仅仅是用来使用每个 Java 对象持有的锁。

    有时程序员使用一个对象的锁来实现额外的原子操作, 实际上称为客户端锁定( clientside locking) 0 例如,考虑 Vector 类,一个列表,它的方法是同步的。现在, 假定在 Vector 中存储银行余额。这里有一个 transfer 方法的原始实现:

    public void transfer(Vector<Double> accounts, int from, int to, int amount)// Error
    {
    	accounts.set(from, accounts.get(from)- amount);
    	accounts.set(to, accounts.get(to) + amount);
    	System.out.println(. . .); 
    }
    

    Vector 类的 get 和 set 方法是同步的, 但是,这对于我们并没有什么帮助。在第一次对get 的调用已经完成之后,一个线程完全可能在 transfer 方法中被剥夺运行权。于是,另一个线程可能在相同的存储位置存入不同的值。
    (这个对象本身的锁安全机制并不牢靠(客户端锁机制不可靠?),我们一般需要避免这样的使用方法,利用自己的锁机制。)

线程间通信:
  • Java提供了几个方法来解决线程之间的通信问题:
方法名作用
wait()表示线程一直等待,知道其他线程通知它,与sleep不同,会释放锁。
wait(long timeout)等待指定的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程会优先调度

注意:这些方法都是Object类的方法,都只能在同步方法或者同步块代码中使用。
并且,在一种对象的同步区域内,只能调用该对象的的相关方法,不可直接调用其他对象的方法。

案例:生产者消费者模式
  • 背景:
    a. 生产者指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
    b. 消费者指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
    c. 费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

  • 背景分析:
    (1). 这一个典型的线程同步问题,因为消费者和生产者共享一种临界资源,并且生产者和消费者之间有着互相依赖的关系。
    (2). 光有同步方法是不够用的,因为:
    a. synchronized可以阻止并发更新同一个共享资源,实现了同步
    b. synchronized不能用来实现线程间的消息传递(通信)
    (3). 对于生产者,在生产过后应该及时通知消费者进行消费。对于消费者,在消费之后,要立即通知生产者生产。
    (4). 缓冲区本身有大小的限制。

  • 生产者示例:

    public class Producer implements Runnable {
    
        /**
         * 缓冲区/柜台的大小
         */
        public ProductPool productPool;
    
        public Producer(ProductPool productPool) {
            this.productPool = productPool;
        }
    
        /**
         * 模拟生产者生产产品
         */
        @Override
        public void run() {
            int i = 0;
            while (true) {
                Product product = new Product("产品名称" + i++);
                try {
                    this.productPool.push(product);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    //            System.out.println("生产者生产了产品" + product.getName());
            }
        }
    }
    
  • 消费者示例:

    public class Consumer implements Runnable {
    
        /**
         * 缓冲区/柜台的大小
         */
        public ProductPool productPool;
    
        public Consumer(ProductPool productPool) {
            this.productPool = productPool;
        }
    
        /**
         * 模拟消费者消费产品
         */
        @Override
        public void run() {
            while (true) {
                Product product  = null;
                try {
                    product = this.productPool.pop();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    //            System.out.println("消费者消费了产品" + product.getName());
            }
        }
    }
    
  • 产品示例:

    public class Product {
    
        public String name;
    
        public Product(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
  • 缓冲区(产品池)示例:

    package edu.nwu.test.producerandcomsumer;
    
    import java.util.List;
    
    public class ProductPool {
        /**
         * 缓冲区大小
         * @return
         */
        public int maxSize = 0;
    
        /**
         * 产品的列表
         */
        public List<Product> list;
    
        public ProductPool(int maxSize, List<Product> list) {
            this.maxSize = maxSize;
            this.list = list;
        }
    
        /**
         *  生产者生产产品,放入产品到缓冲区。
         */
        public synchronized void push(Product product) throws InterruptedException {
            // 产品的数量不能大于缓冲区的大小
            // 如果大于缓冲区,就等待消费者消费。
            if (this.list.size() >= this.maxSize) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            // 生产者生产了一个产品
            this.list.add(product);
            System.out.println("生产者生产了产品" + product.getName());
            // 通知消费者消费。
            this.notify();
    
        }
    
        /**
         * 消费者消费产品,从缓冲区移除产品。
         * @return
         */
        public synchronized Product pop() throws InterruptedException {
            // 产品数量不能小于0
            // 如果小于0,就等待生产者生产产品。
            if (this.list.size() <= 0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            // 消费者消费了一个产品
            Product product = this.list.remove(0);
            System.out.println("消费者消费了产品" + product.getName());
            // 通知生产者生产产品。
            this.notifyAll();
            return product;
        }
    }
    
  • 主方法示例:

    public class MainMethod {
        /**
         * 1、产品/食物
         * 2、缓冲区/柜台的大小
         * 3、生产者/厨师
         * 4、消费者/顾客
         */
    
        /**
         * 主方法
         * @param args
         */
        public static void main(String[] args) {
    
            // 实例化缓冲区/柜台
            ProductPool productPool = new ProductPool(8, new LinkedList<Product>());
    
            // 实例化生产者/厨师
            Producer producer = new Producer(productPool);
            Thread thread1 = new Thread(producer, "生产者线程");
            thread1.start();
    
            // 实例化消费者/顾客
            Consumer consumer = new Consumer(productPool);
            Thread thread2 = new Thread(consumer,"消费者线程");
            thread2.start();
    
        }
    }
    ---------
    输出:
    ...
    消费者消费了产品产品名称27991
    消费者消费了产品产品名称27992
    消费者消费了产品产品名称27993
    消费者消费了产品产品名称27994
    ...
    (程序将永远地运行下去!)
    
  • 生产者和消费者案例中要搞懂
    1)synchronized 方法锁的是ProductPool对象,调用pop()或者push()方法都会使得锁成立,故而有一个线程肯定会被锁住,但是它并没被锁死,还是会不断地去调用函数。
    2)调用this.wait()或者this.notify()方法,针对的也是ProductPool对象。
    3)this.wait()会导致让出该对象锁,然后自己进入等待状态。(需要别人唤醒才能进入就绪状态)
    4)this.notify()会导致通知调用this.wait()方法的线程,使其进入就绪状态,从等待位置继续执行。
    5)这里的核心是:this.wait()会让出对象锁,所以使得其他被synchronized()方法进入阻塞状态的线程有机会重新获得该锁。而this.nofity()并不会影响没有进入synchronized()方法的线程,执行完毕后,还会去重新争夺CPU的使用权。

高级部分:
线程池:
  • 背景:
    经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能的影响很大。

  • 思路:
    提前创建好多个线程,放入线程池中,使用的时候可以直接获取(不用重新创建),使用完放回池中。可以避免频繁创建销毁、实现重复利用。(减少创建和销毁带来的性能开销)

  • 好处:
    (1). 提高响应速度(减少了创建新线程的时间)
    (2). 降低资源消耗 (重复利用线程池中的线程,不需要每次都创建)
    (3). 便于线程管理

  • 示例:

    package Test2;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class ThreadPool {
        public static void main(String[] args) {
            // 实例化服务:
            ExecutorService executorService = Executors.newFixedThreadPool(10);
            // 开启线程
            executorService.execute(new HelloRunnable());
            executorService.execute(new HelloRunnable());
            executorService.execute(new HelloRunnable());
            // 关闭服务
            executorService.shutdown();
        }
    }
    
    class HelloRunnable implements Runnable{
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"正在运行!");
        }
    }
    ---------------
    输出:
    pool-1-thread-1正在运行!
    pool-1-thread-2正在运行!
    pool-1-thread-3正在运行!
    

    注意:如果是需要运行带返回值的线程池的话,则利用submit方法,方法中传入的参数是:Future对象。

BlockQueen(JAVA官方提供的并发集合)
* 生产者消费者使用使用BlockQueen(JAVA官方提供的并发集合)来充当缓冲区的实例
静态代理:
  • 示例:

    public class StaticProxy {
        public static void main(String[] args) {
            Person me = new Person();
            MarryCompany marryCompany = new MarryCompany(me);
            marryCompany.marryMethod();
        }
    }
    
    interface Marry{
        void marryMethod();
    }
    
    class Person implements Marry{
        @Override
        public void marryMethod() {
            System.out.println("结婚了,真开心!");
        }
    }
    
    class MarryCompany implements Marry{
        private Marry marry;
    
        public MarryCompany(Marry marry) {
            this.marry = marry;
        }
    
        @Override
        public void marryMethod() {
            after();
            this.marry.marryMethod();
            before();
        }
    
        private void before() {
            System.out.println("婚礼后,收取费用!");
        }
    
        private void after() {
            System.out.println("婚礼前,布置会场!");
        }
    
    }
    ------------------
    输出:
    婚礼前,布置会场!
    结婚了,真开心!
    婚礼后,收取费用!
    
  • 小结:静态代理的运作方式跟我们的多线程的创建与使用方法息息相关!!!

Lambda表达式:
  • 示例:
    public class LambdaExpression {
        // 2.静态内部类
        static class Love2 implements ILove{
            @Override
            public void lambda() {
                System.out.println("I Love Lambda 2!");
            }
        }
        public static void main(String[] args) {
            //外部类!
            Love love = new Love();
            love.lambda();
            //内部类
            Love2 love2 = new Love2();
            love2.lambda();
    
            //3.局部内部类:
            class Love3 implements ILove{
                @Override
                public void lambda() {
                    System.out.println("I Love Lambda 3!");
                }
            }
    
            Love3 love3 = new Love3();
            love3.lambda();
    
            //4. 匿名内部类:
            ILove love4 = new ILove(){
                @Override
                public void lambda() {
                    System.out.println("I Love lambda 4!");
                }
            };
            love4.lambda();
    
            //5. Lambda表达式:
            ILove love5 = ()->{
                System.out.println("I Love lambda 5!");
            };
            love5.lambda();
    
        }
    }
    
    interface ILove{
        void lambda();
    }
    
    /*
    * 1.外部类
    */
    class Love implements ILove{
    
        @Override
        public void lambda() {
            System.out.println("I Love Lambda!");
        }
    }
    ----------------------
    输出:
    I Love Lambda 1!
    I Love Lambda 2!
    I Love Lambda 3!
    I Love lambda 4!
    I Love lambda 5!
    
  • Java8 Lambda表达式和匿名内部类的区别这一篇文章中提及了:
    • "函数接口"这个概念(函数接口,是指内部只有一个抽象方法的接口),这个概念是很重要的,也是我一直不理解Lambda的原因,为何简写到这种程度还能对应修改的是某个函数。
    • Lambda表达式另一个依据是类型推断机制,在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指名。可以根据上述代码的love5来理解,正因为被赋值的变量拥有了相应的类型,从而也就确定了Lambda所表示的类是哪个,(再根据函数接口)就可以进一步指导其重写的函数是哪个。
写时复制:(JAVA官方提供的线程安全的的工具类)
  • 示例:
    import java.util.ArrayList;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class CopyWriteArrayListTest {
        public static void main(String[] args) throws InterruptedException {
            CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
            for (int i = 0; i < 100; i++) {
                new Thread(()->{
                    list.add(Thread.currentThread().getName());
                }).start();
            }
            // 需要让主线程语句休眠一会儿,不然子线程没有执行完,结果就输出了!
            Thread.sleep(1000);
            System.out.println(list.size());
        }
    }
    ---------------------
    输出:
    100
    
  • 问题:在此示例的Thread的创建过程中使用了lambda表达式,如何可知给Thread构造函数中里面传送的是Runnable还是Callable的实例?
  • 回答:Callable实例的传递方法还要经过一个代理类FutureTask,而Runnable实例的传递不需要通过FutureTask的代理,所以在此过程中传递的是Runnable实例。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值