第十八章 多线程

18.1 基本概念

18.1.1 程序和进程的概念

程序 - 数据结构 + 算法,主要指存放在硬盘上的可执行文件。
进程 - 主要指运行在内存中的可执行文件。
目前主流的操作系统都支持多进程,为了让操作系统同时可以执行多个任务,但进程是重量级的,
也就是新建一个进程会消耗 CPU 和内存空间等系统资源,因此进程的数量比较局限。

18.1.2 线程的概念

为了解决上述问题就提出线程的概念,线程就是进程内部的程序流,也就是说操作系统内部支持多进程的,而每个进程的内部又是支持多线程的,
线程是轻量的,新建线程会共享所在进程的系统资 源,因此目前主流的开发都是采用多线程。
多线程是采用时间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机 制。

18.2 线程的创建(重中之重)

18.2.1 Thread类的概念

java.lang.Thread 类代表线程,任何线程对象都是 Thread 类(子类)的实例。
Thread 类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性。

18.2.2 创建方式

(1)方式一:自定义类继承 Thread 类并重写 run 方法,然后创建该类的对象调用 start方法,start方法作用是开启线程调用run方法。
(2)方式二:自定义类实现 Runnable 接口并重写 run 方法,创建该类的对象作为实参来构造 Thread 类型的对象,然后使用Thread 类型的对象调用 start 方法。 
两种方式的优缺点: 当自定义类还需要继承被的父类时,就不能实现,因为java中只支持单继承, 但是可以实现接口在继承父类,开发中常用方式二
 

18.2.3 相关的方法

 
 

18.2.4 执行流程

(1)执行 main 方法的线程叫做主线程,执行 run 方法的线程叫做新线程 / 子线程。
(2)main 方法是程序的入口,对于 start 方法之前的代码来说,由主线程执行一次,当 start 方法调用成 功后线程的个数由1 个变成了 2 个,
    新启动的线程去执行 run 方法的代码,主线程继续向下执行,两 个线程各自独立运行互不影响。
(3)当 run 方法执行完毕后子线程结束,当 main 方法执行完毕后主线程结束。
(4)两个线程执行没有明确的先后执行次序,由操作系统调度算法来决定。
注意:run方法是由java虚拟机调用的。

18.2.5 方式的比较

继承 Thread 类的方式代码简单,但是若该类继承 Thread 类后则无法继承其它类,而实现 Runnable接口的方式代码复杂,
但不影响该类继承其它类以及实现其它接口,因此以后的开发中推荐使用第二种方式。

18.2.6 匿名内部类的方式

使用匿名内部类的方式来创建和启动线程。
 
 

18.3 线程的生命周期(熟悉)

 
 
 
(1)新建状态 - 使用 new 关键字创建之后进入的状态,此时线程并没有开始执行。
(2)就绪状态 - 调用 start 方法后进入的状态,此时线程还是没有开始执行。
(3)运行状态 - 使用线程调度器调用该线程后进入的状态,此时线程开始执行,当线程的时间片执行完
(4)毕后任务没有完成时回到就绪状态。
(5)消亡状态 - 当线程的任务执行完成后进入的状态,此时线程已经终止。
(6)阻塞状态 - 当线程执行的过程中发生了阻塞事件进入的状态,如: sleep 方法。 阻塞状态解除后进入就绪状态。
 

18.4 线程的编号和名称(熟悉)

18.4.1 相关方法

18.4.2 案例题目

自定义类继承 Thread 类并重写 run 方法,在 run 方法中先打印当前线程的编号和名称,然后将线程 的名称修改为"zhangfei" 后再次打印编号和名称。
要求在 main 方法中也要打印主线程的编号和名称。代码如下:
package com.lagou.task17;
public class ThreadIdNameTest extends Thread{

    public ThreadIdNameTest(String name) {
        //调用父类的构造方法指定线程名称
        super(name);
    }

    @Override
    public void run() {                 //super.getId()     super.getName()
        System.out.println("子线程编号是:"+getId()+"; 名称是:"+getName());  //子线程编号是:14; 名称是:zhangfei
        //修改后的线程名称
        setName("guanyu");
        System.out.println("子线程编号是:"+getId()+"; 修改后名称是:"+getName()); //子线程编号是:14; 修改后名称是:guanyu
    }

    public static void main(String[] args) {

        //创建ThreadIdNameTest对象
        ThreadIdNameTest tit = new ThreadIdNameTest("zhangfei");
        //启动线程
        tit.start();

        //获取主线程的线程编号和线程名称
        //获取当前线程就是主线程
        Thread thread = Thread.currentThread();
        System.out.println("主线程编号是:"+thread.getId()+" ;主线程名称是:"+thread.getName());
    }
}

18.5 线程的常用方法(重点)

(1) sleep 线程阻塞

package com.lagou.task17;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadSleepTest extends Thread {
    public boolean flag = true;
    @Override
    public void run() {
        //每隔一秒钟打印系统时间,模拟时钟效果
        while(flag){
            Date date = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.out.println("当前系统时间:"+sdf.format(date));

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        //创建ThreadSleepTest对象
        ThreadSleepTest tst = new ThreadSleepTest();
        //启动线程
        tst.start();

        //主线程等待5秒之后结束子线程
        System.out.println("主线程开始等待....");
        try{
            Thread.sleep(5000);
        } catch(InterruptedException e){
            e.printStackTrace();
        }
        //tst.stop();
        tst.flag = false;
        System.out.println("主线程等待结束");
    }

}

(2)线程优先级管理

优先级高的线程不一定先执行, 但是优先级高的线程整体一定是先执行完成的.

Thread.MAX_PRIORITY        高     10 
Thread.MIN_PRIORITY         低      1
Thread.NORM_PRIORITY    默认   5

package com.lagou.task17;

public class ThreadPriorityTest extends Thread{
    //子线程和主线程默认的优先级是5
    @Override
    public void run() {
        //System.out.println("子线程的优先级是:"+getPriority());
        //设置子线程优先级为MAX_PRIORITY  10
        setPriority(Thread.MAX_PRIORITY);
        for (int i = 0; i < 30; i++) {
            System.out.println("run方法中i="+i);
        }
    }

    public static void main(String[] args) {
        ThreadPriorityTest tpt = new ThreadPriorityTest();
        tpt.start();

        for (int i = 0; i < 30; i++) {
            System.out.println("--main方法中i="+i);
        }
        //Thread td = Thread.currentThread();
        //System.out.println("主线程的优先级是:"+td.getPriority());

    }

}

(3)线程等待

package com.lagou.task17;

public class ThreadJoinTest extends Thread {

    @Override
    public void run() {
        //模拟倒数10个数的效果
        System.out.println("倒计时开始...");
        for (int i = 10; i > 0; i--) {
            System.out.println(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("新年快乐");
    }

    public static void main(String[] args) {

        ThreadJoinTest tjt = new ThreadJoinTest();
        tjt.start();

        System.out.println("主线程开始等待......");
        try {
            //表示当前正在执行的线程对象等待调用线程的对象, 也就是主线程等待子线程
            
            //tjt.join();//主线程一直等待tjt线程执行完成才加入,等待谁就让谁调用join()方法
            tjt.join(5000); //主线程等待子线程5秒钟,不管子线程执行完成与否,主线程都加入
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("终于等到你,还好没放弃!");
        
    }
    
}


打印结果
主线程开始等待......
倒计时开始...
10
9
8
7
6
终于等到你,还好没放弃!
5
4
3
2
1
新年快乐

(4)线程守护

当子线程不是守护线程时,虽然主线程先结束了,但是子线程依然会继续执行,直到打印完毕所有数据为止。
当子线程是守护线程时,当主线程结束后,则子线程随之结束,但是不会马上停止,因为刹车也是需要时间的。

package com.lagou.task17;

public class ThreadDaemonTest extends  Thread {
    @Override
    public void run() {
        //System.out.println(isDaemon()? "该线程是守护线程": "该线程不是守护线程"); // 默认不是守护线程
        // 当子线程不是守护线程时,虽然主线程先结束了,但是子线程依然会继续执行,直到打印完毕所有数据为止
        // 当子线程是守护线程时,当主线程结束后,则子线程随之结束
        for (int i = 0; i < 50; i++) {
            System.out.println("子线程中:i = " + i);
        }
    }

    public static void main(String[] args) {

        ThreadDaemonTest tdt = new ThreadDaemonTest();
        // 必须在线程启动之前设置子线程为守护线程
        tdt.setDaemon(true);
        tdt.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("-------主线程中:i = " + i);
        }
    }
}

(5)案例题目

编程创建两个线程,线程一负责打印 1 ~ 100 之间的所有奇数,其中线程二负责打印 1 ~ 100 之间的所有偶数。
main 方法启动上述两个线程同时执行 , 主线程等待两个线程终止。
代码中是继承Thread类的线程, 也可以实现Runnable接口的线程
 

18.6 线程同步机制(重点)

18.6.1 基本概念

当多个线程同时访问同一种共享资源时,比如多个线程对同一个变量赋值,可能会造成数据的覆盖等不一致性问题,此时就需要对线
程之间进行通信和协调,该机制就叫做线程的同步机制。
多个线程并发读写同一个临界资源时会发生线程并发安全问题。
异步操作 : 多线程并发的操作,各自独立运行。
同步操作 : 多线程串行的操作,先后执行的顺序。

18.6.2 解决方案

由程序结果可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理。
引发原因:线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款。
解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作。
经验分享:在以后的开发尽量减少串行操作的范围,从而提高效率。

18.6.3 实现方式

Java 语言中使用 synchronized 关键字来实现同步 / 对象锁机制从而保证线程执行的原子性

(1)实现方式1:同步代码块锁定部分代码

使用同步代码块的方式实现部分代码的锁定,格式如下:
synchronized( 类类型的引用 ) {
       编写所有需要锁定的代码;
}
 
代码1:实现Runnable接口
package com.lagou.task17;
public class AccountRunnableTest implements Runnable {

    //私有化成员变量  账户余额
    private int balance;
    //为synchronized关键字声明一个引用类型变量
    private Demo dm = new Demo();

    public AccountRunnableTest(int balance) {
        this.balance = balance;
    }

    public AccountRunnableTest() {
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    @Override
    public void run() {
        System.out.println("线程名称"+Thread.currentThread().getName()+"正在执行");
        synchronized (dm) {
        //synchronized (new Demo()) {  锁不住,相当于每开一个线程都会创建新的对象加一把新锁,必须是同一个对象才可以
            //模拟银行取款的过程
            //获取账户余额
            int tempBalance = getBalance();
            //余额大于200进行取款
            if (tempBalance >= 200) {
                System.out.println("线程"+Thread.currentThread().getName()+"正在出钞......");
                try {
                    Thread.sleep(7000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tempBalance -= 200;
                System.out.println("出钞结束");
            }else{
                System.out.println("余额不足,请核对账户余额");
            }
            //取款后重新设置账户余额
            setBalance(tempBalance);
        }
    }

    public static void main(String[] args) {
        AccountRunnableTest art = new AccountRunnableTest(1000);
        Thread td1 = new Thread(art);
        Thread td2 = new Thread(art);
        td1.start();
        td2.start();

        System.out.println("主线程正在等待..........");
        try {
            td1.join();
            //td2.start();//等待线程1结束,在启动线程2,但是这样和创建两个方法分别执行线程1和线程2的代码没什么区别
            td2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程等待结束,取款完毕!!!");
        System.out.println("最终账户余额为:"+art.getBalance());
        
    }
}

class Demo{}

代码2:继承Thread类    主要是因为用static修饰dm变量,将dm变量提升为类层级,所有对象共用一个dm变量。

package com.lagou.task17;
public class AccountThreadTest extends Thread{

    //私有化成员变量  账户余额
    private int balance;
    //为synchronized关键字声明一个引用类型变量
    private static Demo dm = new Demo();  //用static修饰后,dm变量就提升为类层级, 所有对象都公用一个

    public AccountThreadTest(int balance) {
        this.balance = balance;
    }

    public AccountThreadTest() {
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    @Override
    public void run() {
        System.out.println("线程名称"+Thread.currentThread().getName()+"正在执行");
        synchronized (dm) {//如果dm不用static修饰,att1和att2会对应两个dm对象,要想锁住相同代码块,两次调用的dm必须是同一个对象,dm用static修饰后提升到类层级,所有该类对象共用一个dm
            //synchronized (new Demo()) {  锁不住,相当于每开一个线程都会创建新的对象加一把新锁,必须是同一个对象才可以
            //模拟银行取款的过程
            //获取账户余额
            int tempBalance = getBalance();
            //余额大于200进行取款
            if (tempBalance >= 200) {
                System.out.println("线程"+Thread.currentThread().getName()+"正在出钞......");
                try {
                    Thread.sleep(7000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tempBalance -= 200;
                System.out.println("出钞结束");
            }else{
                System.out.println("余额不足,请核对账户余额");
            }
            //取款后重新设置账户余额
            setBalance(tempBalance);
        }
    }


    public static void main(String[] args) {
        AccountThreadTest att1 = new AccountThreadTest(1000);
        AccountThreadTest att2 = new AccountThreadTest(1000);
        att1.start();
        att2.start();

        System.out.println("主线程正在等待..........");
        try {
            att1.join();
            //td2.start();//等待线程1结束,在启动线程2,但是这样和创建两个方法分别执行线程1和线程2的代码没什么区别
            att2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程等待结束,取款完毕!!!");
        System.out.println("最终账户att1余额为:"+att1.getBalance());
        System.out.println("最终账户att2余额为:"+att2.getBalance());

    }

}

 

(2)实现方式二:同步方法锁定方法内所有代码

使用同步方法的方式实现所有代码的锁定。
直接使用 synchronized 关键字来修饰整个方法即可
该方式等价于 :
public synchronized void run()   <=>  synchronized(this) { 整个方法体的代码 }
 
代码1:实现Runnable接口
this表示当前正在调用的对象,启动线程实际是调用Runnable实现类中的run方法,只要Runnable实现类是同一个对象就锁得住
package com.lagou.task17;
public class AccountRunnableTest implements Runnable {

    //私有化成员变量  账户余额
    private int balance;
    //为synchronized关键字声明一个引用类型变量
    private Demo dm = new Demo();

    public AccountRunnableTest(int balance) {
        this.balance = balance;
    }

    public AccountRunnableTest() {
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    @Override
    public /*synchronized*/ void run() {
        synchronized (this) {//由源码可知:启动线程最终是art调用run(),因此当前正在调用的对象就是art,即this=art;因为this表示的是同一个对象,所以可以锁住代码
        System.out.println("线程名称"+Thread.currentThread().getName()+"正在执行");
        //synchronized (dm) {
        //synchronized (new Demo()) {  锁不住,相当于每开一个线程都会创建新的对象加一把新锁,必须是同一个对象才可以
            //模拟银行取款的过程
            //获取账户余额
            int tempBalance = getBalance();
            //余额大于200进行取款
            if (tempBalance >= 200) {
                System.out.println("线程"+Thread.currentThread().getName()+"正在出钞......");
                try {
                    Thread.sleep(7000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tempBalance -= 200;
                System.out.println("出钞结束");
            }else{
                System.out.println("余额不足,请核对账户余额");
            }
            //取款后重新设置账户余额
            setBalance(tempBalance);
        }
    }

    public static void main(String[] args) {
        AccountRunnableTest art = new AccountRunnableTest(1000);
        Thread td1 = new Thread(art);
        Thread td2 = new Thread(art);
        td1.start();
        td2.start();

        System.out.println("主线程正在等待..........");
        try {
            td1.join();
            //td2.start();//等待线程1结束,在启动线程2,但是这样和创建两个方法分别执行线程1和线程2的代码没什么区别
            td2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程等待结束,取款完毕!!!");
        System.out.println("最终账户余额为:"+art.getBalance());

    }
}

class Demo{}

代码1:继承Thread类     

att1和att2是两个对象  synchronized(this){}   this分别代表att1和att2   所以锁不住

 

方案:在run()前加synchronized修饰是锁不住的, 需要在加一个static修饰,但是run()是继承父类Thread来的,父类中run()方法没有static,

        可以新创建一个test()并用synchronized 和 static修饰,然后在run方法中调用test方法,等价于synchornized(类名.class)

原因在于:静态方法锁的是类对象(类层级)而非静态方法锁的是当前方法所属对象(对象层级)。

package com.lagou.task17;
public class AccountThreadTest extends Thread{

    //私有化成员变量  账户余额
    private int balance;
    //为synchronized关键字声明一个引用类型变量
    private static Demo dm = new Demo();  //用static修饰后,dm变量就提升为类层级, 所有对象都公用一个

    public AccountThreadTest(int balance) {
        this.balance = balance;
    }

    public AccountThreadTest() {
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    //dm用static修饰,理论上用static修饰run()也可以,但是run()是父类继承来的,父类中run()没有用static修饰,所以run()不能用static修饰;
    //方案:我们可以把run()中的代码写到另一个用static修饰的方法test()中,在run()内部调用test();
    @Override
    public  /*synchronized*/ void run() {
        /*System.out.println("线程名称"+Thread.currentThread().getName()+"正在执行");
        synchronized (dm) {
            //synchronized (new Demo()) {  锁不住,相当于每开一个线程都会创建新的对象加一把新锁,必须是同一个对象才可以
            //模拟银行取款的过程
            //获取账户余额
            int tempBalance = getBalance();
            //余额大于200进行取款
            if (tempBalance >= 200) {
                System.out.println("线程"+Thread.currentThread().getName()+"正在出钞......");
                try {
                    Thread.sleep(7000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tempBalance -= 200;
                System.out.println("出钞结束");
            }else{
                System.out.println("余额不足,请核对账户余额");
            }
            //取款后重新设置账户余额
            setBalance(tempBalance);
          }*/
        test();
    }

    
    public static /*synchronized*/  void test(){
        synchronized (AccountThreadTest.class) { //该类型对象的class对象:因为类型是固定的,所以class对象也是唯一的,因此可以实现同步
        System.out.println("线程名称"+Thread.currentThread().getName()+"正在执行");
        //synchronized (dm) {
            //synchronized (new Demo()) {  锁不住,相当于每开一个线程都会创建新的对象加一把新锁,必须是同一个对象才可以
            //模拟银行取款的过程
            //获取账户余额
            int tempBalance = 1000; //getBalance();  getBalance()没有static修饰
            //余额大于200进行取款
            if (tempBalance >= 200) {
                System.out.println("线程"+Thread.currentThread().getName()+"正在出钞......");
                try {
                    Thread.sleep(7000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tempBalance -= 200;
                System.out.println("出钞结束");
            }else{
                System.out.println("余额不足,请核对账户余额");
            }
            //取款后重新设置账户余额
            //setBalance(tempBalance); 因为test方法用static修饰,但是setBalance没有static修饰,所以会报错,先注释掉
        }
    }

    public static void main(String[] args) {
        AccountThreadTest att1 = new AccountThreadTest(1000);
        AccountThreadTest att2 = new AccountThreadTest(1000);
        att1.start();
        att2.start();

        System.out.println("主线程正在等待..........");
        try {
            att1.join();
            //td2.start();//等待线程1结束,在启动线程2,但是这样和创建两个方法分别执行线程1和线程2的代码没什么区别
            att2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程等待结束,取款完毕!!!");
        System.out.println("最终账户att1余额为:"+att1.getBalance());
        System.out.println("最终账户att2余额为:"+att2.getBalance());

    }


}

18.6.4 静态方法的锁定

当我们对一个静态方法加锁,如 :
public synchronized static void test(){….}   <=> synchronized (AccountThreadTest.class){...}
public synchronized static void xxx(){….}  等价于 synchronized(类名.class){代码}
那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式 : 类名 .class
静态方法与非静态方法同时使用了 synchronized 后它们之间是非互斥关系的。
原因在于:静态方法锁的是类对象(类层级)而非静态方法锁的是当前方法所属对象(对象层级)。

18.6.5 注意事项

使用 synchronized 保证线程同步应当注意 :
多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用。
在使用同步块时应当尽量减少同步范围以提高并发的执行效率。

18.6.6 线程安全类和不安全类

StringBuffffer类是线程安全的类(方法有synchronized 修饰),但StringBuilder类不是线程安全的类(方法没有synchronized 修饰)。
Vector 类和 Hashtable 类是线程安全的类(基本淘汰了),但 ArrayList 类和 HashMap 类不是线程安全的类。
Collections.synchronizedList() Collections.synchronizedMap() 等方法实现安全。

18.6.7 死锁的概念

线程一执行的代码:
public void run(){
      synchronized(a){ // 持有对象锁 a ,等待对象锁 b
             synchronized(b){
                    编写锁定的代码 ;
              }
       }
}
线程二执行的代码:
public void run(){
      synchronized(b){ // 持有对象锁 b ,等待对象锁 a
             synchronized(a){
                    编写锁定的代码 ;
             }
       }
}
注意:
在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!

18.6.8 使用Lock(锁)实现线程同步

(1)基本概念

a. 从 Java5 开始提供了更强大的线程同步机制 使用显式定义的同步锁对象来实现。
b. java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。
c. 该接口的主要实现类是 ReentrantLock 类,该类拥有与 synchronized 相同的并发性,在以后的线程安全控制中,经常使用ReentrantLock 类显式加锁和释放锁。

(2)常用的方法

(3)与synchronized方式的比较

a. Lock 是显式锁,需要手动实现开启和关闭操作,而 synchronized 是隐式锁,执行锁定代码后自动 释放。
b. Lock 只有同步代码块方式的锁,而 synchronized 有同步代码块方式和同步方法两种锁。
c. 使用 Lock 锁方式时, Java 虚拟机将花费较少的时间来调度线程,因此性能更好。

18.6.9 Object类常用的方法  实现线程间的通信

案例一:两个线程交错打印1-100之间的整数,不能重复打印
package com.lagou.task17;

public class ThreadCommunicateTest implements Runnable {
    private  int cnt = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                notify();//唤醒之前阻塞的线程, 每次进到循环在判断条件之前就唤醒阻塞的线程
                //打印1-100之间的整数
                if (cnt <= 100) {
                    System.out.println("线程" + Thread.currentThread().getName() + "中cnt=" + cnt);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    cnt++;
                    //当前线程打印完一个整数后,为了防止打印下一个整数,调用wait()阻塞
                    try {
                        wait(); //线程进入阻塞状态后,自送释放对象锁,wait和notify必须在锁定的代码中调用
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }

        }
    }

    public static void main(String[] args) {
        ThreadCommunicateTest tct = new ThreadCommunicateTest();
        Thread td1 = new Thread(tct);
        Thread td2 = new Thread(tct);
        td1.start();
        td2.start();
    }
}

案例二:生产者和消费者共用一个仓库,产品数量<10就生产, 产品数量>0就消费

package com.lagou.task17;

/**
 * 仓库类: 用于生产产品  和  消费产品
 * 注意: 生产者线程 和 消费者线程 共用一个仓库
 *       仓库产品总数量不能超过10个
 */
public class StoreHouse {
    
    private int cnt = 0; // 用于记录产品的数量

    //生产产品方法
    public synchronized void produceProduct() {
        notify();
        if (cnt < 10) {
            System.out.println("线程" + Thread.currentThread().getName() + "正在生产第" + (cnt+1) + "个产品...");
            cnt++;
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //消费产品方法
    public synchronized void consumerProduct() {
        notify();
        if (cnt > 0) {
            System.out.println("线程" + Thread.currentThread().getName() + "消费第" + cnt + "个产品");
            cnt--;
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
------------------------
package com.lagou.task17;

/**
 * 生产者线程 sleep 100
 */
public class ProduceThread extends Thread {

    //声明一个仓库类对象,为了可以调用仓库类的生产产品的方法
    private StoreHouse sh;
    //创建生产者线程对象时给仓库类引用赋值
    public ProduceThread(StoreHouse sh){
        this.sh = sh;
    }

    @Override
    public void run() {
        while(true){
            //调用仓库类生产产品的方法
            sh.produceProduct();
            //睡一会继续100
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}
--------------------
package com.lagou.task17;

/**
 * 消费者线程  sleep 2000
 */
public class ConsumerThread extends Thread {
    //为了调用仓库类中消费产品的方法
    private  StoreHouse sh;
    //创建生产者线程对象时给仓库类引用赋值
    public ConsumerThread(StoreHouse sh){
        this.sh = sh;
    }

    @Override
    public void run() {
        while(true){
            //调用仓库类中消费产品的方法
            sh.consumerProduct();
            //睡一会继续 2000
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


}
--------------------
package com.lagou.task17;

public class StoreHouseTest {

    public static void main(String[] args) {
        //创建仓库类
        StoreHouse sh = new StoreHouse();
        //创建生产者线程
        ProduceThread ptd = new ProduceThread(sh);
        //创建消费者线程
        ConsumerThread ctd = new ConsumerThread(sh);

        ptd.start();
        ctd.start();

    }
}


18.6.10 线程池(熟悉)

(1)实现Callable接口

Java5 开始新增加创建线程的第三种方式为实现 java.util.concurrent.Callable 接口。
常用的方法如下:
方法声明                  功能介绍
V call()                    计算结果并返回

(2)FutureTask

java.util.concurrent.FutureTask 类用于描述可取消的异步计算,该类提供了 Future 接口的基本实
现,包括启动和取消计算、查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用后的返回结果。
常用的方法如下:
方法声明                                        功能介绍
FutureTask(Callable callable)       根据参数指定的引用来创建一个未来任务
V get()                                           获取call 方法计算的结果
代码:实现Callable接口启动线程
package com.lagou.task17;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadCallableTest implements Callable {

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 10000; i++) {
            sum = sum+i;
        }
        System.out.println("计算1-10000累加的和是:"+sum);
        return  sum;
    }

    public static void main(String[] args) {

        ThreadCallableTest tct = new ThreadCallableTest();
        FutureTask ft = new FutureTask(tct);
        Thread td = new Thread(ft);
        td.start(); //调用call()方法

        /*//利用FutureTask的get()方法获取call()的返回值
        Object obj = null;
        try {
            obj = ft.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("线程中call方法的返回值是:"+obj);*/

    }


}

(3)线程池的由来

在服务器编程模型的原理,每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束
时,线程也就结束了,即每来一个客户端连接,服务器端就要创建一个新线程。
如果访问服务器的客户端很多,那么服务器要不断地创建和销毁线程,这将严重影响服务器的性 能。

(4)概念和原理

线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就
从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池 中。
在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务
后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程
池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

(5)相关类和方法

a. 从 Java5 开始提供了线程池的相关类和接口: java.util.concurrent.Executors 类和 java.util.concurrent.ExecutorService接口。
 
b. 其中 Executors 是个工具类和线程池的工厂类,可以创建并返回不同类型的线程池,常用方法如下:
 
c. 其中 ExecutorService 接口是真正的线程池接口,主要实现类是 ThreadPoolExecutor ,常用方法如下:
 

(6)测试代码

package com.lagou.task18;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolTest {

    public static void main(String[] args) {

        // 1.创建一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 2.向线程池中布置任务
        executorService.submit(new ThreadCallableTest());
        // 3.关闭线程池
        executorService.shutdown();
    }
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值