zoukankan      html  css  js  c++  java
  • java中的多线程

    1、多线程概述
    2、启动线程的方式
    ---2.1第一种
    ---2.2run和start的区别
    ---2.3第二种
    ---2.4采用匿名内部类的方式
    3、线程生命周期
    ---3.1图例
    ---3.2线程对象的生命周期
    4、线程的一些方法
    ---4.1关于线程的名字
    ---4.2获取当前线程对象
    ---4.3sleep方法
    ---4.4interrupt方法
    ---4.5stop方法(不建议使用)
    ---4.6合理终止线程
    5、线程调度
    ---5.1概述
    ---5.2与线程调度有关的方法
    6、线程的同步和异步
    ---6.1多线程并发环境下,数据的安全问题
    ---6.2模拟银行取钱的例子
    ---6.3线程同步机制
    7、synchronized
    ---7.1对synchronized的理解
    ---7.2思考题
    8、死锁
    ---8.1概述
    ---8.2死锁代码示例
    9、如何解决线程安全问题
    10、线程的其它内容
    ---10.1守护线程
    ---10.2定时器
    ---10.3实现线程的第三种方式
    ---10.4关于Object类中的wait和notify方法。

    多线程概述

    线程池:链接

    • 概述
      1、什么是进程?
      进程是一个应用程序(1个进程是一个软件)。
      2、什么是线程?
      线程是一个进程中的执行场景/执行单元。
      3、一个进程可以启动多个线程。
      例子:
      对于java程序来说,当在DOS命令窗口中输入,java HelloWorld 回车之后会先启动JVM,而JVM就是一个进程。JVM再启动一个主线程调用main方法。同时再启动一个垃圾回收线程负责看护,回收垃圾。最起码现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。
      4、进程之间内存独立不共享。线程之间堆内存和方法区内存共享。但是栈内存独立,一个线程一个栈。
      5、多线程
      假设启动10个线程,会有10个栈空间,每个栈和每个栈之间互不干扰,各自执行,这就是多线程。
      多线程并发可以提高程序的处理效率。(车站的多窗口购票)
      6、main方法结束只代表主线程结束了,其它线程可能还在执行。
      7、真正的多线程并发
      两个线程同时执行,互不影响。
      单核CPU不能做到真正的多线程并发,只是CPU处理速度极快,多个线程之间频繁切换执行,感觉起来是并发的。(多核就能实现真正的并发)

    启动线程的方式

    • 第一种
      编写一个类,直接继承java.lang.Thread,重写run方法。
    public class Demo{
        public static void main(String[] args) {
            //这里属于主线程,在主栈中运行
            //新建一个分支线程对象
            Thread thread = new MyThread();
            /*直接调用run方法,不会启动线程,
            不会分配新的分支栈(单线程)*/
            //thread.run();
            //启动线程
            thread.start();
            //下面的代码还是运行在主线程中
            for (int i = 0; i < 5; i++) {
                System.out.println("主线程---》" + i);
            }
        }
    }
    
    class MyThread extends Thread{
        @Override
        public void run() {
            //编写程序,这段程序运行在分支线程中(分支栈)。
            for (int i = 0; i < 5; i++) {
                System.out.println("线程分支---》" + i);
            }
        }
    }
    

    输出:
    在这里插入图片描述
    输出结果中主线程和分支线程有多有少、有先有后是为什么?
    创建出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态。所以输出结果有多有少、有先有后。

    • run和start的区别
      1、run只是在主线程里面调用了run方法,没有开辟新的栈空间,还是属于单线程。
      2、start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,只要新的栈空间开出来, start()方法就结束了。
      线程就启动成功,启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。run方法在分支栈的栈底部,main方法在主栈的底部。 run和main是平级的(并发)。
    • 第二种
      编写一个类,实现java.lang.Runnable接口,实现run方法。
    public class Demo{
        public static void main(String[] args) {
            /*创建一个可运行的对象*/
            //Runnable mr = new MyRunnable();
            /*将可运行的对象封装成一个线程对象*/
            //Thread thread = new Thread(mr);
            /*合并代码*/
            Thread thread = new Thread(new MyRunnable());
            //启动线程
            thread.start();
            for (int i = 0; i < 100; i++) {
                System.out.println("主线程---》" + i);
            }
        }
    }
    
    /**这并不是一个线程类,是一个可运行的类。它还不是一个线程。*/
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("分支线程---》" + i);
            }
        }
    }
    

    输出(部分):
    在这里插入图片描述
    优先选择第二种。第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其它的类,更灵活

    • 采用匿名内部类的方式
    public class Demo{
        public static void main(String[] args) {
            /*匿名内部类*/
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.println("分支线程---》" + i);
                    }
                }
            });
            //启动线程
            thread.start();
            for (int i = 0; i < 100; i++) {
                System.out.println("主线程---》" + i);
            }
        }
    }
    

    输出(部分):
    在这里插入图片描述

    线程生命周期

    • 图例
      在这里插入图片描述
    • 线程对象的生命周期
      新建状态
      就绪状态
      运行状态
      阻塞状态
      死亡状态

    线程的一些方法

    关于线程的名字

    1、修改线程对象的名字:
    void setName(String name)
    将此线程的名称更改为参数 name 。

    线程对象.setName("线程名字");
    

    2、获取线程对象的名字:
    String getName()
    返回此线程的名称。

    String name = 线程对象.getName();
    

    3、当线程没有设置名字的时候,默认的名字规律:
    Thread-0
    Thread-1
    Thread-2
    ...
    4、代码示例

    public class Demo{
        public static void main(String[] args) {
            MyThread myThread1 = new MyThread();
            myThread1.setName("t1");
            System.out.println(myThread1.getName());
    
            MyThread myThread3 = new MyThread();
            System.out.println(myThread3.getName());
    
            MyThread myThread4 = new MyThread();
            System.out.println(myThread4.getName());
    
            myThread1.start();
        }
    }
    
    class MyThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println("分支线程---》" + i);
            }
        }
    }
    

    输出:
    在这里插入图片描述

    获取当前线程对象

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

    public class Demo{
        public static void main(String[] args) {
            /*currentThread就是当前线程对象,
            这个代码出现在main方法当中,所以当前线程就是主线程。*/
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName());
            //主线程名字就叫main
    
            MyThread myThread1 = new MyThread();
            myThread1.setName("t1");
            System.out.println(myThread1.getName());
            myThread1.start();
    
            MyThread myThread2 = new MyThread();
            myThread2.setName("t2");
            System.out.println(myThread2.getName());
            myThread2.start();
        }
    }
    
    class MyThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                /*currentThread就是当前线程对象。
                谁执行run方法,当前线程就是谁。*/
                Thread currentThread = Thread.currentThread();
                System.out.println(currentThread.getName() + "线程---》" + i);
            }
        }
    }
    

    输出:
    在这里插入图片描述

    sleep方法

    1、static void sleep(long millis)
    使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
    static void sleep(long millis, int nanos)
    导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。
    (放弃占有CPU时间片,让给其它线程使用。)
    2、可以实现这样一种功能:
    间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。
    3、代码示例

    public class Demo{
        public static void main(String[] args) {
            //让当前线程休眠5秒
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hello kitty");
            //5秒后才会输出hello kitty
        }
    }
    

    4、sleep是一个静态方法,无论前面引用是什么(???.sleep)都会转换成Thread.sleep,并且只会使当前正在执行的线程休眠。

    interrupt方法

    1、void interrupt()
    中断这个线程。
    2、实际不是中断线程的执行,是终止线程的阻塞状态。这种中断阻塞状态的方式依靠了java的异常处理机制。interrupt() 方法只是改变中断状态而已,它不会中断一个正在运行的线程。如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常。
    3、例如
    线程通过sleep()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。
    4、代码示例

    public class Demo{
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable());
            t.setName("t");
            t.start();
            //睡眠5秒
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //中段t线程的睡眠
            t.interrupt();
        }
    }
    
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"--->begin");
            System.out.println("5秒后输出以下信息:");
            try {
                Thread.sleep(1000*60);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("测试");
            }
            System.out.println(Thread.currentThread().getName()+"--->end");
        }
    }
    

    输出:
    在这里插入图片描述

    • 控制台的两种输出方式
      1、注释掉上面的睡眠5秒的代码后则输出:
      在这里插入图片描述
      2、控制台的输出方式总共两种,分别是:
      正常输出:System.out.println();
      发生错误时的输出:System.err.println();
      3、原因:
      可能是错误输出延迟打印到控制台了。
    stop方法(不建议使用)

    1、强行终止线程的执行
    2、这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。不建议使用。
    3、代码示例

    public class Demo{
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable());
            t.setName("t");
            t.start();
            //5秒后强行终止t线程
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //该方法已过时(不建议使用)
            t.stop();
        }
    }
    
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+i+"--->begin");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"--->end");
        }
    }
    

    输出:
    在这里插入图片描述

    合理终止线程

    代码示例:

    public class Demo{
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread t = new Thread(myRunnable);
            t.setName("t");
            t.start();
            //睡眠5秒
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //按意愿手动终止线程
            myRunnable.run = false;
        }
    }
    
    class MyRunnable implements Runnable{
        //做一个布尔标记
        boolean run = true;
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                if (run){
                    System.out.println(Thread.currentThread().getName()+i+"--->begin");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    /*在终止线程之前,
                    加上需要执行的操作。
                    比如保存数据*/
                    System.out.println("数据以保存!");
                    //终止当前线程
                    return;
                }
            }
        }
    }
    

    输出:
    在这里插入图片描述

    线程调度

    概述
    • 常见的线程调度模型有:
      1、抢占式调度模型:
      哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。java采用的就是抢占式调度模型。
      2、均分式调度模型:
      平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样,平均分配,一切平等。
    与线程调度有关的方法
    • void setPriority(int newPriority)
      更改此线程的优先级。
      (最低优先级1;默认优先级是5;最高优先级10。)
      优先级较高的,不是优先执行;只是抢到的CPU时间片相对多一些,处于运行状态的时间多一点。
    public class Demo{
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable());
            t.setPriority(10);
            t.setName("t");
            t.start();
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            }
        }
    }
    
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            }
        }
    }
    

    输出:
    在这里插入图片描述

    • int getPriority()
      返回此线程的优先级。
    public class Demo{
        public static void main(String[] args) {
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName()+"默认优先级是:"+currentThread.getPriority());
            Thread t = new Thread(new MyRunnable());
            t.setName("t");
            t.start();
        }
    }
    
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName()+"默认优先级是:"+thread.getPriority());
        }
    }
    

    输出:
    在这里插入图片描述

    • static void yield()(让位方法)
      暂停当前正在执行的线程对象,并执行其它线程。
      yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
      yield()方法的执行会让当前线程从“运行状态"回到”就绪状态"。
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                //让位给主线程
                if(i % 100 == 0){
                    Thread.yield();
                }
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            }
        }
    }
    
    • void join()(线程合并)
      等待这个线程死亡。
      合并线程。将某线程合并到当前线程中,当前线程受阻塞,该线程执行直到结束。
    public class Demo{
        public static void main(String[] args) {
            System.out.println("main begin");
            Thread t = new Thread(new MyRunnable());
            t.setName("t");
            t.start();
            /*合并线程.
            t合并到当前线程中,当前线程受阻塞,
            t线程执行直到结束。*/
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main over");
        }
    }
    
    class MyRunnable implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            }
        }
    }
    

    输出:
    在这里插入图片描述

    线程的同步和异步

    多线程并发环境下,数据的安全问题
    • 线程安全问题
      1、为什么这个是重点?
      以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义、线程对象的创建、线程的启动等...都已经实现完了。这些代码我们都不需要编写。
      2、最重要的是:
      要知道自己編写的程序需要放到一个多线程的环境下运行,则更需要关注的是这些数据在多线程并发的环境下是否是安全的。
      3、线程不安全的例子:
      A和B同时去银行在同一个账户(有1W)取钱,A取出1W后由于网络延迟,银行没有及时更新数据;这时B又去取了1W,银行才更新了数据,这时就出问题了。
      4、什么时候数据在多线程并发的环境下会存在安全问题呢?
      三个条件:
      条件一:多线程并发。
      条件二:有共享数据(实例变量、静态变量)。
      条件三:共享数据有修改的行为。
      满足以上3个条件之后,就会存在线程安全问题。
    • 线程同步机制
      1、怎么解决线程安全问题呢?
      线程排队执行(不能并发)。
      用排队执行解决线程安全问题。这种机制被称为:线程同步机制。专业术语叫做:线程同步。实际上就是线程不能并发了,线程必须排队执行。
      2、线程排队了就会牺牲一部分效率,但是数据安全第一位。
      3、线程同步涉及到的两个专业术语
      异步编程模型:线程1和线程2,各自执行各自的,谁也不需要等谁,其实就是多线程并发(效率较高)。
      总结:异步就是并发。
      同步编程模型:线程1和线程2,在线程1执行的时候,必须等线程2执行结束;反之亦然。两个线程之间发生了等待关系,线程排队执行(效率较低)。
      总结:同步就是排队。
    模拟银行取钱的例子
    public class Demo{
        private String actno;
        private double balance;
    
        public Demo() {
        }
    
        public Demo(String actno, double balance) {
            this.actno = actno;
            this.balance = balance;
        }
    
        public String getActno() {
            return actno;
        }
    
        public void setActno(String actno) {
            this.actno = actno;
        }
    
        public double getBalance() {
            return balance;
        }
    
        public void setBalance(double balance) {
            this.balance = balance;
        }
    
        //取款方法
        public void withdraw(double money){
            //取款之前的余额
            double before = this.getBalance();
            //取款之后的余额
            double after = before-money;
            //模拟网络延迟(睡眠一秒)
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //更新余额
            this.setBalance(after);
        }
    }
    
    public class DemoThread extends Thread{
        //两个线程共享一个账户对象
        private Demo act;
        //构造方法传递账户对象
        public DemoThread(Demo act) {
            this.act = act;
        }
    
        @Override
        public void run(){
            //取款操作
            double money = 5000;
            act.withdraw(money);
            System.out.println(Thread.currentThread().getName()+
                    "账户"+act.getActno()+"取款"+money+"成功,余额为:"+
                    act.getBalance());
        }
    }
    
    public class DemoTest{
        public static void main(String[] args) {
            //创建一个账户对象
            Demo demo = new Demo("act-001",10000);
            //创建两个线程
            Thread t1 = new DemoThread(demo);
            Thread t2 = new DemoThread(demo);
            //设置name
            t1.setName("t1");
            t2.setName("t2");
            //启动线程取款
            t1.start();
            t2.start();
        }
    }
    

    输出:
    在这里插入图片描述

    线程同步机制
    • 语法
    synchronized (线程共享对象){
                //线程同步代码块
            }
    
    • synchronized后的括号写什么?
      1、synchronized后面小括号中传的这个“数据”,是相当关键的。这个数据必须是多线程共享的数据,才能达到多线程排队。
      2、括号中写什么?
      要看需要将哪些线程同步。假设t1、t2、t3、t4、t5,有5个线程,你只希望t1、t2、t3排队,t4、t5不需要排队。则要在()中写一个t1、t2、t3共享的对象。而这个对就对于t4、t5来说不是共享的。
    • 改进模拟银行取钱
      修改withdraw方法即可:
        public void withdraw(double money){
            /*以下这几行代码必须是线捏排队的,不能并发。
            一个线程把这里的代码全部执行结束之后,
            另一个线程才能进来。*/
            synchronized (this){
                double before = this.getBalance();
                double after = before-money;
                /*这里等待1秒钟是为了延迟结束线程,
                保证run方法中的输出语句中的
                act.getBalance()输出的是当前线程
                执行完后的余额。
                如果不睡眠的话,输出语句中的信息
                就会是两个线程结束后的信息,
                输出的余额都为0*/
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.setBalance(after);
            }
        }
    

    输出:
    在这里插入图片描述

    synchronized

    更多关于synchronized①
    更多关于synchronized②
    更多关于synchronized③

    对synchronized的理解
    • 使用场景
      在这里插入图片描述
    • 对象锁
      1、在java语言中每个对象都有一把“锁”。
      synchronized后面小括号中加的是需要上锁的对象,同对象下的A、B线程:
      在这里插入图片描述
      2、在Demo中创建一个实例变量:
      Object obj = new Object();
      也可以使用:synchronized (obj){}(只要是共享对象都行)
      (在synchronized (){}大括号中的局部变量就不行)
      3、synchronized (“abc”){}也可以,“abc”在字符串常量池中是共享的。但是“abc”会使所有线程都同步,因为不是某几个共享的,而是所有线程共享的。
    • 在实例方法上使用
      synchronized出现在实例方法上,锁的是类的实例对象,这种方式不灵活。整个方法体都需要同步,可能会无故扩大同步的范围,导致程序的执行效率低。优点就是代码少。
    思考题
    • 题目一:同一对象有t1、t2线程。t1先执行,调用doSome方法;t2调用doOther方法;问:t2需不需要等待t1?
      MyClass mc = new MyClass();
    class MyClass{
        public synchronized void doSome(){ }
        public void doOther(){ }
    }
    

    答:不需要,因为doOther没有synchronized关键字修饰,执行doOther不需要获取共享对象的对象锁。

    • 题目二:同一对象有t1、t2线程。t1先执行,调用doSome方法;t2调用doOther方法;问:t2需不需要等待t1?
    class MyClass{
        public synchronized void doSome(){ }
        public synchronized void doOther(){ }
    }
    

    答:需要。

    • 题目三:两个对象分别有t1、t2线程。t1先执行,调用doSome方法;t2调用doOther方法;问:t2需不需要等待t1?
      MyClass mc1 = new MyClass();
      MyClass mc2 = new MyClass();
    class MyClass{
        public synchronized void doSome(){ }
        public synchronized void doOther(){ }
    }
    

    答:不需要。因为有两个MyClass对象,两把锁。

    • 题目四:两个对象分别有t1、t2线程。t1先执行,调用doSome方法;t2调用doOther方法;问:t2需不需要等待t1?
    class MyClass{
        public synchronized static void doSome(){ }
        public synchronized static void doOther(){ }
    }
    

    答:需要。因为这是类锁,锁的是MyClass这个类,不管创建了几个对象,这时就只有一把锁。

    死锁

    概述

    1、线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
    2、例子:
    当线程A持有独占锁a,并尝试去获取独占锁b的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相持有对方需要的锁,会发生阻塞现象。不出现异常,也不会出现错误,程序一直僵持在那里。
    3、图例:
    在这里插入图片描述
    4、死锁产生的4个必要条件
    1.互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
    2.请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
    3.不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
    4.循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

    死锁代码示例
    public class Demo{
        public static void main(String[] args) {
            Object o1 = new Object();
            Object o2 = new Object();
            //t1、t2两个线程共享o1、o2
            Thread t1 = new MyThread1(o1,o2);
            Thread t2 = new MyThread2(o1,o2);
            t1.start();
            t2.start();
        }
    }
    
    class MyThread1 extends Thread{
        Object o1;
        Object o2;
    
        public MyThread1(Object o1, Object o2) {
            this.o1 = o1;
            this.o2 = o2;
        }
    
        @Override
        public void run() {
            synchronized (o1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2){
                }
            }
        }
    }
    
    class MyThread2 extends Thread{
        Object o1;
        Object o2;
    
        public MyThread2(Object o1, Object o2) {
            this.o1 = o1;
            this.o2 = o2;
        }
    
        @Override
        public void run() {
            synchronized (o2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){
                }
            }
        }
    }
    

    控制台(永远不会结束,只能手动stop):
    在这里插入图片描述

    如何解决线程安全问题

    如果一开始就选择线程同步(synchronized)会让程序的执行效率降低,系统的用户吞吐量降低,用户体验不好。在不得已的情况下再选择线程同步机制。
    1、第一种方案:尽量使用局部变量代替“实例变量和静态变量"。
    2、第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象。)
    3、第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized---线程同步机制。

    线程的其它内容

    守护线程
    • 概述
      1、java语言中线程分为两大类:
      用户线程
      守护线程(后台线程)
      2、它是系统的守护者,在后台默默完成一些系统性的服务。任何一个守护线程都是整个JVM中所有非守护线程的“保姆”:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。其中具有代表性的就是:垃圾回收线程(守护线程)。守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。注意:主线程main方法是一个用户线程。
      3、守护线程的应用(例子)
      每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程。一直在那里看着,没到00:00的时候就备份一次。所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了。
      4、void setDaemon(boolean on)
      将此线程标记为守护线程(true)或用户线程(false)。
    • 代码示例
    public class Demo{
        public static void main(String[] args) {
            Thread t = new BakDataThread();
            t.setName("备份数据的线程");
            /*启动线程之前,将线程设置为守护线程*/
            t.setDaemon(true);
            t.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{
        @Override
        public void run() {
            int i = 0;
            /*即使是死循环,但由于该线程是守护者,
            当用户线程结束,守护线程自动终止。*/
            while (true){
                System.out.println(Thread.currentThread().getName()+"--->"+(++i));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    没有设置守护线程之前(会一直死循环):
    在这里插入图片描述
    设置后:
    在这里插入图片描述

    定时器
    • 概述
      1、间隔特定的时间,执行特定的程序。
      2、例如:每周要进行银行账户的总账操作。每天要进行数据的备份操作,在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见。
      3、在java中可以采用多种方式实现:可以使用sleep方法(睡眠),设置睡眠时间,醒来就执行任务。这种方式是最原始的定时器。
      4、java的类库中的一个定时器:java.util.Timer,可以直接用。(但是不常用,许多框架支持定时器)
    • 代码示例(也可以使用匿名内部类)
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Timer;
    import java.util.TimerTask;
    
    public class Demo{
        public static void main(String[] args) throws Exception{
            //创建定时器对象
            Timer timer = new Timer();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            //设置开始时间
            Date firstTime = sdf.parse("2020-06-09 21:49:00");
            //指定定时任务
            timer.schedule(new LogTimerTask(),firstTime,1000*2);
        }
    }
    //编写一个定时任务类(记录日志的定时任务)
    class LogTimerTask extends TimerTask {
        @Override
        public void run() {
            //编写需要执行的任务
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String sTime = sdf.format(new Date());
            System.out.println(sTime + "数据备份完成!");
        }
    }
    

    输出(需要手动停止):
    在这里插入图片描述

    实现线程的第三种方式
    • 概述
      1、FutureTask方式,实现Callable接口。
      2、这种方式实现的线程可以获取线程的返回值。
      3、优点:可以获取到线程的执行结果。
      缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
    • 代码示例
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    public class Demo{
        public static void main(String[] args){
            //创建FutureTask对象
            FutureTask task = new FutureTask(new Callable() {
                //call方法相当于run方法,只不过有返回值
                @Override
                public Object call() throws Exception {
                    System.out.println("call begin");
                    Thread.sleep(1000*2);
                    System.out.println("call over");
                    int a = 11;
                    int b = 30;
                    return a + b;
                }
            });
    
            //创建线程对象
            Thread t = new Thread(task);
            t.start();
            //获取t线程的返回结果
            try {
                //get方法的执行会导致当前线程阻塞
                Object obj = task.get();
                System.out.println(obj);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    

    输出:
    在这里插入图片描述

    关于Object类中的wait和notify方法。
    • 概述
      1、wait和notify方法(生产者和消费者模式)
      2、wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是object类中自带的。wait方法和notify方法不是通过线程对象调用。
    • wait()方法
    object o = new object();
    o.wait();
    

    o.wait();方法的调用,会让"当前线程(正在o对象上活动的线程) "进入等待状态,直到被唤醒为止。直到最终调用o.notify0方法。

    • notify()方法
    object o = new object ();
    o.notify();
    

    o.notify();方法的调用可以唤醒正在o对象上等待的线程。
    还有一个notifyAll()方法:唤醒o对象上处于等待的所有线程。

    • 生产者和消费者模式
      1、为了专门解决某个特定需求的:
      生产线程负责生产,消费线程负责消费。最终要达到生产和消费必须均衡。
      2、例如:生产满了,就不能再继续生产了,必须让消费线程进行消费;消费完了,就不能再消费了,必须让生产线程进行生产。
      3、仓库是多线程共享,所以需要考虑仓库的线程安全问题。仓库对象最终调用wait和notify方法。wait方法和notify方法建立在synchronized线程同步的基础之上
      4、重点:
      o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁。
      o.notify()方法只会通知,不会释放之前占有的o对象的锁。
    • 代码示例
    import java.util.ArrayList;
    import java.util.List;
    
    public class Demo{
        public static void main(String[] args){
            //创建一个共享的仓库对象
            List list = new ArrayList();
            //创建两个线程对象
            //生产者
            Thread t1 = new Thread(new Producer(list));
            //消费者
            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){
                //给仓库对象list加锁
                synchronized (list){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (list.size() > 0){
                        try {
                            //当前线程进入等待状态时释放占有的list集合的锁。
                            list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //程序到这儿说明仓库是空的,可以生产
                    Object obj = new Object();
                    list.add(obj);
                    System.out.println(Thread.currentThread().getName()+"--->"+obj);
                    //唤醒消费者进行消费
                    list.notify();
                }
            }
        }
    }
    
    /**消费线程*/
    class Consumer implements Runnable{
        //仓库
        private List list;
    
        public Consumer(List list) {
            this.list = list;
        }
    
        @Override
        public void run() {
            //消费
            while (true){
                synchronized (list){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (list.size() == 0){
                        try {
                            //等待状态,释放锁
                            list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //程序到这儿说明仓库有产品,可以消费
                    Object obj = list.remove(0);
                    System.out.println(Thread.currentThread().getName()+"--->"+obj);
                    //唤醒生产者进行生产
                    list.notify();
                }
            }
        }
    }
    

    输出(手动结束):
    在这里插入图片描述

  • 相关阅读:
    Linux与Windows共享文件夹之samba的安装与使用(Ubuntu为例)
    linux下导入、导出mysql数据库命令
    navicat for mysql (10038)如何解决,远程无法连接问题
    js过滤前后空格
    js大小写转换
    Sublime Text 超好用的侧栏插件SideBarEnhancements
    nginx,FastCGI启动语句
    Samba Linux和Windows互访
    JS 添加千分位,测试可以使用
    Sublime Text插件:HTML+CSS+JAVASCRIPT+JSON快速格式化
  • 原文地址:https://www.cnblogs.com/yu011/p/13058201.html
Copyright © 2011-2022 走看看