线程-dljd-java基础-LaoDu-jdk13

框架的设计思想
体系的组织结构设计(重要组件、模块划分、模块间交互)

详细设计:实现方法(技术)
使用说明:常用配置
常用工具

一、需求调研、需求分析(即应用场景)

          假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。
        火车站,可以看做一个进程。火车站中的每一个售票窗口可以看做是一个线程。我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
        所以多线程并发可以提高效率。java中之所以有多线程机制,目的就是为了
提高程序的处理效率

二、工作原理、运行流程

  1. java支持多线程机制。并且java已经将多线程实现了,我们只需要继承就行了。
  2. 输出结果是:总是有一个先一个后、有多有少。这是因为线程会一起抢夺到cpu时间片,即抢夺cpu执行权。      
  3. java虚拟机(JVM)的调度,调度使得当前线程频繁在就绪状态和运行状态之间进行切换。
  4. JVM线程调度(这部分内容属于了解):常见的线程调度模型有哪些?

    抢占式调度模型:

           那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。

           java采用的就是抢占式调度模型。

    均分式调度模型:

           平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。

           平均分配,一切平等。

           有一些编程语言,线程调度模型采用的是这种方式。

三、具体:重点1:cpu控制着线程的时间片,留给程序员控制线程的方式不多了,都有哪些?

1、获取当前线程对象

package com.bjpowernode.thread;

/**
 * 1、怎么获取当前线程对象?
 *      Thread t = Thread.currentThread();
 *      返回值t,就是当前线程对象
 * 2、获取当前线程对象的名字
 * 3、修改当前线程对象的名字
 * 4、注意:当线程没有设置名字,默认的名字有什么规律?(了解一下)
 *      Thread-0
 *      Thread-1
 *      Thread-2
 *      Thread-3
 *      ......
 */
public class ThreadTest05 {
    public static void main(String[] args) {
        //  获取当前线程对象
        Thread tt = Thread.currentThread();
        System.out.println("主线程的名字:"+tt.getName());
        
        //  创建线程对象
        MyThread t1 = new MyThread();
        t1.start();
        //  创建线程对象
        MyThread t2 = new MyThread();
        t1.start();
        /**
         * 什么是当前线程呢?
         *      答:
         *          这句代码“Thread t = Thread.currentThread();”出现在哪个线程中,获取到的当前线程对象就是哪个线程。
         *          这里,这句代码“Thread t = Thread.currentThread();”出现在main()方法当中,所以当前线程对象就是主线程。
         */
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<100;i++) {
            Thread t = Thread.currentThread();
            System.out.println("分支线程的名字:"+t.getName());
            /**
             * 什么是当前线程呢?
             *      答:
             *          这句代码“Thread t = Thread.currentThread();”出现在哪个线程中,获取到的当前线程对象就是哪个线程。
             *          这里,这句代码“Thread t = Thread.currentThread();”出现在分支线程t1的run()方法当中,所以当前线程对象就是分支线程t(即上面的MyThread t1 = new MyThread())。
             *
             *   当t1线程执行run()方法,那么这个当前线程就是t1
             *   当t2线程执行run()方法,那么这个当前线程就是t2
             */
            System.out.println("分支线程--->" + i);
        }
    }
}

2、获取、修改当前线程对象的名字

package com.bjpowernode.thread;

/**
 * 1、怎么获取当前线程对象?
 * 2、获取当前线程对象的名字
 * 3、修改当前线程对象的名字
 * 4、注意:当线程没有设置名字,默认的名字有什么规律?(了解一下)
 *      Thread-0
 *      Thread-1
 *      Thread-2
 *      Thread-3
 *      ......
 */
public class ThreadTest05 {
    public static void main(String[] args) {
        //  创建线程对象
        MyThread t = new MyThread();
        //  修改当前线程对象的名字
        t.setName("ttt");
        //  获取当前线程对象的名字
        String tName = t.getName(); //  默认的线程的名称为Thread-0、Thread-1
        System.out.println(tName);
        //  启动线程
        t.start();
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<100;i++) {
            System.out.println("分支线程--->" + i);
        }
    }
}

3、sleep()方法的使用

(1)sleep()

package com.bjpowernode.thread;

/**
 *关于线程的sleep()方法:static void sleep(long millis)
 *  1、静态方法:Thread.sleep(1000)
 *  2、参数是毫秒
 *  3、作用:让当前线程进入休眠,即进入“阻塞状态”,放弃占有的cpu时间片,让给其它线程使用。
 *      这行代码出现在t1线程的run()方法中,t1线程就会进入休眠。
 *      这行代码出现在t2线程的run()方法中,t2线程就会进入休眠。
 *  4、Thread.sleep()方法,可以做到这种效果:
 *      间隔特定的时间,去执行一段特定的代码,每隔多久执行一次
 */
/**
 * 什么是当前线程呢?
 *      答:
 *          这句代码“Thread.sleep(1000)”出现在哪个线程中,获取到的当前线程对象就是哪个线程。
 *
 *          当t1线程执行run()方法,那么这个当前线程就是t1
 *          当t2线程执行run()方法,那么这个当前线程就是t2
 */
public class ThreadTest06 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
        MyThread t2 = new MyThread();
        t2.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<100;i++) {
            try {
                //  让当前线程进入休眠,睡眠1秒
                //  当t1线程执行run()方法,那么这个当前线程就是t1
                //  当t2线程执行run()方法,那么这个当前线程就是t2
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("分支线程--->" + i);
        }
    }
}

(2)关于sleep()的一个面试题

package com.bjpowernode.thread;

/**
 *  关于Thread.sleep()方法的一个面试题目
 */
public class ThreadTest07 {
    public static void main(String[] args) throws InterruptedException {
        //  创建线程对象
        Thread t = new MyThread();
        t.setName("t");
        t.start();

        //  调用sleep方法
        //  面试题目:这行代码会让线程t进入休眠状态吗?不会
        //  因为.sleep()方法是一个静态方法,调用它的时候和t没关系,最终运行的代码实际上是Thread.sleep(),
        //  而这行代码出现在main()方法中,
        //  因此进入休眠状态的当前线程对象是main()方法所在的主线程。
        t.sleep(1000*5);
        System.out.println("hello World!");
    }
}

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

(3)interrupt():终止线程的休眠

package com.bjpowernode.thread;

/**
 *  sleep睡眠太久了,如果希望半道上醒来,你应该怎么办?也就是说怎么叫醒一个正在睡眠的线程?
 *  注意:这个不是终止线程的执行,是
 */
public class ThreadTest08 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.setName("t");
        t.start();

        //  希望5秒之后,t线程醒来(5秒之后主线程手里的活干完了,那个t线程该醒来干活了啊)
        Thread.sleep(1000*5);

        //  中断t线程的睡眠(这种中断睡眠的方式依靠了java的异常处理机制)
        t.interrupt();//  干扰,一盘冷水过去
        /**
         * 首先,此方法会让正在睡眠的t线程出异常,报错的是31行(Thread.sleep(1000*60*60*24*365))。
         * 其次,Thread.sleep(1000*60*60*24*365)报出异常以后,进入catch块中去执行,即e.printStackTrace()在控制台打印出异常信息吧。此时整个try-catch就结束了,代码会继续往下执行。
         */
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<10000;i++) {
            System.out.println(Thread.currentThread().getName()+"--->begin");
            //  睡眠1年
            try {
                Thread.sleep(1000*60*60*24*365);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //  1年之后才会执行下面这行代码
            System.out.println(Thread.currentThread().getName()+"--->end");
        }
    }
}

4、stop()

package com.bjpowernode.thread;

/**
 *  在java中怎么强行终止一个线程?
 *      这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了。
 *      线程没有保存的数据会丢失。
 *      不建议使用。
 */
public class ThreadTest08 {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.setName("t");
        t.start();

        //  模拟5秒
        Thread.sleep(1000*5);
        //  5秒之后强行终止t线程
        t.stop();//  已过时(不建议使用),相当于windows任务管理器中的"结束进程"操作
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            System.out.println(Thread.currentThread().getName()+"--->"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

5、布尔标识+return合理终止线程

package com.bjpowernode.thread;

/**
 *  在java中怎么合理地终止一个线程的执行?
 *  这种方式是很常用的:
 */
public class ThreadTest08 {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.setName("t");
        t.start();

        //  模拟5秒
        Thread.sleep(1000*5);
        //  5秒之后强行终止t线程
        //  你想要在什么时候终止t的执行,那么你就所标记修改为false,就结了。
        t.run = false;
    }
}

class MyThread extends Thread{
    //  打一个布尔标记
    boolean run = true;
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            if(run){
                System.out.println(Thread.currentThread().getName()+"--->"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                //  return就结束了,你在结束之前还有什么没保存的
                //  在这里可以保存呀
                //  save......
                //  终止当前线程
                return;
            }
        }
    }
}

6、java中提供了哪些方法是和线程调度有关系的呢?(了解)

(1)实例方法:setPriority()、getPriority():设置和获取线程的优先级

        void setPriority(int newPriority) 设置线程的优先级

        int getPriority() 获取线程优先级

        最低优先级1

        默认优先级是5

        最高优先级10

        优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)

(2)静态方法:yield():线程让位方法

        static void yield()  让位方法

        暂停当前正在执行的线程对象,并执行其他线程

        yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。

        yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。

        注意:在回到就绪之后,有可能还会再次抢到。

(3)实例方法:join():合并线程

        void join()  :合并线程

        案例1:

                class MyThread1 extends Thread {

                        public void doSome(){

                                MyThread2 t = new MyThread2();

                                t.join(); // 当前线程进入阻塞,t线程执行,直到t线程结束。当前线程才可以继续。

                        }

                }

                class MyThread2 extends Thread{

                }

        案例2:

四、具体:重点2:线程之间的数据共享问题

1、为什么这个是重点?

        以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。

        最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点:*****)

        ds:在以后工作中,去实现一个线程的概率还是比较低的。也就是说,去new一个线程对象,创建线程对象,调用线程对象的.start()方法,这些代码我们几乎不用去写。这是因为工作中,我们的程序都是运行在服务器当中的。服务器已经把多线程这种机制,给我们实现了。服务器帮我们创建线程(new Thread()),而且帮我们把线程启动起来(.start())。也就是说,服务器是支持多线程的,它已经把线程给我们实现了。但是大家一定要记住,以后我们所开发的程序,定完之后肯定是要扔到一个多线程环境下去运行的。因此,我们更应该注重的是,我们所写的代码是否在这种多线程并发环境下,数据的共享是安全的,即线程是安全的。

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

三个条件:

        条件1:多线程并发。

        条件2:有共享数据。

        条件3:共享数据有修改的行为。

满足以上3个条件之后,就会存在线程安全问题。

案例:两个人对同一个银行账户进行取款。

  1. 首先,银行账户里面有10000元。
  2. 其次,我是t1线程,我去柜台取钱。
  3. 然后,张三是t2线程,去ATM机上取钱。
  4. 此时,t1和t2线程同时并发,共享同一个数据,即此账户下的10000元。
  5. 接着,首先:t1线程会先把余额读出来,余额自然是10000元。其次:t1线程就是要取走这10000元,即10000-10000=0元。最后:需要把余额0元更新到数据库中。
  6. 情况,如果t1在把余额0元更新到数据库的过程中,出现了网络延时,延迟更新了,即此时银行账户中的钱还是10000元。
  7. 此时,首先,t2线程在ATM机中刚好在取钱,余额竟然还是10000元。其次:t2线程也是要取走这10000元,即10000-10000=0元。最后:也是需要把余额0元更新到数据库中。
  8. 情况,这时t1在把余额0元更新到数据库的网络延时恢复了,成功把数据库中的余额改成0元了。
  9. 接着,t2在把余额0元更新到数据库也成功。
  10. 结局,最后一共取款20000元,银行就哭了。
  11. 结论:多线程并发的环境下,如果对同一个银行账户进行操作,那么这个时候就必然涉及到一个安全的问题。

问题:如果上面的t1和t2线程,不是去取钱,而只是去柜台和ATM机上查询账户的余额,那么会不会出现多线程安全问题?

        答:没有,只是读余额,而不涉及到数据的修改就不会出现多线程安全问题。

问题:如何解决上面的多线程安全问题?

        答:t1线程对账户进行操作的时候,t2线程必须挂起(排除等待)。只有t1线程把对账户的所有操作都做完了,t2线程才可以操作。这样就可以解决数据的安全问题,多线程的安全问题了。

3、怎么解决线程安全问题呢?

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

        线程排队执行(不能并发)。用排队执行解决线程安全问题,这种机制被称为:线程同步机制。专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。

        怎么解决线程安全问题呀?使用“线程同步机制”。线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全。第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

4、说到线程同步这块,涉及到这两个专业术语:

(1)异步编程模型:

                线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高。)

                异步就是并发。

(2)同步编程模型:

        线程t1和线程t2,在线程t1执行的时候,t2线程必须等待t1线程执行结束,或者说在t2线程执行的时候,t1线程必须等待t2线程执行结束。两个线程之间发生了等待关系,这就是同步编程模型。

        效率较低。线程排队执行。

        同步就是排队。

5、出现线程安全问题的代码

package com.bjpowernode.ThreadSsfe;

/**
 * 银行账户
 * 不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。
 */
public class Account {
    // 账户
    private String actno;
    // 余额
    private double balance;

    public Account() {
    }

    public Account(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) throws InterruptedException {
        //  取款之前的余额
        double before = this.getBalance();
        //  取款之后的余额
        double after = before - money;
        //  在这里模拟一下网络延时,100%会出问题
        Thread.sleep(1000);
        //  更新余额
        this.setBalance(after);
    }
}

package com.bjpowernode.ThreadSsfe;

public class AccountThread extends Thread{
    // 满足条件2:有共享数据。
    private Account act;
    public AccountThread(Account act){//  通过构造方法传递过来账户对象
        this.act = act;
    }

    //  t1和t2并发这个方法......(t1和t2是两个栈,两个栈操作堆中同一个对象(即main方法中唯一创建的act对象)。)
    @Override
    public void run() {//  run()方法的执行表示取款操作
        double money = 5000;
        //  满足条件3:共享数据有修改的行为。
        //  思考:假设t1执行到这里的,但还没有来得及执行这行代码。此时t2线程进来withdraw方法,此时一定出问题。
        try {
            act.withdraw(money);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"线程对账户:"+act.getActno()+",取款"+money+"成功,余额 = "+act.getBalance());
    }
}

package com.bjpowernode.ThreadSsfe;

public class Test {
    public static void main(String[] args) {
        //  创建账户对象(只创建1个)
        Account act = new Account("act-001",10000);

        //  创建两个线程
        //  满足条件1:多线程并发。
        //  满足条件2:有共享数据。
        Thread t1 = new AccountThread(act);
        t1.setName("t1");
        t1.start();//   取款
        Thread t2 = new AccountThread(act);
        t2.setName("t2");
        t2.start();//   取款
    }
}

6、解决方案1:synchronized (共享对象){ }

 //  取款方法
    public void withdraw(double money) throws InterruptedException {
        //  以下这几行代码必须是线程排除的,不能并发
        //  一个线程把这里的代码全部执行结束之后,另一个线程才能进来
        /**
         * 基础:
         *      在java语言中,任何一个对象都有”一把锁“,其实这把锁就只是一个标记(只是我们习惯把它叫做锁)。一个对象1把锁,100个对象,100把锁。

         *
         * 线程同步机制的语法是:
         *      synchronized (){
         *          //  线程同步代码块
         *      }
         *
         * synchronized后面小括号中传的这个”数据“是相当关键的,这个数据必须是多线程共享的数据,才能达到多线程排除的效果。
         *      ()中到底写什么?填什么?填共享对象。
         *      那这个共享对象是谁的共享对象?那要看你想让哪些线程同步,就写那些线程共享的对象(数据)。
         *      注意:()中不一定填写的是this,这里只要填写的是多线程共享的那个对象就行。
         *
         * 案例1:
         *      假设有:t1、t2、t3、t4、t5,有5个线程。
         *      如果你只希望t1、t2、t3排队,t4、t5不需要排队,怎么办?你一定要在()中写一个t1、t2、t3共享的对象,而这个对象对于t4、t5来说不是共享的。
         *
         * 案例2:
         *      这里的t1、t2线程共享对象是:账户对象。而this代表的就是当前的账户对象,因此这里()中填写的是this。
         *      注意:
         *          这里只是碰巧
         *          ()中不一定填写的是this,这里只要填写的是多线程共享的那个对象就行。
         *
         *
         * 以下代码的运行流程是:
         * 1、假设t1租t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
         * 2、假设t1先执行了,遇到了synchronized ,这个时候自动找“后面共享对象”的对象锁,
         *    找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
         *    占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
         * 3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也想会去占有后面
         *    共享对象的这把锁,结果这把锁被t1占有, t2,只能在同步代码块外面等待t1的结束,
         *    直到1把同步代码块执行结束了, t1会归还这把锁,此时t2终于等到这把锁,然后
         *    t2占有这把锁之后,进入同步代码块执行程序。
         * 4、这样就达到了线程排队执行。
         * 注意:这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。
         */
        synchronized (this){
            double before = this.getBalance();
            double after = before - money;
            Thread.sleep(1000);//  在这里模拟一下网络延时,非同步机制下100%会出问题
            this.setBalance(after);
        }
    }

7、(共享对象),中共享对象的理解

方式1:不行:synchronized (this){}


public class Account {
    ......
    //  取款方法
    public void withdraw(double money) throws InterruptedException {
        synchronized (this){//    此时不行了
            double before = this.getBalance();
            double after = before - money;
            Thread.sleep(1000);//  在这里模拟一下网络延时,非同步机制下100%会出问题
            this.setBalance(after);
        }
    }
}


public class Test {
    public static void main(String[] args) {
        Account act = new Account("act-001",10000);

        Thread t1 = new AccountThread(act);
        t1.setName("t1");
        t1.start();//   取款
        Thread t2 = new AccountThread(act);
        t2.setName("t2");
        t2.start();//   取款

        Account act2 = new Account("act-001",10001);
        Thread t3 = new AccountThread(act2);
        t3.setName("t2");
        t3.start();//   取款
    }
}

注意:此时,如果是t3线程遇到synchronized (this){},那么t3线程不会排队。因为此时的t3线程有自己的Account act2对象,所以t3线程进来会直接拿到act2的锁,就直接执行了,不用排队。
        记住()中填写的是“多个线程共享的对象”,这是更古不变的道理。比如说,t1和t2线程共享的是1楼的厕所,那如果你想让t1和t2同步,那么()中填写的是“1楼厕所”。而对于使用2楼厕所的t3、t4、t5来说,遇到(1楼厕所)肯定不会实现同步、不排队。

方式2:行:synchronized (obj){}

public class Account {
    // 账户
    private String actno;
    // 余额
    private double balance;
    // 对象
    private Object obj = new Object(); //   实例变量。Account对象是多线程共享的,而Account对象中的实例变量obj也是共享的。
    
    ......

    //  取款方法
    public void withdraw(double money) throws InterruptedException {
        //synchronized (this){
        synchronized (obj){
            double before = this.getBalance();
            double after = before - money;
            Thread.sleep(1000);//  在这里模拟一下网络延时,非同步机制下100%会出问题
            this.setBalance(after);
        }
    }
}
  1. 首先,只要是多线程共享的对象,都可以往()中填写。
  2. 其次,t1和t2线程共享Account对象,这是肯定的,因为t1和t2线程的构造方法中传递的是同一个Account act,因此也就有了synchronized (this){}的写法。
  3. 接着,Account对象只有一个,所以Account对象的实例变量obj也只有一个。对比一下,这填写this是可以的,而this指的是当前的Account对象,也是只有一个吧。
  4. 然后,Account对象是多线程共享的,而Account对象中的实例变量obj也是共享的。
  5. 结论:因此这里写synchronized (obj){}和synchronized (this){}是一样的、没区别。

如上图所示: 

  1. 首先,jvm,只有一个。
  2. 其次,方法区,只有一个。
  3. 然后,堆内存,只有一个。在堆中我们只new了一个Account对象,Account对象有实例变量actno(账户)、实例变量balance(余额)、实例变量obj(Object对象)。其中,因为实例变量obj是一个引用,因此在堆内存中,首先:会new出一个Object对象,然后:把内存中的地址赋值给实例变量obj(0x1234),即实例变量obj指向内存中的Object对象。
  4. 结论:因为现在Account对象只有一个,因此Account对象中的实例变量obj也只有一个。
  5. 问题:那这里写成synchronized (obj){}行不行?
  6. 答答:行的。因为如果写的是synchronized (obj){},那么t1和t2线程执行run()方法遇到synchronized (obj){}后,都会去锁池中找共享对象obj的对象锁。而现在共享对象obj只有一个,t1或t2线程会所obj对象的对象锁占有,其中会有一个线程需要等待排队。因此最终也是实现了我们所说的同步机制,所以此时synchronized (obj){}的写法是正确的。

方式3:不行:synchronized (obj2){}


public class Account {
    ......

    //  取款方法
    public void withdraw(double money) throws InterruptedException {
        
        //synchronized (this){
        //synchronized (obj){
        Object obj2 = new Object();
        synchronized (obj2){//  这样编写就不安全了,因为obj2不是共享对象
            double before = this.getBalance();
            double after = before - money;
            Thread.sleep(1000);//  在这里模拟一下网络延时,非同步机制下100%会出问题
            this.setBalance(after);
        }
    }
}

synchronized (obj2){}行不行?这个为啥就不行呢?这个为啥就不可以呀?

  1. 首先,t1线程执行run()方法时,这个地方创建了一个obj2对象(Object obj2 = new Object())。
  2. 其次,因为obj2是局部变量,不同的线程进来会new出多个不同的obj2对象。也就是说,此时t2线程运行run()方法时,来到这个地方会创建了一个新的obj2对象(Object obj2 = new Object())。
  3. 结论:此时t1和t2线程所使用的obj2对象并不是同一个对象,所以obj2不是共享的对象,因此t1和t2线程都会在锁池中拿到对象锁。
  4. 效果:此时t1和t2线程不能实现同步机制,对于Account对象的balance实例变量来说,还是会出现线程不安全的情况。

方式4:行:synchronized ("abc"){}


public class Account {
    ......
    //  取款方法
    public void withdraw(double money) throws InterruptedException {
        //synchronized (this){
        //synchronized (obj){
        //Object obj2 = new Object();
        //synchronized (obj2){//  这样编写就不安全了,因为obj2不是共享对象
        synchronized ("abc"){// "abc"在字符串常量池当中,
            double before = this.getBalance();
            double after = before - money;
            Thread.sleep(1000);//  在这里模拟一下网络延时,非同步机制下100%会出问题
            this.setBalance(after);
        }
    }
}

因为"abc"在字符串常量池里面,所以String "abc"对象只有一个,t1和t2线程只能共享这个对象。

注意:如果写"abc"的话,所有的线程(t1、t2、t3、......t100......t100000)都会同步。

方式5:不行:synchronized (null){},编译都通不过,即语法错误,没有这个语法:


public class Account {
    ......
    //  取款方法
    public void withdraw(double money) throws InterruptedException {
        //synchronized (this){
        //synchronized (obj){
        //Object obj2 = new Object();
        //synchronized (obj2){//  这样编写就不安全了,因为obj2不是共享对象
        //synchronized ("abc"){// "abc"在字符串常量池当中,
        //synchronized (null){//  编译出错
        Object o = null;
        synchronized (null){//  报空指针异常
            double before = this.getBalance();
            double after = before - money;
            Thread.sleep(1000);//  在这里模拟一下网络延时,非同步机制下100%会出问题
            this.setBalance(after);
        }
    }
}

8、哪些变量有线程安全问题【重要的内容。】?

记住:搞线程安全,就是要保护java中三大变量的线程安全。

Java中有三大变量?【重要的内容。】

        实例变量:在堆中。

        静态变量:在方法区。

        局部变量:在栈中。

        注意:实例变量和静态变量统称成员变量。

以上三大变量中:

      

        局部变量永远都不会存在线程安全问题。

        因为局部变量不共享。(一个线程一个栈。)

        局部变量在栈中。所以局部变量永远都不会共享。

        实例变量在堆中,堆只有1个。

        静态变量在方法区中,方法区只有1个。

        堆和方法区都是多线程共享的,所以可能存在线程安全问题。

        局部变量+常量(不可变):不会有线程安全问题。

        成员变量 = 实例变量+静态变量:可能会有线程安全问题。

        

        如果使用局部变量的话:

                建议使用:StringBuilder。

                因为局部变量不存在线程安全问题。选择StringBuilder。

                StringBuffer效率比较低。

        ArrayList是非线程安全的。

        Vector是线程安全的。

        HashMap HashSet是非线程安全的。

        Hashtable是线程安全的。

9、扩大同步范围

注意:同步范围越大效率越低

原来:


public class Account {
    ......

    //  取款方法
    public void withdraw(double money) throws InterruptedException {
        synchronized (this){
            double before = this.getBalance();
            double after = before - money;
            Thread.sleep(1000);//  在这里模拟一下网络延时,非同步机制下100%会出问题
            this.setBalance(after);
        }
    }
}

package com.bjpowernode.ThreadSsfe;

public class AccountThread extends Thread{
    private Account act;
    public AccountThread(Account act){//  通过构造方法传递过来账户对象
        this.act = act;
    }

    public void run() {//  run()方法的执行表示取款操作
        double money = 5000;
        try {
            act.withdraw(money);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"线程对账户:"+act.getActno()+",取款"+money+"成功,余额 = "+act.getBalance());
    }
}

public class Test {
    public static void main(String[] args) {
        Account act = new Account("act-001",10000);

        Thread t1 = new AccountThread(act);
        t1.setName("t1");
        t1.start();//   取款
        Thread t2 = new AccountThread(act);
        t2.setName("t2");
        t2.start();//   取款
    }
}

扩大范围后:


public class Account {
    ......

    //  取款方法
    public void withdraw(double money) throws InterruptedException {
        //synchronized (this){
            double before = this.getBalance();
            double after = before - money;
            Thread.sleep(1000);//  在这里模拟一下网络延时,非同步机制下100%会出问题
            this.setBalance(after);
        //}
    }
}

package com.bjpowernode.ThreadSsfe;

public class AccountThread extends Thread{
    private Account act;
    public AccountThread(Account act){
        this.act = act;
    }

    public void run() {
        double money = 5000;
        try {
            synchronized (act){ //  同样实现t1和t2的同步,但同步范围扩大,效率更低了
                                //  注意,这里不能写(this),因为这里的this指的是AccountThread 对象,它不共享。因为我们会new出t1和t2线程,即两个AccountThread 对象。
                act.withdraw(money);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"线程对账户:"+act.getActno()+",取款"+money+"成功,余额 = "+act.getBalance());
    }
}

public class Test {
    public static void main(String[] args) {
        Account act = new Account("act-001",10000);

        Thread t1 = new AccountThread(act);
        t1.setName("t1");
        t1.start();//   取款
        Thread t2 = new AccountThread(act);
        t2.setName("t2");
        t2.start();//   取款
    }
}

10、总结1:synchronized有三种写法

第一种:同步代码块

        灵活

        synchronized(线程共享对象){

                同步代码块;

        }

第二种:在实例方法上使用synchronized

        表示共享对象一定是this,不灵活

        并且同步代码块是整个方法体,可能会无敌扩大同步的范围,导致程序执行效率的降低。所以这种方式不常用。

第三种:在静态方法上使用synchronized

        类锁的使用场景有哪些?类锁是保证静态变量的安全。

        表示找类锁。

        类锁永远只有1把。

        就算创建了100个对象,那类锁也只有一把。

对象锁:1个对象1把锁,100个对象100把锁。

类锁:100个对象,也可能只是1把类锁。

11、面试题1:doOther()方法在执行的时候需要不需要等待doSome()方法的结束?

package com.bjpowernode.exam1;

//  面试题:doOther()方法在执行的时候需要不需要等待doSome()方法的结束?
//  答:不需要,因为doOther()方法没有synchronized
public class Exam01 {
    public static void main(String[] args) throws InterruptedException {
        MyClass mc = new MyClass();
        //  t1和t2共享mc
        Thread t1 = new MyThread(mc);
        Thread t2 = new MyThread(mc);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        //  这里的睡眠的作用是为了保证t1线程先执行
        Thread.sleep(1000);
        t2.start();
    }
}
class MyThread extends Thread{
    private MyClass mc;
    public MyThread(MyClass mc) {
        this.mc = mc;
    }
    public void run() {
        if(Thread.currentThread().getName().equals("t1")){
            try {
                mc.doSome();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if(Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}
class MyClass{
    //  synchronized出现在实例方法上,表示锁this
    public synchronized  void doSome() throws InterruptedException {
        System.out.println("doSome begin");
        Thread.sleep(1000*10);
        System.out.println("doSome over");
    }
    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

问题:doOther()方法在执行的时候需要不需要等待doSome()方法的结束?
答:不需要。因为doOther()方法中没有synchronized,因此执行doOther()方法时不需要获取共享对象的对象锁,也就是说doOther()方法进来就直接执行了不需要排队。

  1. 首先,doSome()有synchronized的。doOther()方法没有synchronized。
  2. 其次,doSome()中:首先,先输出"doSome begin"。然后,睡眠10秒。此时,在睡眠过程中方法没有结束,所以这把锁(synchronized)是不会释放的。
  3. 接着,首先,t1线程启动了。然后,t1线程去执行doSome()方法。
  4. 然后,首先,t2线程启动。然后,t1线程去执行doOther()方法。
  5. 理论:synchronized出现在实例方法上,表示锁this。现在t1和t2线程共享mc对象,也就是synchronized(this)

12、面试题2:doOther()方法在执行的时候需要不需要等待doSome()方法的结束?

class MyClass{
    public synchronized  void doSome() throws InterruptedException {
        System.out.println("doSome begin");
        Thread.sleep(1000*10);
        System.out.println("doSome over");
    }
    //  如果doOther()方法上也加上synchronized关键字呢?
    //  doOther()方法在执行的时候需要不需要等待doSome()方法的结束?  
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

答案:需要。

首先,t1进来会把共享对象mc的对象锁拿到,执行doSome()方法。由于我们睡眠了10秒,所以10秒钟之后t1线程才会翻译共享对象mc的对象锁。 

其次,t2线程进来想去执行doOther()方法的时候,也需要共享对象mc的对象锁。在10秒之内,共享对象mc的对象锁还在t1手上,所以t2线程需要排队获取共享对象mc的对象锁才能去执行doOther方法。

13、面试题3:doOther()方法在执行的时候需要不需要等待doSome()方法的结束?

//  面试题:doOther()方法在执行的时候需要不需要等待doSome()方法的结束?
//  答:不需要。因为MyClass对象是两个,有两把锁
public class Exam01 {
    public static void main(String[] args) throws InterruptedException {
        MyClass mc1 = new MyClass();
        MyClass mc2 = new MyClass();
        //  修改:t1和t2线程没有使用同一个mc对象
        Thread t1 = new MyThread(mc1);
        Thread t2 = new MyThread(mc2);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        //  这里的睡眠的作用是为了保证t1线程先执行
        Thread.sleep(1000);
        t2.start();
    }
}
class MyClass{
    //  synchronized出现在实例方法上,表示锁this
    public synchronized  void doSome() throws InterruptedException {
        System.out.println("doSome begin");
        Thread.sleep(1000*10);
        System.out.println("doSome over");
    }
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

答案:不需要。

首先,两个对象,就说明有两把对象锁。其次,t1和t2线程使用不同的对象锁,因此不需要等待。

13、面试题4:doOther()方法在执行的时候需要不需要等待doSome()方法的结束?

//  面试题:doOther()方法在执行的时候需要不需要等待doSome()方法的结束?
//  答:需要。因为静态方法是类锁,不管创建了几个对象,类锁只有1把。
public class Exam01 {
    public static void main(String[] args) throws InterruptedException {
        MyClass mc1 = new MyClass();
        MyClass mc2 = new MyClass();
        //  t1和t2共享mc
        Thread t1 = new MyThread(mc1);
        Thread t2 = new MyThread(mc2);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        //  这里的睡眠的作用是为了保证t1线程先执行
        Thread.sleep(1000);
        t2.start();
    }
}
class MyClass{
    //  修改:静态方法
    public synchronized static void doSome() throws InterruptedException {
        System.out.println("doSome begin");
        Thread.sleep(1000*10);
        System.out.println("doSome over");
    }
    //  修改:静态方法
    public synchronized static void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}

答案:需要。

首先,synchronized出现在静态方法上,锁的类型是类锁。
其次,虽然我们这里new了两个MyClass对象,但类只有一个。
接着,t1和t2线程在分别执行doSome()和doOther()时,都需要MyClass类的类锁。因为类锁只有一把,所以需要等待。

14、死锁

        ds:锁这种东西用不好,就会出现死锁。

        ds:怎么会发生死锁呢?死锁是什么呢?

死锁会让程序停在那里,而且不出任何异常和错误,程序一直僵持在那里,这种错误最难测试。

首先,有两个对象。有t1和t2两个线程。
其次,t1线程从上往下执行,t2线程是从下往上执行。
然后,t1线程先锁住对象1,再去锁住对象2。如果t2线程先锁住对象2,再去锁住对象1。
情况:假如,t1线程先把对象1锁住,但由于t2线程事先已经把对象2锁住了,所以锁不了对象2。
           同理,t2线程先把对象2锁住,但由于t1线程事先已经把对象1锁住了,所以锁不了对象1。

案例:以下代码运行10年,都是这样,卡在那里

结论:synchronized在开发当中最好不要嵌套使用,一不小心就会出现死锁。死锁发生之后,很难调试,即这时错误到底出现在哪里了,你不知道、不确定。

package com.bjpowernode.deadlock;

/**
 * 死锁代码要会写,一般面试官要求你会写。
 * 因为死锁很难调试,只有会写的,才会在以后的开发中注意这个事儿。
 */
//  死锁案例
public class DeadLock {
    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.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}
class MyThread1 extends Thread{
    Object o1;
    Object o2;
    public MyThread1(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o1){      //先锁住o1
            try {
                //  这句代码的作用是,用来保证出现死锁
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){  //再锁住o2

            }
        }
    }
}

class MyThread2 extends Thread{
    Object o1;
    Object o2;
    public MyThread2(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o2){      //先锁住o2
            try {
                //  这句代码的作用是,用来保证出现死锁
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){  //再锁住o1

            }
        }
    }
}

15、总结2:我们以后开发中应该怎么解决线程安全问题?

是一上来就选择线程同步吗?synchronized

不是,synchronized会让程序的执行效率降低,用户体验不好。

系统的用户吞吐量(并发量,如秒杀功能)降低。用户体验差。在不得已的情况下再选择

线程同步机制。

第一种方案:尽量使用局部变量代替“实例变量和静态变量”。

                      后续学servlet时,都是使用局部变量的方式去解决线程安全问题的。

                     局部变量在栈里面,不有线程安全问题。

                    “实例变量(堆)和静态变量(方法区)”,堆和方法区是共享的,会线程安全问题。

        

第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样

实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,

对象不共享,就没有数据安全问题了。)

第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候

就只能选择synchronized了。线程同步机制。

16、关于Object类中的wait和notify方法。(生产者和消费者模式!)

(1)概述

第一:wait和notify方法不是线程对象的方法,

          是java中任何一个java对象都有的方法,

          因为这两个方法是Object类中自带的。

          wait方法和notify方法不是通过线程对象调用,

         不是这样的:t.wait(),也不是这样的:t.notify()..不对。

         这两个方法是通过java对象去调用的,是调用java对象的wait()和notify()。

第二:wait()方法作用?

          Object o = new Object();

          o.wait();

    表示:

         让正在o对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。

         也就是说:o.wait()方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态。

第三:notify()方法作用?

          Object o = new Object();

          o.notify();

         表示:

                唤醒正在o对象上等待的线程。

还有一个o.notifyAll()方法:这个方法是唤醒o对象上处于等待的所有线程。

  

如上图所示,o.wait()和o.notify()方法的配合:

  1. 首先,有一个对象Object o。
  2. 其次,线程t是当前线程对象,线程t在o对象上活动。
  3. 然后,当调用o.wait()方法之后,线程t进入无无期限等待状态(当前线程进入等待状态),直到最终调用0.notify()方法。
  4. 接着,o.notify()方法的调用,可以让正在o对象上等待的线程被唤醒。

现实案例:

  1. 首先,有一哥们恨我。
  2. 其次,有一天,我下班时他在半路堵我,看到我就一顿揍我。
  3. 解析:揍我的他,相当于是一个线程。我相当于是o对象。
  4. 然后,我喊了一句话,“等会,先听我说,别打脸”,然后这哥们就停手了。
  5. 最后,我再说一句,“我准备好了,继续揍吧”,那哥们又动手了。

(2)生产者消费者模式

       生产者和消费者模式是为了专门解决某个特定需求的。它比较特殊,在特定的需求下才会用生产者和消费者模式。

  1. 首先,有需求/场景:“一个线程负责生产,一个线程负责消费,并且要最终达到生产和消费的均衡”。
  2. 其次,创建一个生产线程,创建一个消费线程。
  3. 然后,相当于,有一个仓库。
  4. 方案:为了最终达到生产和消费的均衡,我们必须使用o.wait()和o.notify()。
  5. 问题:那么谁去调用wait()?谁去调用notify()?
  6. 答案:仓库。

               因为在这里仓库对象是多线程共享的,所以需要考虑仓库的线程安全问题。

              仓库对象最终调用wait()和notify()。    

  7. 原理1:wait()和notify()方法的使用,需要建立在synchronnized线程同步的基础之上。

                 因为必须保证仓库的线程安全问题,才能谈wai()和notify()。  

  8. 原理2:wait()会让当前线程处于等待状态,并且释放对象锁。
  9. 原理3:notify()方法只会通知,不会释放之前占有的o对象锁。

(3)使用synchronized、wait、notify/notifyAll模拟生产者和消费者模式

package com.bjpowernode.df;

import java.util.ArrayList;
import java.util.List;

/**
 * 1、使用wait()方法和notify()方法实现“生产者和消费者模式”
 * 2、什么是“生产者和消费者模式”?
 *      生产线程负责生产,消费线程负责消费。
 *      生产线程和消费线程要达到均衡。
 *      这是一种特殊的业务需求,在这种特殊的情况下需要使用wait()方法和notify()方法。
 * 3、wait()和notify()方法不是线程对象的方法,是普通java对象的方法。
 * 4、wait()和notify()方法需要建立在线程同步的基础上,因为多线程要同时操作一个仓库,仓库有线程安全问题。
 * 5、wait()方法的作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。
 * 6、notify()方法的作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。
 * 7、模拟这样一个需求:
 *      仓库我们采用List集合。
 *      List集合中假设只能存储一个元素。
 *      1个元素就表示仓库满了。
 *      如果List集合中元素个数是0,就表示仓库空了。
 *      永远保证List集合中永远都是最多存储1个元素。
 *      必须做到这种效果:生产1个,就消费1个。
 */
public class ProviderConsumerClass {
    public static void main(String[] args) {
        //  创建一个仓库对象,t1和t2线程共享的
        List list = new ArrayList();
        //  创建两个对象
        //  生产者线程
        Thread provider = new Thread(new ProviderThread(list));
        provider.setName("生产者线程");
        //  消费者线程
        Thread consumer = new Thread(new ConsumerThread(list));
        consumer.setName("消费者线程");
        //  启动两个线程
        provider.start();
        consumer.start();
    }
}

//  生产线程
class ProviderThread implements Runnable{
    //  仓库
    private List list;
    public ProviderThread(List list){
        this.list = list;
    }
    public void run() {
        //  一直生产(使用列循环来模拟一直生产)
        while (true) {
            //  给仓库对象list加锁
            synchronized (list){
                if (list.size()>0) {//  仓库满了
                    try {
                        //  当前线程(即生产线程ProviderThread)进入等待状态,并且释放ProviderThread之前占有的list集合的锁。
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //  程序能够执行到这里说明什么?仓库是空的,可以生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName()+"--->"+obj);
                //  仓库满了,需要唤醒消费者进行消费
                list.notify();

                //  这里使用notifyAll()唤醒所有也没关系,因为notify()和notifyAll()只负责唤醒,不会释放锁。
                //list.notifyAll();
            }
        }
    }
}

//  消费线程
class ConsumerThread implements Runnable{
    //  仓库
    private List list;
    public ConsumerThread(List list){
        this.list = list;
    }
    public void run() {
        //  一直消费
        while (true) {
            //  给仓库对象list加锁
            synchronized (list){
                if (list.size()==0) {// 仓库空了
                    //  当前线程(即消费者线程ConsumerThread)进入等待状态,并且释放ConsumerThread之前占有的list集合的锁。
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //  程序能够执行到此处,说明仓库list中有数据(满的),需要进行消费
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName()+"--->"+obj);
                //  消费完了,需要去唤醒生产者去生产商品
                list.notify();

                //  这里使用notifyAll()唤醒所有也没关系,因为notify()和notifyAll()只负责唤醒,不会释放锁。
                //list.notifyAll();
            }
        }
    }
}
  1. 首先,有仓库list。
  2. 其次,有provider线程和consumer线程,两个线程共享同一个仓库list。
  3. 然后,provider线程和consumer线程分别启动。
  4. 情况:
  5. 第一次抢时间片:
  6. (1)假如,provider线程先抢到时间片,provider线程获得仓库list的对象锁。
  7. (2)这时仓库list中没有元素,所以要生产商品放到仓库list中。
  8. (3)并且唤醒仓库list中所有等待的线程,但此时无线程需要唤醒。
  9. 第二次抢时间片:
  10. (1)还是provider线程先抢到时间片,provider线程获得仓库list的对象锁。
  11. (2)这时仓库list中有1个元素,满仓,所以provider线程等待并释放仓库list的对象锁。
  12. (3)此时consumer线程获取仓库list的对象锁,发现仓库list中有元素,进行消费。
  13. (4)consumer线程调用notify()方法唤醒,provider线程。
  14. 第三次抢时间片:
  15. (1)假如,这次轮到consumer线程抢到时间片,占有仓库list的对象锁。
  16. (2)由于仓库list没有元素,因此consumer线程等待并释放仓库list的对象锁。
  17. (3)provider线程先抢到时间片,占有仓库list的对象锁。
  18. (4)由于仓库没有元素,因此provider线程需要生产元素
  19. ......无限循环

五、具体:重点3:关于线程对象的生命周期?

  1. 首先,新建状态。刚new出来的线程对象。
  2. 其次,新建状态 》可运行状态/就绪状态。当前线程调用.start()方法启动线程。
    1. 当前线程具有抢夺cpu的时间片的权利(CPU时间片就是执行权)
  3. 然后,可运行状态/就绪状态 》运行状态。当一个线程抢夺到cpu时间片之后,就开始执行run()方法。run()方法的开始执行,标志着线程进入运行状态。
  4. 接着,运行状态 》可运行状态/就绪状态。当前线程占有的cpu时间片用完之后,会重新回到就绪状态,继续抢夺cpu时间片。
  5. 继续,可运行状态/就绪状态 》运行状态。当此线程再次抢到cpu时间片之后,会重新进入run()方法,进入运行状态,接着上一次的代码继续往下执行。
  6. 情况1:运行状态 》阻塞状态。当线程在运行run()方法的过程当中,假如遇到sleep()方法 或者 遇到了接收用户输入的情况:例如,Scanner s = new Scanner() s.netInt()要求在控制台输入整形数据。只要s.netInt()一执行,线程的run()方法就会卡住,线程会进入阻塞状态。进入阻塞状态的线程会放弃当前占有的cpu时间片。
  7. 情况2:阻塞状态 》可运行状态/就绪状态 》运行状态。当阻塞状态的线程完成阻塞工作/阻塞解除(如Scanner s = new Scanner() s.netInt()时,用户在控制台输入整型数据完成。如sleep()方法执行完成,即休眠时间到),线程会从阻塞状态恢复到可运行状态/就绪状态,即重新去抢夺cpu时间片。如果抢到cpu时间片之,会重新进入run()方法,进入运行状态,接着上一次的代码继续往下执行
  8. 最后,运行状态 》死亡状态。run()方法执行结束。

注意:java虚拟机(JVM)的调度,调度使得当前线程频繁在就绪状态和运行状态之间进行切换。

六、其它具体

1、什么是进程?什么是线程?进程和线程的关系是什么?

        进程是一个应用程序(一个进程是一个软件)。

        线程是一个进程中的执行场景/执行单元。

        一个进程可以启动多个线程。

场景分析1:什么是进程?:

        有以下场景:cmd > 回车,效果是:进程列表中多了一个cmd.exe进程

 右击“结束cmd.exe进程”,dos命令窗口会关闭退出。所以说,一个进程就对应一个应用程序,一个软件。

  

场景分析2:进程和线程的关系?:

 我们看到有一个java进程吧。
        如何理解“线程是一个进程中的执行场景/执行单元”这句话呢?比如说,我们在cmd > java HelloWorld > 回车,实际上这时会启动一个进程。而这个进程会去启动多个线程:


 

其中一个线程,可能会去程序HelloWorld.main()方法;另一个线程,可能会去负责进行JVM的垃圾回收; 

        对于java程序来说,当在Dos命令窗口中输入:java HelloWorld,并按回车时:

  1. 首先,会启动JVM,而JVM就是一个进程。
  2. 然后,JVM再启动一个主线程调用HelloWorld.main()方法。
  3. 接着,同时再启动一个垃圾回收线程负责看护,回收垃圾。
  4. 总结,最起码,现在的JVM进程中至少有两个线程并发,一个是执行HelloWorld.main()方法的主线程,一个是垃圾回收线程。

场景分析3:进程与线程的关系:

        阿里巴巴:进程;
                马云:阿里巴巴的一一个线程
                童文红:阿里巴巴的-一个线程
        京东:进程
                强东:京东的一一个线程
                妹妹:京东的-一个线程
        进程可以看做是现实生活当中的公司。
                线程可以看做是公司当中的某个员工。

2、注意:进程A和进程B的内存独立不共享。

        魔兽游戏是一个进程
        酷狗音乐是一个进程
        这两个进程是独立的,不共享资源。也就是说,魔兽游戏里面的装备不会跑到酷狗音乐的内存中去存放;反之,酷狗音乐的音乐也不会跑到魔兽游戏所占用的内存空间中。

        ds:看上面的场景,即,比如说阿里巴巴和京东的资源不共享。

3、注意:线程A和线程B共享其进程中的内存和资源

        结论:在java语言中:

  1. 同一个进程中的线程共享其进程中的内存和资源。     
  2. 即线程A和线程B,共享所属进程的堆内存和方法区。
  3.  但是栈内存独立,一个线程一个栈。

        假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。
        火车站,可以看做一个进程。
        火车站中的每一个售票窗口可以看做是一个线程。
        我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
        所以多线程并发可以提高效率。
        java中之所以有多线程机制,目的就是为了提高程序的处理效率。

        ds:依然看上面的场景,马云和童文红有些资源是不共享的,大家都有自己的小秘密。但他们共享的是阿里巴巴,这个公司的资源吧。

        有以下场景:

 ds:如上图所示:在同一个进程中,栈1、栈2、栈3并发运行,相互之间不干扰,即栈1、栈2、栈3同时进行压栈和弹栈的工作,这就这叫多线程并发。

        但大家一定要记住,这个堆内存和方法区,在这里只有一块。所以说,在多线程的情况下,堆内存和方法区是共享的,但栈内存是一个线程一个栈、是独立的、是并发的、是线程间是互不干扰的。

4、思考:使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束?


        如上图所示:main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈。

5、总结:进程、线程的JVM内存图

  如上图所示: 

        在jvm中,有方法区、堆内存、栈空间。

        在jvm中,方法区永远只有一块。

        在jvm中,堆内存永远只有一块。

        对于多线程来说,线程所属的进程中的堆内存和方法区是共享的。

        上面有两个栈:一个是主栈空间/主线程,即主线程对应的主栈。一个是m1方法开启的分支线程,即分支栈。
        首先,主栈/主线程最先调用的方法肯定是栈底的main()方法。main()方法会去调用别的方法,如上图:先调用m1()方法,再调用m2()方法。此时的情况是,main()、m1()、m2()方法都在一个线程中。
        然后,在主线程m1()方法的执行过程中,启动一个分支线程。这时,JVM会分配一个新的栈内存/栈空间给这个分支线程,我们称之为支栈,如上图的t1。
接着,分支栈t1也会去压栈、也会去弹栈。比如,t1的代码中从栈底分别会去调用x()、y()、z()这3个方法。此时,x()、y()、z()这3个方法在同一个线程t1中。
        最后,情况1:有可能主栈结束执行了,支线程t1还在执行中。因此,main()方法不代表所有的线程都结束。main()方法结束只是代表主线程结束了,其它线程可能还在执行。
        注意1:栈空间是独立的,一个线程一个栈空间。

6、思考:对于单核的CPU来说,真的可以做到真正的多线程并发吗?

        真正的多线程并发是指:t1线程执行t1的,t2线程执行t2的。t1和t2线程之间互不干扰,即t1不会影响t2,t2也不会影响t1。
        多核cpu电脑表示有多个大脑,相当于有多个人呀。多个人,你做你的,我做我的,肯定是能够做到多线程并发的。比如4核cpu电脑来说,表示同一个时间点上,可以真正的有4个进程并发执行。
        单核cpu电脑表示只有一个大脑。不能够做到真正的多线程并发,但是可以做到给人一人种“多线程并发”的感觉。对于单核cpu电脑为说,在某一个时间点上实际上只能处理一件事情。
但是由于cup的处理速度极快,多个线程之间频繁切换执行,给人的感觉是:多个事情同时在做!!!比如,线程A:播放音乐;线程B:运行魔兽游戏;线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,给我们的感觉是同时并发的。

        电影院采用胶卷播放电影,--江胶卷一个胶卷播放速度达到一定程度之后,人类的眼睛产生了错觉,感觉是动画的。这说明人类的反应速度很慢,就像一根钢针扎到手上,到最终感觉到疼,这个过程是需要“很长的"时间的,在这个期间计算机可以进行亿万次的循环。所以计算机的执行速度很快。

        就像你你抽一根香烟,点燃了。烟雾在空中轻轻划过,你会看到一条红线,而不是一个一个点。

7、线程的创建方式、相关说明

序言

(1)extends Thread

(2)run()方法的内存图 

  1. 首先,有方法区,把类加载到方法区,因此方法区中会加载很多.class文件。比如,会加载ThreadTest02.class、System.class、Thread.class、MyThread.class......等等。
  2. 其次,有堆内存。
  3. 接着,有主栈。因为这个程序,会先去执行ThreadTest02.main()方法,表明会启动一个主线程,所以会有一个主栈。在主栈中,压栈的是ThreadTest02.main()方法,放在最底部。
  4. 然后,开始执行ThreadTest02.main()方法:
    1. 第一句代码是:MyThread t=new MyThread()就是实例化一个线程对象。因此会在堆内存中会开辟一个空间,这个没得说,即堆内存中存放的是MyThread线程对象。同时这个MyThread线程对象的内存地址,赋给了引用MyThread t,即此时引用MyThread t指向的是堆内存中的MyThread线程对象。如上图所示,我们假设MyThread线程对象的内存地址是:0x1234,所以有MyThread t = 0x1234。
    2. 第二句代码:t.run(),以前的结论是:首先,方法只要被调用,程序必须会停下来,往下的代码也会停下来,不继续执行。然后,把要被调用的方法,继续进行压栈操作。如上图所示run()方法压在ThreadTest02.main()上面。接着,t.run()方法会输出0~999。最后,t.run()方法执行结束。
    3. 第三句代码,往下继续执行main()方法其它的代码。即执行for(int i=0;i<10000;i++){System.out.println("主线程---"+i)},效果是打印0~999吧。

(3)start()方法的内存图 

  1. 首先,有方法区,把类加载到方法区,因此方法区中会加载很多.class文件。比如,会加载ThreadTest02.class、System.class、Thread.class、MyThread.class......等等。
  2. 其次,有堆内存。
  3. 接着,有主栈。因为这个程序,会先去执行ThreadTest02.main()方法,表明会启动一个主线程,所以会有一个主栈。在主栈中,压栈的是ThreadTest02.main()方法,放在最底部。
  4. 然后,开始执行ThreadTest02.main()方法:
    1. 第一句代码是:MyThread t=new MyThread()就是实例化一个线程对象。因此会在堆内存中会开辟一个空间,这个没得说,即堆内存中存放的是MyThread线程对象。同时这个MyThread线程对象的内存地址,赋给了引用MyThread t,即此时引用MyThread t指向的是堆内存中的MyThread线程对象。如上图所示,我们假设MyThread线程对象的内存地址是:0x1234,所以有MyThread t = 0x1234。
    2. 第二句代码是:t.start()。以前的结论是:首先,方法只要被调用,程序必须会停下来,往下的代码也会停下来,不继续执行。然后,把要被调用的方法,继续进行压栈操作。如上图所示start()方法压在ThreadTest02.main()上面。记住,在java中更古不变的道理:在方法体中的代码永远都是自上而下的顺序,依次逐行执行的。也就是说,在上面的程序中,t.start()方法没执行完,main()方法中t.start()后面的代码是不会执行的。

             t.start()方法的作用是:开辟一个新的栈空间称为分支栈,只要开完.start()方法就会结束。于似乎,JVM中就开辟了一个分支栈空间,即分支栈Thread t的栈空间。在开辟了栈空间以后,.start()方法就结束了。

    3. .start()方法在JVM开辟分支栈空间完成以后,执行瞬间结束 ,此时会继续执行main()方法的第三句代码,即执行for(int i=0;i<10000;i++){System.out.println("主线程---"+i)},准备在控制台输出0~999吧。

    4. 在分支栈中。由于需要调用MyThread.run()方法,因此分支栈中也会把MyThread.run()方法压入栈。MyThread.run()方法此时是栈底,所以在这个分支栈中MyThread.run()会先被执行,它准备在控制台输出0~999。

             其中,MyThread.run()不需要我们手动去调度,是由JVM线程调度机制来调度并运行的。

    5. 注意:main()方法和MyThread.run()方法是不同的线程,他们是并行的。也就是说,再次输出0~999也是并行的,因此会出现交替输出的情况。

八、守护线程 

理论

java语言中线程分为两大类:

        一类是:用户线程

        一类是:守护线程(后台线程)

其中具有代表性的就是:垃圾回收线程(守护线程)。

注意:主线程main方法是一个用户线程。

ds:昨天所写的所有线程,都属于用户线程。包括主线程main(),也都属于用户线程。
ds:什么是守护线程?就是后台线程,就是有一个线程默默在后台运行着,比如垃圾回收器。

守护线程的特点:

一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动(ds:不用你管)结束。

ds:守护线程的目的是守护,那所有的用户线程都结束了,那就同有必要去守护了。

ds:就像说我们的java程序,如果所有的用户线程都结束了,那垃圾回收线程也应该自动结束,没有可守护的了。

守护线程用在什么地方呢?

        每天00:00的时候系统数据自动备份。这个需要使用到定时器,并且我们可以将定时器设置为守护线程。一直在那里看着,每到00:00的时候就备份一次。如果所有的用户线程结束了,守护线程自动退出,没有必要进行数据备份了。

案例

package com.bjpowernode.shouhu;

/**
 * 守护线程
 */
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new BakDataThread();
        t.setName("备份数据的线程");
        t.start();

        //  主线程:主线程是用户线程
        for(int i=0;i<10;i++){
            System.out.println("主线程:"+Thread.currentThread().getName()+"--->"+(++i));
            Thread.sleep(1000);
        }
    }
}
class BakDataThread extends Thread{
    public void run(){
        int i = 0;
        while(true){//死循环
            System.out.println("守护线程:"+Thread.currentThread().getName()+"--->"+(++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

没使用守护线程时:

  1. 首先,线程t1和主线程都是用户线程。
  2. 效果,首先:程序中虽然主线程在循环10次后不结束了。其次:由于自定义线程中run()方法中有死循环,所以备份线程一直不会结束。
  3. 希望,所有用户线程(这里只有主线程)一结束,那么备份线程也一起结束(虽然你是死循环)。
  4. 方案,使用守护线程。用户线程在你需要守护,用户线程不在那就没必须去守护了。守护线程自动结束退出,不用你管。
package com.bjpowernode.shouhu;

/**
 * 守护线程
 */
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        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));
            Thread.sleep(1000);
        }
    }
}
class BakDataThread extends Thread{
    public void run(){
        int i = 0;
        while(true){//即使是死循环,但由于该线程是守护者,因此当所有的用户线程结束,守护线程也会自动终止。
            System.out.println("守护线程:"+Thread.currentThread().getName()+"--->"+(++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

九、定时器

理论

定时器的作用:

        间隔特定的时间,执行特定的程序。

        每周要进行银行账户的总账操作。

        每天要进行数据的备份操作。

在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,

那么在java中其实可以采用多种方式实现:

  1. 可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行

    任务。这种方式是最原始的定时器。(比较low)

  2. 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。

    不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持

    定时任务的。

  3. 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,

    这个框架只要进行简单的配置,就可以完成定时器的任务。

ds:TimerTask实现了Runable接口,因此可以把TimerTask理解为一个线程。

ds:我们也可以把Timer理解为一个线程,甚至它有一个构造方法Timer(boolean isDaemo)指定了把Timer对象设置成一个后台线程,以做为守护线程。

ds:以后spring中的定时器,底层原理使用的就是Timer。

案例

package com.bjpowernode.timer;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

/**
 * 使用定时器指定定时任务
 */
public class TimerTest {
    public static void main(String[] args) throws ParseException {
        //  创建定时器对象
        Timer timer = new Timer();
        //Timer timer = new Timer(true); //  守护线程的方式

        //第一个参数:定时任务
        //第二个参数:第一次执行时间
        //第三个参数:间隔多久执行一次
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2022-04-06 22:48:00");
        timer.schedule(new LogTimerTask(),firstTime,1000*10);
    }
}

//也可以使用匿名内部类的方式
//编写一个定时任务类
//假设这是一个记录日志的定时任务
class LogTimerTask extends TimerTask {
    public void run() {
        //编写你需要执行的任务就行了。
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());
        System.out.println(strTime+":成功完成了一次数据备份");
    }
}

十、实现线程的第三种方式

理论

实现线程的第三种方式:实现Callable接口。(JDK8新特性。)

这种方式实现的线程可以获取线程的返回值。

之前讲解的那两种方式(extends Thread、implement Runnalbe)是无法获取线程返回值的,因为run方法返回void。

思考:

        系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们怎么能拿到这个执行结果呢?使用第三种方式:实现Callable接口方式。

案例

package com.bjpowernode.timer;

// JDK中JUC包下的,属性java的并发包,老JDK中没有这个包。新特性。
import java.sql.SQLOutput;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 实现线程的第三种方式:实现Callable接口
 */
public class CallableThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //  第一步:创建一个“未来任务类”对象
        //  两个构造方法,如果需要返回值则需要使用有Callable参数的构造方法
        //  参数非常重要,需要给一个Callable接口的实现类对象。
        FutureTask task = new FutureTask(
            new Callable() {
                //  call()方法相当于原来的run()方法,只不过这个有返回值
                public Object call() throws Exception {
                    //  线程执行一个任务,执行之后可能会有一个执行结果
                    System.out.println("call method begin");
                    Thread.sleep(1000*10);
                    System.out.println("call method edn");
                    return 100+200; // 自动装箱(300结果变成Integer)
                }
            }
        );

        //  第二步:创建线程对象
        Thread t = new Thread(task);

        //  第三步:启动线程
        t.start();

        //  第四步:拿返回值
        //  这里是main()方法,这是在主线程中。
        //  在主线程中,怎么获取t线程执行的返回结果?
        //  get()方法的执行会导致当前线程(这里是主线程main())阻塞
        Object obj = task.get();
        System.out.println("线程执行结果"+obj);
        //  思考:这个get()方法的执行会不会导致main()方法受阻塞,即主线程阻塞?
        /**
         * 首先,task.get()想要获取t线程执行的返回结果,就需要等待call()方法执行结束。
         * 所以,Object obj = task.get();会导致主线程阻塞(停下来)。
         * 因此,缺点:效率比较低。在获取t线程执行结果的时候,当前线程(这里是主线程main())受阻塞,效率较低。
         *      优点:获取线程执行的返回值
         *      优点:可以在call()方法上抛出异常
         */
        System.out.println("hello world");
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值