day17【Lock、并发包、线程池】

今日内容

  • Lock
  • 并发包
  • 线程池

教学目标

  • 能够使用Lock解决线程安全问题
  • 能够描述ConcurrentHashMap类的作用
  • 能够描述CountDownLatch类的作用
  • 能够描述CyclicBarrier类的作用
  • 能够表述Semaphore类的作用
  • 能够描述Exchanger类的作用
  • 能够使用线程池

第一章 Lock锁(掌握)

从jdk5后java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大.Lock锁是Java中更加符合编程习惯的解决方式。

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock():加同步锁。
  • public void unlock():释放同步锁。

由于Lock属于接口,不能创建对象,所以我们可以使用它的子类ReentrantLock来创建对象并使用Lock接口中的函数。

需求:使用Lock实现线程安全的卖票。

分析和步骤:

1)定义一个卖票的任务类SellTicketTask 类并实现Runnable接口;

2)在任务类中定义一个成员变量tickets保存票数100;

3)定义一把锁Lock的对象l;

4)在run函数中模拟卖票,if语句的上面使用锁对象l调用lock()函数获取锁,等待if语句结束之后,使用锁对象l调用unlock()函数释放锁;

5)定义测试类SellTicketDemo ,在这个类中分别创建任务类的对象和线程类的对象,并使用线程类的对象调用start()函数来启动线程;

/*
 * 需求:使用Lock实现线程安全的卖票。
 * Lock是接口,只能通过他的子类ReentrantLock创建对象
 * 构造函数 ReentrantLock()  创建一个 ReentrantLock 的实例。
 *  void lock() 获取锁。 
 *  void unlock() 试图释放此锁。 
 */
//定义一个任务类用来卖票
class SellTicketTask implements Runnable
{
	//定义100张票
	private static int tickets=100;
	//创建对象作为任意一把锁
//	private Object obj=new Object();
	//定义一把锁
	Lock l=new ReentrantLock();
	//模拟卖票
	public void run() {
		/*while(true)
		{
			synchronized (obj) {
				if(tickets>0)
				{
					System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
				}
			}
		}*/
		//使用Lock锁替换synchronized
		while(true)
		{
				//获取锁
				l.lock();
				if(tickets>0)
				{
					try {Thread.sleep(1);} catch (InterruptedException e) {}
					System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
				}
				//释放锁
				l.unlock();
		}
	}
}
public class SellTicketDemo {
	public static void main(String[] args) {
		// 创建任务类对象
		SellTicketTask stt = new SellTicketTask();
		//创建线程对象
		Thread t1 = new Thread(stt,"窗口1");
		Thread t2 = new Thread(stt,"窗口2");
		Thread t3 = new Thread(stt,"窗口3");
		Thread t4 = new Thread(stt,"窗口4");
		//启动线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

第二章 并发包

在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。

2.1 CopyOnWriteArrayList(理解)

  • ArrayList和CopyOnWriteArrayList效果演示

    • ArrayList在多线程的情况下线程不安全

    • CopyOnWriteArrayList是一个线程安全的集合,解决ArrayList安全问题。 数据不唯一。

      • 这个类的内部其实也就是使用了同步锁解决线程安全问题,和我们自己写同步锁是一样的意思。
  • 需求:给list集合添加1000个元素,然后打印集合的长度。

    public class MyRun implements Runnable{
        //定义一个集合
        //普通的ArrayList是线程不安全的类
        //static ArrayList<Integer> list = new ArrayList<>();
        
        //只要换成CopyOnWriteArrayList就变成了线程安全的
        static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    
        @Override
        public void run() {
            //给集合添加了1000个元素
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        }
    }
    
    public class Demo_ArrayList {
        public static void main(String[] args) throws InterruptedException {
            MyRun mr = new MyRun();
            //开启线程
            Thread t1 = new Thread(mr);
            t1.start();
            Thread t2 = new Thread(mr);
            t2.start();
    
            //为了让for循环先执行结束再打印
            //睡两秒钟
            Thread.sleep(2000);
    
            //在这里打印集合的长度,里面应该存放了多少个元素?
            System.out.println(MyRun.list.size());
    
    
        }
    }
    
    执行效果:
    	ArrayList:
            一个线程给集合添加1000个元素,两个线程应该添加2000个元素
            但是打印的结果是小于2000或者可能会抛异常
            因为ArrayList集合本身就是线程不安全的
    	
    	CopyOnWriteArrayList:
    		结果是2000
    

2.2 CopyOnWriteArraySet(理解)

  • HashSet是线程不安全的
  • CopyOnWriteArraySet是线程安全的
  • 需求:向HashSet中存储1000个数字。

代码演示如下:

package com.itheima.sh.demo_14;
public class MyRun implements Runnable {
    //定义集合
    //HashSet集合本身是有线程安全问题的
//   static HashSet<Integer> set = new HashSet<>();

    //CopyOnWriteArraySet解决了安全问题
    static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();

    @Override
    public void run() {
        //添加1000个数字   0-999
        for (int i = 0; i < 1000; i++) {
            set.add(i);
        }
    }
}

public class Test01 {
    public static void main(String[] args) throws InterruptedException {
        MyRun mr = new MyRun();
        //开启线程
        Thread t1 = new Thread(mr);
        t1.start();
        //开启线程
        Thread t2 = new Thread(mr);
        t2.start();
        //让主程序睡两秒钟
        Thread.sleep(2000);
        //打印集合的长度 大于1000
        System.out.println(MyRun.set.size());

    }
}

执行效果:
	HashSet:
		本来集合中保存的是1000个元素,但是打印的长度size大于1000.
    CopyOnWriteArraySet:
    	打印的集合长度就是1000

说明:上述打印集合的长度size大于1000的原因如下图所示:

假设线程一和线程二都向set集合中添加数据0,线程一正在添加的过程中先判断集合中是否含有0,如果没有则添加数字0,此时线程二也开始判断set集合中是否有0,此时没有则线程二也准备添加,添加一次,那么底层size就会+1,但实际上set集合只存储一个数字0.size加错了。

在这里插入图片描述

注意:

CopyOnWriteArraySet 底层就是CopyOnWriteArrayList 。CopyOnWriteArraySet 如何保证唯一的?使用CopyOnWriteArrayList中的方法:

 boolean addIfAbsent(E e) 添加元素(如果不存在)。 
     如果添加的元素存在,则不存储,如果添加元素不存在则添加。
     举例:
     aaa 第一次添加可以添加
     bbb 第一次添加可以添加
     aaa 已经存在aaa 不添加

2.3 ConcurrentHashMap(理解)

  • HashMap和Hashtable和ConcurrentHashMap效果演示

  • 需求:向HashMap集合中存储1000个键值对数据

    import java.util.HashMap;
    import java.util.Hashtable;
    import java.util.concurrent.ConcurrentHashMap;
    
    public class MyRun implements Runnable {
        //HashMap线程不安全
        //HashMap<Integer,Integer> map = new HashMap<>();
        
        //Hashtable线程安全的
        //Hashtable<Integer,Integer> map = new Hashtable<>();
    
        //ConcurrentHashMap线程安全的
        ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
    
        @Override
        public void run() {
            //添加1000个元素   0-999
            for (int i = 0; i < 1000; i++) {
                map.put(i,i);
            }
        }
    }
    
    public class DemoHashMap {
        public static void main(String[] args) throws InterruptedException {
    
            MyRun mr = new MyRun();
    
            //开启线程
            Thread t1 = new Thread(mr);
            t1.start();
    
            //开启线程
            Thread t2 = new Thread(mr);
            t2.start();
    
            //先睡2秒钟让循环执行结束
            Thread.sleep(2000);
    
            //打印集合的长度
            System.out.println(mr.map.size());
    
        }
    }
    
    执行效果:
    	HashMap本来应该打印的长度是1000,但是执行的结果大于1000
    	
    	Hashtable  			 结果是1000
    	ConcurrentHashMap    结果是1000
    

    Hashtable和ConcurrentHashMap的速度区别

    • Hashtable解决线程安全的方式,但是效率很低。

      public synchronized V get(Object key) {}
      public synchronized V put(K key, V value) {}
      
      每个方法全都是同步的,当任意一个方法在执行时,别的方法都不能执行。
      

      HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
      在这里插入图片描述

    • ConcurrentHashMap

      ​ CAS + 局部(synchronized)锁定

在这里插入图片描述

  • Hashtable和ConcurrentHashMap效率高低的代码演示

    public class MyRun implements Runnable {
        //HashMap线程不安全
        //HashMap<Integer,Integer> map = new HashMap<>();
    
        //Hashtable线程安全的
        //Hashtable<Integer,Integer> map = new Hashtable<>();
    
        //ConcurrentHashMap线程安全的
        ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
    
        @Override
        public void run() {
            //获取系统当前时间
            long time1 = System.currentTimeMillis();
    
            //添加1000个元素   0-1999
            for (int i = 0; i < 2000; i++) {
                map.put(i,i);
            }
    
            long time2 = System.currentTimeMillis();
            System.out.println((time2-time1) + "毫秒");
        }
    }
    
    public class DemoHashMap {
        public static void main(String[] args) throws InterruptedException {
    
            MyRun mr = new MyRun();
    
            //开启线程
            //开启了很多个线程能够看到更明显的速度的区别
            for (int i = 0; i < 1000; i++) {
                Thread t1 = new Thread(mr);
                t1.start();
            }
        }
    }
    

2.4 CountDownLatch(了解)

  • 作用

    CountDownLatch允许一个或多个线程等待其他线程完成操作。

  • 方法介绍

    public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象
        
    void await()     // 让当前线程等待 
    void countDown() //每调用一次countDown()方法,计数器count就会进行减1,如果减到0等待的线程就会执行
    
    
  • 代码演示

    • 要求: 必须在先打印C 再打印B
    import java.util.concurrent.CountDownLatch;
    
    public class MyRun1 implements Runnable {
        CountDownLatch latch;
        //使用构造方法传入参数
        public MyRun1(CountDownLatch latch){
            this.latch = latch;
        }
    
        @Override
        public void run() {
    
            System.out.println("A");
            /*try {
                Thread.sleep(1000);//效率太低
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            //等待
            try {
                latch.await();
            } catch (InterruptedException e) {
            }
    
            System.out.println("B");
        }
    }
    
    
    import java.util.concurrent.CountDownLatch;
    
    public class MyRun2 implements Runnable{
        CountDownLatch latch;
        //使用构造方法传入参数
        public MyRun2(CountDownLatch latch){
            this.latch = latch;
        }
    
        @Override
        public void run() {
            System.out.println("C");
            //计数器减一
            //latch在计数器等于0的时候就会让等待的线程执行
            latch.countDown();
        }
    }
    
    
    import java.util.concurrent.CountDownLatch;
    //有一个要求:必须在先打印C 再打印B
    public class Test01 {
        public static void main(String[] args) throws InterruptedException {
    
            //创建对象
            //这里的参数1表示计数器,只要执行latch.countDown();那么计数器就会减1
            CountDownLatch latch = new CountDownLatch(1);
            //开启线程
            MyRun1 myRun1 = new MyRun1(latch);
            Thread t = new Thread(myRun1);
            t.start();
    
            //开启线程
            MyRun2 myRun2 = new MyRun2(latch);
            Thread t2 = new Thread(myRun2);
            t2.start();
        }
    }
    
    

如果没有使用CountDownLatch的情况下,会出现如下所示结果:

在这里插入图片描述

小结:

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。

CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch的await()方法的线程阻塞状态解除,继续执行。

2.5 CyclicBarrier(了解)

概述

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。

  • 作用

    ​ 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

  • 方法介绍

    public CyclicBarrier(int parties, Runnable barrierAction) // 创建对象
                                                //parties代表要达到屏障的线程数量
        										//barrierAction达到屏障之后要执行的线程
        
     int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
    
    
  • 示例代码

    需求:有5个学生一起考试,考试有100道题,每个人答题速度不一样,但是要求,所有人做完之后才能交卷。

    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    public class MyRun implements Runnable {
    
        //创建屏障对象
        CyclicBarrier cb = new CyclicBarrier(5,new JiaoJuan());
    
        @Override
        public void run() {
    
            //考试一共100道题
            for (int i = 1; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + "做完了第" + i + "道题");
            }
    
            System.out.println(Thread.currentThread().getName() + "做完了所有题!");
    
            //等待别人写题
            try {
                cb.await();
            } catch (InterruptedException e) {
    
            } catch (BrokenBarrierException e) {
            }
        }
    }
    
    public class JiaoJuan implements Runnable {
        @Override
        public void run() {
            System.out.println("大家一起交卷!!!!");
        }
    }
    
    public class Demo01 {
        public static void main(String[] args) {
    
            //创建线程
            MyRun mr = new MyRun();
    
            //开启5个线程
            new Thread(mr,"张三").start();
            new Thread(mr,"李四").start();
            new Thread(mr,"王五").start();
            new Thread(mr,"赵六").start();
            new Thread(mr,"田七").start();
        }
    }
    

使用场景

使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

例如:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

2.6 Semaphore(了解)

  • 作用

    1)Semaphore的主要作用是控制线程的并发数量。

    2)之前学习的synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。Semaphore 可以设置同时允许几个线程执行。

    3)Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。

  • 方法介绍

    public Semaphore(int permits)               			 //permits 表示许可线程的数量 
    
     void acquire()       //表示获取许可 
     void release()       //表示释放许可
    
  • 代码演示

    需求:开了一个饭店,饭店里面只有3张桌子,有5桌人来吃饭。

    //饭店
    public class FanDian {
        //定义Semaphore控制最大接客数量
        //public Semaphore(int permits)permits 表示许可线程的数量
        Semaphore s = new Semaphore(3);
    
        //饭店的服务吃饭方法(方法名无所谓)
        public void service() throws InterruptedException {
            //获取方法
            //public void acquire() throws InterruptedException	表示获取许可
            s.acquire(); //调用了这个方法就表示进来了
            //显示进来的时间
            Calendar c = Calendar.getInstance();
            int minute = c.get(Calendar.MINUTE);
            int second = c.get(Calendar.SECOND);
            System.out.println(Thread.currentThread().getName() + minute + "分钟" + second + "秒进来吃饭");
    
            //吃饭
            //等待3秒钟 模拟吃饭的过程,表示线程正在吃饭
            Thread.sleep(3000);
    
            //显示出去的时间
            Calendar c2 = Calendar.getInstance();
            int minute2 = c2.get(Calendar.MINUTE);
            int second2 = c2.get(Calendar.SECOND);
            System.out.println(Thread.currentThread().getName() + minute2 + "分钟" + second2 + "秒吃完饭出去了");
            //释放
            //public void release()	表示释放许可
            s.release();
        }
    }
    
    
    public class MyRun implements Runnable {
        //创建对象
        FanDian fd = new FanDian();
        @Override
        public void run() {
            //调用吃饭的方法
            try {
                fd.service();
            } catch (InterruptedException e) {
    
            }
        }
    }
    
    public class Test01 {
        public static void main(String[] args) throws InterruptedException {
            //创建线程
            MyRun mr = new MyRun();
            //开启5个线程
            new Thread(mr,"张三").start();
            new Thread(mr,"李四").start();
            new Thread(mr,"王五").start();
            new Thread(mr,"锁哥").start();
            new Thread(mr,"赵六").start();
        }
    }
    

2.7 Exchanger(了解)

概述

  • 作用

    Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行2个线程间的数据交换。

    说明:

    两个线程通过exchange()方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

  • 方法介绍

    public Exchanger()    		//构造方法,创建对象
    public V exchange(V x)      //参数是给对方的数据,返回值是对方发来的数据
        //说明:如果一个线程执行了exchange(),此时就会阻塞等待另一个线程执行exchange()方法,才结束阻塞
    
  • 代码演示

    需求:在一个银行,要统计今年收入一共是多少,让两个人都去计算,算完之后两个人互相交换数据。

    import java.util.concurrent.Exchanger;
    
    public class MyRun1 implements Runnable {
        Exchanger<String> e;
        //构造方法
        public MyRun1(Exchanger<String> e) {
            this.e = e;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "开始计算今年的收入。。");
    
            String s = null;
            try {
                s = e.exchange("收入表A");
            } catch (InterruptedException e1) {
            }
    
            System.out.println(Thread.currentThread().getName() + "收到了 " + s);
        }
    }
    
    public class MyRun2 implements Runnable {
    
        Exchanger<String> e;
        //构造方法
        public MyRun2(Exchanger<String> e) {
            this.e = e;
        }
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "开始计算今年的收入。。。");
    
            String s = null;
            try {
                s = e.exchange("收入表B");
            } catch (InterruptedException e1) {
            }
    
            System.out.println(Thread.currentThread().getName() + "收到了 " +s);
        }
    }
    
    import java.util.concurrent.Exchanger;
    
    public class Demo {
        public static void main(String[] args) {
            //在主方法中创建对象
            //泛型用来代表交换的数据的类型
            Exchanger<String> e = new Exchanger<>();
    
            MyRun1 myRun1 = new MyRun1(e);
            new Thread(myRun1,"张三").start();
    
            MyRun2 myRun2 = new MyRun2(e);
            new Thread(myRun2,"李四").start();
        }
    }
    

使用场景

使用场景:可以做数据校对工作

需求:比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,

并对两个文件数据进行校对,看看是否录入一致,

【自学,了解,有兴趣的同学可以自学下】

  • exchange方法的超时

1).制作线程A:

public class ThreadA extends Thread {
	private Exchanger<String> exchanger;
	public ThreadA(Exchanger<String> exchanger) {
		super();
		this.exchanger = exchanger;
	}
	@Override
	public void run() {
		try {
			System.out.println("线程A欲传递值'礼物A'给线程B,并等待线程B的值,只等5秒...");
			System.out.println("在线程A中得到线程B的值 =" + exchanger.exchange("礼物A",5, TimeUnit.SECONDS));
			System.out.println("线程A结束!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (TimeoutException e) {
			System.out.println("5秒钟没等到线程B的值,线程A结束!");
		}
	}
}

2).制作测试类:

public class Run {
	public static void main(String[] args) {
		Exchanger<String> exchanger = new Exchanger<String>();
		ThreadA a = new ThreadA(exchanger);
		a.start();
	}
}

3).测试结果:

在这里插入图片描述

第三章 线程池方式

3.1 线程池的思想

在这里插入图片描述

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。今天我们就来讲解一下Java的线程池。

3.2 线程池概念

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。因为启动线程的时候会在内存中开辟一块空间,消耗系统资源,同时销毁线程的时候首先要把和线程相关东西进行销毁,还要把系统的资源还给系统。这些操作都会降低操作性能。尤其针对一个线程用完就销毁的更加降低效率。

而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,生存期较短的线程指的是用完一次线程就丢掉。更应该考虑使用线程池。

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

线程池工作原理如下图所示:

需求:我有一段任务,需要执行100次。

在这里插入图片描述

在这里插入图片描述

说明:

每次线程执行完任务以后,线程不会销毁,会放回线程池中,每次在执行任务的时候又会到线程池中去取线程。这样会提高效率。

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机或者宕机)。

3.3 线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,以及他的子接口java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

​ A:自定义一个类,作为任务类并实现Runnable接口;

​ B:实现Runnable接口中的run方法;

​ C:创建任务类的对象;

​ D:获取线程池对象;

​ E:直接执行任务;​

需求:使用线程池来完成卖票任务。

Runnable实现类代码:

//A:自定义一个类,作为任务类并实现Runnable接口;
class SellTicketTask implements Runnable
{
	//定义成员变量存储100张票
	private static int tickets=100;
	//创建锁对象
	private Lock l=new ReentrantLock();
	//B:实现Runnable接口中的run方法;
	public void run() {
		// 模拟卖票
		while(true)
		{
			//获取锁
			l.lock();
			if(tickets>0)
			{
				//休眠
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
				}
				System.out.println(Thread.currentThread().getName()+"出票:"+tickets);
                  tickets--;
			}
			//释放锁
			l.unlock();
		}
	}
}

线程池测试类:

public class SellTicketDemo {
	public static void main(String[] args) {
		//C:创建任务类的对象;
		SellTicketTask stt = new SellTicketTask();
		//D:获取线程池对象; 获取2个线程
		ExecutorService es = Executors.newFixedThreadPool(2);
		//E:直接执行任务; 
      	  //自己创建线程对象的方式
        // Thread t = new Thread(stt);
        // t.start(); ---> 调用MyRunnable中的run()

        // 从线程池中获取线程对象,然后调用SellTicketTask中的run()
        es.submit(stt);
        // 再获取个线程对象,调用SellTicketTask中的run()
        es.submit(stt);
	}
}

3.4 Callable开启多线程

  • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.

    问题1:Callable是什么?

在这里插入图片描述

/*
 * 演示:演示Callable
 * 我们忽略返回值,这个接口就与Runnable接口一样了
 */
class MyTask implements Callable<Object>{

	@Override
	public Object call() throws Exception {
		for( int i = 0; i < 10; i++){
			System.out.println(Thread.currentThread().getName() + " ... " + i);
		}
		return null;
	}
}
public class CallableDemo {
	public static void main(String[] args) {
		// 创建任务对象
		MyTask mt = new MyTask();
		// 获取线程池
		ExecutorService es = Executors.newFixedThreadPool(2);
		// 执行任务
		es.submit(mt);
		es.submit(mt);
	}
}

问题2:Future是什么?

在这里插入图片描述

  • 方法:V get() : 获取计算完成的结果。
  • 需求:通过Callable计算从1到5的和

A:我们自定义类,实现Callable接口

B:实现Call方法,Call方法有返回值

C:然后吧任务类对象交给线程池执行

D:执行完成的结果保存Future中

E:最后我们调用Future的get方法拿到真正的结果。

/*
 * 演示:带返回值的线程任务
 * 需求:通过Callable计算从1到任意数字的和
 */
class SumTask implements Callable<Integer>{
	
	@Override
	public Integer call() throws Exception {
		int sum = 0;
		for(int i = 1; i <= 5; i++){
			sum += i;
		}
		return sum ;
	}
}
public class CallableDemo02 {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		// 创建任务对象
		SumTask st = new SumTask();
		// 获取线程池
		ExecutorService es = Executors.newFixedThreadPool(1);
		// 执行任务
		Future<Integer> future = es.submit(st);
		// 等待运算结束,获取结果
		Integer i = future.get();
		System.out.println(i);
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

娃娃 哈哈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值