java学习记录(Day10)
- 多个线程打印
1.通过定义Run类实现Runnable接口,重写其中的run方法。在RunA中定义一个ArrayList来存储。在主函数中,声明Run类的实例run来执行RunA中定义的方法,然后定义线程a,b都来执行run中的方法。因为指向的是同一个数组,所以线程a,b都会往里面添加10个数据,一共20个(不发生数组下标越界时)。下面是代码实现:
2.定义一个ThreadA类继承Thread类,定义一个ArrayList来存储,重写里面的run方法,向list中添加字符a。在主函数中,实例出了两个ThreadA的线程a,b,执行线程a,b,这两个线程就会分别创建一个数组来添加字符a,所以输出结果为两个数组的长度都为10,里面的内容全是a。下面是代码实现:
3.在EasyThreadA类中定义一个method方法,在里面添加字符a,主函数中使用 EasyThreadA 类中的 method 方法作为实现 Runnable 接口中 run() 方法的主体。线程a,b都执行run中的任务,向EasyThreadA类中定义的数组添加数据,所以输出结果长度是20(不发生数据缺失时)。下面是代码实现:
- 锁对象 Lock
1.创建锁对象
ReentrantLock是可重入锁
可重入锁:
可重入就是说某个线程已经获得某个锁,可以再次获取该锁。
它提供了一种显式的锁定机制,使得你可以在需要的时候精确地控制锁的获取和释放。
特点:
a.可重入锁支持公平性和非公平性的锁获取策略,可以根据需要选择。
b.可重入锁支持尝试锁定,即尝试获取锁但不阻塞。
c.可重入锁支持锁定的中断操作,即在等待锁的过程中可以响应中断。
- 获取锁 加锁 .lock()方法
- 尝试加锁 加锁成功true 失败返回false
- 解锁 .unlock()方法
- 示例:
- ReentrantReadWriteLock分为读锁和写锁
当设置读锁时,所有的线程都可以访问,因为他只是读取数据。当设为写锁时,因为他要修改里面的内容,所以为了避免出现错误,所以在某个线程修改数据时,其余的线程不能执行,也就是设置写锁时,其余线程不能执行,除非写锁解除。
- 设置读锁
- 设置写锁
- 示例
声明发生读锁和发生写锁的方法,设置多个打印结果便于观察代码执行到哪一个线程。在主函数中,声明两个Runnable类型的实例来实现上面定义的两个方法。定义多个线程,分别执行不同的加锁方法,下面是具体代码:
执行代码,输出结果为:
在结果中可以发现,设置读锁时,其余的线程也是可以设置读锁的,但是一旦设置写锁,其他线程就只能等写锁解除,才能继续进行读锁或者写锁的操作。
通过传入参数将锁设为公平锁/非公平锁
- 锁对象
synchronized非公平锁
(1)设置锁对象(看门大爷)
首先定义一个常量OBJ使它作为锁对象,来进行下面的wait、notify和notifyAll方法的操作。
(2)编写方法
线程开启后会执行method方法中的输出“进入方法”,因为这个没有设置锁,所以所有的线程都可以同时执行该行代码。然后进入同步代码块,由于设置了锁,所以一次只能有一个代码来进行,当进入的线程执行到wait方法时就会进入等待状态(等待池),若没有唤醒就会一直在等待状态。所以要在下一个线程进入同步代码块时,执行一个notify方法来唤醒前一个进入等待的线程,被唤醒的线程重新在外面等待资源执行,一旦分配到资源开始执行时,就会执行上一次wait方法的后面操作。
代码如下:
代码的最后还有一个唤醒操作,是因为执行到最后会有一个代码一直在等待池,所以在倒数第二个线程结束前要将最后一个线程从等待池里唤醒。接下来只要在主函数中声明几个线程来执行上面的方法,就可以验证。
(3)wait和sleep的区别
wait是Object中定义的方法,只有锁对象调用,让执行到该代码的线程进入到等待状态
sleep是Thread类中定义的静态方法,可以让执行到该行的线程进入等待状态
区别:
1.sleep需要传入一个毫秒数,到达时间后会自动唤醒 wait不能自动唤醒,必须调用notify/notifyAll方法唤醒
2.sleep方法保持锁状态进入等待状态 wait 方法会解除锁状态,其他线程可以进入运行
示例
- 线程池
1.定义:
池的目标是重用,完成线程创建和管理,销毁工作
2.线程池的实现
(1)定义一个队列
设置它的容量为12
(2)设置线程池对象
其中有七个参数,分别为核心线程数,最多线程数,线程空闲时间、时间单位、工作队列、线程工厂、拒绝策略
核心线程数:即使线程池中没有任务执行,也会一直维持这个数量的线程。
最大线程数:当任务到来且线程池中的线程数已经达到核心线程数但仍然不足以处理所有任务时,线程池会创建新的线程,直到达到这个最大值。
线程空闲时间:当线程池中的线程数量大于核心线程数时,如果某线程空闲了指定的时间,那么线程池会终止该线程,直到线程池中的线程数量等于核心线程数。
时间单位:指定keepAliveTime参数的时间单位,例如秒、分钟
工作队列:这是一个阻塞队列,用来存储等待执行的任务。当线程池中的线程数量达到最大值时,新来的任务将会被放入此队列中等待执行。
拒绝策略:当线程池无法处理更多任务时(即线程池已满并且工作队列也已满),会采用拒绝策略来处理新提交的任务。
(3)线程池的执行原理
1.池中是否有空闲的线程,如果有就让该线程执行任务
2.如果没有空闲的线程,判断池中的线程数量有没有达到核心线程数,如果没有达到核心线程数,就会创建新的线程执行任务
3.如果已经达到,优先在队列中存储,直到队列填满
4.工作队列填满后在添加新的任务,再判断是否达到最大线程数,如果没有创建新的线程执行任务
直到填满最大线程数
- 已经填满最大线程数,队列也已经填满,没有空闲的线程,这时候就执行回绝策略
- 线程池中的线程达到(超过)核心线程数,超出的数量会根据存活时间进行销毁,直到数量达到核心线程数,如果线程数少于核心线程数,就不会销毁
(4)四种回绝策略
AbortPolicy 放弃该任务并会抛出一个异常
CallerRunsPolicy 调用者执行,让传递任务的线程执行此任务
DiscardOldestPolicy 放弃队列中时间最长的任务,不会抛出异常
DiscardPolicy 直接放弃新的任务
(5)java中内置的线程池对象
设定最大线程数量:Executors.newFixedThreadPool(10);
提供定时运行的处理方案:Executors.newScheduledThreadPool(10);
创建一个具有单个线程的线程池,保证任务队列完全按照顺序执行: Executors.newSingleThreadExecutor();
可以根据工作任务创建线程 如果没有空闲的线程就创建新的线程,线程存活时间60秒:
- 线程任务 Runnable 和Callable
Runnable类的实例可以通过execute()和submit()方法加入到线程池中,Callable类型只能是submit()方法。
Submit()方法
submit 方法返回一个 Future<String> 对象 f,代表了异步计算的未来结果。f 可以用来查询任务的状态或者获取任务的结果。
通过调用 Future 对象 f 的 get 方法来阻塞地等待任务完成,并打印任务的结果。get 方法会一直阻塞当前线程,直到任务完成或者抛出异常,或者在可选的超时时间内任务没有完成。
Runnable 和Callable的区别:
Runnable 接口中只有一个抽象方法 run()。run() 方法没有返回值。
Callable<V> 接口中有一个抽象方法 call(),它返回一个泛型类型的值 V。call() 方法可以抛出异常。
总结:
Runnable 更适合执行那些不需要返回值的任务。
Callable 更适合执行那些需要返回值的任务,并且可以处理更复杂的异常情况。